From a227465bf59184be696647d613952622bc7c1bdf Mon Sep 17 00:00:00 2001
From: Henrich Neumann <henrich.neumann@usit.uio.no>
Date: Wed, 4 Jan 2023 12:40:01 +0100
Subject: [PATCH] Add linting python and yaml and error checking json files to
 pipeline

---
 .gitignore               |   1 +
 .gitlab-ci.yml           |  15 +++
 .yamllint.yaml           |  32 ++++++
 setra_client/__init__.py |   2 +-
 setra_client/client.py   | 236 +++++++++++++++++++++------------------
 setra_client/models.py   |  39 ++++---
 setra_client/version.py  |   9 +-
 setup.py                 |  66 ++++++-----
 tests/conftest.py        |  39 +++----
 tests/test_client.py     |  84 +++++++++-----
 tox.ini                  |  21 ++++
 11 files changed, 333 insertions(+), 211 deletions(-)
 create mode 100644 .yamllint.yaml

diff --git a/.gitignore b/.gitignore
index e00047d..a7136ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ dist/
 junit-*.xml
 .mypy_cache/
 venv/
+.venv/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 48cd4ae..2df9383 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,6 +7,21 @@ before_script:
   - pip install -r requirements.txt
   - pip install -r requirements-test.txt
 
+code-format:
+  image: python:3.8
+  stage: test
+  script: tox -e black
+
+yamllint:
+  image: python:3.8
+  stage: test
+  script: tox -e yamllint
+
+json:
+  image: python:3.8
+  stage: test
+  script: tox -e json
+
 python36:
   image: python:3.6
   stage: test
diff --git a/.yamllint.yaml b/.yamllint.yaml
new file mode 100644
index 0000000..0246e48
--- /dev/null
+++ b/.yamllint.yaml
@@ -0,0 +1,32 @@
+ignore-from-file: .gitignore
+
+rules:
+  braces:
+    max-spaces-inside-empty: 0
+  brackets:
+    max-spaces-inside-empty: 0
+  colons: enable
+  commas: enable
+  comments: enable
+  comments-indentation: enable
+  document-end: disable
+  document-start: disable
+  empty-lines:
+    max: 100
+  empty-values: enable
+  float-values: disable
+  hyphens: enable
+  indentation:
+    spaces: 2
+  key-duplicates: enable
+  key-ordering: disable
+  line-length: disable
+  new-line-at-end-of-file: enable
+  new-lines: enable
+  octal-values: disable
+  quoted-strings:
+    quote-type: double
+    required: false
+    allow-quoted-quotes: true
+  trailing-spaces: enable
+  truthy: enable
diff --git a/setra_client/__init__.py b/setra_client/__init__.py
index a055290..7b5c675 100644
--- a/setra_client/__init__.py
+++ b/setra_client/__init__.py
@@ -2,5 +2,5 @@ from .client import SetraClient
 from .version import get_distribution
 
 
-__all__ = ['SetraClient']
+__all__ = ["SetraClient"]
 __version__ = get_distribution().version
diff --git a/setra_client/client.py b/setra_client/client.py
index fa92bf8..e8fbc99 100644
--- a/setra_client/client.py
+++ b/setra_client/client.py
@@ -49,27 +49,26 @@ class IncorrectPathError(Exception):
 
 
 class SetraEndpoints:
-    def __init__(self,
-                 url,
-                 batch_url='api/batch/',
-                 transaction_url='api/transaction/',
-                 voucher_url='api/voucher/',
-                 new_batch_url='api/addtrans/',
-                 put_batch_url='api/addtrans/',
-                 batch_complete_url='api/batch_complete/',
-                 batch_error_url="api/batch_error/",
-                 parameters_url="api/parameters/",
-
-                 # Order urls (sotra):
-                 order_url="api/order/",
-                 order_complete_url="api/order_complete/",
-                 detail_url="api/detail/",
-                 details_in_order_url="api/details_in_order/",
-                 abw_order_complete_url="api/abw_order_complete/",
-                 post_add_abw_order_url="api/add_abw_order/",
-                 abw_order_errors_url="api/abw_order_errors/",
-                 ):
-
+    def __init__(
+        self,
+        url,
+        batch_url="api/batch/",
+        transaction_url="api/transaction/",
+        voucher_url="api/voucher/",
+        new_batch_url="api/addtrans/",
+        put_batch_url="api/addtrans/",
+        batch_complete_url="api/batch_complete/",
+        batch_error_url="api/batch_error/",
+        parameters_url="api/parameters/",
+        # Order urls (sotra):
+        order_url="api/order/",
+        order_complete_url="api/order_complete/",
+        detail_url="api/detail/",
+        details_in_order_url="api/details_in_order/",
+        abw_order_complete_url="api/abw_order_complete/",
+        post_add_abw_order_url="api/add_abw_order/",
+        abw_order_errors_url="api/abw_order_errors/",
+    ):
         self.baseurl = url
         self.batch_url = batch_url
         self.transaction_url = transaction_url
