Repository: sennder/python-client-generator (language: python)

Issue:
Error on OpenAPI v3.0.3 file
The file [openapi.json](https://github.com/sennder/python-client-generator/files/14649468/openapi.json) comes from https://editor.swagger.io/ as an example OpenAPI schema.

Running `python -m python_client_generator --open-api openapi.json --package-name foo_bar --project-name foo-bar --outdir clients` gives
```
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/__main__.py", line 4, in <module>
    main()
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/main.py", line 48, in main
    generate_models(dereferenced_swagger, package_path / "models.py")
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 199, in generate_models
    models = get_models(schemas)
             ^^^^^^^^^^^^^^^^^^^
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 146, in get_models
    p["refs"] = get_references(o)
                ^^^^^^^^^^^^^^^^^
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 69, in get_references
    refs += _get_schema_references(p_schema)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 51, in _get_schema_references
    return [schema["title"]]
            ~~~~~~^^^^^^^^^
KeyError: 'title'
```

Error on OpenAPI v3.0.3 file
The file [openapi.json](https://github.com/sennder/python-client-generator/files/14649468/openapi.json) comes from https://editor.swagger.io/ as an example OpenAPI schema.

Running `python -m python_client_generator --open-api openapi.json --package-name foo_bar --project-name foo-bar --outdir clients` gives
```
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/__main__.py", line 4, in <module>
    main()
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/main.py", line 48, in main
    generate_models(dereferenced_swagger, package_path / "models.py")
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 199, in generate_models
    models = get_models(schemas)
             ^^^^^^^^^^^^^^^^^^^
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 146, in get_models
    p["refs"] = get_references(o)
                ^^^^^^^^^^^^^^^^^
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 69, in get_references
    refs += _get_schema_references(p_schema)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/.venv/lib/python3.11/site-packages/python_client_generator/generate_models.py", line 51, in _get_schema_references
    return [schema["title"]]
            ~~~~~~^^^^^^^^^
KeyError: 'title'
```



Discussion / hints:
Hi @amacfie, thank you for reporting the issue, I have a PR ready for review, once its approved we will merge it to fix this issue, in the meanwhile you can have a look at the code comments to understand what is the root cause and update your OpenAPI JSON file accordingly until we release the fix.
Hi @amacfie, thank you for reporting the issue, I have a PR ready for review, once its approved we will merge it to fix this issue, in the meanwhile you can have a look at the code comments to understand what is the root cause and update your OpenAPI JSON file accordingly until we release the fix.

Fix patch:
diff --git a/.flake8 b/.flake8
index 459aa5d..0cb0cc6 100644
--- a/.flake8
+++ b/.flake8
@@ -4,4 +4,4 @@
 # W503: Line break occurred before a binary operator
 ignore = F403,F405,W503,B008
 max-line-length = 100
-exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache,*versions*,*patches*,.venv,scripts,tests/expected
+exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache,*versions*,*patches*,.venv,scripts,tests/expected,clients
diff --git a/.gitignore b/.gitignore
index 8e99882..763b5f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,7 @@ __pycache__/
 # Generated dirs
 clients/
 tmp/
-.idea/
\ No newline at end of file
+.idea/
+
+# Development files
+.test_files/
diff --git a/README.md b/README.md
index 82c94b0..d4d176f 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,38 @@
 # python-client-generator
 
 Python package to generate an [httpx](https://github.com/encode/httpx)- and
-[pydantic](https://github.com/pydantic/pydantic)-based async (or sync) 
-client off an OpenAPI spec >= 3.0.
+[pydantic](https://github.com/pydantic/pydantic)-based async (or sync) client off an OpenAPI spec >= 3.0 (partial specification support).
 
 ```mermaid
 flowchart LR
     generator["python-client-generator"]
-    app["REST API app"]
-    package["app HTTP client"]
+    app["REST API App"]
+    package["App HTTP client"]
 
-    app -- "OpenAPI json" --> generator
-    generator -- "generates" --> package
+    app -- "OpenAPI JSON" --> generator
+    generator -- "Generates" --> package
 ```
 
-> :warning: **Currently does not support OpenAPI 2.0**: PR for < 3.0 support are welcome
+> :warning: **Currently does not support OpenAPI 2.x** (PRs for < 3.0 support are welcome).
+
+> :exclamation: **Only partial OpenAPI specification support, if you encounter unsupported spec. please feel free to contribute to the project.**
 
 ## Using the generator
 
+Install the package (see on [PyPi](https://pypi.org/project/python-client-generator/)):
+
+```bash
+pip install python-client-generator
+```
+
+Run:
+
 ```bash
 python -m python_client_generator --open-api openapi.json --package-name foo_bar --project-name foo-bar --outdir clients
 ```
 
 This will produce a Python package with the following structure:
+
 ```bash
 clients
 ├── foo_bar
@@ -41,12 +51,13 @@ will ensure that only the fields that are set in the update object are sent to t
 
 ```python
 await api_client.update_contact_v1_contacts__contact_id__patch(
-                body=patch_body,
-                contact_id=contact.id,
-                tenant=tenant,
-                body_serializer_args={"exclude_unset": True}
+    body=patch_body,
+    contact_id=contact.id,
+    tenant=tenant,
+    body_serializer_args={"exclude_unset": True}
 )
 ```
 
 ## Contributing
+
 Please refer to [CONTRIBUTING.md](.github/CONTRIBUTING.md).
diff --git a/mypy.ini b/mypy.ini
index 4ea9142..f037bd6 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,4 +1,4 @@
 [mypy]
-exclude =(\.venv|tests/expected/apis\.py|tmp)
+exclude =(\.venv|tests/expected/apis\.py|tmp|clients|tests/expected)
 ignore_missing_imports = True
 disallow_untyped_defs = True
diff --git a/poetry.lock b/poetry.lock
index 6832c50..08e10da 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,24 +2,26 @@
 
 [[package]]
 name = "anyio"
-version = "3.6.2"
+version = "4.4.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 category = "dev"
 optional = false
-python-versions = ">=3.6.2"
+python-versions = ">=3.8"
 files = [
-    {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
-    {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
+    {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
+    {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
 ]
 
 [package.dependencies]
+exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
 idna = ">=2.8"
 sniffio = ">=1.1"
+typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""}
 
 [package.extras]
-doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
-test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
-trio = ["trio (>=0.16,<0.22)"]
+doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
+test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
+trio = ["trio (>=0.23)"]
 
 [[package]]
 name = "atomicwrites"
@@ -34,22 +36,23 @@ files = [
 
 [[package]]
 name = "attrs"
-version = "23.1.0"
+version = "23.2.0"
 description = "Classes Without Boilerplate"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
-    {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
+    {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
+    {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
 ]
 
 [package.extras]
 cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]", "pre-commit"]
+dev = ["attrs[tests]", "pre-commit"]
 docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
 tests = ["attrs[tests-no-zope]", "zope-interface"]
-tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
+tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
 
 [[package]]
 name = "black"
@@ -100,14 +103,14 @@ uvloop = ["uvloop (>=0.15.2)"]
 
 [[package]]
 name = "certifi"
-version = "2022.12.7"
+version = "2024.6.2"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
-    {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
+    {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+    {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
 ]
 
 [[package]]
@@ -124,14 +127,14 @@ files = [
 
 [[package]]
 name = "click"
-version = "8.1.3"
+version = "8.1.7"
 description = "Composable command line interface toolkit"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
-    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
 ]
 
 [package.dependencies]
@@ -217,6 +220,21 @@ toml = {version = "*", optional = true, markers = "extra == \"toml\""}
 [package.extras]
 toml = ["toml"]
 
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
 [[package]]
 name = "fastapi"
 version = "0.92.0"
@@ -258,13 +276,113 @@ pyflakes = ">=2.3.0,<2.4.0"
 
 [[package]]
 name = "grimp"
-version = "2.3"
+version = "3.3"
 description = "Builds a queryable graph of the imports within one or more Python packages."
 category = "dev"
 optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "grimp-2.3.tar.gz", hash = "sha256:78e4ace38be714bbfc0c2f64322722daa72e4d6e9b2cb0d2452058222e5b58b8"},
+python-versions = ">=3.8"
+files = [
+    {file = "grimp-3.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0185f7a676403ff8eb0cd772b1a7d30a79b3fd490718f7b26037a40c608dc1ba"},
+    {file = "grimp-3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8cff6a9b1c8f44ba9f42862be8c484be74fc748b954e643aa6ab93a5cf2d0b0e"},
+    {file = "grimp-3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df94ec1283e653a97890c390847e5077195b235f3f1b699bab0f8c22b8b45877"},
+    {file = "grimp-3.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed0e319e2b3f613f7e650364a6f583c086a6263c8f99c32f7711f69c9c101198"},
+    {file = "grimp-3.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7197607ff9f06f34ebbf61b494dace076efc6057f720c5c5e6bb309149d495c3"},
+    {file = "grimp-3.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1c845dc492629510edbd473a19f7559d21c5bd39e44dfdb51c03012cad1f21c"},
+    {file = "grimp-3.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cf059072c557ebe07339e6f0fd4d47be57784c8c85013456177fbb7cf366b13"},
+    {file = "grimp-3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f512c0062af1fc79d2b49f2e13eb91878f4ea072137bcf711eef2c4c9480a9e2"},
+    {file = "grimp-3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:620f4a68a6fe8799b9378f6399e1603a25c35d5e291a6ee01b6f20cc868d8680"},
+    {file = "grimp-3.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f5da111cf6c7ac34a8f525d42042c3e3d1c57ed2bb25a0378945cef471f6c00c"},
+    {file = "grimp-3.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14235c6bf375f2858979418cc78cd5b8273d8207bb264fcb1a232410f01ba1b1"},
+    {file = "grimp-3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f84072c59738da90cf74e3ff638727e02210ba5b5e0b584450726f38ee86ef9"},
+    {file = "grimp-3.3-cp310-none-win32.whl", hash = "sha256:49a8cebfeff2e1fbfa5e14b0e9f5d5f8db6c1ad6aed8a4524cf54b78657e6bc1"},
+    {file = "grimp-3.3-cp310-none-win_amd64.whl", hash = "sha256:7f7ea5b91e232b5cf298e0fc0158d79feedb158cefb84da608c6dc3a0febc6f6"},
+    {file = "grimp-3.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5a395f959ec7936f015e5c834d551ea5b9e1b8217d2e9001ddbb80e46dfff822"},
+    {file = "grimp-3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70d9f9c5fe7b186c2bd3f2229badd1b970c5d0c62a6a0e02725157d31143daf5"},
+    {file = "grimp-3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a554929ea71369d8ebefb1610593d839abd5a4a92dcedda374969d7c9df86"},
+    {file = "grimp-3.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa647e85fdda49e693487b44bc8360b6c9431cbc7cd3aaff007c3d8261f3d42c"},
+    {file = "grimp-3.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a65467d222ba3d8e32895f9a0abe3473c1936138abf008916554d6913477d210"},
+    {file = "grimp-3.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac43fc1b6862991951a8657a22b693f40b4a3b9be7a9848d6b752234cdb9b2e0"},
+    {file = "grimp-3.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4219adfdf212e77d31fd43bde018200b42a54be95fddfcf13388bddb91ca15be"},
+    {file = "grimp-3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ef3293a0f7f645c123189b6f017dcef98a7471608d9d3150f74be953f13b6b9"},
+    {file = "grimp-3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec0119d3925901d04a63bc28ddc8fdfa0c40eecb7d8cbd4ffbf1e7e74336fde9"},
+    {file = "grimp-3.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9b69bc145b095542258f1a8c934084557356c54efc6f11c66ca1b476c308ea26"},
+    {file = "grimp-3.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aec933d3e156f7e0091db8ce9fc7d05c8bfbec05efe2c469042ed9587738ba28"},
+    {file = "grimp-3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:691e006fc28e86529309ea630f4854ce39fab013f56029644f8eb6b1c87e88a6"},
+    {file = "grimp-3.3-cp311-none-win32.whl", hash = "sha256:785a4b08ff077f1799aa4bfe5b7e7e8f3a55371fb848c00ed99ff300142768a0"},
+    {file = "grimp-3.3-cp311-none-win_amd64.whl", hash = "sha256:9a28ec4df3faab3acfdebe0c354889a4b77da40840bfe5f1d422e1efc3d4cb18"},
+    {file = "grimp-3.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:72da9eefbd21cf52eecfa9d3c3bbf75caf2d2545bf5cc27c09b5b0949f1ad826"},
+    {file = "grimp-3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e060c76a40ac8510b109443a38de90308c8ef41d9b081b3410d72acbbaaea77"},
+    {file = "grimp-3.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef507c9e09fb70dc3c92c41a84f33d66c47cfdb99b6851195b82eb857adedab3"},
+    {file = "grimp-3.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f764c8d284bcca2b2a16a659e5203bcfe6ad8240b2da0aba519c744f319bd4d4"},
+    {file = "grimp-3.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2eb2fe0876b116dfc9b7ab59bacb8d27b440fb5b098e24002d9f19e33d5915a"},
+    {file = "grimp-3.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bd67c31f914d3040481772eaa31a6da9adf7867429681258d866de9ee385063"},
+    {file = "grimp-3.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:048b138a744e5f0c97a53427b7f6ccd02b6dfe683b040b3dbfdf558a3760ebd7"},
+    {file = "grimp-3.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca330c047e520d9b86b9436005fe126e8945f4e5d6471dd14f12eca0f41350d"},
+    {file = "grimp-3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b50f373836ea96aaef2271edb361f326f06d167fd82a603da1f569a6ddc0986e"},
+    {file = "grimp-3.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90f907788aad83e444c4e3809a8a68c6aa1035033f01994acc7d3c4e8371bddd"},
+    {file = "grimp-3.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d9b8329cf523a95bb458d5a0099983732669144f0bd4757d8260a3e8ed9fdb49"},
+    {file = "grimp-3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:45c783477e8d82e3e40321e9c77276bb29b2d65fe09b6c7696b6179ffd291021"},
+    {file = "grimp-3.3-cp312-none-win32.whl", hash = "sha256:bcdf272f2284c27992cfd3f74757cc7398759fe193e4418e147f9c7f77d12c5b"},
+    {file = "grimp-3.3-cp312-none-win_amd64.whl", hash = "sha256:8f983b67471ff7d64d0a2ae9580768b863cb71dce3fc27f428f72bac8519ea97"},
+    {file = "grimp-3.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:caef602bfda3fcb3e9e344605e742412b1027c6f5e89e40c57b2847ac847973c"},
+    {file = "grimp-3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28f18fae9e45da0126d396e77740a0b22b6cc05147c8e2ed01a3028952917399"},
+    {file = "grimp-3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd825f5133d02b0f43bc631b4a3d647547fc8beea1ac3e67e0b26d73cc2fe14"},
+    {file = "grimp-3.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:883f9048e9523e381721c83df69b68a51fdcbeed37f8a3f89be47f594ec906b3"},
+    {file = "grimp-3.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d76cd15de278cda693b60446641774266c9f93e1688c92b46f76909d3ee79"},
+    {file = "grimp-3.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdfd90286ee32a3106a13c2ed2208c3f0a26c1c0477654c6daa7213c4bcea363"},
+    {file = "grimp-3.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7bbe0a996c323cb2f89f097a0f30d478964f001d84a67b948e9c460abd54fac8"},
+    {file = "grimp-3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:354e5201a190fae37423d4d8a1b3ce10fdeb0d75c6de0dfda2bc9d8c49c832ff"},
+    {file = "grimp-3.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:21a335e3dcb74bfcc928ef6eec08c8bed836b492ac81dd08ca1199bb55659097"},
+    {file = "grimp-3.3-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:52dea0611dbad57e1b58a663066a6218a0846e4b6d43e7325e97811803715a6b"},
+    {file = "grimp-3.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb6a422547602d1045c621baaa58e12752bc365d8da74edce13dd93cc354a298"},
+    {file = "grimp-3.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d95571888d6397810d10cba18c79a8ec879a6541eebe19ae79bfe529e9098462"},
+    {file = "grimp-3.3-cp38-none-win32.whl", hash = "sha256:2e9745072cc7ffd806948685c9b02904dbf7094596747642f202b9f2935ad520"},
+    {file = "grimp-3.3-cp38-none-win_amd64.whl", hash = "sha256:282b0b6d461af9194a86c6c4950a2efa5a2205efbfa22d3f685b326187dde5b4"},
+    {file = "grimp-3.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7135308594fbeba645205ee7077c89648e9f6dffac5ad4e3eadda4654cb74d38"},
+    {file = "grimp-3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:36ee0c707b60b359aace1f0c1f519be3b7e75d2f3bbdd7283e9026aa3cb6562c"},
+    {file = "grimp-3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b759491c147d8a935d43bd1657922b41a798454125888504fa108c0499fa666"},
+    {file = "grimp-3.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23ddd88c101c464e9977e8fbdc3e8b068a2e90f47e308c4eaf6feac5ce873061"},
+    {file = "grimp-3.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:916586e1dc087ec446e77f5f63aefabeaaa62cab55f3cde064bd5685acc8c118"},
+    {file = "grimp-3.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94129847bebc48f50fac771179da555af0c4bda71868171373afb0d343dce766"},
+    {file = "grimp-3.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fa8541f51b1aa75ca53589a609c8a0e5d1567abda12d688421f5ab371529ce7"},
+    {file = "grimp-3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb1b9ffe23185d00ccace2bc56c1b73a5a52fb724d1f3dfba4ed66a8997bff92"},
+    {file = "grimp-3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8d4b144190e9774cdf0b20c2a5673e0fc3f5d104f813d21c7ee327249d860dd4"},
+    {file = "grimp-3.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:69fea066125d5339ce025136e1081facf68015d46320020ede7d4ac8b7284df9"},
+    {file = "grimp-3.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2d35114966b1ea10104fb7cfc1a7df9729dc4d7f1e4b7ac4f129cdbea0c3d14f"},
+    {file = "grimp-3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:590e85a4bfe58fd4de0384eaccdf545ef7145cbbcaf4f8c52a134100e94db2b9"},
+    {file = "grimp-3.3-cp39-none-win32.whl", hash = "sha256:099617459d81e21a455303fe4a0d696956c2b0bbb8690ebd806647c1f423aaeb"},
+    {file = "grimp-3.3-cp39-none-win_amd64.whl", hash = "sha256:e229a06a24696f117b7a3a1c39bdb589b76d087b4145eba3eb38a83ff58a153e"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086e879dbb482aa6c7f55b4608bf635cee4f01bf0a8774308410531e6f22d6c7"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47bb45ec3f697eb0b3e3d28750ef27691e618068a1f1b6f00f4a6ded182e5713"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff88f8797d2ae378fcef28f3771b6bf34afda2900436cd7d8c2764f9887a55a7"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97ec6e0111d921815e24bc10d0cca2e4e252ccf64b66a907310e402bbd7f3c85"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b474acb40ff17b0a740c510ab63a49ababe62316403135dacfa0a42d7be5e2b"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4071a27074d7bc2ee23a5e5d9760be1ea25ea57519e454718da6f83f4038397"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c7feb64a196c4904d5d388a231382dfe9d248665cdc82169a6f62b1ecd918940"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c79d3081a1f62ee7b16f74ef074b933688ae170c9fe7f46f42c78726147529d3"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:cd7fae24434962b780d179967c31dafd9417e8660bb56c6286b3e4b7cbe97191"},
+    {file = "grimp-3.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8fd6b12baca6d673a1f13efb60090c6b69844e5d8bab932f029ad1cd26c43129"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:646679d42f6c7ac0cd341870e4e420556c0e64d7c52a9434a82c4e764bf0929d"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c56225e015a707737b55830f21730034139668d4164709d682e61be359be5e43"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e01ddd07d054fa827ad9901d84a490ded6edd203ad07bbb77e1c9e82617e4c8"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd62a1eb7798ddd62a16ab9087802adec18bc0de85aa34c7cb1cef6ba16bbe54"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20dddc49109058d5635038f9b69d75b69fd740febdb26018cf6f2b6026da6cc7"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34842b3dca54d5cfac22380d4c2fe02b8880dc41fb9300d52a8c24d1185c178a"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8248cc63e3c5fb7aef3cec71b4132cc4668f40b8b76c51b275c95a16e4c1b7d1"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:3d6dfd86413a79c936508bb75f2e59e5ee0b076d5dfc08b60a827edfe5889ada"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:7a33be44cd833d7c108fd2ee856cae33164870a4a176ec995781fd8bdd63ec7d"},
+    {file = "grimp-3.3-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:b1ac4b644ab6ec10f742e5d21764a212be63f2edd2f55c1418486a670cbc756c"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049fe46ad180bf35437469905345c8281ffb126c4dfc7cd6a126cd26d9a2bb58"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f3cdc853bad614198a3e34214b8db5043957bfbb140d8521e6e76f6ae7954ca"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b8acddab24f9f0cc7a95f0e775a39d57bd0938e5ac9a9b5c94c01a3b6db2067"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28b8e77d88c2fa101289dada95c10906ebabdc8f8d75d529df95686d8ac39280"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d655cf9bb9be978ed89236472f978f84a1dddd6119dfcef02b614fb7a1760fcf"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c918e8cc3898aea5b1fd8d2a12aa23dc9021565ab0a543788f0c75292b86ad5d"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:47080077af734fa9409c092317a99401e0eae2dc6f3ae97515f63379fd847f63"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:cf4f4bfea5c384349e59b4eb31e09d92853bdc1fb097e3b3707da2a05ac469cb"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:415f6192d67379852291e04ce151dc91f49cc69dc14740bc3e2737707718ed4b"},
+    {file = "grimp-3.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:805ab4bc78d9942aff583151d212d2f1fb1cd009491864b627690f6f4be3d86a"},
+    {file = "grimp-3.3.tar.gz", hash = "sha256:4fdd9a30cc60c94e27f29207f14ec1268a9b5198d739eef51c87d1679dde2e7e"},
 ]
 
 [package.dependencies]
@@ -330,31 +448,31 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
 
 [[package]]
 name = "idna"
-version = "3.4"
+version = "3.7"
 description = "Internationalized Domain Names in Applications (IDNA)"
 category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
-    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
-    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
 ]
 
 [[package]]
 name = "import-linter"
-version = "1.8.0"
+version = "1.12.1"
 description = "Enforces rules for the imports within and between Python packages."
 category = "dev"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "import-linter-1.8.0.tar.gz", hash = "sha256:482fc2cb0c036cf17d9ee6552077acabb426fa9dab36927589bb857935b82e69"},
-    {file = "import_linter-1.8.0-py3-none-any.whl", hash = "sha256:2d2ff5161456f0c3b4f2f3b314a1be9837cb4ea788eb12b57fa7013e27bf5290"},
+    {file = "import-linter-1.12.1.tar.gz", hash = "sha256:3aa4da5bbed4b2e575fd2479bc7962090ae95666676fb872a4819a293b973463"},
+    {file = "import_linter-1.12.1-py3-none-any.whl", hash = "sha256:92f5ddec77c523bd95c0af88d3fdb354b705a776f7a0e59a7b25b035828a0257"},
 ]
 
 [package.dependencies]
 click = ">=6"
-grimp = ">=2.3"
+grimp = ">=3.1"
 tomli = {version = ">=1.2.1", markers = "python_version < \"3.11\""}
 typing-extensions = ">=3.10.0.0"
 
@@ -372,21 +490,18 @@ files = [
 
 [[package]]
 name = "isort"
-version = "5.12.0"
+version = "5.13.2"
 description = "A Python utility / library to sort Python imports."
 category = "dev"
 optional = false
 python-versions = ">=3.8.0"
 files = [
-    {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
-    {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
+    {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+    {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
 ]
 
 [package.extras]
-colors = ["colorama (>=0.4.3)"]
-pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
-plugins = ["setuptools"]
-requirements-deprecated-finder = ["pip-api", "pipreqs"]
+colors = ["colorama (>=0.4.6)"]
 
 [[package]]
 name = "mccabe"
@@ -453,54 +568,55 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "23.1"
+version = "24.1"
 description = "Core utilities for Python packages"
 category = "dev"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
-    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+    {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
 ]
 
 [[package]]
 name = "pathspec"
-version = "0.11.1"
+version = "0.12.1"
 description = "Utility library for gitignore style pattern matching of file paths."
 category = "dev"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
-    {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
+    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
 ]
 
 [[package]]
 name = "platformdirs"
-version = "3.5.0"
-description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+version = "4.2.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
 category = "dev"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "platformdirs-3.5.0-py3-none-any.whl", hash = "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4"},
-    {file = "platformdirs-3.5.0.tar.gz", hash = "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"},
+    {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+    {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
 ]
 
 [package.extras]
-docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
 
 [[package]]
 name = "pluggy"
-version = "1.0.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
@@ -533,48 +649,55 @@ files = [
 
 [[package]]
 name = "pydantic"
-version = "1.10.7"
+version = "1.10.17"
 description = "Data validation and settings management using python type hints"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pydantic-1.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e79e999e539872e903767c417c897e729e015872040e56b96e67968c3b918b2d"},
-    {file = "pydantic-1.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01aea3a42c13f2602b7ecbbea484a98169fb568ebd9e247593ea05f01b884b2e"},
-    {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:516f1ed9bc2406a0467dd777afc636c7091d71f214d5e413d64fef45174cfc7a"},
-    {file = "pydantic-1.10.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae150a63564929c675d7f2303008d88426a0add46efd76c3fc797cd71cb1b46f"},
-    {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ecbbc51391248116c0a055899e6c3e7ffbb11fb5e2a4cd6f2d0b93272118a209"},
-    {file = "pydantic-1.10.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f4a2b50e2b03d5776e7f21af73e2070e1b5c0d0df255a827e7c632962f8315af"},
-    {file = "pydantic-1.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:a7cd2251439988b413cb0a985c4ed82b6c6aac382dbaff53ae03c4b23a70e80a"},
-    {file = "pydantic-1.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:68792151e174a4aa9e9fc1b4e653e65a354a2fa0fed169f7b3d09902ad2cb6f1"},
-    {file = "pydantic-1.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe2507b8ef209da71b6fb5f4e597b50c5a34b78d7e857c4f8f3115effaef5fe"},
-    {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a86d8c8db68086f1e30a530f7d5f83eb0685e632e411dbbcf2d5c0150e8dcd"},
-    {file = "pydantic-1.10.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75ae19d2a3dbb146b6f324031c24f8a3f52ff5d6a9f22f0683694b3afcb16fb"},
-    {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:464855a7ff7f2cc2cf537ecc421291b9132aa9c79aef44e917ad711b4a93163b"},
-    {file = "pydantic-1.10.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:193924c563fae6ddcb71d3f06fa153866423ac1b793a47936656e806b64e24ca"},
-    {file = "pydantic-1.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:b4a849d10f211389502059c33332e91327bc154acc1845f375a99eca3afa802d"},
-    {file = "pydantic-1.10.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cc1dde4e50a5fc1336ee0581c1612215bc64ed6d28d2c7c6f25d2fe3e7c3e918"},
-    {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0cfe895a504c060e5d36b287ee696e2fdad02d89e0d895f83037245218a87fe"},
-    {file = "pydantic-1.10.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:670bb4683ad1e48b0ecb06f0cfe2178dcf74ff27921cdf1606e527d2617a81ee"},
-    {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:950ce33857841f9a337ce07ddf46bc84e1c4946d2a3bba18f8280297157a3fd1"},
-    {file = "pydantic-1.10.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c15582f9055fbc1bfe50266a19771bbbef33dd28c45e78afbe1996fd70966c2a"},
-    {file = "pydantic-1.10.7-cp37-cp37m-win_amd64.whl", hash = "sha256:82dffb306dd20bd5268fd6379bc4bfe75242a9c2b79fec58e1041fbbdb1f7914"},
-    {file = "pydantic-1.10.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c7f51861d73e8b9ddcb9916ae7ac39fb52761d9ea0df41128e81e2ba42886cd"},
-    {file = "pydantic-1.10.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6434b49c0b03a51021ade5c4daa7d70c98f7a79e95b551201fff682fc1661245"},
-    {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d34ab766fa056df49013bb6e79921a0265204c071984e75a09cbceacbbdd5d"},
-    {file = "pydantic-1.10.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:701daea9ffe9d26f97b52f1d157e0d4121644f0fcf80b443248434958fd03dc3"},
-    {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf135c46099ff3f919d2150a948ce94b9ce545598ef2c6c7bf55dca98a304b52"},
-    {file = "pydantic-1.10.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0f85904f73161817b80781cc150f8b906d521fa11e3cdabae19a581c3606209"},
-    {file = "pydantic-1.10.7-cp38-cp38-win_amd64.whl", hash = "sha256:9f6f0fd68d73257ad6685419478c5aece46432f4bdd8d32c7345f1986496171e"},
-    {file = "pydantic-1.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c230c0d8a322276d6e7b88c3f7ce885f9ed16e0910354510e0bae84d54991143"},
-    {file = "pydantic-1.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:976cae77ba6a49d80f461fd8bba183ff7ba79f44aa5cfa82f1346b5626542f8e"},
-    {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d45fc99d64af9aaf7e308054a0067fdcd87ffe974f2442312372dfa66e1001d"},
-    {file = "pydantic-1.10.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d2a5ebb48958754d386195fe9e9c5106f11275867051bf017a8059410e9abf1f"},
-    {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:abfb7d4a7cd5cc4e1d1887c43503a7c5dd608eadf8bc615413fc498d3e4645cd"},
-    {file = "pydantic-1.10.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:80b1fab4deb08a8292d15e43a6edccdffa5377a36a4597bb545b93e79c5ff0a5"},
-    {file = "pydantic-1.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:d71e69699498b020ea198468e2480a2f1e7433e32a3a99760058c6520e2bea7e"},
-    {file = "pydantic-1.10.7-py3-none-any.whl", hash = "sha256:0cd181f1d0b1d00e2b705f1bf1ac7799a2d938cce3376b8007df62b29be3c2c6"},
-    {file = "pydantic-1.10.7.tar.gz", hash = "sha256:cfc83c0678b6ba51b0532bea66860617c4cd4251ecf76e9846fa5a9f3454e97e"},
+    {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
+    {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
+    {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
+    {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
+    {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
+    {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
+    {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
+    {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
+    {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
+    {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
+    {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
+    {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
+    {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
+    {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
+    {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
+    {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
+    {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
+    {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
+    {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
+    {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
+    {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
+    {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
+    {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
+    {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
+    {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
+    {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
+    {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
+    {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
+    {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
+    {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
+    {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
+    {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
+    {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
+    {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
+    {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
+    {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
+    {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
+    {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
+    {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
+    {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
+    {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
+    {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
+    {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
 ]
 
 [package.dependencies]
@@ -598,14 +721,14 @@ files = [
 
 [[package]]
 name = "pytest"
-version = "6.2.5"
+version = "7.0.0"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
-    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+    {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"},
+    {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"},
 ]
 
 [package.dependencies]
@@ -616,10 +739,29 @@ iniconfig = "*"
 packaging = "*"
 pluggy = ">=0.12,<2.0"
 py = ">=1.8.2"
-toml = "*"
+tomli = ">=1.0.0"
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.23.7"
+description = "Pytest support for asyncio"
+category = "dev"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
+    {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
 
 [[package]]
 name = "pytest-cov"
@@ -656,6 +798,21 @@ files = [
 [package.extras]
 dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"]
 
+[[package]]
+name = "respx"
+version = "0.21.1"
+description = "A utility for mocking out the Python HTTPX and HTTP Core libraries."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"},
+    {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"},
+]
+
+[package.dependencies]
+httpx = ">=0.21.0"
+
 [[package]]
 name = "rfc3986"
 version = "1.5.0"
@@ -676,26 +833,26 @@ idna2008 = ["idna"]
 
 [[package]]
 name = "semver"
-version = "3.0.0"
+version = "3.0.2"
 description = "Python helper for Semantic Versioning (https://semver.org)"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "semver-3.0.0-py3-none-any.whl", hash = "sha256:ab4f69fb1d1ecfb5d81f96411403d7a611fa788c45d252cf5b408025df3ab6ce"},
-    {file = "semver-3.0.0.tar.gz", hash = "sha256:94df43924c4521ec7d307fc86da1531db6c2c33d9d5cdc3e64cca0eb68569269"},
+    {file = "semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4"},
+    {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"},
 ]
 
 [[package]]
 name = "sniffio"
-version = "1.3.0"
+version = "1.3.1"
 description = "Sniff out which async library your code is running under"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
-    {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
+    {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
+    {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
 ]
 
 [[package]]
@@ -783,17 +940,17 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.5.0"
-description = "Backported and Experimental Type Hints for Python 3.7+"
+version = "4.12.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
 category = "main"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
-    {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "caf1d992c1f95a8353e36d1ccae51ac04cfafbcc9959e7c430318218a021e31b"
+content-hash = "95170a977570b153fd8d466c3aef9a180766a353c6ec67c632dc494dcc9e8d96"
diff --git a/pyproject.toml b/pyproject.toml
index a1cf642..df6efce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,6 @@ chevron = "^0.14.0"
 semver = "^3.0.0"
 
 [tool.poetry.dev-dependencies]
-pytest = "^6.1.1"
 coverage = { version = "^5.3", extras = ["toml"]}
 pytest-cov = "^2.10.1"
 flake8 = "^3.8.4"
@@ -34,6 +33,11 @@ semver = "^3.0.0"
 [tool.poetry.scripts]
 poetry = "poetry.console:main"
 
+[tool.poetry.group.dev.dependencies]
+respx = "^0.21.1"
+pytest = "7"
+pytest-asyncio = "^0.23.7"
+
 [tool.semantic_release]
 version_toml = [
     "pyproject.toml:tool.poetry.version"
@@ -74,6 +78,7 @@ exclude = '''
   | build
   | dist
   | tests/expected
+  | clients
 )/
 '''
 
diff --git a/python_client_generator/generate_apis.py b/python_client_generator/generate_apis.py
index a38350b..56de9d5 100755
--- a/python_client_generator/generate_apis.py
+++ b/python_client_generator/generate_apis.py
@@ -33,21 +33,68 @@ def resolve_property_default(property: Dict[str, Any]) -> Optional[str]:
 
 
 def get_return_type(responses: Dict[str, Any]) -> Optional[str]:
-    successful_responses = [v for k, v in responses.items() if int(k) >= 200 and int(k) < 300]
-    if len(successful_responses) != 1:
-        raise Exception("Incorrect number of successful responses:", len(successful_responses))
+    def check_if_valid_success_response(key: str) -> bool:
+        if key == "default":
+            return True
 
-    schema = successful_responses[0]["content"]["application/json"]["schema"]
-    if not schema:
+        if key == "2XX":
+            return True
+
+        if 200 <= int(key) < 300:
+            return True
+
+        return False
+
+    # Only consider successful responses
+    successful_responses_raw = {
+        k: v for k, v in responses.items() if check_if_valid_success_response(k)
+    }
+
+    if len(successful_responses_raw) == 0:
+        return None
+
+    # Pop the default response if there are multiple successful responses (e.g. 200, 201, 204)
+    if len(successful_responses_raw) > 1:
+        successful_responses_raw.pop("default", None)
+
+    # Map the responses to a list
+    successful_responses = [v for _, v in successful_responses_raw.items()]
+
+    # Not all successful responses have a content key, see: https://spec.openapis.org/oas/v3.0.3#responses-object  # noqa E501
+    if "content" not in successful_responses[0]:
         return None
 
-    return sanitize_name(schema["title"])
+    content = successful_responses[0]["content"]
+
+    if "application/json" not in content:
+        return None
+
+    schema = successful_responses[0]["content"]["application/json"].get("schema")
+    if schema is None:
+        return None
+
+    if "type" not in schema:
+        return sanitize_name(schema["title"]) if "title" in schema else None
+
+    if schema["type"] == "array":
+        return f"List[{resolve_type(schema['items'])}]"
+    if schema["type"] == "object":
+        return sanitize_name(schema.get("title", "Dict[str, Any]"))
+
+    return None
 
 
 def _get_request_body_params(method: Dict[str, Any]) -> List[Dict[str, Any]]:
-    args = []
+    """
+    Supported media types are:
+    - application/json
+    - multipart/form-data
 
+    Media types' ranges are not supported at the moment (e.g. '*/*', 'application/*', etc).
+    """
+    args = []
     content = method["requestBody"]["content"]
+
     if "application/json" in content:
         schema = content["application/json"]["schema"]
         args.append(
@@ -56,9 +103,14 @@ def _get_request_body_params(method: Dict[str, Any]) -> List[Dict[str, Any]]:
                 "schema": schema,
             }
         )
-    else:
+    elif "multipart/form-data" in content:
+        schema = content["multipart/form-data"].get("schema")
+
+        # If schema is not defined with properties, we can't generate arguments
+        if schema is None or "properties" not in schema:
+            return args
+
         # Create argument for each multipart upload property
-        schema = content["multipart/form-data"]["schema"]
         for k, v in schema["properties"].items():
             args.append({"name": k, "schema": v})
 
@@ -76,9 +128,9 @@ def get_function_args(method: Dict[str, Any]) -> List[Dict[str, Any]]:
     keys = ["path", "query", "header"]
     parameters = method.get("parameters", [])
     for k in keys:
-        params += [p for p in parameters if p["in"] == k and p["required"]]
+        params += [p for p in parameters if p["in"] == k and p.get("required", False)]
     for k in keys:
-        params += [p for p in parameters if p["in"] == k and not p["required"]]
+        params += [p for p in parameters if p["in"] == k and not p.get("required", False)]
 
     # Convert params to args format required for templating
     return [
diff --git a/python_client_generator/generate_models.py b/python_client_generator/generate_models.py
index da53e66..238e17d 100755
--- a/python_client_generator/generate_models.py
+++ b/python_client_generator/generate_models.py
@@ -47,8 +47,41 @@ def _get_schema_references(schema: Dict[str, Any]) -> List[str]:
         return []
     elif schema["type"] == "array":
         return _get_schema_references(schema["items"])
-    elif schema["type"] == "object" or "enum" in schema:
-        return [schema["title"]]
+    elif schema["type"] == "object" or (schema["type"] == "string" and "enum" in schema):
+        # As some nested enums may not have a title, we need to check for it.
+        # This is observed to happen inside the properties of a schema that uses an enum with referencing to another enum schema (raw values instead)  # noqa E501
+        # Example:
+        # "properties": { # properties of an object schema (type is object)
+        #     "status": {
+        #         "type": "string",
+        #         "enum": [
+        #             "active",
+        #             "inactive"
+        #         ]
+        #     }
+        # }
+        # In this case, the enum values are not defined in a schema with a title, so we need to check for it.  # noqa E501
+        # For the case where the enum values are defined in a schema with a title, the title will be used.  # noqa E501
+        # Example:
+        # "schemas": {
+        #     ...,  # other schemas
+        #     "Status": {
+        #         "title": "Status",
+        #         "type": "string",
+        #         "enum": [
+        #             "active",
+        #             "inactive"
+        #         ]
+        #     }
+        # }
+        # And then it will be referenced in the properties like this:
+        # "properties": { # properties of an object schema (type is object)
+        #     "status": {
+        #         "$ref": "#/components/schemas/Status"
+        #     }
+        # }
+
+        return [schema.get("title", "")]
     else:
         return []
 
diff --git a/python_client_generator/main.py b/python_client_generator/main.py
index ce4101d..6135b54 100644
--- a/python_client_generator/main.py
+++ b/python_client_generator/main.py
@@ -5,7 +5,11 @@
 
 from pathlib import Path
 
-from python_client_generator.utils import assert_openapi_version, dereference_swagger
+from python_client_generator.utils import (
+    add_schema_title_if_missing,
+    assert_openapi_version,
+    dereference_swagger,
+)
 
 from .generate_apis import generate_apis
 from .generate_base_client import generate_base_client
@@ -32,6 +36,7 @@ def main() -> None:
         swagger = json.load(f)
 
     assert_openapi_version(swagger)
+    add_schema_title_if_missing(swagger["components"]["schemas"])
     dereferenced_swagger = dereference_swagger(swagger, swagger)
 
     # Create root directory
diff --git a/python_client_generator/utils.py b/python_client_generator/utils.py
index fa2916e..f724a33 100644
--- a/python_client_generator/utils.py
+++ b/python_client_generator/utils.py
@@ -81,7 +81,28 @@ def resolve_type(schema: Dict[str, Any], depth: int = 0, use_literals: bool = Fa
     elif "type" not in schema:
         return "Any"
     elif schema["type"] == "object":
-        if "properties" in schema:
+        # If a schema has properties and a title, we can use the title as the type
+        # name. Otherwise, we just return a generic Dict[str, Any]
+        # This happens when a schema has an object in the properties that doesn't reference another schema.  # noqa E501
+        # Example:
+        # {
+        #    "Schema_Name": {
+        #        "title": "Schema_Name",
+        #        "type": "object",
+        #        "properties": {
+        #            "property_name": {
+        #                "type": "object",
+        #                "properties": {
+        #                    "nested_property": {
+        #                        "type": "string"
+        #                    }
+        #                }
+        #            }
+        #        }
+        #    }
+        # }
+
+        if "properties" in schema and "title" in schema:
             return sanitize_name(schema["title"])
         else:
             return "Dict[str, Any]"
@@ -111,3 +132,30 @@ def resolve_type(schema: Dict[str, Any], depth: int = 0, use_literals: bool = Fa
 def assert_openapi_version(schema: Dict[str, Any]) -> None:
     if not schema.get("openapi") or semver.Version.parse(schema.get("openapi")).major != 3:  # type: ignore # noqa: E501
         raise UnsupportedOpenAPISpec("OpenAPI file provided is not version 3.x")
+
+
+def add_schema_title_if_missing(schemas: Dict[str, Any]) -> Dict[str, Any]:
+    """
+    Add 'title' key to schemas if missing to prevent issues with type resolution.
+    Only adds title to object and enum schemas.
+
+    Args:
+        schemas (Dict[str, Any]): Swagger schemas under components.schemas
+    Returns:
+        Dict[str, Any]: Schemas with 'title' key added if missing
+
+    Raises:
+        ValueError: If schema is missing 'type' key
+    """
+
+    for k, v in schemas.items():
+        if "title" not in v and isinstance(v, dict):
+            schema_type = v.get("type")
+
+            if not schema_type:
+                raise ValueError(f"Schema {k} is missing 'type' key")
+
+            if schema_type == "object" or (schema_type == "string" and "enum" in v):
+                v["title"] = k
+
+    return schemas


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 290dd8e..9b5c5a1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,24 +6,38 @@
 
 import pytest
 
-from python_client_generator.utils import dereference_swagger
+from python_client_generator.utils import (
+    add_schema_title_if_missing,
+    dereference_swagger,
+)
 
-from .input_app import app
+from .inputs.fastapi_app import app
 
 
 PATH = Path(os.path.dirname(os.path.realpath(__file__)))
 
 
 @pytest.fixture(scope="session")
-def openapi_file(tmp_path_factory: pytest.TempPathFactory) -> Path:
-    path = tmp_path_factory.mktemp("input") / "input_openapi.json"
+def fastapi_app_openapi_file(tmp_path_factory: pytest.TempPathFactory) -> Path:
+    path = tmp_path_factory.mktemp("input") / "fastapi_app_openapi.json"
     with open(path, "w") as f:
         json.dump(app.openapi(), f)
     return path
 
 
 @pytest.fixture()
-def openapi(openapi_file: Path) -> Dict[str, Any]:
-    with open(openapi_file, "r") as f:
+def fastapi_app_openapi(fastapi_app_openapi_file: Path) -> Dict[str, Any]:
+    with open(fastapi_app_openapi_file, "r") as f:
         swagger = json.load(f)
+
+    add_schema_title_if_missing(swagger["components"]["schemas"])
+    return dereference_swagger(swagger, swagger)
+
+
+@pytest.fixture()
+def swagger_petstore_openapi() -> Dict[str, Any]:
+    with open(PATH / "inputs" / "swagger-petstore.json", "r") as f:
+        swagger = json.load(f)
+
+    add_schema_title_if_missing(swagger["components"]["schemas"])
     return dereference_swagger(swagger, swagger)
diff --git a/tests/expected/apis.py b/tests/expected/fastapi_app_client/apis.py
similarity index 100%
rename from tests/expected/apis.py
rename to tests/expected/fastapi_app_client/apis.py
diff --git a/tests/expected/base_client.py b/tests/expected/fastapi_app_client/base_client.py
similarity index 100%
rename from tests/expected/base_client.py
rename to tests/expected/fastapi_app_client/base_client.py
diff --git a/tests/expected/models.py b/tests/expected/fastapi_app_client/models.py
similarity index 100%
rename from tests/expected/models.py
rename to tests/expected/fastapi_app_client/models.py
diff --git a/tests/expected/fastapi_app_client/pyproject.toml b/tests/expected/fastapi_app_client/pyproject.toml
new file mode 100644
index 0000000..38fdc9e
--- /dev/null
+++ b/tests/expected/fastapi_app_client/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.poetry]
+name = "fastapi-project"
+version = "0.1.0"
+description = "Autogenerated httpx async client for fastapi-project"
+authors = ["Autogenerated Client <autogenerated@client.com>"]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+httpx = ">=0.22, <1"
+pydantic = "^1"
+
+[tool.poetry.scripts]
+poetry = "poetry.console:main"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/expected/swagger_petstore_client/__init__.py b/tests/expected/swagger_petstore_client/__init__.py
new file mode 100644
index 0000000..f33de32
--- /dev/null
+++ b/tests/expected/swagger_petstore_client/__init__.py
@@ -0,0 +1,2 @@
+from .apis import *
+from .models import *
diff --git a/tests/expected/swagger_petstore_client/apis.py b/tests/expected/swagger_petstore_client/apis.py
new file mode 100644
index 0000000..69fe071
--- /dev/null
+++ b/tests/expected/swagger_petstore_client/apis.py
@@ -0,0 +1,383 @@
+from typing import Any, Dict, Optional, Union
+
+import httpx
+
+from .base_client import BaseClient
+from .models import *
+
+
+class Api(BaseClient):
+    """
+    Autogenerated httpx async client
+    """
+    async def updatePet(
+        self,
+        body: Pet,
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> Pet:
+        """
+        Update an existing pet by Id
+        """ # noqa 
+
+        response = await self._request(
+            "PUT",
+            "/pet",
+            _body=body,
+            body_serializer_args=body_serializer_args,
+            **kwargs
+        )
+        response.raise_for_status()
+        return Pet.parse_raw(response.content)
+
+    async def addPet(
+        self,
+        body: Pet,
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> Pet:
+        """
+        Add a new pet to the store
+        """ # noqa 
+
+        response = await self._request(
+            "POST",
+            "/pet",
+            _body=body,
+            body_serializer_args=body_serializer_args,
+            **kwargs
+        )
+        response.raise_for_status()
+        return Pet.parse_raw(response.content)
+
+    async def findPetsByStatus(
+        self,
+        status: Optional[str] = "available",
+        **kwargs: Any
+    ) -> List[Pet]:
+        """
+        Multiple status values can be provided with comma separated strings
+        """ # noqa 
+
+        _query_params = { 
+            "status": status,
+        }
+
+        response = await self._request(
+            "GET",
+            "/pet/findByStatus",
+            _query_params=_query_params, 
+            **kwargs
+        )
+        response.raise_for_status()
+        return List[Pet].parse_raw(response.content)
+
+    async def findPetsByTags(
+        self,
+        tags: Optional[List[str]] = None,
+        **kwargs: Any
+    ) -> List[Pet]:
+        """
+        Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+        """ # noqa 
+
+        _query_params = { 
+            "tags": tags,
+        }
+
+        response = await self._request(
+            "GET",
+            "/pet/findByTags",
+            _query_params=_query_params, 
+            **kwargs
+        )
+        response.raise_for_status()
+        return List[Pet].parse_raw(response.content)
+
+    async def getPetById(
+        self,
+        petId: int,
+        **kwargs: Any
+    ) -> Pet:
+        """
+        Returns a single pet
+        """ # noqa 
+
+        response = await self._request(
+            "GET",
+            f"/pet/{petId}",
+            **kwargs
+        )
+        response.raise_for_status()
+        return Pet.parse_raw(response.content)
+
+    async def updatePetWithForm(
+        self,
+        petId: int,
+        name: str,
+        status: str,
+        **kwargs: Any
+    ) -> None:
+        """
+        
+        """ # noqa 
+
+        _query_params = { 
+            "name": name,
+            "status": status,
+        }
+
+        response = await self._request(
+            "POST",
+            f"/pet/{petId}",
+            _query_params=_query_params, 
+            **kwargs
+        )
+        response.raise_for_status()
+
+    async def deletePet(
+        self,
+        petId: int,
+        api_key: Optional[str] = None,
+        **kwargs: Any
+    ) -> None:
+        """
+        delete a pet
+        """ # noqa 
+
+        _headers = { 
+            "api_key": api_key,
+        }
+
+        response = await self._request(
+            "DELETE",
+            f"/pet/{petId}",
+            _headers=_headers,
+            **kwargs
+        )
+        response.raise_for_status()
+
+    async def uploadFile(
+        self,
+        petId: int,
+        additionalMetadata: Optional[str] = None,
+        **kwargs: Any
+    ) -> ApiResponse:
+        """
+        
+        """ # noqa 
+
+        _query_params = { 
+            "additionalMetadata": additionalMetadata,
+        }
+
+        response = await self._request(
+            "POST",
+            f"/pet/{petId}/uploadImage",
+            _query_params=_query_params, 
+            **kwargs
+        )
+        response.raise_for_status()
+        return ApiResponse.parse_raw(response.content)
+
+    async def getInventory(
+        self,
+        **kwargs: Any
+    ) -> DictstrAny:
+        """
+        Returns a map of status codes to quantities
+        """ # noqa 
+
+        response = await self._request(
+            "GET",
+            "/store/inventory",
+            **kwargs
+        )
+        response.raise_for_status()
+        return DictstrAny.parse_raw(response.content)
+
+    async def placeOrder(
+        self,
+        body: Order,
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> Order:
+        """
+        Place a new order in the store
+        """ # noqa 
+
+        response = await self._request(
+            "POST",
+            "/store/order",
+            _body=body,
+            body_serializer_args=body_serializer_args,
+            **kwargs
+        )
+        response.raise_for_status()
+        return Order.parse_raw(response.content)
+
+    async def getOrderById(
+        self,
+        orderId: int,
+        **kwargs: Any
+    ) -> Order:
+        """
+        For valid response try integer IDs with value &lt;= 5 or &gt; 10. Other values will generate exceptions.
+        """ # noqa 
+
+        response = await self._request(
+            "GET",
+            f"/store/order/{orderId}",
+            **kwargs
+        )
+        response.raise_for_status()
+        return Order.parse_raw(response.content)
+
+    async def deleteOrder(
+        self,
+        orderId: int,
+        **kwargs: Any
+    ) -> None:
+        """
+        For valid response try integer IDs with value &lt; 1000. Anything above 1000 or nonintegers will generate API errors
+        """ # noqa 
+
+        response = await self._request(
+            "DELETE",
+            f"/store/order/{orderId}",
+            **kwargs
+        )
+        response.raise_for_status()
+
+    async def createUser(
+        self,
+        body: User,
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> User:
+        """
+        This can only be done by the logged in user.
+        """ # noqa 
+
+        response = await self._request(
+            "POST",
+            "/user",
+            _body=body,
+            body_serializer_args=body_serializer_args,
+            **kwargs
+        )
+        response.raise_for_status()
+        return User.parse_raw(response.content)
+
+    async def createUsersWithListInput(
+        self,
+        body: List[User],
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> User:
+        """
+        Creates list of users with given input array
+        """ # noqa 
+
+        response = await self._request(
+            "POST",
+            "/user/createWithList",
+            _body=body,
+            body_serializer_args=body_serializer_args,
+            **kwargs
+        )
+        response.raise_for_status()
+        return User.parse_raw(response.content)
+
+    async def loginUser(
+        self,
+        username: Optional[str] = None,
+        password: Optional[str] = None,
+        **kwargs: Any
+    ) -> None:
+        """
+        
+        """ # noqa 
+
+        _query_params = { 
+            "username": username,
+            "password": password,
+        }
+
+        response = await self._request(
+            "GET",
+            "/user/login",
+            _query_params=_query_params, 
+            **kwargs
+        )
+        response.raise_for_status()
+
+    async def logoutUser(
+        self,
+        **kwargs: Any
+    ) -> None:
+        """
+        
+        """ # noqa 
+
+        response = await self._request(
+            "GET",
+            "/user/logout",
+            **kwargs
+        )
+        response.raise_for_status()
+
+    async def getUserByName(
+        self,
+        username: str,
+        **kwargs: Any
+    ) -> User:
+        """
+        
+        """ # noqa 
+
+        response = await self._request(
+            "GET",
+            f"/user/{username}",
+            **kwargs
+        )
+        response.raise_for_status()
+        return User.parse_raw(response.content)
+
+    async def updateUser(
+        self,
+        body: User,
+        username: str,
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> None:
+        """
+        This can only be done by the logged in user.
+        """ # noqa 
+
+        response = await self._request(
+            "PUT",
+            f"/user/{username}",
+            _body=body,
+            body_serializer_args=body_serializer_args,
+            **kwargs
+        )
+        response.raise_for_status()
+
+    async def deleteUser(
+        self,
+        username: str,
+        **kwargs: Any
+    ) -> None:
+        """
+        This can only be done by the logged in user.
+        """ # noqa 
+
+        response = await self._request(
+            "DELETE",
+            f"/user/{username}",
+            **kwargs
+        )
+        response.raise_for_status()
+
+
diff --git a/tests/expected/swagger_petstore_client/base_client.py b/tests/expected/swagger_petstore_client/base_client.py
new file mode 100644
index 0000000..a31a066
--- /dev/null
+++ b/tests/expected/swagger_petstore_client/base_client.py
@@ -0,0 +1,61 @@
+from enum import Enum
+from typing import Any, Dict, Optional
+from uuid import UUID
+
+import httpx
+
+from pydantic import BaseModel
+
+
+class BaseClient(httpx.AsyncClient):
+    """
+    Base client for serializing Pydantic models and enums into httpx requests
+    """
+
+    @staticmethod
+    def _serialize_param(v: Any) -> Any:
+        if isinstance(v, Enum):
+            return v.value
+        elif isinstance(v, UUID):
+            return str(v)
+        else:
+            return v
+
+    async def _request(
+        self,
+        *args: Any,
+        _query_params: Dict[str, Any] = {},
+        _headers: Dict[str, Any] = {},
+        _multipart_data: Dict[str, Any] = {},
+        _body: Optional[BaseModel] = None,
+        body_serializer_args: Dict[str, Any] = {},
+        **kwargs: Any
+    ) -> httpx.Response:
+        """
+        Wrapper class for serializing pydantic models and enums into params/header/body
+        and sending a request
+        """
+
+        kwargs["params"] = {
+            **{k: self._serialize_param(v) for k, v in _query_params.items() if v is not None},
+            **kwargs.get("params", {}),
+        }
+
+        kwargs["headers"] = {
+            **{k: self._serialize_param(v) for k, v in _headers.items() if v is not None},
+            **kwargs.get("headers", {}),
+        }
+
+        kwargs["data"] = {
+            **{k: self._serialize_param(v) for k, v in _multipart_data.items() if v is not None},
+            **kwargs.get("data", {}),
+        }
+
+        if _body:
+            kwargs["content"] = _body.json(**body_serializer_args)
+            kwargs["headers"] = {
+                **{"Content-Type": "application/json"},
+                **kwargs.get("headers", {}),
+            }
+
+        return await self.request(*args, **kwargs)
diff --git a/tests/expected/swagger_petstore_client/models.py b/tests/expected/swagger_petstore_client/models.py
new file mode 100644
index 0000000..ece062f
--- /dev/null
+++ b/tests/expected/swagger_petstore_client/models.py
@@ -0,0 +1,66 @@
+from datetime import datetime
+from enum import Enum
+from typing import Any, Dict, List, Literal, Optional, Union
+from uuid import UUID
+
+from pydantic import BaseModel, Field
+
+
+class Order(BaseModel):
+    id: Optional[int]
+    petId: Optional[int]
+    quantity: Optional[int]
+    shipDate: Optional[datetime]
+    status: Optional[str]
+    complete: Optional[bool]
+
+
+class Address(BaseModel):
+    street: Optional[str]
+    city: Optional[str]
+    state: Optional[str]
+    zip: Optional[str]
+
+
+class Customer(BaseModel):
+    id: Optional[int]
+    username: Optional[str]
+    address: Optional[List[Address]]
+
+
+class Category(BaseModel):
+    id: Optional[int]
+    name: Optional[str]
+
+
+class User(BaseModel):
+    id: Optional[int]
+    username: Optional[str]
+    firstName: Optional[str]
+    lastName: Optional[str]
+    email: Optional[str]
+    password: Optional[str]
+    phone: Optional[str]
+    userStatus: Optional[int]
+
+
+class Tag(BaseModel):
+    id: Optional[int]
+    name: Optional[str]
+
+
+class Pet(BaseModel):
+    id: Optional[int]
+    name: str
+    category: Optional[Category]
+    photoUrls: List[str]
+    tags: Optional[List[Tag]]
+    status: Optional[str]
+
+
+class ApiResponse(BaseModel):
+    code: Optional[int]
+    type: Optional[str]
+    message: Optional[str]
+
+
diff --git a/tests/expected/pyproject.toml b/tests/expected/swagger_petstore_client/pyproject.toml
similarity index 95%
rename from tests/expected/pyproject.toml
rename to tests/expected/swagger_petstore_client/pyproject.toml
index 07c4057..c4db91e 100644
--- a/tests/expected/pyproject.toml
+++ b/tests/expected/swagger_petstore_client/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "test-project"
-version = "0.1.0"
+version = "1.0.11"
 description = "Autogenerated httpx async client for test-project"
 authors = ["Autogenerated Client <autogenerated@client.com>"]
 
diff --git a/tests/input_app.py b/tests/inputs/fastapi_app.py
similarity index 100%
rename from tests/input_app.py
rename to tests/inputs/fastapi_app.py
diff --git a/tests/inputs/swagger-petstore.json b/tests/inputs/swagger-petstore.json
new file mode 100644
index 0000000..1573eb8
--- /dev/null
+++ b/tests/inputs/swagger-petstore.json
@@ -0,0 +1,1231 @@
+{
+    "openapi": "3.0.3",
+    "info": {
+        "title": "Swagger Petstore - OpenAPI 3.0",
+        "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification.  You can find out more about\nSwagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\n_If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)",
+        "termsOfService": "http://swagger.io/terms/",
+        "contact": {
+            "email": "apiteam@swagger.io"
+        },
+        "license": {
+            "name": "Apache 2.0",
+            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+        },
+        "version": "1.0.11"
+    },
+    "externalDocs": {
+        "description": "Find out more about Swagger",
+        "url": "http://swagger.io"
+    },
+    "servers": [
+        {
+            "url": "https://petstore3.swagger.io/api/v3"
+        }
+    ],
+    "tags": [
+        {
+            "name": "pet",
+            "description": "Everything about your Pets",
+            "externalDocs": {
+                "description": "Find out more",
+                "url": "http://swagger.io"
+            }
+        },
+        {
+            "name": "store",
+            "description": "Access to Petstore orders",
+            "externalDocs": {
+                "description": "Find out more about our store",
+                "url": "http://swagger.io"
+            }
+        },
+        {
+            "name": "user",
+            "description": "Operations about user"
+        }
+    ],
+    "paths": {
+        "/pet": {
+            "put": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Update an existing pet",
+                "description": "Update an existing pet by Id",
+                "operationId": "updatePet",
+                "requestBody": {
+                    "description": "Update an existent pet in the store",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        }
+                    },
+                    "required": true
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Pet not found"
+                    },
+                    "422": {
+                        "description": "Validation exception"
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            },
+            "post": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Add a new pet to the store",
+                "description": "Add a new pet to the store",
+                "operationId": "addPet",
+                "requestBody": {
+                    "description": "Create a new pet in the store",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Pet"
+                            }
+                        }
+                    },
+                    "required": true
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid input"
+                    },
+                    "422": {
+                        "description": "Validation exception"
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            }
+        },
+        "/pet/findByStatus": {
+            "get": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Finds Pets by status",
+                "description": "Multiple status values can be provided with comma separated strings",
+                "operationId": "findPetsByStatus",
+                "parameters": [
+                    {
+                        "name": "status",
+                        "in": "query",
+                        "description": "Status values that need to be considered for filter",
+                        "required": false,
+                        "explode": true,
+                        "schema": {
+                            "type": "string",
+                            "default": "available",
+                            "enum": [
+                                "available",
+                                "pending",
+                                "sold"
+                            ]
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid status value"
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            }
+        },
+        "/pet/findByTags": {
+            "get": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Finds Pets by tags",
+                "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+                "operationId": "findPetsByTags",
+                "parameters": [
+                    {
+                        "name": "tags",
+                        "in": "query",
+                        "description": "Tags to filter by",
+                        "required": false,
+                        "explode": true,
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "type": "string"
+                            }
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "type": "array",
+                                    "items": {
+                                        "$ref": "#/components/schemas/Pet"
+                                    }
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid tag value"
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            }
+        },
+        "/pet/{petId}": {
+            "get": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Find pet by ID",
+                "description": "Returns a single pet",
+                "operationId": "getPetById",
+                "parameters": [
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "ID of pet to return",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Pet"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Pet not found"
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    },
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            },
+            "post": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Updates a pet in the store with form data",
+                "description": "",
+                "operationId": "updatePetWithForm",
+                "parameters": [
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "ID of pet that needs to be updated",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    },
+                    {
+                        "name": "name",
+                        "in": "query",
+                        "description": "Name of pet that needs to be updated",
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "name": "status",
+                        "in": "query",
+                        "description": "Status of pet that needs to be updated",
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "responses": {
+                    "400": {
+                        "description": "Invalid input"
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            },
+            "delete": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "Deletes a pet",
+                "description": "delete a pet",
+                "operationId": "deletePet",
+                "parameters": [
+                    {
+                        "name": "api_key",
+                        "in": "header",
+                        "description": "",
+                        "required": false,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "Pet id to delete",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+                    "400": {
+                        "description": "Invalid pet value"
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            }
+        },
+        "/pet/{petId}/uploadImage": {
+            "post": {
+                "tags": [
+                    "pet"
+                ],
+                "summary": "uploads an image",
+                "description": "",
+                "operationId": "uploadFile",
+                "parameters": [
+                    {
+                        "name": "petId",
+                        "in": "path",
+                        "description": "ID of pet to update",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    },
+                    {
+                        "name": "additionalMetadata",
+                        "in": "query",
+                        "description": "Additional Metadata",
+                        "required": false,
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "requestBody": {
+                    "content": {
+                        "application/octet-stream": {
+                            "schema": {
+                                "type": "string",
+                                "format": "binary"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/ApiResponse"
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "petstore_auth": [
+                            "write:pets",
+                            "read:pets"
+                        ]
+                    }
+                ]
+            }
+        },
+        "/store/inventory": {
+            "get": {
+                "tags": [
+                    "store"
+                ],
+                "summary": "Returns pet inventories by status",
+                "description": "Returns a map of status codes to quantities",
+                "operationId": "getInventory",
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "object",
+                                    "additionalProperties": {
+                                        "type": "integer",
+                                        "format": "int32"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "security": [
+                    {
+                        "api_key": []
+                    }
+                ]
+            }
+        },
+        "/store/order": {
+            "post": {
+                "tags": [
+                    "store"
+                ],
+                "summary": "Place an order for a pet",
+                "description": "Place a new order in the store",
+                "operationId": "placeOrder",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Order"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Order"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/Order"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Order"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid input"
+                    },
+                    "422": {
+                        "description": "Validation exception"
+                    }
+                }
+            }
+        },
+        "/store/order/{orderId}": {
+            "get": {
+                "tags": [
+                    "store"
+                ],
+                "summary": "Find purchase order by ID",
+                "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.",
+                "operationId": "getOrderById",
+                "parameters": [
+                    {
+                        "name": "orderId",
+                        "in": "path",
+                        "description": "ID of order that needs to be fetched",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Order"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Order"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Order not found"
+                    }
+                }
+            },
+            "delete": {
+                "tags": [
+                    "store"
+                ],
+                "summary": "Delete purchase order by ID",
+                "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors",
+                "operationId": "deleteOrder",
+                "parameters": [
+                    {
+                        "name": "orderId",
+                        "in": "path",
+                        "description": "ID of the order that needs to be deleted",
+                        "required": true,
+                        "schema": {
+                            "type": "integer",
+                            "format": "int64"
+                        }
+                    }
+                ],
+                "responses": {
+                    "400": {
+                        "description": "Invalid ID supplied"
+                    },
+                    "404": {
+                        "description": "Order not found"
+                    }
+                }
+            }
+        },
+        "/user": {
+            "post": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Create user",
+                "description": "This can only be done by the logged in user.",
+                "operationId": "createUser",
+                "requestBody": {
+                    "description": "Created user object",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "default": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/user/createWithList": {
+            "post": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Creates list of users with given input array",
+                "description": "Creates list of users with given input array",
+                "operationId": "createUsersWithListInput",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "type": "array",
+                                "items": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "Successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    },
+                    "default": {
+                        "description": "successful operation"
+                    }
+                }
+            }
+        },
+        "/user/login": {
+            "get": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Logs user into the system",
+                "description": "",
+                "operationId": "loginUser",
+                "parameters": [
+                    {
+                        "name": "username",
+                        "in": "query",
+                        "description": "The user name for login",
+                        "required": false,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "name": "password",
+                        "in": "query",
+                        "description": "The password for login in clear text",
+                        "required": false,
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "headers": {
+                            "X-Rate-Limit": {
+                                "description": "calls per hour allowed by the user",
+                                "schema": {
+                                    "type": "integer",
+                                    "format": "int32"
+                                }
+                            },
+                            "X-Expires-After": {
+                                "description": "date in UTC when token expires",
+                                "schema": {
+                                    "type": "string",
+                                    "format": "date-time"
+                                }
+                            }
+                        },
+                        "content": {
+                            "application/xml": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            },
+                            "application/json": {
+                                "schema": {
+                                    "type": "string"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid username/password supplied"
+                    }
+                }
+            }
+        },
+        "/user/logout": {
+            "get": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Logs out current logged in user session",
+                "description": "",
+                "operationId": "logoutUser",
+                "parameters": [],
+                "responses": {
+                    "default": {
+                        "description": "successful operation"
+                    }
+                }
+            }
+        },
+        "/user/{username}": {
+            "get": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Get user by user name",
+                "description": "",
+                "operationId": "getUserByName",
+                "parameters": [
+                    {
+                        "name": "username",
+                        "in": "path",
+                        "description": "The name that needs to be fetched. Use user1 for testing. ",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "successful operation",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            },
+                            "application/xml": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User"
+                                }
+                            }
+                        }
+                    },
+                    "400": {
+                        "description": "Invalid username supplied"
+                    },
+                    "404": {
+                        "description": "User not found"
+                    }
+                }
+            },
+            "put": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Update user",
+                "description": "This can only be done by the logged in user.",
+                "operationId": "updateUser",
+                "parameters": [
+                    {
+                        "name": "username",
+                        "in": "path",
+                        "description": "name that need to be deleted",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "requestBody": {
+                    "description": "Update an existent user in the store",
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/xml": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        },
+                        "application/x-www-form-urlencoded": {
+                            "schema": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "default": {
+                        "description": "successful operation"
+                    }
+                }
+            },
+            "delete": {
+                "tags": [
+                    "user"
+                ],
+                "summary": "Delete user",
+                "description": "This can only be done by the logged in user.",
+                "operationId": "deleteUser",
+                "parameters": [
+                    {
+                        "name": "username",
+                        "in": "path",
+                        "description": "The name that needs to be deleted",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    }
+                ],
+                "responses": {
+                    "400": {
+                        "description": "Invalid username supplied"
+                    },
+                    "404": {
+                        "description": "User not found"
+                    }
+                }
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "Order": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 10
+                    },
+                    "petId": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 198772
+                    },
+                    "quantity": {
+                        "type": "integer",
+                        "format": "int32",
+                        "example": 7
+                    },
+                    "shipDate": {
+                        "type": "string",
+                        "format": "date-time"
+                    },
+                    "status": {
+                        "type": "string",
+                        "description": "Order Status",
+                        "example": "approved",
+                        "enum": [
+                            "placed",
+                            "approved",
+                            "delivered"
+                        ]
+                    },
+                    "complete": {
+                        "type": "boolean"
+                    }
+                },
+                "xml": {
+                    "name": "order"
+                }
+            },
+            "Customer": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 100000
+                    },
+                    "username": {
+                        "type": "string",
+                        "example": "fehguy"
+                    },
+                    "address": {
+                        "type": "array",
+                        "xml": {
+                            "name": "addresses",
+                            "wrapped": true
+                        },
+                        "items": {
+                            "$ref": "#/components/schemas/Address"
+                        }
+                    }
+                },
+                "xml": {
+                    "name": "customer"
+                }
+            },
+            "Address": {
+                "type": "object",
+                "properties": {
+                    "street": {
+                        "type": "string",
+                        "example": "437 Lytton"
+                    },
+                    "city": {
+                        "type": "string",
+                        "example": "Palo Alto"
+                    },
+                    "state": {
+                        "type": "string",
+                        "example": "CA"
+                    },
+                    "zip": {
+                        "type": "string",
+                        "example": "94301"
+                    }
+                },
+                "xml": {
+                    "name": "address"
+                }
+            },
+            "Category": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 1
+                    },
+                    "name": {
+                        "type": "string",
+                        "example": "Dogs"
+                    }
+                },
+                "xml": {
+                    "name": "category"
+                }
+            },
+            "User": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 10
+                    },
+                    "username": {
+                        "type": "string",
+                        "example": "theUser"
+                    },
+                    "firstName": {
+                        "type": "string",
+                        "example": "John"
+                    },
+                    "lastName": {
+                        "type": "string",
+                        "example": "James"
+                    },
+                    "email": {
+                        "type": "string",
+                        "example": "john@email.com"
+                    },
+                    "password": {
+                        "type": "string",
+                        "example": "12345"
+                    },
+                    "phone": {
+                        "type": "string",
+                        "example": "12345"
+                    },
+                    "userStatus": {
+                        "type": "integer",
+                        "description": "User Status",
+                        "format": "int32",
+                        "example": 1
+                    }
+                },
+                "xml": {
+                    "name": "user"
+                }
+            },
+            "Tag": {
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "name": {
+                        "type": "string"
+                    }
+                },
+                "xml": {
+                    "name": "tag"
+                }
+            },
+            "Pet": {
+                "required": [
+                    "name",
+                    "photoUrls"
+                ],
+                "type": "object",
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64",
+                        "example": 10
+                    },
+                    "name": {
+                        "type": "string",
+                        "example": "doggie"
+                    },
+                    "category": {
+                        "$ref": "#/components/schemas/Category"
+                    },
+                    "photoUrls": {
+                        "type": "array",
+                        "xml": {
+                            "wrapped": true
+                        },
+                        "items": {
+                            "type": "string",
+                            "xml": {
+                                "name": "photoUrl"
+                            }
+                        }
+                    },
+                    "tags": {
+                        "type": "array",
+                        "xml": {
+                            "wrapped": true
+                        },
+                        "items": {
+                            "$ref": "#/components/schemas/Tag"
+                        }
+                    },
+                    "status": {
+                        "type": "string",
+                        "description": "pet status in the store",
+                        "enum": [
+                            "available",
+                            "pending",
+                            "sold"
+                        ]
+                    }
+                },
+                "xml": {
+                    "name": "pet"
+                }
+            },
+            "ApiResponse": {
+                "type": "object",
+                "properties": {
+                    "code": {
+                        "type": "integer",
+                        "format": "int32"
+                    },
+                    "type": {
+                        "type": "string"
+                    },
+                    "message": {
+                        "type": "string"
+                    }
+                },
+                "xml": {
+                    "name": "##default"
+                }
+            }
+        },
+        "requestBodies": {
+            "Pet": {
+                "description": "Pet object that needs to be added to the store",
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "$ref": "#/components/schemas/Pet"
+                        }
+                    },
+                    "application/xml": {
+                        "schema": {
+                            "$ref": "#/components/schemas/Pet"
+                        }
+                    }
+                }
+            },
+            "UserArray": {
+                "description": "List of user object",
+                "content": {
+                    "application/json": {
+                        "schema": {
+                            "type": "array",
+                            "items": {
+                                "$ref": "#/components/schemas/User"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "securitySchemes": {
+            "petstore_auth": {
+                "type": "oauth2",
+                "flows": {
+                    "implicit": {
+                        "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize",
+                        "scopes": {
+                            "write:pets": "modify pets in your account",
+                            "read:pets": "read your pets"
+                        }
+                    }
+                }
+            },
+            "api_key": {
+                "type": "apiKey",
+                "name": "api_key",
+                "in": "header"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_fastapi_client.py b/tests/test_fastapi_client.py
new file mode 100644
index 0000000..b270910
--- /dev/null
+++ b/tests/test_fastapi_client.py
@@ -0,0 +1,112 @@
+import json
+import typing as t
+
+import httpx
+import pytest
+import respx
+
+from tests.utils import does_not_raise
+
+from .expected.fastapi_app_client.apis import Api as FastApiAppClient
+from .expected.fastapi_app_client.models import Foo, Document, PaginatedFoo
+
+
+client_base_url = "https://domain.tld"
+client = FastApiAppClient(base_url=client_base_url)
+
+foo_id = "00000000-0000-0000-0000-000000000000"
+
+
+@respx.mock
+@pytest.mark.asyncio
+async def test_read_foo() -> None:
+    """
+    Check that the client sends a GET request to the correct endpoint
+    and returns the expected model (parsing the response content correctly).
+    """
+
+    expected_response = Foo(field_1="field_1", field_2=1)
+
+    route = respx.get(f"{client_base_url}/api/foo/{foo_id}")
+    route.mock(return_value=httpx.Response(200, content=expected_response.json()))
+
+    response = await client.read_foo_api_foo__foo_id__get(foo_id=foo_id)
+
+    assert route.called
+    assert response == expected_response
+
+
+@respx.mock
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    "status_code, expectation, expected_response",
+    [
+        (200, does_not_raise(), Foo(field_1="field_1", field_2=1, field_3=1.1).dict()),
+        (400, pytest.raises(httpx.HTTPStatusError), {"detail": "Bad Request"}),
+        (401, pytest.raises(httpx.HTTPStatusError), {"detail": "Unauthorized"}),
+        (403, pytest.raises(httpx.HTTPStatusError), {"detail": "Forbidden"}),
+        (404, pytest.raises(httpx.HTTPStatusError), {"detail": "Not Found"}),
+        (500, pytest.raises(httpx.HTTPStatusError), {"detail": "Internal Server Error"}),
+    ],
+)
+async def test_read_foo_raises(
+    status_code: int, expectation: t.Any, expected_response: t.Any
+) -> None:
+    """
+    Check that the client sends a GET request to the correct endpoint
+    and raises an exception when the response status code is not 2xx.
+    """
+
+    route = respx.get(f"{client_base_url}/api/foo/{foo_id}")
+    route.mock(return_value=httpx.Response(status_code, content=json.dumps(expected_response)))
+
+    with expectation:
+        await client.read_foo_api_foo__foo_id__get(foo_id=foo_id)
+
+
+@respx.mock
+@pytest.mark.asyncio
+async def test_create_foo() -> None:
+    foo = Foo(field_1="field_1", field_2=1)
+
+    route = respx.post(f"{client_base_url}/api/foo")
+    route.mock(return_value=httpx.Response(200, content=foo.json()))
+
+    response = await client.create_foo_api_foo_post(body=foo)
+
+    assert route.called
+    assert response == foo
+
+
+@respx.mock
+@pytest.mark.asyncio
+async def test_upload_doc() -> None:
+    file_data = ("file_name", "file")
+    document = Document(field_1="field_1")
+
+    route = respx.post(f"{client_base_url}/api/foo/{foo_id}/documents")
+    route.mock(return_value=httpx.Response(200, content=document.json()))
+
+    response = await client.upload_doc_api_foo__foo_id__documents_post(file=file_data, foo_id=foo_id)
+
+    assert route.called
+    assert response == document
+
+
+@respx.mock
+@pytest.mark.asyncio
+async def test_list_foos() -> None:
+    paginated_response = PaginatedFoo(
+        results=[Foo(field_1="field_1", field_2=1)],
+        offset=0,
+        limit=10,
+        size=1,
+    )
+
+    route = respx.get(f"{client_base_url}/api/foo")
+    route.mock(return_value=httpx.Response(200, content=paginated_response.json()))
+
+    response = await client.list_foos_api_foo_get()
+
+    assert route.called
+    assert response == paginated_response
diff --git a/tests/test_generator.py b/tests/test_fastapi_client_generator.py
similarity index 54%
rename from tests/test_generator.py
rename to tests/test_fastapi_client_generator.py
index 9de29ba..6083c45 100644
--- a/tests/test_generator.py
+++ b/tests/test_fastapi_client_generator.py
@@ -1,25 +1,20 @@
 import filecmp
 import os
 
-from copy import deepcopy
 from pathlib import Path
 from typing import Any, Dict
 
-import pytest
-
-from python_client_generator.exceptions import UnsupportedOpenAPISpec
 from python_client_generator.generate_apis import generate_apis
 from python_client_generator.generate_base_client import generate_base_client
 from python_client_generator.generate_models import generate_models
 from python_client_generator.generate_pyproject import generate_pyproject
-from python_client_generator.utils import assert_openapi_version
 
 
-EXPECTED_PATH = Path(os.path.dirname(os.path.realpath(__file__))) / "expected"
+EXPECTED_PATH = Path(os.path.dirname(os.path.realpath(__file__))) / "expected/fastapi_app_client"
 
 
-def test_models(openapi: Dict[str, Any], tmp_path: Path) -> None:
-    generate_models(openapi, tmp_path / "models.py")
+def test_models(fastapi_app_openapi: Dict[str, Any], tmp_path: Path) -> None:
+    generate_models(fastapi_app_openapi, tmp_path / "models.py")
     assert filecmp.cmp(EXPECTED_PATH / "models.py", tmp_path / "models.py", shallow=False) is True
 
 
@@ -31,21 +26,16 @@ def test_base_client(tmp_path: Path) -> None:
     )
 
 
-def test_apis(openapi: Dict[str, Any], tmp_path: Path) -> None:
-    generate_apis(openapi, tmp_path / "apis.py", group_by_tags=False, sync=False)
+def test_apis(fastapi_app_openapi: Dict[str, Any], tmp_path: Path) -> None:
+    generate_apis(fastapi_app_openapi, tmp_path / "apis.py", group_by_tags=False, sync=False)
     assert filecmp.cmp(EXPECTED_PATH / "apis.py", tmp_path / "apis.py", shallow=False) is True
 
 
-def test_pyproject(openapi: Dict[str, Any], tmp_path: Path) -> None:
-    generate_pyproject(openapi, tmp_path / "pyproject.toml", project_name="test-project")
+def test_pyproject(fastapi_app_openapi: Dict[str, Any], tmp_path: Path) -> None:
+    generate_pyproject(
+        fastapi_app_openapi, tmp_path / "pyproject.toml", project_name="fastapi-project"
+    )
     assert (
         filecmp.cmp(EXPECTED_PATH / "pyproject.toml", tmp_path / "pyproject.toml", shallow=False)
         is True
     )
-
-
-def test_assert_openapi_version(openapi: Dict[str, Any]) -> None:
-    openapi_copy = deepcopy(openapi)
-    openapi_copy["openapi"] = "2.0.0"
-    with pytest.raises(UnsupportedOpenAPISpec):
-        assert_openapi_version(openapi_copy)
diff --git a/tests/test_swagger_file_generator.py b/tests/test_swagger_file_generator.py
new file mode 100644
index 0000000..dc55814
--- /dev/null
+++ b/tests/test_swagger_file_generator.py
@@ -0,0 +1,43 @@
+import filecmp
+import os
+
+from pathlib import Path
+from typing import Any, Dict
+
+from python_client_generator.generate_apis import generate_apis
+from python_client_generator.generate_base_client import generate_base_client
+from python_client_generator.generate_models import generate_models
+from python_client_generator.generate_pyproject import generate_pyproject
+
+
+EXPECTED_PATH = (
+    Path(os.path.dirname(os.path.realpath(__file__))) / "expected/swagger_petstore_client"
+)
+
+
+def test_models(swagger_petstore_openapi: Dict[str, Any], tmp_path: Path) -> None:
+    generate_models(swagger_petstore_openapi, tmp_path / "models.py")
+    assert filecmp.cmp(EXPECTED_PATH / "models.py", tmp_path / "models.py", shallow=False) is True
+
+
+def test_base_client(tmp_path: Path) -> None:
+    generate_base_client(tmp_path / "base_client.py", sync=False)
+    assert (
+        filecmp.cmp(EXPECTED_PATH / "base_client.py", tmp_path / "base_client.py", shallow=False)
+        is True
+    )
+
+
+def test_apis(swagger_petstore_openapi: Dict[str, Any], tmp_path: Path) -> None:
+    generate_apis(swagger_petstore_openapi, tmp_path / "apis.py", group_by_tags=False, sync=False)
+    assert filecmp.cmp(EXPECTED_PATH / "apis.py", tmp_path / "apis.py", shallow=False) is True
+
+
+def test_pyproject(swagger_petstore_openapi: Dict[str, Any], tmp_path: Path) -> None:
+    generate_pyproject(
+        swagger_petstore_openapi, tmp_path / "pyproject.toml", project_name="test-project"
+    )
+    assert (
+        filecmp.cmp(EXPECTED_PATH / "pyproject.toml", tmp_path / "pyproject.toml", shallow=False)
+        is True
+    )
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..86cd25a
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,139 @@
+import typing as t
+
+import pytest
+
+from python_client_generator.exceptions import UnsupportedOpenAPISpec
+from python_client_generator.utils import (
+    add_schema_title_if_missing,
+    assert_openapi_version,
+    dereference_swagger,
+)
+from tests.utils import does_not_raise
+
+
+@pytest.mark.parametrize(
+    "raw_schema, expected_schema",
+    [
+        (
+            {"X": {"type": "string", "enum": ["A", "B"]}},
+            {
+                "X": {
+                    "title": "X",
+                    "type": "string",
+                    "enum": ["A", "B"],
+                }
+            },
+        ),  # Should add title to enum schema
+        (
+            {
+                "X": {
+                    "type": "object",
+                    "properties": {
+                        "a": {"type": "string"},
+                        "b": {"type": "string"},
+                    },
+                }
+            },
+            {
+                "X": {
+                    "title": "X",
+                    "type": "object",
+                    "properties": {
+                        "a": {"type": "string"},
+                        "b": {"type": "string"},
+                    },
+                }
+            },
+        ),  # Should add title to object schema
+        (
+            {
+                "X": {
+                    "title": "X",
+                    "type": "string",
+                    "enum": ["A", "B"],
+                }
+            },
+            {
+                "X": {
+                    "title": "X",
+                    "type": "string",
+                    "enum": ["A", "B"],
+                }
+            },
+        ),  # Shouldn't fail if title is already present
+    ],
+)
+def test_add_schema_title_if_missing(
+    raw_schema: t.Dict[str, t.Any], expected_schema: t.Dict[str, t.Any]
+) -> None:
+    assert add_schema_title_if_missing(raw_schema) == expected_schema
+
+
+@pytest.mark.parametrize(
+    "schema, expectation",
+    [
+        ({"openapi": "3.0.0"}, does_not_raise()),  # Should not raise for 3.x.x
+        ({"openapi": "2.0.0"}, pytest.raises(UnsupportedOpenAPISpec)),  # Should raise for 2.x.x
+        ({}, pytest.raises(UnsupportedOpenAPISpec)),  # Should raise for missing openapi key
+    ],
+)
+def test_assert_openapi_version(schema: t.Dict[str, t.Any], expectation: t.Any) -> None:
+    with expectation:
+        assert_openapi_version(schema)
+
+
+class TestDereferenceSwagger:
+    def test_dereference_swagger_with_dict_containing_ref(self) -> None:
+        original: t.Dict[str, t.Any] = {
+            "a": {
+                "$ref": "#/schemas/X",
+            },
+            "schemas": {
+                "X": {
+                    "type": "object",
+                    "properties": {
+                        "a": {"type": "string"},
+                        "b": {"type": "integer"},
+                    },
+                }
+            },
+        }
+        expected: t.Dict[str, t.Any] = original.copy()
+        expected["a"] = original["schemas"]["X"]
+
+        assert dereference_swagger(original, original) == expected
+
+    def test_dereference_swagger_with_dict_not_containing_ref(self) -> None:
+        original: t.Dict[str, t.Any] = {
+            "type": "object",
+            "properties": {"name": {"type": "string"}},
+        }
+
+        assert dereference_swagger(original, original) == original
+
+    def test_dereference_swagger_with_list(self) -> None:
+        original: t.Dict[str, t.Any] = {
+            "a": {
+                "$ref": "#/schemas/X",
+            },
+            "b": [
+                {"$ref": "#/schemas/X"},
+                {"$ref": "#/schemas/X"},
+            ],
+            "schemas": {
+                "X": {
+                    "type": "object",
+                    "properties": {
+                        "a": {"type": "string"},
+                        "b": {"type": "integer"},
+                    },
+                }
+            },
+        }
+        current: t.Dict[str, t.Any] = original.copy()
+
+        expected: t.Dict[str, t.Any] = original.copy()
+        expected["a"] = original["schemas"]["X"]
+        expected["b"] = [original["schemas"]["X"], original["schemas"]["X"]]
+
+        assert dereference_swagger(current, original) == expected
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..f799271
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,8 @@
+import typing as t
+
+from contextlib import contextmanager
+
+
+@contextmanager
+def does_not_raise() -> t.Generator:
+    yield