clientele 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clientele-2.1.0 → clientele-2.2.0}/.claude/settings.local.json +2 -1
- {clientele-2.1.0 → clientele-2.2.0}/.github/workflows/ci.yml +6 -2
- {clientele-2.1.0 → clientele-2.2.0}/CHANGELOG.md +9 -1
- {clientele-2.1.0 → clientele-2.2.0}/PKG-INFO +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/SECURITY.md +2 -2
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/config_py.jinja2 +2 -4
- clientele-2.2.0/clientele/generators/api/templates/schema_root_model.jinja2 +4 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/schemas_py.jinja2 +2 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/base_clients.py +8 -4
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/cicerone_compat.py +8 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/shared/generators/schemas.py +17 -5
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/shared/utils.py +20 -4
- clientele-2.2.0/clientele/schemas.py +20 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/settings.py +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/docs/CHANGELOG.md +9 -1
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-overview.md +3 -25
- clientele-2.2.0/docs/injected-parameters.md +135 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/install.md +1 -1
- clientele-2.2.0/example_openapi_specs/issue_248.json +85 -0
- {clientele-2.1.0 → clientele-2.2.0}/mkdocs.yml +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/pyproject.toml +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/client/MANIFEST.md +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/client/schemas.py +4 -1
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/MANIFEST.md +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/schemas.py +4 -1
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/client/MANIFEST.md +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/client/schemas.py +4 -1
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/async_test_client/MANIFEST.md +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/async_test_client/schemas.py +2 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/MANIFEST.md +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_client/MANIFEST.md +1 -1
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_client/schemas.py +2 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/shared/test_utils_coverage.py +22 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/test_base_and_basic_coverage.py +42 -0
- clientele-2.2.0/tests/generators/test_schemas_coverage.py +233 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_complex_schemas.py +10 -9
- clientele-2.2.0/tests/test_issue_248.py +126 -0
- {clientele-2.1.0 → clientele-2.2.0}/uv.lock +1 -1
- clientele-2.1.0/docs/mypy.md +0 -63
- clientele-2.1.0/tests/generators/test_schemas_coverage.py +0 -96
- {clientele-2.1.0 → clientele-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/.github/compatibility.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/.github/dependabot.yml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/.github/pull_request_template.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/.gitignore +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/CONTRIBUTING.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/CONTRIBUTORS.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/LICENSE +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/Makefile +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/PUBLISHING.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/README.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/exceptions.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/requests.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/stream/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/stream/parser.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/api/type_utils.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/cache/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/cache/backends.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/cache/decorator.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/cache/key_generator.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/cache/types.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/cli.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/README.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/generator.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/generators/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/generators/clients.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/generators/schemas.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/api_get_method.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/api_post_method.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/client_py.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/manifest.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/pyproject.toml.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/schema_class.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/schema_helpers.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/templates/schema_type_alias.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/api/writer.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/base.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/generator.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/templates/client_py.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/templates/config_py.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/templates/manifest.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/templates/schemas_py.jinja2 +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/basic/writer.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/schema_utils.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/shared/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/generators/shared/generators/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/graphql/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/graphql/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/aiohttp_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/backends.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/fake_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/httpx_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/niquests_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/requests_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/response.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/http/status_codes.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/mypy.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/py.typed +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/retries/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/retries/decorators.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/testing.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/clientele/utils.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/CNAME +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-async.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-authentication.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-cache.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-configuration.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-direct-requests.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-examples.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-exceptions.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-graphql.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-http-backends.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-logging.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-retries.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-stream.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/api-testing.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/clientele_generate.gif +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/clientele_header.png +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/favicon.png +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/index.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/openapi-cli.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/server-django-ninja.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/server-drf.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/docs/server-fastapi.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/example_openapi_specs/best.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/example_openapi_specs/complex_schemas.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/example_openapi_specs/simple.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/example_openapi_specs/test_303.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/README.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/api.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/client/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/client/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/client/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/client/pyproject.toml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/example_project/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/example_project/settings.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/example_project/urls.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/manage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_ninja/openapi.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/README.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/pyproject.toml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/example_project/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/example_project/settings.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/example_project/urls.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/manage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/openapi.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/apps.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/migrations/0001_initial.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/migrations/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/models.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/serializers.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/views.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/README.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/client/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/client/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/client/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/client/pyproject.toml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/main.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/server_examples/fastapi/openapi.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/retries/test_retry.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/stream/test_parser.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_api_client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_api_client_logging.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_api_client_singleton.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_api_response_parser.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_api_typed_dict.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_direct_requests.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_requests.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_stream.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_stream_httpx.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api/test_type_utils.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/async_test_client/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/async_test_client/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/async_test_client/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/async_test_client/pyproject.toml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/schemas.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_client/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_client/client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_client/config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/api_clients/test_client/pyproject.toml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/cache/fixtures.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/cache/test_backends.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/cache/test_decorator.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/cache/test_integration.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/cache/test_key_generator.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/callback_example.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/complex_api.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/README.md +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/api-with-examples.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/callback-example.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/link-example.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/non-oauth-scopes.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/petstore-expanded.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/petstore.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/tictactoe.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/uspto.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/webhook-example.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/petstore_openapi3.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/realworld/1password.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/realworld/ably.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/realworld/google.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/realworld/medium.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/realworld/spacetraders.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/realworld/twilio.yaml +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/fixtures/regression/dep_query_alias.json +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/framework/test_framework_generator_coverage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/framework/test_framework_generator_integration.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/integration_utils.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/shared/__init__.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/shared/test_utils.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/test_base.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/generators/test_cicerone_compat.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/graphql/test_graphql_client.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/http/backends/test_aiohttp_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/http/backends/test_backend_config.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/http/backends/test_fake_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/http/backends/test_httpx_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/http/backends/test_niquests_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/http/backends/test_requests_backend.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/mypy/mypy_plugin_test_config.ini +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/mypy/test_mypy_plugin.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_additional_coverage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_cli.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_cli_coverage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_cli_full_coverage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_edge_case_coverage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_fixture_schemas.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_generator_coverage.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_nullable_fields.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_openapi_31_null_type.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_ref_handling.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_settings.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_testing_utilities.py +0 -0
- {clientele-2.1.0 → clientele-2.2.0}/tests/test_utils.py +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
name: CI - Lint and Test
|
|
2
2
|
|
|
3
|
-
on:
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
4
8
|
|
|
5
9
|
jobs:
|
|
6
10
|
build:
|
|
7
11
|
runs-on: ubuntu-latest
|
|
8
12
|
strategy:
|
|
9
13
|
matrix:
|
|
10
|
-
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev"]
|
|
11
15
|
steps:
|
|
12
16
|
#----------------------------------------------
|
|
13
17
|
# check-out repo and set-up python
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
- Support OpenAPI `default` and `const` in schema generation ([#247](https://github.com/phalt/clientele/pull/247)).
|
|
6
|
+
- Fix `$ref` parameter types and double-nullable handling in client generation ([#247](https://github.com/phalt/clientele/pull/247)).
|
|
7
|
+
- Fix array response schemas being emitted as plain type aliases (`list[...]`) which pydantic rejects in `response_map` — they are now emitted as `clientele.schemas.ListResponse` subclasses, which support `len()`, indexing, and iteration directly ([#248](https://github.com/phalt/clientele/issues/248)).
|
|
8
|
+
- Fix generated `config.py` documenting `API_BASE_URL` as the environment variable name when pydantic-settings reads `BASE_URL` — the example now uses the correct name ([#248](https://github.com/phalt/clientele/issues/248)).
|
|
9
|
+
- Initial CI support for Python 3.15
|
|
10
|
+
|
|
11
|
+
## 2.1.0
|
|
4
12
|
|
|
5
13
|
- Introduce `RequestsHTTPBackend` for making HTTP requests using the `requests` library.
|
|
6
14
|
- This backend implements synchronous HTTP requests only.
|
|
@@ -19,14 +19,12 @@ class Config(clientele_api.BaseConfig):
|
|
|
19
19
|
|
|
20
20
|
Example:
|
|
21
21
|
# From environment variables
|
|
22
|
-
export
|
|
23
|
-
export BEARER_TOKEN="my-secret-token"
|
|
22
|
+
export BASE_URL="https://api.example.com"
|
|
24
23
|
config = Config()
|
|
25
24
|
|
|
26
25
|
# Direct instantiation
|
|
27
26
|
config = Config(
|
|
28
|
-
|
|
29
|
-
bearer_token="my-token",
|
|
27
|
+
base_url="https://api.example.com",
|
|
30
28
|
timeout=10.0
|
|
31
29
|
)
|
|
32
30
|
"""
|
|
@@ -143,16 +143,20 @@ class BaseClientsGenerator:
|
|
|
143
143
|
required = param.get("required", False) or in_ != "query"
|
|
144
144
|
if in_ == "query":
|
|
145
145
|
# URL query string values
|
|
146
|
+
param_type = utils.resolve_forward_refs_for_client(utils.get_type(param["schema"]))
|
|
146
147
|
if required:
|
|
147
|
-
query_args[clean_key] =
|
|
148
|
+
query_args[clean_key] = param_type
|
|
148
149
|
else:
|
|
149
|
-
|
|
150
|
+
param_type = utils.strip_none_from_type(param_type)
|
|
151
|
+
query_args[clean_key] = f"typing.Optional[{param_type}]"
|
|
150
152
|
elif in_ == "path":
|
|
151
153
|
# Function arguments
|
|
154
|
+
param_type = utils.resolve_forward_refs_for_client(utils.get_type(param["schema"]))
|
|
152
155
|
if required:
|
|
153
|
-
path_args[clean_key] =
|
|
156
|
+
path_args[clean_key] = param_type
|
|
154
157
|
else:
|
|
155
|
-
|
|
158
|
+
param_type = utils.strip_none_from_type(param_type)
|
|
159
|
+
path_args[clean_key] = f"typing.Optional[{param_type}]"
|
|
156
160
|
elif in_ == "header":
|
|
157
161
|
# Header object arguments
|
|
158
162
|
headers_args[param["name"]] = utils.get_type(param["schema"])
|
|
@@ -191,6 +191,14 @@ def schema_to_dict(schema) -> dict:
|
|
|
191
191
|
if hasattr(schema, "__pydantic_extra__") and schema.__pydantic_extra__ and "enum" in schema.__pydantic_extra__:
|
|
192
192
|
result["enum"] = schema.__pydantic_extra__["enum"]
|
|
193
193
|
|
|
194
|
+
# Handle default - it's in the extra fields; use "in" check since defaults can be falsy
|
|
195
|
+
if hasattr(schema, "__pydantic_extra__") and schema.__pydantic_extra__ and "default" in schema.__pydantic_extra__:
|
|
196
|
+
result["default"] = schema.__pydantic_extra__["default"]
|
|
197
|
+
|
|
198
|
+
# Handle const - it's in the extra fields
|
|
199
|
+
if hasattr(schema, "__pydantic_extra__") and schema.__pydantic_extra__ and "const" in schema.__pydantic_extra__:
|
|
200
|
+
result["const"] = schema.__pydantic_extra__["const"]
|
|
201
|
+
|
|
194
202
|
# Handle properties
|
|
195
203
|
if hasattr(schema, "properties") and schema.properties:
|
|
196
204
|
result["properties"] = {k: schema_to_dict(v) for k, v in schema.properties.items()}
|
|
@@ -54,12 +54,19 @@ class SchemasGenerator:
|
|
|
54
54
|
sanitized_arg = utils.snake_case_prop(arg)
|
|
55
55
|
arg_type = utils.get_type(arg_details)
|
|
56
56
|
is_optional = required and arg not in required
|
|
57
|
+
has_default = "default" in arg_details
|
|
57
58
|
|
|
58
59
|
needs_alias = sanitized_arg != arg
|
|
59
60
|
if needs_alias:
|
|
60
61
|
has_aliases = True
|
|
61
62
|
|
|
62
|
-
if
|
|
63
|
+
if has_default:
|
|
64
|
+
py_default = repr(arg_details["default"])
|
|
65
|
+
if needs_alias:
|
|
66
|
+
type_string = f'{arg_type} = pydantic.Field(default={py_default}, alias="{arg}")'
|
|
67
|
+
else:
|
|
68
|
+
type_string = f"{arg_type} = {py_default}"
|
|
69
|
+
elif is_optional and not arg_type.startswith("typing.Optional["):
|
|
63
70
|
if needs_alias:
|
|
64
71
|
type_string = f'typing.Optional[{arg_type}] = pydantic.Field(default=None, alias="{arg}")'
|
|
65
72
|
else:
|
|
@@ -128,10 +135,15 @@ class SchemasGenerator:
|
|
|
128
135
|
return
|
|
129
136
|
|
|
130
137
|
if schema.get("type") == "array":
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
# Emit a pydantic.RootModel subclass rather than a plain type alias.
|
|
139
|
+
# list[...] is a GenericAlias, not a type, so pydantic rejects it
|
|
140
|
+
# when it appears in response_map: dict[int, type[Any]]. A RootModel
|
|
141
|
+
# subclass is a proper class and satisfies that constraint.
|
|
142
|
+
items_schema = schema.get("items") or {}
|
|
143
|
+
item_type = utils.get_type(items_schema) if items_schema else "typing.Any"
|
|
144
|
+
item_type = utils.remove_forward_ref_quotes(item_type)
|
|
145
|
+
template = self.writer.templates.get_template("schema_root_model.jinja2")
|
|
146
|
+
content = template.render(class_name=schema_key, inner_type=item_type)
|
|
135
147
|
self.writer.write_to_schemas(content, output_dir=self.output_dir)
|
|
136
148
|
self.schemas[schema_key] = ""
|
|
137
149
|
return
|
|
@@ -97,6 +97,9 @@ def get_func_name(operation: dict, path: str) -> str:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def get_type(t):
|
|
100
|
+
if "const" in t:
|
|
101
|
+
return f"typing.Literal[{repr(t['const'])}]"
|
|
102
|
+
|
|
100
103
|
t_type = t.get("type")
|
|
101
104
|
t_format = t.get("format")
|
|
102
105
|
t_nullable = t.get("nullable", False)
|
|
@@ -277,8 +280,21 @@ def remove_forward_ref_quotes(type_string: str) -> str:
|
|
|
277
280
|
This is used for type aliases where forward references are not needed
|
|
278
281
|
because all types are defined in the same module and model_rebuild() is called.
|
|
279
282
|
"""
|
|
280
|
-
import re
|
|
281
|
-
|
|
282
|
-
# Replace quoted strings within type annotations
|
|
283
|
-
# Pattern: matches quoted strings that are type names (alphanumeric + underscore)
|
|
284
283
|
return re.sub(r'"([A-Za-z_][A-Za-z0-9_]*)"', r"\1", type_string)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def resolve_forward_refs_for_client(type_string: str) -> str:
|
|
287
|
+
"""Replace forward references with schemas module references for client.py context."""
|
|
288
|
+
return re.sub(r'"([A-Za-z_][A-Za-z0-9_]*)"', r"schemas.\1", type_string)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def strip_none_from_type(type_string: str) -> str:
|
|
292
|
+
"""Strip None/Optional wrapping so the caller can re-wrap without duplication."""
|
|
293
|
+
if type_string.startswith("typing.Optional[") and type_string.endswith("]"):
|
|
294
|
+
return type_string[len("typing.Optional[") : -1]
|
|
295
|
+
m = re.match(r"^typing\.Union\[(.+),\s*None\]$", type_string)
|
|
296
|
+
if m:
|
|
297
|
+
return m.group(1)
|
|
298
|
+
if type_string.endswith(" | None"):
|
|
299
|
+
return type_string[: -len(" | None")]
|
|
300
|
+
return type_string
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
_Item = typing.TypeVar("_Item")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ListResponse(pydantic.RootModel[list[_Item]]):
|
|
11
|
+
"""Base class for array response schemas. Provides list-like access to the root list."""
|
|
12
|
+
|
|
13
|
+
def __len__(self) -> int:
|
|
14
|
+
return len(self.root)
|
|
15
|
+
|
|
16
|
+
def __getitem__(self, index: int) -> _Item:
|
|
17
|
+
return self.root[index]
|
|
18
|
+
|
|
19
|
+
def __iter__(self) -> typing.Iterator[_Item]: # ty: ignore[invalid-method-override]
|
|
20
|
+
return iter(self.root)
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Change log
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
- Support OpenAPI `default` and `const` in schema generation ([#247](https://github.com/phalt/clientele/pull/247)).
|
|
6
|
+
- Fix `$ref` parameter types and double-nullable handling in client generation ([#247](https://github.com/phalt/clientele/pull/247)).
|
|
7
|
+
- Fix array response schemas being emitted as plain type aliases (`list[...]`) which pydantic rejects in `response_map` — they are now emitted as `clientele.schemas.ListResponse` subclasses, which support `len()`, indexing, and iteration directly ([#248](https://github.com/phalt/clientele/issues/248)).
|
|
8
|
+
- Fix generated `config.py` documenting `API_BASE_URL` as the environment variable name when pydantic-settings reads `BASE_URL` — the example now uses the correct name ([#248](https://github.com/phalt/clientele/issues/248)).
|
|
9
|
+
- Initial CI support for Python 3.15
|
|
10
|
+
|
|
11
|
+
## 2.1.0
|
|
4
12
|
|
|
5
13
|
- Introduce `RequestsHTTPBackend` for making HTTP requests using the `requests` library.
|
|
6
14
|
- This backend implements synchronous HTTP requests only.
|
|
@@ -26,7 +26,7 @@ How Clientele works:
|
|
|
26
26
|
- Path parameters inside `{}` are filled from the function arguments (e.g. `user_id`).
|
|
27
27
|
- Any remaining keyword arguments (like `include_details` above) become query parameters, but you can also provide a dict function parameter `query={...}` instead.
|
|
28
28
|
- The **`result` parameter is mandatory** and its type annotation (`User`) drives response parsing.
|
|
29
|
-
- Your function is injected with the `result` parameter - this is the response payload hydrated into your `result` parameter's type.
|
|
29
|
+
- Your function is injected with the `result` parameter - this is the response payload hydrated into your `result` parameter's type. Callers never supply it; Clientele injects it after the HTTP response arrives.
|
|
30
30
|
- The function's return value is independent - you can return the result directly, transform it, or return something completely different.
|
|
31
31
|
|
|
32
32
|
## HTTP POST example
|
|
@@ -147,31 +147,9 @@ Clientele will inject the following parameters into your function once an http r
|
|
|
147
147
|
- `result`: an instance of the type specified in the `result` parameter annotation. This parameter is **mandatory** and its type annotation determines how the response is parsed. Can be a Pydantic model or a TypedDict.
|
|
148
148
|
- `response`: the `httpx.Response` - useful for logging, debugging etc. (optional)
|
|
149
149
|
|
|
150
|
-
**Both `result` and `response` must be declared first in your function signature**, before any caller-supplied parameters (path params, query params, `data`).
|
|
150
|
+
**Both `result` and `response` must be declared first in your function signature**, before any caller-supplied parameters (path params, query params, `data`).
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
Clientele does some Python typing magic to give type checkers (mypy, pyright, ty) a correct view of your decorated functions.
|
|
155
|
-
|
|
156
|
-
The decorator type signature says: *"I accept a function whose first parameter(s) are injected values, and I return a new callable with those parameters removed."* This means type checkers see the public API — for example `get_user` is typed as `(user_id: int) -> User`, with `result` invisible to callers.
|
|
157
|
-
|
|
158
|
-
For this to work, **`result` and `response` must appear first** in the parameter list:
|
|
159
|
-
|
|
160
|
-
```python
|
|
161
|
-
# Correct — injected params first, type checker sees: (user_id: int) -> User
|
|
162
|
-
@client.get("/users/{user_id}")
|
|
163
|
-
def get_user(result: User, user_id: int) -> User:
|
|
164
|
-
return result
|
|
165
|
-
|
|
166
|
-
# Incorrect — result is not first, typing will not work correctly
|
|
167
|
-
@client.get("/users/{user_id}")
|
|
168
|
-
def get_user(user_id: int, result: User) -> User:
|
|
169
|
-
return result
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
At runtime, Clientele also patches the function's signature to hide `result` and `response` from introspection tools (IDEs, `inspect.signature()`, the cache key generator). This uses the same ordering rule: injected params are identified by name, stripped from the public signature, and injected automatically when the HTTP response arrives.
|
|
173
|
-
|
|
174
|
-
If you use the [mypy plugin](mypy.md), mypy will also strip injected parameters by name regardless of position — but for other type checkers (pyright, ty) the position-first rule is required.
|
|
152
|
+
For a full explanation of why this constraint exists, how Clientele hides these parameters from callers and type checkers, and how to configure mypy support, see [Injected parameters & typing](injected-parameters.md).
|
|
175
153
|
|
|
176
154
|
## Response parsing rules
|
|
177
155
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Injected parameters & typing
|
|
2
|
+
|
|
3
|
+
## The problem they solve
|
|
4
|
+
|
|
5
|
+
When you decorate a function with `@client.get(...)`, Clientele needs to hand back the parsed HTTP response to your function body. The natural way to do that is through a parameter, but that creates an awkward situation:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
@client.get("/users/{user_id}")
|
|
9
|
+
def get_user(result: User, user_id: int) -> User:
|
|
10
|
+
return result
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
From *inside* the function, `result` is essential because it holds the `User` parsed from the response. But from the *caller's* perspective, `result` makes no sense: callers supply `user_id`, not a `User` they already have. If Clientele exposed `result` in the public signature, every call site would look broken:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# Without injection stripping the callers would need to provide result themselves:
|
|
17
|
+
get_user(result=???, user_id=42)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Injected parameters are how Clientele resolves this: `result` (and the optional `response`) live in the function definition for *your* use, but Clientele **strips them from the public signature** so callers never see them.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What callers and IDEs see
|
|
25
|
+
|
|
26
|
+
Clientele uses Python's `typing.Concatenate` and `typing.ParamSpec` to express this contract to type checkers at the decorator level. The decorator signature says: *"I accept a function whose first N parameters are injected values, and I return a new callable with those parameters removed."*
|
|
27
|
+
|
|
28
|
+
The result is that your IDE and type checker see the **public API**, not the implementation detail:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
# What you write:
|
|
32
|
+
def get_user(result: User, user_id: int) -> User
|
|
33
|
+
|
|
34
|
+
# What your IDE and type checker see after decoration:
|
|
35
|
+
get_user(user_id: int) -> User
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
So autocomplete shows `user_id`, not `result`. Passing `user_id=42` produces no type error. Passing `result=...` produces an error, because `result` isn't in the public signature at all.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
get_user(user_id=42) # ✅ correct
|
|
42
|
+
get_user(result=my_user, user_id=42) # ❌ type error: unknown argument
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
At runtime, Clientele also patches the function's `__signature__` so that `inspect.signature()`, IDEs using runtime introspection, and Clientele's own cache key generator all agree with what the type checker sees.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Injected parameters
|
|
50
|
+
|
|
51
|
+
Clientele injects the following parameters:
|
|
52
|
+
|
|
53
|
+
| Parameter | Type | Required | Description |
|
|
54
|
+
|------------|----------------------------|----------|---------------------------------------------------------------------|
|
|
55
|
+
| `result` | Your annotated type | Yes | The HTTP response body, parsed and validated into the declared type |
|
|
56
|
+
| `response` | `http.Response` | No | The raw `http.Response`, useful for headers, status, debugging |
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pydantic import BaseModel
|
|
60
|
+
from clientele import api as clientele_api
|
|
61
|
+
|
|
62
|
+
client = clientele_api.APIClient(base_url="https://api.example.com")
|
|
63
|
+
|
|
64
|
+
class User(BaseModel):
|
|
65
|
+
id: int
|
|
66
|
+
name: str
|
|
67
|
+
|
|
68
|
+
# result only — the common case
|
|
69
|
+
@client.get("/users/{user_id}")
|
|
70
|
+
def get_user(result: User, user_id: int) -> User:
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
# result + response — when you need headers or status
|
|
74
|
+
@client.get("/users/{user_id}")
|
|
75
|
+
def get_user_with_meta(result: User, response: httpx.Response, user_id: int) -> tuple[User, int]:
|
|
76
|
+
return result, response.status_code
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Ordering requirement
|
|
82
|
+
|
|
83
|
+
**`result` and `response` must appear first in the parameter list**, before any caller-supplied parameters (path params, query params, `data`).
|
|
84
|
+
|
|
85
|
+
This is required because the `Concatenate` trick works by stripping parameters from the *front* of the signature. If injected params appear anywhere else, the type math breaks and type checkers will see the wrong signature.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# ✅ Correct: injected params first
|
|
89
|
+
# Type checker sees: get_user(user_id: int) -> User
|
|
90
|
+
@client.get("/users/{user_id}")
|
|
91
|
+
def get_user(result: User, user_id: int) -> User:
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
# ❌ Incorrect: result is not first
|
|
95
|
+
# Type checker sees: get_user(user_id: int, result: User) -> User
|
|
96
|
+
@client.get("/users/{user_id}")
|
|
97
|
+
def get_user(user_id: int, result: User) -> User:
|
|
98
|
+
return result
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The runtime injection works regardless of position (Clientele finds injected params by name), but **correct typing requires them first**.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Mypy plugin
|
|
106
|
+
|
|
107
|
+
Mypy does not natively support the `Concatenate`-based approach for stripping positional parameters. Clientele ships a mypy plugin that teaches mypy the same rule explicitly:
|
|
108
|
+
|
|
109
|
+
- Removes `result` and `response` from decorated function signatures during type checking
|
|
110
|
+
- Allows `dict` types to be passed where Pydantic models are expected (TypedDict ergonomics)
|
|
111
|
+
|
|
112
|
+
### Setup
|
|
113
|
+
|
|
114
|
+
The plugin is installed automatically with Clientele. Add it to your mypy configuration:
|
|
115
|
+
|
|
116
|
+
=== "mypy.ini / setup.cfg"
|
|
117
|
+
|
|
118
|
+
```ini
|
|
119
|
+
[mypy]
|
|
120
|
+
plugins = clientele.mypy
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
=== "pyproject.toml"
|
|
124
|
+
|
|
125
|
+
```toml
|
|
126
|
+
[tool.mypy]
|
|
127
|
+
plugins = ["clientele.mypy"]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
With the plugin active, mypy behaves the same as pyright and ty: `result` and `response` are invisible to callers.
|
|
131
|
+
|
|
132
|
+
!!! note
|
|
133
|
+
If you use **pyright** or **ty**, no plugin is needed — the `Concatenate`-based decorator signature is supported natively. Just ensure injected params appear first (see above).
|
|
134
|
+
|
|
135
|
+
---
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.2",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Issue 248 Reproduction",
|
|
5
|
+
"description": "Reproduces Bug 2 (array RootModel) and Bug 3 (config env var name) from issue #248",
|
|
6
|
+
"version": "1.0.0"
|
|
7
|
+
},
|
|
8
|
+
"servers": [
|
|
9
|
+
{"url": "https://api.example.com"}
|
|
10
|
+
],
|
|
11
|
+
"paths": {
|
|
12
|
+
"/faces": {
|
|
13
|
+
"get": {
|
|
14
|
+
"summary": "Get Faces",
|
|
15
|
+
"operationId": "get_faces",
|
|
16
|
+
"parameters": [
|
|
17
|
+
{
|
|
18
|
+
"name": "faces_type",
|
|
19
|
+
"in": "query",
|
|
20
|
+
"required": false,
|
|
21
|
+
"schema": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {"$ref": "#/components/schemas/FaceTypeEnum"}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"responses": {
|
|
28
|
+
"200": {
|
|
29
|
+
"description": "Successful Response",
|
|
30
|
+
"content": {
|
|
31
|
+
"application/json": {
|
|
32
|
+
"schema": {"$ref": "#/components/schemas/FaceItem"}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"/upload-faces": {
|
|
40
|
+
"post": {
|
|
41
|
+
"summary": "Upload Faces",
|
|
42
|
+
"operationId": "upload_faces",
|
|
43
|
+
"requestBody": {
|
|
44
|
+
"required": true,
|
|
45
|
+
"content": {
|
|
46
|
+
"application/json": {
|
|
47
|
+
"schema": {"$ref": "#/components/schemas/FaceItem"}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"responses": {
|
|
52
|
+
"200": {
|
|
53
|
+
"description": "Successful Response",
|
|
54
|
+
"content": {
|
|
55
|
+
"application/json": {
|
|
56
|
+
"schema": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"items": {"$ref": "#/components/schemas/FaceItem"}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"components": {
|
|
68
|
+
"schemas": {
|
|
69
|
+
"FaceTypeEnum": {
|
|
70
|
+
"type": "string",
|
|
71
|
+
"enum": ["FRONT", "SIDE", "BACK"],
|
|
72
|
+
"title": "FaceTypeEnum"
|
|
73
|
+
},
|
|
74
|
+
"FaceItem": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"title": "FaceItem",
|
|
77
|
+
"properties": {
|
|
78
|
+
"id": {"type": "integer"},
|
|
79
|
+
"name": {"type": "string"}
|
|
80
|
+
},
|
|
81
|
+
"required": ["id", "name"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -49,7 +49,7 @@ nav:
|
|
|
49
49
|
- 🪵 Logging: api-logging.md
|
|
50
50
|
- 🧪 Testing: api-testing.md
|
|
51
51
|
- 🎯 Direct requests: api-direct-requests.md
|
|
52
|
-
-
|
|
52
|
+
- 💉 Injected parameters & typing: injected-parameters.md
|
|
53
53
|
- 📟 Command line tools:
|
|
54
54
|
- 🏗️ Client creator: openapi-cli.md
|
|
55
55
|
- ⚡️ API Server Integrations:
|
|
@@ -5,6 +5,8 @@ import typing
|
|
|
5
5
|
|
|
6
6
|
import pydantic
|
|
7
7
|
|
|
8
|
+
from clientele.schemas import ListResponse # noqa
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class UserOut(pydantic.BaseModel):
|
|
10
12
|
id: int
|
|
@@ -19,7 +21,8 @@ class UserIn(pydantic.BaseModel):
|
|
|
19
21
|
age: int
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
Response
|
|
24
|
+
class Response(ListResponse[UserOut]):
|
|
25
|
+
pass
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
def get_subclasses_from_same_file() -> list[typing.Type[pydantic.BaseModel]]:
|
|
@@ -5,6 +5,8 @@ import typing
|
|
|
5
5
|
|
|
6
6
|
import pydantic
|
|
7
7
|
|
|
8
|
+
from clientele.schemas import ListResponse # noqa
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class PatchedUser(pydantic.BaseModel):
|
|
10
12
|
id: int
|
|
@@ -25,7 +27,8 @@ class UserRequest(pydantic.BaseModel):
|
|
|
25
27
|
email: str
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
ListUsers200Response
|
|
30
|
+
class ListUsers200Response(ListResponse[User]):
|
|
31
|
+
pass
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def get_subclasses_from_same_file() -> list[typing.Type[pydantic.BaseModel]]:
|
|
@@ -5,6 +5,8 @@ import typing
|
|
|
5
5
|
|
|
6
6
|
import pydantic
|
|
7
7
|
|
|
8
|
+
from clientele.schemas import ListResponse # noqa
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class CreateUserRequest(pydantic.BaseModel):
|
|
10
12
|
name: str
|
|
@@ -29,7 +31,8 @@ class ValidationError(pydantic.BaseModel):
|
|
|
29
31
|
model_config = pydantic.ConfigDict(populate_by_name=True)
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
ResponseListUsers
|
|
34
|
+
class ResponseListUsers(ListResponse[UserResponse]):
|
|
35
|
+
pass
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
def get_subclasses_from_same_file() -> list[typing.Type[pydantic.BaseModel]]:
|