@@ -91,9 +90,7 @@ class SetraEndpoints:
     """ Get endpoints relative to the SETRA API URL. """
 
     def __repr__(self):
-        return '{cls.__name__}({url!r})'.format(
-            cls=type(self),
-            url=self.baseurl)
+        return "{cls.__name__}({url!r})".format(cls=type(self), url=self.baseurl)
 
     def batch(self, batch_id: str = None):
         """
@@ -102,8 +99,9 @@ class SetraEndpoints:
         if batch_id is None:
             return urllib.parse.urljoin(self.baseurl, self.batch_url)
         else:
-            return urllib.parse.urljoin(self.baseurl,
-                                        '/'.join((self.batch_url, batch_id)))
+            return urllib.parse.urljoin(
+                self.baseurl, "/".join((self.batch_url, batch_id))
+            )
 
     def transaction(self, trans_id: str = None):
         """
@@ -112,9 +110,9 @@ class SetraEndpoints:
         if trans_id is None:
             return urllib.parse.urljoin(self.baseurl, self.transaction_url)
         else:
-            return urllib.parse.urljoin(self.baseurl,
-                                        '/'.join((self.transaction_url,
-                                                  trans_id)))
+            return urllib.parse.urljoin(
+                self.baseurl, "/".join((self.transaction_url, trans_id))
+            )
 
     def voucher(self, vouch_id: str = None):
         """
@@ -123,8 +121,9 @@ class SetraEndpoints:
         if vouch_id is None:
             return urllib.parse.urljoin(self.baseurl, self.voucher_url)
         else:
-            return urllib.parse.urljoin(self.baseurl,
-                                        '/'.join((self.voucher_url, vouch_id)))
+            return urllib.parse.urljoin(
+                self.baseurl, "/".join((self.voucher_url, vouch_id))
+            )
 
     def post_new_batch(self):
         return urllib.parse.urljoin(self.baseurl, self.new_batch_url)
@@ -136,15 +135,16 @@ class SetraEndpoints:
         """
         URL for Batch endpoint
         """
-        return urllib.parse.urljoin(self.baseurl,
-                                    '/'.join((self.batch_complete_url, batch_id)))
+        return urllib.parse.urljoin(
+            self.baseurl, "/".join((self.batch_complete_url, batch_id))
+        )
 
     def batch_error(self, batch_id: str):
         """
         URL for batch_error endpoint
         """
         return urllib.parse.urljoin(
-            self.baseurl, '/'.join((self.batch_error_url, batch_id))
+            self.baseurl, "/".join((self.batch_error_url, batch_id))
         )
 
     def parameters(self):
@@ -155,8 +155,9 @@ class SetraEndpoints:
         """
         URL for getting an abw order including a list of its orders, and each order contains a list of its detail objects
         """
-        return urllib.parse.urljoin(self.baseurl,
-                                    '/'.join((self.abw_order_complete_url, abw_order_id)))
+        return urllib.parse.urljoin(
+            self.baseurl, "/".join((self.abw_order_complete_url, abw_order_id))
+        )
 
     def add_abw_order(self):
         """
@@ -171,49 +172,55 @@ class SetraEndpoints:
         if order_id is None:
             return urllib.parse.urljoin(self.baseurl, self.order_url)
         else:
-            return urllib.parse.urljoin(self.baseurl,
-                                        '/'.join((self.order_url, order_id)))
+            return urllib.parse.urljoin(
+                self.baseurl, "/".join((self.order_url, order_id))
+            )
 
     def order_complete(self, order_id: str):
         """
         URL for getting an order object including a list of its detail objects
         """
-        return urllib.parse.urljoin(self.baseurl,
-                                    '/'.join((self.order_complete_url, order_id)))
+        return urllib.parse.urljoin(
+            self.baseurl, "/".join((self.order_complete_url, order_id))
+        )
 
     def detail(self, detail_id: str):
         """
         URL for detail endpoint
         """
-        return urllib.parse.urljoin(self.baseurl,
-                                        '/'.join((self.detail_url, detail_id)))
+        return urllib.parse.urljoin(
+            self.baseurl, "/".join((self.detail_url, detail_id))
+        )
 
     def details_in_order(self, order_id: str):
         """
         URL for getting a list of detail objects in an order
         """
-        return urllib.parse.urljoin(self.baseurl,
-                                    '/'.join((self.details_in_order_url, order_id)))
+        return urllib.parse.urljoin(
+            self.baseurl, "/".join((self.details_in_order_url, order_id))
+        )
 
     def abw_order_errors(self, abw_order_id: str):
         """
         URL for getting an object containing lists of all errors for AbwOrder, Orders and Details
         """
