Repository: line/centraldogma-python (language: python)

Issue:
Support HTTP/2
`requests` only supports HTTP/1 and is almost archived not to add new features. We need to consider other packages to support both HTTP/1 and HTTP/2. [`httpx`](https://github.com/encode/httpx) is one of powerful candidates.


Discussion / hints:

Fix patch:
diff --git a/centraldogma/base_client.py b/centraldogma/base_client.py
index 9044bfc..f865c2c 100644
--- a/centraldogma/base_client.py
+++ b/centraldogma/base_client.py
@@ -13,57 +13,55 @@
 # under the License.
 from centraldogma.exceptions import AuthorizationException, NotFoundException
 from http import HTTPStatus
-from typing import Dict
-import requests
+from httpx import Client, Response
+from typing import Dict, Union
 
 
 class BaseClient:
     PATH_PREFIX = "/api/v1"
 
-    def __init__(self, base_url: str, token: str, **configs):
-        self.base_url = base_url[:-1] if base_url[-1] == "/" else base_url
+    def __init__(self, base_url: str, token: str, http2: bool = True, **configs):
+        base_url = base_url[:-1] if base_url[-1] == "/" else base_url
+        self.client = Client(
+            base_url=f"{base_url}{self.PATH_PREFIX}", http2=http2, **configs
+        )
         self.token = token
         self.headers = self._get_headers(token)
         self.patch_headers = self._get_patch_headers(token)
-        self.configs = configs
 
-    def request(self, method: str, path: str, **kwargs) -> requests.Response:
-        kwargs = self._get_kwargs(method, **kwargs)
-        return self._request(method, self._create_url(path), **kwargs)
+    def request(self, method: str, path: str, **kwargs) -> Union[Response]:
+        kwargs = self._set_request_headers(method, **kwargs)
+        return self._httpx_request(method, path, **kwargs)
 
-    def _create_url(self, path) -> str:
-        return self.base_url + self.PATH_PREFIX + path
-
-    def _get_kwargs(self, method: str, **kwargs) -> Dict:
+    def _set_request_headers(self, method: str, **kwargs) -> Dict:
         kwargs["headers"] = self.patch_headers if method == "patch" else self.headers
-        kwargs.update(self.configs)
         return kwargs
 
-    def _request(self, method: str, url: str, **kwargs) -> requests.Response:
-        return self._handle_response(getattr(requests, method)(url, **kwargs))
+    def _httpx_request(self, method: str, url: str, **kwargs) -> Response:
+        return self._handle_response(self.client.request(method, url, **kwargs))
 
     @staticmethod
     def _get_headers(token: str) -> Dict:
         return {
-            "Authorization": "bearer " + token,
+            "Authorization": f"bearer {token}",
             "Content-Type": "application/json",
         }
 
     @staticmethod
     def _get_patch_headers(token: str) -> Dict:
         return {
-            "Authorization": "bearer " + token,
+            "Authorization": f"bearer {token}",
             "Content-Type": "application/json-patch+json",
         }
 
     @staticmethod
-    def _handle_response(response: requests.Response):
-        if response.status_code >= HTTPStatus.BAD_REQUEST:
+    def _handle_response(response: Response):
+        if response.is_error:
             BaseClient._handle_exception(response)
         return response
 
     @staticmethod
-    def _handle_exception(response: requests.Response):
+    def _handle_exception(response: Response):
         if response.status_code == HTTPStatus.UNAUTHORIZED:
             raise AuthorizationException(response)
         if response.status_code == HTTPStatus.NOT_FOUND:
diff --git a/centraldogma/content_service.py b/centraldogma/content_service.py
index 3f63c3c..b6849e6 100644
--- a/centraldogma/content_service.py
+++ b/centraldogma/content_service.py
@@ -52,8 +52,10 @@ def get_file(
         json_path: Optional[str],
     ) -> Content:
         params = {}
-        params["revision"] = revision if revision else None
-        params["jsonpath"] = json_path if json_path else None
+        if revision:
+            params["revision"] = revision
+        if json_path:
+            params["jsonpath"] = json_path
         if not file_path.startswith("/"):
             file_path = "/" + file_path
         path = f"/projects/{project_name}/repos/{repo_name}/contents{file_path}"
diff --git a/centraldogma/dogma.py b/centraldogma/dogma.py
index faff8d6..b9e749c 100644
--- a/centraldogma/dogma.py
+++ b/centraldogma/dogma.py
@@ -25,8 +25,8 @@ def __init__(self, base_url: str, token: str, **configs):
 
         : param base_url: a base URL indicating Central Dogma server such as domain.
         : param token: a token for authorization.
-        : param configs: (optional) configurations for requests. For example, timeout can be
-            applied by using it.
+        : param configs: (optional) configurations for an HTTP client.
+            For example, cert and timeout can be applied by using it.
         """
         self.base_client = BaseClient(base_url, token, **configs)
         self.project_service = ProjectService(self.base_client)
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 037b1b2..c7404e4 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,4 +1,4 @@
 black
 pytest
 setuptools
-requests_mock
\ No newline at end of file
+respx
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 2c7702f..df50a0b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
 dataclasses-json
-marshmallow
-requests
\ No newline at end of file
+httpx[http2]
+marshmallow
\ No newline at end of file


Test patch:
diff --git a/tests/test_base_client.py b/tests/test_base_client.py
index 8565577..e089aac 100644
--- a/tests/test_base_client.py
+++ b/tests/test_base_client.py
@@ -13,28 +13,35 @@
 # under the License.
 from centraldogma.exceptions import AuthorizationException, NotFoundException
 from centraldogma.base_client import BaseClient
+from httpx import Limits, Response
 import pytest
-import requests_mock
 
 
 client = BaseClient("http://baseurl", "token")
 
 configs = {
-    "timeout": 5,
-    "cookies": None,
     "auth": None,
-    "allow_redirects": False,
-    "proxies": None,
-    "hooks": None,
-    "stream": None,
+    "cookies": None,
+    "verify": None,
     "cert": None,
+    "proxies": None,
+    "mounts": None,
+    "timeout": 5,
+    "limits": Limits(max_connections=100, max_keepalive_connections=20),
+    "max_redirects": 1,
+    "event_hooks": None,
+    "transport": None,
+    "app": None,
+    "trust_env": True,
 }
 client_with_configs = BaseClient("http://baseurl", "token", **configs)
 
 
-def test_get_kwargs():
+def test_set_request_headers():
     for method in ["get", "post", "delete", "patch"]:
-        kwargs = client_with_configs._get_kwargs(method, a="b")
+        kwargs = client_with_configs._set_request_headers(
+            method, params={"a": "b"}, allow_redirects=True
+        )
         content_type = (
             "application/json-patch+json" if method == "patch" else "application/json"
         )
@@ -42,115 +49,107 @@ def test_get_kwargs():
             "Authorization": "bearer token",
             "Content-Type": content_type,
         }
-        assert kwargs["timeout"] == 5
-        assert kwargs["cookies"] == None
-        assert kwargs["auth"] == None
-        assert kwargs["allow_redirects"] == False
-        assert kwargs["proxies"] == None
-        assert kwargs["hooks"] == None
-        assert kwargs["stream"] == None
-        assert kwargs["cert"] == None
-
-
-def test_request_with_configs():
-    with requests_mock.mock() as m:
-        m.get("http://baseurl/api/v1/path", text="success")
-        for method in ["get", "post", "delete", "patch"]:
-            getattr(m, method)("http://baseurl/api/v1/path", text="success")
-            client.request(
-                method,
-                "/path",
-                timeout=5,
-                cookies=None,
-                auth=None,
-                allow_redirects=False,
-                proxies=None,
-                hooks=None,
-                stream=None,
-                cert=None,
-            )
-            client.request(method, "/path", timeout=(3.05, 27))
-            client_with_configs.request(method, "/path")
-
-
-def test_delete():
-    with requests_mock.mock() as m:
-        matcher = m.delete("http://baseurl/api/v1/path", text="success")
-        client.request("delete", "/path", params={"a": "b"})
-
-        request = matcher.last_request
-        assert request.headers["Authorization"] == "bearer token"
-        assert request.headers["Content-Type"] == "application/json"
-        assert request.url.endswith("?a=b")
-
-
-def test_delete_exception_authorization():
+        assert kwargs["params"] == {"a": "b"}
+        assert kwargs["allow_redirects"]
+        assert "limits" not in kwargs
+        assert "event_hooks" not in kwargs
+        assert "transport" not in kwargs
+        assert "app" not in kwargs
+        assert "trust_env" not in kwargs
+
+
+def test_request_with_configs(respx_mock):
+    methods = ["get", "post", "put", "delete", "patch", "options"]
+    for method in methods:
+        getattr(respx_mock, method)("http://baseurl/api/v1/path").mock(
+            return_value=Response(200, text="success")
+        )
+        client.request(
+            method,
+            "/path",
+            timeout=5,
+            cookies=None,
+            auth=None,
+            allow_redirects=False,
+        )
+        client.request(method, "/path", timeout=(3.05, 27))
+        client_with_configs.request(method, "/path")
+    assert respx_mock.calls.call_count == len(methods) * 3
+
+
+def test_delete(respx_mock):
+    route = respx_mock.delete("http://baseurl/api/v1/path").mock(
+        return_value=Response(200, text="success")
+    )
+    resp = client.request("delete", "/path", params={"a": "b"})
+
+    assert route.called
+    assert resp.request.headers["Authorization"] == "bearer token"
+    assert resp.request.headers["Content-Type"] == "application/json"
+    assert resp.request.url.params.multi_items() == [("a", "b")]
+
+
+def test_delete_exception_authorization(respx_mock):
     with pytest.raises(AuthorizationException):
-        with requests_mock.mock() as m:
-            m.delete("http://baseurl/api/v1/path", status_code=401)
-            client.request("delete", "/path")
+        respx_mock.delete("http://baseurl/api/v1/path").mock(return_value=Response(401))
+        client.request("delete", "/path")
 
 
-def test_get():
-    with requests_mock.mock() as m:
-        matcher = m.get("http://baseurl/api/v1/path", text="success")
-        client.request("get", "/path", params={"a": "b"})
+def test_get(respx_mock):
+    route = respx_mock.get("http://baseurl/api/v1/path").mock(
+        return_value=Response(200, text="success")
+    )
+    resp = client.request("get", "/path", params={"a": "b"})
 
-        request = matcher.last_request
-        assert request.headers["Authorization"] == "bearer token"
-        assert request.headers["Content-Type"] == "application/json"
-        assert request.url.endswith("?a=b")
+    assert route.called
+    assert resp.request.headers["Authorization"] == "bearer token"
+    assert resp.request.headers["Content-Type"] == "application/json"
+    assert resp.request.url.params.multi_items() == [("a", "b")]
 
 
-def test_get_exception_authorization():
+def test_get_exception_authorization(respx_mock):
     with pytest.raises(AuthorizationException):
-        with requests_mock.mock() as m:
-            m.get("http://baseurl/api/v1/path", status_code=401, json={"message": ""})
-            client.request("get", "/path")
+        respx_mock.get("http://baseurl/api/v1/path").mock(return_value=Response(401))
+        client.request("get", "/path")
 
 
-def test_get_exception_not_found():
+def test_get_exception_not_found(respx_mock):
     with pytest.raises(NotFoundException):
-        with requests_mock.mock() as m:
-            m.get("http://baseurl/api/v1/path", status_code=404, json={"message": ""})
-            client.request("get", "/path")
+        respx_mock.get("http://baseurl/api/v1/path").mock(return_value=Response(404))
+        client.request("get", "/path")
 
 
-def test_patch():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/path"
-        matcher = m.patch(url, text="success")
-        client.request("patch", "/path", json={"a": "b"})
+def test_patch(respx_mock):
+    route = respx_mock.patch("http://baseurl/api/v1/path").mock(
+        return_value=Response(200, text="success")
+    )
+    resp = client.request("patch", "/path", json={"a": "b"})
 
-        request = matcher.last_request
-        assert request.headers["Authorization"] == "bearer token"
-        assert request.headers["Content-Type"] == "application/json-patch+json"
-        assert request.url == url
-        assert request.body == b'{"a": "b"}'
+    assert route.called
+    assert resp.request.headers["Authorization"] == "bearer token"
+    assert resp.request.headers["Content-Type"] == "application/json-patch+json"
+    assert resp.request._content == b'{"a": "b"}'
 
 
-def test_patch_exception_authorization():
+def test_patch_exception_authorization(respx_mock):
     with pytest.raises(AuthorizationException):
-        with requests_mock.mock() as m:
-            m.patch("http://baseurl/api/v1/path", status_code=401)
-            client.request("patch", "/path", json={"a": "b"})
+        respx_mock.patch("http://baseurl/api/v1/path").mock(return_value=Response(401))
+        client.request("patch", "/path", json={"a": "b"})
 
 
-def test_post():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/path"
-        matcher = m.post(url, text="success")
-        client.request("post", "/path", json={"a": "b"})
+def test_post(respx_mock):
+    route = respx_mock.post("http://baseurl/api/v1/path").mock(
+        return_value=Response(200, text="success")
+    )
+    resp = client.request("post", "/path", json={"a": "b"})
 
-        request = matcher.last_request
-        assert request.headers["Authorization"] == "bearer token"
-        assert request.headers["Content-Type"] == "application/json"
-        assert request.url == url
-        assert request.body == b'{"a": "b"}'
+    assert route.called
+    assert resp.request.headers["Authorization"] == "bearer token"
+    assert resp.request.headers["Content-Type"] == "application/json"
+    assert resp.request._content == b'{"a": "b"}'
 
 
-def test_post_exception_authorization():
+def test_post_exception_authorization(respx_mock):
     with pytest.raises(AuthorizationException):
-        with requests_mock.mock() as m:
-            m.post("http://baseurl/api/v1/path", status_code=401)
-            client.request("post", "/path")
+        respx_mock.post("http://baseurl/api/v1/path").mock(return_value=Response(401))
+        client.request("post", "/path")
diff --git a/tests/test_dogma.py b/tests/test_dogma.py
index 1bee477..94a734b 100644
--- a/tests/test_dogma.py
+++ b/tests/test_dogma.py
@@ -15,8 +15,8 @@
 from centraldogma.dogma import Dogma
 from datetime import datetime
 from http import HTTPStatus
+from httpx import Response
 import json
-import requests_mock
 
 
 client = Dogma("http://baseurl", "token")
@@ -50,373 +50,400 @@
 }
 
 
-def test_list_projects():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects"
-        matcher = m.get(url, json=[mock_project, mock_project])
-        projects = client.list_projects()
-
-        assert matcher.last_request.url == url
-        assert len(projects) == 2
-        for project in projects:
-            assert project.name == mock_project["name"]
-            assert project.creator == Creator.from_json(
-                json.dumps(mock_project["creator"])
-            )
-            assert project.url == mock_project["url"]
-            assert project.created_at == datetime.strptime(
-                mock_project["createdAt"], DATE_FORMAT_ISO8601
-            )
-
-
-def test_list_projects_removed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects"
-        matcher = m.get(url, status_code=HTTPStatus.NO_CONTENT)
-        projects = client.list_projects(removed=True)
-
-        assert matcher.last_request.url == (url + "?status=removed")
-        assert projects == []
-
-
-def test_create_project():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects"
-        matcher = m.post(url, json=mock_project, status_code=HTTPStatus.CREATED)
-        project = client.create_project("newProject")
-
-        assert matcher.last_request.url == url
-        assert matcher.last_request.body == b'{"name": "newProject"}'
-        assert project == Project.from_json(json.dumps(mock_project))
-
-
-def test_create_project_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects"
-        matcher = m.post(url, status_code=HTTPStatus.BAD_REQUEST)
-        project = client.create_project("newProject")
-
-        assert matcher.last_request.url == url
-        assert matcher.last_request.body == b'{"name": "newProject"}'
-        assert project == None
-
-
-def test_remove_project():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/project1"
-        matcher = m.delete(url, status_code=HTTPStatus.NO_CONTENT)
-        removed = client.remove_project("project1")
-
-        assert matcher.last_request.url == url
-        assert removed == True
-
-
-def test_remove_project_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/project1"
-        matcher = m.delete(url, status_code=HTTPStatus.BAD_REQUEST)
-        removed = client.remove_project("project1")
-
-        assert matcher.last_request.url == url
-        assert removed == False
-
-
-def test_unremove_project():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/project1"
-        matcher = m.patch(url, json=mock_project, status_code=HTTPStatus.OK)
-        project = client.unremove_project("project1")
-
-        request = matcher.last_request
-        assert request.url == url
-        assert (
-            request.body == b'[{"op": "replace", "path": "/status", "value": "active"}]'
-        )
-        assert project == Project.from_json(json.dumps(mock_project))
-
-
-def test_unremove_project_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/project1"
-        matcher = m.patch(url, status_code=HTTPStatus.SERVICE_UNAVAILABLE)
-        project = client.unremove_project("project1")
-
-        request = matcher.last_request
-        assert request.url == url
-        assert (
-            request.body == b'[{"op": "replace", "path": "/status", "value": "active"}]'
+def test_list_projects(respx_mock):
+    url = "http://baseurl/api/v1/projects"
+    route = respx_mock.get(url).mock(
+        return_value=Response(200, json=[mock_project, mock_project])
+    )
+    projects = client.list_projects()
+
+    assert route.called
+    assert len(projects) == 2
+    for project in projects:
+        assert project.name == mock_project["name"]
+        assert project.creator == Creator.from_json(json.dumps(mock_project["creator"]))
+        assert project.url == mock_project["url"]
+        assert project.created_at == datetime.strptime(
+            mock_project["createdAt"], DATE_FORMAT_ISO8601
         )
-        assert project == None
-
 
-def test_purge_project():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/project1/removed"
-        matcher = m.delete(url, status_code=HTTPStatus.NO_CONTENT)
-        purged = client.purge_project("project1")
 
-        assert matcher.last_request.url == url
-        assert purged == True
+def test_list_projects_removed(respx_mock):
+    url = "http://baseurl/api/v1/projects"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    projects = client.list_projects(removed=True)
 
+    assert route.called
+    assert respx_mock.calls.last.request.url == (url + "?status=removed")
+    assert projects == []
 
-def test_purge_project_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/project1/removed"
-        matcher = m.delete(url, status_code=HTTPStatus.FORBIDDEN)
-        purged = client.purge_project("project1")
 
-        assert matcher.last_request.url == url
-        assert purged == False
+def test_create_project(respx_mock):
+    url = "http://baseurl/api/v1/projects"
+    route = respx_mock.post(url).mock(
+        return_value=Response(HTTPStatus.CREATED, json=mock_project)
+    )
+    project = client.create_project("newProject")
 
-
-def test_list_repositories():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos"
-        matcher = m.get(url, json=[mock_repository, mock_repository])
-        repos = client.list_repositories("myproject")
-
-        assert matcher.last_request.url == url
-        assert len(repos) == 2
-        for repo in repos:
-            assert repo.name == mock_repository["name"]
-            assert repo.creator == Creator.from_json(
-                json.dumps(mock_repository["creator"])
-            )
-            assert repo.head_revision == mock_repository["headRevision"]
-            assert repo.url == mock_repository["url"]
-            assert repo.created_at == datetime.strptime(
-                mock_repository["createdAt"], DATE_FORMAT_ISO8601
-            )
-
-
-def test_list_repositories_removed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos"
-        matcher = m.get(url, status_code=HTTPStatus.NO_CONTENT)
-        repos = client.list_repositories("myproject", removed=True)
-
-        assert matcher.last_request.url == (url + "?status=removed")
-        assert repos == []
-
-
-def test_create_repository():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos"
-        matcher = m.post(url, json=mock_repository, status_code=HTTPStatus.CREATED)
-        repo = client.create_repository("myproject", "newRepo")
-
-        assert matcher.last_request.url == url
-        assert matcher.last_request.body == b'{"name": "newRepo"}'
-        assert repo == Repository.from_json(json.dumps(mock_repository))
-
-
-def test_create_repository_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos"
-        matcher = m.post(url, status_code=HTTPStatus.BAD_REQUEST)
-        repo = client.create_repository("myproject", "newRepo")
-
-        assert matcher.last_request.url == url
-        assert matcher.last_request.body == b'{"name": "newRepo"}'
-        assert repo == None
-
-
-def test_remove_repository():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
-        matcher = m.delete(url, status_code=HTTPStatus.NO_CONTENT)
-        removed = client.remove_repository("myproject", "myrepo")
-
-        assert matcher.last_request.url == url
-        assert removed == True
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert request._content == b'{"name": "newProject"}'
+    assert project == Project.from_json(json.dumps(mock_project))
+
+
+def test_create_project_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects"
+    route = respx_mock.post(url).mock(return_value=Response(HTTPStatus.BAD_REQUEST))
+    project = client.create_project("newProject")
+
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert request._content == b'{"name": "newProject"}'
+    assert project == None
+
+
+def test_remove_project(respx_mock):
+    url = "http://baseurl/api/v1/projects/project1"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    removed = client.remove_project("project1")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert removed == True
+
+
+def test_remove_project_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/project1"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.BAD_REQUEST))
+    removed = client.remove_project("project1")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert removed == False
+
+
+def test_unremove_project(respx_mock):
+    url = "http://baseurl/api/v1/projects/project1"
+    route = respx_mock.patch(url).mock(
+        return_value=Response(HTTPStatus.OK, json=mock_project)
+    )
+    project = client.unremove_project("project1")
+
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert (
+        request._content == b'[{"op": "replace", "path": "/status", "value": "active"}]'
+    )
+    assert project == Project.from_json(json.dumps(mock_project))
+
+
+def test_unremove_project_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/project1"
+    route = respx_mock.patch(url).mock(
+        return_value=Response(HTTPStatus.SERVICE_UNAVAILABLE)
+    )
+    project = client.unremove_project("project1")
+
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert (
+        request._content == b'[{"op": "replace", "path": "/status", "value": "active"}]'
+    )
+    assert project == None
+
+
+def test_purge_project(respx_mock):
+    url = "http://baseurl/api/v1/projects/project1/removed"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    purged = client.purge_project("project1")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert purged == True
+
+
+def test_purge_project_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/project1/removed"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.FORBIDDEN))
+    purged = client.purge_project("project1")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert purged == False
+
+
+def test_list_repositories(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos"
+    route = respx_mock.get(url).mock(
+        return_value=Response(HTTPStatus.OK, json=[mock_repository, mock_repository])
+    )
+    repos = client.list_repositories("myproject")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert len(repos) == 2
+    for repo in repos:
+        assert repo.name == mock_repository["name"]
+        assert repo.creator == Creator.from_json(json.dumps(mock_repository["creator"]))
+        assert repo.head_revision == mock_repository["headRevision"]
+        assert repo.url == mock_repository["url"]
+        assert repo.created_at == datetime.strptime(
+            mock_repository["createdAt"], DATE_FORMAT_ISO8601
+        )
 
 
-def test_remove_repository_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
-        matcher = m.delete(url, status_code=HTTPStatus.FORBIDDEN)
-        removed = client.remove_repository("myproject", "myrepo")
+def test_list_repositories_removed(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    repos = client.list_repositories("myproject", removed=True)
 
-        assert matcher.last_request.url == url
-        assert removed == False
+    assert route.called
+    assert respx_mock.calls.last.request.url == (url + "?status=removed")
+    assert repos == []
 
 
-def test_unremove_repository():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
-        matcher = m.patch(url, json=mock_repository)
-        repo = client.unremove_repository("myproject", "myrepo")
+def test_create_repository(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos"
+    route = respx_mock.post(url).mock(
+        return_value=Response(HTTPStatus.CREATED, json=mock_repository)
+    )
+    repo = client.create_repository("myproject", "newRepo")
 
-        request = matcher.last_request
-        assert request.url == url
-        assert (
-            request.body == b'[{"op": "replace", "path": "/status", "value": "active"}]'
-        )
-        assert repo == Repository.from_json(json.dumps(mock_repository))
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert request._content == b'{"name": "newRepo"}'
+    assert repo == Repository.from_json(json.dumps(mock_repository))
 
 
-def test_unremove_repository_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
-        matcher = m.patch(url, status_code=HTTPStatus.BAD_REQUEST)
-        repo = client.unremove_repository("myproject", "myrepo")
+def test_create_repository_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos"
+    route = respx_mock.post(url).mock(return_value=Response(HTTPStatus.BAD_REQUEST))
+    repo = client.create_repository("myproject", "newRepo")
 
-        request = matcher.last_request
-        assert request.url == url
-        assert (
-            request.body == b'[{"op": "replace", "path": "/status", "value": "active"}]'
-        )
-        assert repo == None
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert request._content == b'{"name": "newRepo"}'
+    assert repo == None
 
 
-def test_purge_repository():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/removed"
-        matcher = m.delete(url, status_code=HTTPStatus.NO_CONTENT)
-        perged = client.purge_repository("myproject", "myrepo")
+def test_remove_repository(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    removed = client.remove_repository("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert perged == True
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert removed == True
 
 
-def test_purge_repository_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/removed"
-        matcher = m.delete(url, status_code=HTTPStatus.FORBIDDEN)
-        perged = client.purge_repository("myproject", "myrepo")
+def test_remove_repository_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.FORBIDDEN))
+    removed = client.remove_repository("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert perged == False
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert removed == False
 
 
-def test_normalize_repository_revision():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/revision/3"
-        matcher = m.get(url, json={"revision": 3})
-        revision = client.normalize_repository_revision("myproject", "myrepo", 3)
+def test_unremove_repository(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
+    route = respx_mock.patch(url).mock(
+        return_value=Response(HTTPStatus.OK, json=mock_repository)
+    )
+    repo = client.unremove_repository("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert revision == 3
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert (
+        request._content == b'[{"op": "replace", "path": "/status", "value": "active"}]'
+    )
+    assert repo == Repository.from_json(json.dumps(mock_repository))
 
 
-def test_normalize_repository_revision_failed():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/revision/3"
-        matcher = m.get(url, status_code=HTTPStatus.BAD_REQUEST)
-        revision = client.normalize_repository_revision("myproject", "myrepo", 3)
+def test_unremove_repository_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo"
+    route = respx_mock.patch(url).mock(return_value=Response(HTTPStatus.BAD_REQUEST))
+    repo = client.unremove_repository("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert revision == None
+    assert route.called
+    request = respx_mock.calls.last.request
+    assert request.url == url
+    assert (
+        request._content == b'[{"op": "replace", "path": "/status", "value": "active"}]'
+    )
+    assert repo == None
 
 
-def test_list_files():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/list"
-        matcher = m.get(url, json=[mock_content_text, mock_content_text])
-        files = client.list_files("myproject", "myrepo")
+def test_purge_repository(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/removed"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    perged = client.purge_repository("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert len(files) == 2
-        for file in files:
-            assert file.path == mock_content_text["path"]
-            assert file.type == mock_content_text["type"]
-            assert file.url == mock_content_text["url"]
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert perged == True
 
 
-def test_list_files_pattern():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/list/foo/*.json"
-        matcher = m.get(url, status_code=HTTPStatus.NO_CONTENT)
-        files = client.list_files("myproject", "myrepo", "/foo/*.json")
+def test_purge_repository_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/removed"
+    route = respx_mock.delete(url).mock(return_value=Response(HTTPStatus.FORBIDDEN))
+    perged = client.purge_repository("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert files == []
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert perged == False
 
 
-def test_list_files_revision():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/list/*.json"
-        matcher = m.get(url, status_code=HTTPStatus.NO_CONTENT)
-        files = client.list_files("myproject", "myrepo", "*.json", 3)
+def test_normalize_repository_revision(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/revision/3"
+    route = respx_mock.get(url).mock(
+        return_value=Response(HTTPStatus.OK, json={"revision": 3})
+    )
+    revision = client.normalize_repository_revision("myproject", "myrepo", 3)
 
-        assert matcher.last_request.url == (url + "?revision=3")
-        assert files == []
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert revision == 3
 
 
-def test_get_files():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents"
-        matcher = m.get(url, json=[mock_content_text, mock_content_text])
-        files = client.get_files("myproject", "myrepo")
+def test_normalize_repository_revision_failed(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/revision/3"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.BAD_REQUEST))
+    revision = client.normalize_repository_revision("myproject", "myrepo", 3)
 
-        assert matcher.last_request.url == url
-        assert len(files) == 2
-        for file in files:
-            assert file == Content.from_json(json.dumps(mock_content_text))
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert revision == None
 
 
-def test_get_files_pattern():
-    with requests_mock.mock() as m:
-        url = (
-            "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo/*.json"
+def test_list_files(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/list"
+    route = respx_mock.get(url).mock(
+        return_value=Response(
+            HTTPStatus.OK, json=[mock_content_text, mock_content_text]
         )
-        matcher = m.get(url, status_code=HTTPStatus.NO_CONTENT)
-        files = client.get_files("myproject", "myrepo", "/foo/*.json")
+    )
+    files = client.list_files("myproject", "myrepo")
 
-        assert matcher.last_request.url == url
-        assert files == []
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert len(files) == 2
+    for file in files:
+        assert file.path == mock_content_text["path"]
+        assert file.type == mock_content_text["type"]
+        assert file.url == mock_content_text["url"]
 
 
-def test_get_files_revision():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/*.json"
-        matcher = m.get(url, status_code=HTTPStatus.NO_CONTENT)
-        files = client.get_files("myproject", "myrepo", "*.json", 3)
+def test_list_files_pattern(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/list/foo/*.json"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    files = client.list_files("myproject", "myrepo", "/foo/*.json")
 
-        assert matcher.last_request.url == (url + "?revision=3")
-        assert files == []
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert files == []
 
 
-def test_get_file_text():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo.text"
-        matcher = m.get(url, json=mock_content_text)
-        file = client.get_file("myproject", "myrepo", "foo.text")
+def test_list_files_revision(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/list/*.json"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    files = client.list_files("myproject", "myrepo", "*.json", 3)
 
-        assert matcher.last_request.url == url
-        assert file.path == mock_content_text["path"]
-        assert file.type == mock_content_text["type"]
-        assert isinstance(file.content, str)
-        assert file.content == mock_content_text["content"]
-        assert file.revision == mock_content_text["revision"]
-        assert file.url == mock_content_text["url"]
+    assert route.called
+    assert respx_mock.calls.last.request.url == (url + "?revision=3")
+    assert files == []
 
 
-def test_get_file_json():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo.json"
-        matcher = m.get(url, json=mock_content_json)
-        file = client.get_file("myproject", "myrepo", "foo.json", 3)
-
-        assert matcher.last_request.url == (url + "?revision=3")
-        assert file.path == mock_content_json["path"]
-        assert file.type == mock_content_json["type"]
-        assert isinstance(file.content, dict) or isinstance(file.content, list)
-        assert file.content == mock_content_json["content"]
-        assert file.revision == mock_content_json["revision"]
-        assert file.url == mock_content_json["url"]
-
-
-def test_get_file_json_path():
-    with requests_mock.mock() as m:
-        url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo.json"
-        matcher = m.get(url, json=mock_content_json)
-        file = client.get_file("myproject", "myrepo", "foo.json", 3, "$.a")
-
-        assert matcher.last_request.url == (url + "?revision=3&jsonpath=%24.a")
-        assert file.path == mock_content_json["path"]
-        assert file.type == mock_content_json["type"]
-        assert isinstance(file.content, dict) or isinstance(file.content, list)
-        assert file.content == mock_content_json["content"]
-        assert file.revision == mock_content_json["revision"]
-        assert file.url == mock_content_json["url"]
+def test_get_files(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents"
+    route = respx_mock.get(url).mock(
+        return_value=Response(
+            HTTPStatus.OK, json=[mock_content_text, mock_content_text]
+        )
+    )
+    files = client.get_files("myproject", "myrepo")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert len(files) == 2
+    for file in files:
+        assert file == Content.from_json(json.dumps(mock_content_text))
+
+
+def test_get_files_pattern(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo/*.json"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    files = client.get_files("myproject", "myrepo", "/foo/*.json")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert files == []
+
+
+def test_get_files_revision(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/*.json"
+    route = respx_mock.get(url).mock(return_value=Response(HTTPStatus.NO_CONTENT))
+    files = client.get_files("myproject", "myrepo", "*.json", 3)
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == (url + "?revision=3")
+    assert files == []
+
+
+def test_get_file_text(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo.text"
+    route = respx_mock.get(url).mock(
+        return_value=Response(HTTPStatus.OK, json=mock_content_text)
+    )
+    file = client.get_file("myproject", "myrepo", "foo.text")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == url
+    assert file.path == mock_content_text["path"]
+    assert file.type == mock_content_text["type"]
+    assert isinstance(file.content, str)
+    assert file.content == mock_content_text["content"]
+    assert file.revision == mock_content_text["revision"]
+    assert file.url == mock_content_text["url"]
+
+
+def test_get_file_json(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo.json"
+    route = respx_mock.get(url).mock(
+        return_value=Response(HTTPStatus.OK, json=mock_content_json)
+    )
+    file = client.get_file("myproject", "myrepo", "foo.json", 3)
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == (url + "?revision=3")
+    assert file.path == mock_content_json["path"]
+    assert file.type == mock_content_json["type"]
+    assert isinstance(file.content, dict) or isinstance(file.content, list)
+    assert file.content == mock_content_json["content"]
+    assert file.revision == mock_content_json["revision"]
+    assert file.url == mock_content_json["url"]
+
+
+def test_get_file_json_path(respx_mock):
+    url = "http://baseurl/api/v1/projects/myproject/repos/myrepo/contents/foo.json"
+    route = respx_mock.get(url).mock(
+        return_value=Response(HTTPStatus.OK, json=mock_content_json)
+    )
+    file = client.get_file("myproject", "myrepo", "foo.json", 3, "$.a")
+
+    assert route.called
+    assert respx_mock.calls.last.request.url == (url + "?revision=3&jsonpath=%24.a")
+    assert file.path == mock_content_json["path"]
+    assert file.type == mock_content_json["type"]
+    assert isinstance(file.content, dict) or isinstance(file.content, list)
+    assert file.content == mock_content_json["content"]
+    assert file.revision == mock_content_json["revision"]
+    assert file.url == mock_content_json["url"]