-        return urllib.parse.urljoin(self.baseurl,
-                                    '/'.join((self.abw_order_errors_url, abw_order_id)))
+        return urllib.parse.urljoin(
+            self.baseurl, "/".join((self.abw_order_errors_url, abw_order_id))
+        )
 
 
 class SetraClient(object):
     default_headers = {
-        'Accept': 'application/json',
+        "Accept": "application/json",
     }
 
-    def __init__(self,
-                 url: str,
-                 headers: Union[None, dict] = None,
-                 return_objects: bool = True,
-                 use_sessions: bool = True,
-                 ):
+    def __init__(
+        self,
+        url: str,
+        headers: Union[None, dict] = None,
+        return_objects: bool = True,
+        use_sessions: bool = True,
+    ):
         """
         SETRA API client.
 
@@ -235,43 +242,45 @@ class SetraClient(object):
         request_headers = {}
         for h in self.headers:
             request_headers[h] = self.headers[h]
-        for h in (headers or ()):
+        for h in headers or ():
             request_headers[h] = headers[h]
         return request_headers
 
-    def call(self,
-             method_name,
-             url,
-             headers=None,
-             params=None,
-             return_response=True,
-             **kwargs):
-        if method_name == 'GET':
+    def call(
+        self,
+        method_name,
+        url,
+        headers=None,
+        params=None,
+        return_response=True,
+        **kwargs,
+    ):
+        if method_name == "GET":
             return_response = False
         headers = self._build_request_headers(headers)
         if params is None:
             params = {}
-        logger.debug('Calling %s %s with params=%r',
-                     method_name,
-                     urllib.parse.urlparse(url).path,
-                     params)
-        r = self.session.request(method_name,
-                                 url,
-                                 headers=headers,
-                                 params=params,
-                                 **kwargs)
+        logger.debug(
+            "Calling %s %s with params=%r",
+            method_name,
+            urllib.parse.urlparse(url).path,
+            params,
+        )
+        r = self.session.request(
+            method_name, url, headers=headers, params=params, **kwargs
+        )
         if r.status_code in (500, 400, 401):
-            logger.warning('Got HTTP %d: %r', r.status_code, r.content)
-        elif r.status_code == 404 and method_name == 'GET':
+            logger.warning("Got HTTP %d: %r", r.status_code, r.content)
+        elif r.status_code == 404 and method_name == "GET":
             try:
                 data = r.json()
-                if 'detail' in data and 'Not found' in data['detail']:
+                if "detail" in data and "Not found" in data["detail"]:
                     return None
                 else:
                     raise IncorrectPathError
             except:
                 data = r.text
-                if 'Not found' in data:
+                if "Not found" in data:
                     return None
                 else:
                     raise IncorrectPathError
@@ -288,17 +297,15 @@ class SetraClient(object):
         return return_data
 
     def get(self, url, **kwargs):
-        return self.call('GET', url, **kwargs)
+        return self.call("GET", url, **kwargs)
 
     def put(self, url, **kwargs):
-        return self.call('PUT', url, **kwargs)
+        return self.call("PUT", url, **kwargs)
 
     def post(self, url, **kwargs):
-        return self.call('POST', url, **kwargs)
+        return self.call("POST", url, **kwargs)
 
-    def object_or_data(
-            self, cls, data: Union[dict, List[dict]]
-    ) -> Union[object, dict]:
+    def object_or_data(self, cls, data: Union[dict, List[dict]]) -> Union[object, dict]:
         """Create list of objects or return data as is"""
         if not self.return_objects:
             return data
@@ -325,10 +332,10 @@ class SetraClient(object):
         """
 
         params = {
-            'created__gte': min_created_date,
-            'created__lte': max_created_date,
-            'batch_progress': batch_progress,
-            'interface': interface,
+            "created__gte": min_created_date,
+            "created__lte": max_created_date,
+            "batch_progress": batch_progress,
+            "interface": interface,
         }
 
         url = self.urls.batch()
@@ -380,23 +387,27 @@ class SetraClient(object):
         POST combination of batch, vouchers and transactions
         """
         url = self.urls.post_new_batch()
-        headers = {'Content-Type': 'application/json'}
-        response = self.post(url,
-                             data=batchdata.json(exclude_unset=True),
-                             headers=headers,
-                             return_response=True)
+        headers = {"Content-Type": "application/json"}
+        response = self.post(
+            url,
+            data=batchdata.json(exclude_unset=True),
+            headers=headers,
+            return_response=True,
+        )
         try:
             content = response.json()
         except ValueError:
             content = response.content
 
         if response.status_code == 202:
-            return ResponseStatusEnum.ACCEPTED, {'code': 202, 'content': None}
+            return ResponseStatusEnum.ACCEPTED, {"code": 202, "content": None}
         elif response.status_code == 409:
-            return ResponseStatusEnum.CONFLICT, {'code': 409, 'content': content}
+            return ResponseStatusEnum.CONFLICT, {"code": 409, "content": content}
         else:
             return ResponseStatusEnum.UNKNOWN, {
-                'code': response.status_code, 'content': content}
+                "code": response.status_code,
+                "content": content,
+            }
 
     def put_update_batch(self, batchdata: InputBatch):
         """
@@ -406,11 +417,13 @@ class SetraClient(object):
         was found, but did not meet the status criteria mentioned above.
         """
         url = self.urls.put_update_batch()
-        headers = {'Content-Type': 'application/json'}
-        response = self.put(url,
-                            data=batchdata.json(exclude_unset=True),
-                            headers=headers,
-                            return_response=True)
+        headers = {"Content-Type": "application/json"}
+        response = self.put(
+            url,
+            data=batchdata.json(exclude_unset=True),
+            headers=headers,
+            return_response=True,
+        )
 
         try:
             content = response.json()
@@ -418,12 +431,14 @@ class SetraClient(object):
             content = response.content
 
         if response.status_code == 204:
-            return ResponseStatusEnum.ACCEPTED, {'code': 204, 'content': None}
+            return ResponseStatusEnum.ACCEPTED, {"code": 204, "content": None}
         elif response.status_code == 409:
-            return ResponseStatusEnum.CONFLICT, {'code': 409, 'content': content}
+            return ResponseStatusEnum.CONFLICT, {"code": 409, "content": content}
         else:
             return ResponseStatusEnum.UNKNOWN, {
-                'code': response.status_code, 'content': content}
+                "code": response.status_code,
+                "content": content,
+            }
 
     def get_batch_complete(self, batch_id: str):
         """
@@ -532,24 +547,25 @@ class SetraClient(object):
         Returns tuple, with (data, status)
         """
         url = self.urls.add_abw_order()
-        headers = {'Content-Type': 'application/json'}
-        response = self.post(url,
-                             data=abworder.json(),
-                             headers=headers,
-                             return_response=True)
-        
+        headers = {"Content-Type": "application/json"}
+        response = self.post(
+            url, data=abworder.json(), headers=headers, return_response=True
+        )
+
         try:
             content = response.json()
         except ValueError:
             content = response.content
 
         if response.status_code == 202:
-            return ResponseStatusEnum.ACCEPTED, {'code': 202, 'content': content}
+            return ResponseStatusEnum.ACCEPTED, {"code": 202, "content": content}
         elif response.status_code == 409:
-            return ResponseStatusEnum.CONFLICT, {'code': 409, 'content': content}
+            return ResponseStatusEnum.CONFLICT, {"code": 409, "content": content}
         else:
             return ResponseStatusEnum.UNKNOWN, {
-                'code': response.status_code, 'content': content}
+                "code": response.status_code,
+                "content": content,
+            }
 
     def get_abw_order_errors(self, abw_order_id: str):
         """
diff --git a/setra_client/models.py b/setra_client/models.py
index c68b27c..0fa2645 100644
--- a/setra_client/models.py
+++ b/setra_client/models.py
@@ -8,8 +8,8 @@ import pydantic
 
 def to_lower_camel(s: str) -> str:
     """Alias generator to avoid breaking PEP8"""
-    first, *others = s.split('_')
-    return ''.join([first.lower(), *map(str.capitalize, others)])
+    first, *others = s.split("_")
+    return "".join([first.lower(), *map(str.capitalize, others)])
 
 
 class BaseModel(pydantic.BaseModel):
@@ -28,26 +28,26 @@ class BaseModel(pydantic.BaseModel):
 
 
 class BatchProgressEnum(str, Enum):
-    CREATED = 'created'
-    VALIDATION_COMPLETED = 'validation_completed'
-    VALIDATION_FAILED = 'validation_failed'
-    SENT_TO_UBW = 'sent_to_ubw'
-    SEND_TO_UBW_FAILED = 'send_to_ubw_failed'
-    POLLING_COMPLETED = 'polling_completed'
-    POLLING_FAILED = 'polling_failed'
-    UBW_IMPORT_OK = 'ubw_import_ok'
-    UBW_IMPORT_FAILED = 'ubw_import_failed'
-    FETCH_FINAL_VOUCHERNO_COMPLETED = 'fetch_final_voucherno_completed'
-    FETCH_FINAL_VOUCHERNO_FAILED = 'fetch_final_voucherno_failed'
+    CREATED = "created"
+    VALIDATION_COMPLETED = "validation_completed"
+    VALIDATION_FAILED = "validation_failed"
+    SENT_TO_UBW = "sent_to_ubw"
+    SEND_TO_UBW_FAILED = "send_to_ubw_failed"
+    POLLING_COMPLETED = "polling_completed"
+    POLLING_FAILED = "polling_failed"
+    UBW_IMPORT_OK = "ubw_import_ok"
+    UBW_IMPORT_FAILED = "ubw_import_failed"
+    FETCH_FINAL_VOUCHERNO_COMPLETED = "fetch_final_voucherno_completed"
+    FETCH_FINAL_VOUCHERNO_FAILED = "fetch_final_voucherno_failed"
 
     def __str__(self):
         return str(self.value)
 
 
 class ResponseStatusEnum(str, Enum):
-    ACCEPTED = 'Accepted'
-    CONFLICT = 'Conflict'
-    UNKNOWN = 'Unknown'
+    ACCEPTED = "Accepted"
+    CONFLICT = "Conflict"
+    UNKNOWN = "Unknown"
 
     def __str__(self):
         return str(self.value)
@@ -98,6 +98,7 @@ class OutputBatch(BaseModel):
     """
     Model representing a batch, with a list of voucher ids connected to that batch.
     """
+
     id: int
     created: str
     batchid: str
@@ -149,6 +150,7 @@ class ErrorBatch(BaseModel):
 
 class BatchErrors(BaseModel):
     """Model for the /batch_error/<id> endpoint"""
+
     batch_errors: List[ErrorBatch]
     voucher_errors: List[ErrorVoucher]
     transaction_errors: List[ErrorTransaction]
@@ -192,6 +194,7 @@ class CompleteVoucher(BaseModel):
 
 class CompleteBatch(BaseModel):
     """Model for the /batch_complete/<id> endpoint"""
+
     created: datetime.datetime
     batchid: str
     period: Optional[int]
@@ -273,6 +276,7 @@ class AbwOrder(BaseModel):
     """
     Model representing an AbwOrder, with a list of orders (and each order has a list of details.)
     """
+
     id: Optional[int]  # Cannot be sent in
     progress: Optional[str]  # Cannot be sent in
     abworder_validated_ok_date: Optional[datetime.datetime]  # Cannot be sent in
@@ -280,7 +284,7 @@ class AbwOrder(BaseModel):
     interface: str
     client: str
     abworderid: str
-    ordrenr_ubw: Optional[str] # Not sent in, but returned by the API.
+    ordrenr_ubw: Optional[str]  # Not sent in, but returned by the API.
     orders: List[Order]
 
 
@@ -312,6 +316,7 @@ class ErrorAbwOrder(BaseModel):
 
 class AbwOrderErrors(BaseModel):
     """Model for the /abw_order_errors/<id> endpoint"""
+
     abw_order_errors: List[ErrorAbwOrder]
     order_errors: List[ErrorOrder]
     detail_errors: List[ErrorDetail]
diff --git a/setra_client/version.py b/setra_client/version.py
index b81165e..6071371 100644
--- a/setra_client/version.py
+++ b/setra_client/version.py
@@ -4,15 +4,16 @@ import os
 import pkg_resources
 
 
-DISTRIBUTION_NAME = 'setra-client'
+DISTRIBUTION_NAME = "setra-client"
 
 
 def get_distribution():
-    """ Get the distribution object for this single module dist. """
+    """Get the distribution object for this single module dist."""
     try:
         return pkg_resources.get_distribution(DISTRIBUTION_NAME)
     except pkg_resources.DistributionNotFound:
         return pkg_resources.Distribution(
             project_name=DISTRIBUTION_NAME,
-            version='0.0.0',
-            location=os.path.dirname(__file__))
+            version="0.0.0",
+            location=os.path.dirname(__file__),
+        )
diff --git a/setup.py b/setup.py
index a72c1fa..665399c 100644
--- a/setup.py
+++ b/setup.py
@@ -6,36 +6,35 @@ import setuptools.command.test
 
 
 def get_requirements(filename):
-    """ Read requirements from file. """
-    with open(filename, mode='rt', encoding='utf-8') as f:
+    """Read requirements from file."""
+    with open(filename, mode="rt", encoding="utf-8") as f:
         for line in f:
             # TODO: Will not work with #egg-info
-            requirement = line.partition('#')[0].strip()
+            requirement = line.partition("#")[0].strip()
             if not requirement:
                 continue
             yield requirement
 
 
 def get_textfile(filename):
-    """ Get contents from a text file. """
-    with open(filename, mode='rt', encoding='utf-8') as f:
+    """Get contents from a text file."""
+    with open(filename, mode="rt", encoding="utf-8") as f:
         return f.read().lstrip()
 
 
 def get_packages():
-    """ List of (sub)packages to install. """
-    return setuptools.find_packages('.', include=('setra_client',
-                                                  'setra_client.*'))
+    """List of (sub)packages to install."""
+    return setuptools.find_packages(".", include=("setra_client", "setra_client.*"))
 
 
 class PyTest(setuptools.command.test.test):
-    """ Run tests using pytest.
+    """Run tests using pytest.
 
     From `http://doc.pytest.org/en/latest/goodpractices.html`.
 
     """
 
-    user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")]
+    user_options = [("pytest-args=", "a", "Arguments to pass to pytest")]
 
     def initialize_options(self):
         super().initialize_options()
@@ -44,6 +43,7 @@ class PyTest(setuptools.command.test.test):
     def run_tests(self):
         import shlex
         import pytest
+
         args = self.pytest_args
         if args:
             args = shlex.split(args)
@@ -52,44 +52,42 @@ class PyTest(setuptools.command.test.test):
 
 
 def run_setup():
-    setup_requirements = ['setuptools_scm']
-    test_requirements = list(get_requirements('requirements-test.txt'))
-    install_requirements = list(get_requirements('requirements.txt'))
+    setup_requirements = ["setuptools_scm"]
+    test_requirements = list(get_requirements("requirements-test.txt"))
+    install_requirements = list(get_requirements("requirements.txt"))
 
-    if {'build_sphinx', 'upload_docs'}.intersection(sys.argv):
-        setup_requirements.extend(get_requirements('docs/requirements.txt'))
+    if {"build_sphinx", "upload_docs"}.intersection(sys.argv):
+        setup_requirements.extend(get_requirements("docs/requirements.txt"))
         setup_requirements.extend(install_requirements)
 
     setuptools.setup(
-        name='setra-client',
-        description='Client for the SETRA API',
-        long_description=get_textfile('README.md'),
-        long_description_content_type='text/markdown',
-
-        url='https://git.app.uib.no/it-bott-integrasjoner/setra-client',
-        author='BOTT-INT',
-        author_email='bnt-int@usit.uio.no',
-
+        name="setra-client",
+        description="Client for the SETRA API",
+        long_description=get_textfile("README.md"),
+        long_description_content_type="text/markdown",
+        url="https://git.app.uib.no/it-bott-integrasjoner/setra-client",
+        author="BOTT-INT",
+        author_email="bnt-int@usit.uio.no",
         use_scm_version=True,
         packages=get_packages(),
         setup_requires=setup_requirements,
         install_requires=install_requirements,
         tests_require=test_requirements,
         cmdclass={
-            'test': PyTest,
+            "test": PyTest,
         },
         classifiers=[
-            'Development Status :: 3 - Alpha',
-            'Intended Audience :: Developers',
-            'Topic :: Software Development :: Libraries',
-            'Programming Language :: Python :: 3 :: Only',
-            'Programming Language :: Python :: 3.6',
-            'Programming Language :: Python :: 3.7',
-            'Programming Language :: Python :: 3.8',
+            "Development Status :: 3 - Alpha",
+            "Intended Audience :: Developers",
+            "Topic :: Software Development :: Libraries",
+            "Programming Language :: Python :: 3 :: Only",
+            "Programming Language :: Python :: 3.6",
+            "Programming Language :: Python :: 3.7",
+            "Programming Language :: Python :: 3.8",
         ],
-        keywords='SETRA API client',
+        keywords="SETRA API client",
     )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     run_setup()
diff --git a/tests/conftest.py b/tests/conftest.py
index 11bc095..34c1121 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,16 +9,16 @@ from setra_client.client import SetraClient, SetraEndpoints
 def load_json_file(name):
     """Load json file from the fixtures directory"""
     here = os.path.realpath(
-        os.path.join(os.getcwd(),
-                     os.path.dirname(__file__).rsplit('/', 1)[0]))
-    with open(os.path.join(here, 'tests/fixtures', name)) as f:
+        os.path.join(os.getcwd(), os.path.dirname(__file__).rsplit("/", 1)[0])
+    )
+    with open(os.path.join(here, "tests/fixtures", name)) as f:
         data = json.load(f)
     return data
 
 
 @pytest.fixture
 def baseurl():
-    return 'https://localhost'
+    return "https://localhost"
 
 
 @pytest.fixture
@@ -28,13 +28,14 @@ def endpoints(baseurl):
 
 @pytest.fixture
 def custom_endpoints(baseurl):
-    return SetraEndpoints(baseurl,
-                          '/custom/batch/',
-                          '/custom/transaction/',
-                          '/custom/voucher/',
-                          '/custom/addtrans/',
-                          '/custom/batch_complete/'
-                          )
+    return SetraEndpoints(
+        baseurl,
+        "/custom/batch/",
+        "/custom/transaction/",
+        "/custom/voucher/",
+        "/custom/addtrans/",
+        "/custom/batch_complete/",
+    )
 
 
 @pytest.fixture
@@ -54,42 +55,42 @@ def client_with_a_header(baseurl):
 
 @pytest.fixture
 def batch_url(baseurl):
-    return SetraEndpoints(baseurl).batch() # example: https://localhost/api/batch
+    return SetraEndpoints(baseurl).batch()  # example: https://localhost/api/batch
 
 
 @pytest.fixture
 def batch_fixture():
-    return load_json_file('batch_fixture.json')
+    return load_json_file("batch_fixture.json")
 
 
 @pytest.fixture
 def voucher_fixture():
-    return load_json_file('voucher_fixture.json')
+    return load_json_file("voucher_fixture.json")
 
 
 @pytest.fixture
 def trans_fixture():
-    return load_json_file('trans_fixture.json')
+    return load_json_file("trans_fixture.json")
 
 
 @pytest.fixture
 def trans_fail_fixture():
-    return load_json_file('trans_fail_fixture.json')
+    return load_json_file("trans_fail_fixture.json")
 
 
 @pytest.fixture
 def batch_with_voucher_fixture():
-    return load_json_file('batch_with_voucher_fixture.json')
+    return load_json_file("batch_with_voucher_fixture.json")
 
 
 @pytest.fixture
 def batch_without_voucher_field():
-    return load_json_file('batch_without_voucher_field.json')
+    return load_json_file("batch_without_voucher_field.json")
 
 
 @pytest.fixture
 def batch_fail_fixture():
-    return load_json_file('batch_fail_fixture.json')
+    return load_json_file("batch_fail_fixture.json")
 
 
 @pytest.fixture
diff --git a/tests/test_client.py b/tests/test_client.py
index f058348..d6132ac 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -85,7 +85,7 @@ def test_get_failing_batch_with_json_content(client, batch_url, requests_mock, b
     """A failing GET call with 404 should pass through the same response from the client, and return HTTP 404,
     and the request with json content"""
     requests_mock.get(batch_url, json={"a": "b"}, status_code=404)
-    
+
     with pytest.raises(IncorrectPathError):
         resp = client.call(method_name="GET", url=batch_url)
 
@@ -261,7 +261,7 @@ def test_successfully_getting_single_voucher(client, requests_mock, baseurl):
 def test_requesting_single_voucher_with_invalid_voucherid(
     client, requests_mock, baseurl
 ):
-    """Requesting a voucher, with None as voucherid, will request get all vouchers instead """
+    """Requesting a voucher, with None as voucherid, will request get all vouchers instead"""
     url = SetraEndpoints(baseurl).voucher()
     requests_mock.get(url, json={"foo": "bar"}, status_code=200)
 
@@ -283,11 +283,12 @@ def test_successfully_getting_single_voucher_with_alphanumeric_voucherid(
     response = client.get_voucher("abcd123efg")  # using alphanum string as voucherid
     assert response == {"foo": "bar"}
 
-#CLARIFY
+
+# CLARIFY
 def test_failing_to_get_all_vouchers(client, requests_mock, baseurl):
-    """A failing GET all vouchers call should still return json""" #REALLY?
+    """A failing GET all vouchers call should still return json"""  # REALLY?
     url = SetraEndpoints(baseurl).voucher()
-    requests_mock.get(url, json={'detail': 'Not found.'}, status_code=404)
+    requests_mock.get(url, json={"detail": "Not found."}, status_code=404)
 
     response = client.get_voucher()
     assert response == None
@@ -308,7 +309,7 @@ def test_successfully_getting_single_transaction(client, requests_mock, baseurl)
 def test_failing_to_get_all_transactions(client, requests_mock, baseurl):
     """A failing GET all vouchers call should still return json"""
     url = SetraEndpoints(baseurl).transaction()
-    requests_mock.get(url, json={'detail': 'Not found.'}, status_code=404)
+    requests_mock.get(url, json={"detail": "Not found."}, status_code=404)
 
     response = client.get_transaction()
     assert response == None
@@ -316,15 +317,23 @@ def test_failing_to_get_all_transactions(client, requests_mock, baseurl):
 
 # Test post_new_batch method
 
-def test_successfully_post_batch_with_voucher(client, batch_with_voucher_fixture, requests_mock, baseurl):
+
+def test_successfully_post_batch_with_voucher(
+    client, batch_with_voucher_fixture, requests_mock, baseurl
+):
     """A working GET call should return HTTP 202, with json content"""
     url = SetraEndpoints(baseurl).post_new_batch()
     batch = InputBatch.from_dict(batch_with_voucher_fixture)
-    requests_mock.post(url, json={}, status_code=202, request_headers={"Content-Type": "application/json"})
+    requests_mock.post(
+        url,
+        json={},
+        status_code=202,
+        request_headers={"Content-Type": "application/json"},
+    )
 
     state, data = client.post_new_batch(batch)  # we get a response object back
-    assert state == 'Accepted'
-    assert data == {'code': 202, 'content': None}
+    assert state == "Accepted"
+    assert data == {"code": 202, "content": None}
 
 
 def test_successfully_post_batch_with_voucher_and_response(
@@ -333,30 +342,49 @@ def test_successfully_post_batch_with_voucher_and_response(
     """A working POST new batch call with return_response=True,
     should return the response with HTTP 202, with json content"""
     url = SetraEndpoints(baseurl).post_new_batch()
-    requests_mock.post(url, json={}, status_code=202, request_headers={"Content-Type": "application/json"})  #expect json content
+    requests_mock.post(
+        url,
+        json={},
+        status_code=202,
+        request_headers={"Content-Type": "application/json"},
+    )  # expect json content
 
     batch = InputBatch.from_dict(batch_with_voucher_fixture)
     state, data = client.post_new_batch(batch)  # we get a response object back
-    assert state == 'Accepted'
-    assert data == {'code': 202, 'content': None}
+    assert state == "Accepted"
+    assert data == {"code": 202, "content": None}
 
 
-def test_conflicting_post_new_batch(client, batch_with_voucher_fixture, requests_mock, baseurl):
+def test_conflicting_post_new_batch(
+    client, batch_with_voucher_fixture, requests_mock, baseurl
+):
     url = SetraEndpoints(baseurl).post_new_batch()
-    requests_mock.post(url, json={'error': 'batch is being processed'}, status_code=409, request_headers={"Content-Type": "application/json"})  #expect json content
+    requests_mock.post(
+        url,
+        json={"error": "batch is being processed"},
+        status_code=409,
+        request_headers={"Content-Type": "application/json"},
+    )  # expect json content
     batch = InputBatch.from_dict(batch_with_voucher_fixture)
     state, data = client.post_new_batch(batch)
-    assert state == 'Conflict'
-    assert data == {'code': 409, 'content': {'error': 'batch is being processed'}}
+    assert state == "Conflict"
+    assert data == {"code": 409, "content": {"error": "batch is being processed"}}
 
 
-def test_unknown_post_new_batch_state(client, batch_with_voucher_fixture, requests_mock, baseurl):
+def test_unknown_post_new_batch_state(
+    client, batch_with_voucher_fixture, requests_mock, baseurl
+):
     url = SetraEndpoints(baseurl).post_new_batch()
-    requests_mock.post(url, json={'error': 'Batch is malformed, no id'}, status_code=500, request_headers={"Content-Type": "application/json"})  #expect json content
+    requests_mock.post(
+        url,
+        json={"error": "Batch is malformed, no id"},
+        status_code=500,
+        request_headers={"Content-Type": "application/json"},
+    )  # expect json content
     batch = InputBatch.from_dict(batch_with_voucher_fixture)
     state, data = client.post_new_batch(batch)
-    assert state == 'Unknown'
-    assert data == {'code': 500, 'content': {'error': 'Batch is malformed, no id'}}
+    assert state == "Unknown"
+    assert data == {"code": 500, "content": {"error": "Batch is malformed, no id"}}
 
 
 def test_successfully_getting_batch_complete(
@@ -519,10 +547,14 @@ def test_send_in_abworder(client, requests_mock, baseurl, complete_abw_order_fix
     response = client.post_add_abw_order(abworder)
     if isinstance(response, tuple):
         assert response[0] == "Accepted"
-        assert response[1] == {'code': 202, 'content': {
-            'responsible': 'responsible2',
-            'interface': 'testinterface',
-            'client': 'testclient'}}
+        assert response[1] == {
+            "code": 202,
+            "content": {
+                "responsible": "responsible2",
+                "interface": "testinterface",
+                "client": "testclient",
+            },
+        }
 
 
 def test_send_in_abworder_failure_conflict(
@@ -540,7 +572,7 @@ def test_send_in_abworder_failure_conflict(
 
     response = client.post_add_abw_order(abworder)
     assert isinstance(response, tuple)
-    assert response[1]['content'] == resp
+    assert response[1]["content"] == resp
     assert response[0] == "Conflict"
 
 
diff --git a/tox.ini b/tox.ini
index 657b6b7..7686b09 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,3 +12,24 @@ commands =
 [pytest]
 xfail_strict = true
 addopts = -rxs -v
+
+[testenv:black]
+basepython = python3
+deps =
+    black
+commands =
+    black --check --diff --target-version py310 .
+
+[testenv:yamllint]
+deps =
+    yamllint
+commands =
+    yamllint --config-file .yamllint.yaml .
+
+[testenv:json]
+deps =
+    demjson3
+allowlist_externals =
+    sh
+commands =
+    sh -c 'git ls-files -z "*.json" | xargs -0 jsonlint --strict'
-- 
GitLab