clientele 2.0.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.0.0 → clientele-2.2.0}/.claude/settings.local.json +6 -1
- {clientele-2.0.0 → clientele-2.2.0}/.github/workflows/ci.yml +6 -2
- {clientele-2.0.0 → clientele-2.2.0}/CHANGELOG.md +20 -0
- {clientele-2.0.0 → clientele-2.2.0}/PKG-INFO +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/SECURITY.md +2 -2
- {clientele-2.0.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.0.0 → clientele-2.2.0}/clientele/generators/api/templates/schemas_py.jinja2 +2 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/base_clients.py +8 -4
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/cicerone_compat.py +8 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/shared/generators/schemas.py +17 -5
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/shared/utils.py +20 -4
- clientele-2.2.0/clientele/http/aiohttp_backend.py +165 -0
- clientele-2.2.0/clientele/http/niquests_backend.py +216 -0
- clientele-2.2.0/clientele/http/requests_backend.py +160 -0
- clientele-2.2.0/clientele/schemas.py +20 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/settings.py +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/docs/CHANGELOG.md +20 -0
- clientele-2.2.0/docs/api-http-backends.md +340 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-overview.md +3 -25
- clientele-2.2.0/docs/injected-parameters.md +135 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/install.md +1 -1
- clientele-2.2.0/example_openapi_specs/issue_248.json +85 -0
- {clientele-2.0.0 → clientele-2.2.0}/mkdocs.yml +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/pyproject.toml +6 -1
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/client/MANIFEST.md +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/client/schemas.py +4 -1
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/MANIFEST.md +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/schemas.py +4 -1
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/client/MANIFEST.md +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/client/schemas.py +4 -1
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/async_test_client/MANIFEST.md +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/async_test_client/schemas.py +2 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/MANIFEST.md +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_client/MANIFEST.md +1 -1
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_client/schemas.py +2 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/shared/test_utils_coverage.py +22 -0
- {clientele-2.0.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.2.0/tests/http/backends/test_aiohttp_backend.py +222 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/http/backends/test_httpx_backend.py +15 -0
- clientele-2.2.0/tests/http/backends/test_niquests_backend.py +235 -0
- clientele-2.2.0/tests/http/backends/test_requests_backend.py +192 -0
- {clientele-2.0.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.0.0 → clientele-2.2.0}/uv.lock +892 -1
- clientele-2.0.0/docs/api-http-backends.md +0 -214
- clientele-2.0.0/docs/mypy.md +0 -63
- clientele-2.0.0/tests/generators/test_schemas_coverage.py +0 -96
- {clientele-2.0.0 → clientele-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/.github/compatibility.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/.github/dependabot.yml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/.github/pull_request_template.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/.gitignore +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/CONTRIBUTING.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/CONTRIBUTORS.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/LICENSE +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/Makefile +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/PUBLISHING.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/README.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/exceptions.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/requests.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/stream/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/stream/parser.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/api/type_utils.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/cache/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/cache/backends.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/cache/decorator.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/cache/key_generator.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/cache/types.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/cli.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/README.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/generator.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/generators/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/generators/clients.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/generators/schemas.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/api_get_method.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/api_post_method.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/client_py.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/manifest.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/pyproject.toml.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/schema_class.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/schema_helpers.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/templates/schema_type_alias.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/api/writer.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/base.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/generator.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/templates/client_py.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/templates/config_py.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/templates/manifest.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/templates/schemas_py.jinja2 +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/basic/writer.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/schema_utils.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/shared/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/generators/shared/generators/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/graphql/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/graphql/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/http/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/http/backends.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/http/fake_backend.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/http/httpx_backend.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/http/response.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/http/status_codes.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/mypy.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/py.typed +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/retries/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/retries/decorators.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/testing.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/clientele/utils.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/CNAME +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-async.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-authentication.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-cache.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-configuration.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-direct-requests.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-examples.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-exceptions.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-graphql.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-logging.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-retries.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-stream.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/api-testing.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/clientele_generate.gif +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/clientele_header.png +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/favicon.png +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/index.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/openapi-cli.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/server-django-ninja.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/server-drf.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/docs/server-fastapi.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/example_openapi_specs/best.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/example_openapi_specs/complex_schemas.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/example_openapi_specs/simple.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/example_openapi_specs/test_303.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/README.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/api.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/client/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/client/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/client/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/client/pyproject.toml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/example_project/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/example_project/settings.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/example_project/urls.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/manage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_ninja/openapi.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/README.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/client/pyproject.toml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/example_project/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/example_project/settings.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/example_project/urls.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/manage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/openapi.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/apps.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/migrations/0001_initial.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/migrations/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/models.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/serializers.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/django_rest_framework/users/views.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/README.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/client/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/client/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/client/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/client/pyproject.toml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/main.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/server_examples/fastapi/openapi.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/retries/test_retry.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/stream/test_parser.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_api_client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_api_client_logging.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_api_client_singleton.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_api_response_parser.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_api_typed_dict.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_direct_requests.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_requests.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_stream.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_stream_httpx.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api/test_type_utils.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/async_test_client/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/async_test_client/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/async_test_client/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/async_test_client/pyproject.toml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_basic_client/schemas.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_client/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_client/client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_client/config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/api_clients/test_client/pyproject.toml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/cache/fixtures.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/cache/test_backends.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/cache/test_decorator.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/cache/test_integration.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/cache/test_key_generator.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/callback_example.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/complex_api.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/README.md +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/api-with-examples.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/callback-example.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/link-example.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/non-oauth-scopes.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/petstore-expanded.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/petstore.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/tictactoe.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/uspto.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/openapi_examples/webhook-example.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/petstore_openapi3.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/realworld/1password.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/realworld/ably.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/realworld/google.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/realworld/medium.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/realworld/spacetraders.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/realworld/twilio.yaml +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/fixtures/regression/dep_query_alias.json +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/framework/test_framework_generator_coverage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/framework/test_framework_generator_integration.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/integration_utils.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/shared/__init__.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/shared/test_utils.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/test_base.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/generators/test_cicerone_compat.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/graphql/test_graphql_client.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/http/backends/test_backend_config.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/http/backends/test_fake_backend.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/mypy/mypy_plugin_test_config.ini +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/mypy/test_mypy_plugin.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_additional_coverage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_cli.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_cli_coverage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_cli_full_coverage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_edge_case_coverage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_fixture_schemas.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_generator_coverage.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_nullable_fields.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_openapi_31_null_type.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_ref_handling.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_settings.py +0 -0
- {clientele-2.0.0 → clientele-2.2.0}/tests/test_testing_utilities.py +0 -0
- {clientele-2.0.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,5 +1,25 @@
|
|
|
1
1
|
# Change log
|
|
2
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
|
|
12
|
+
|
|
13
|
+
- Introduce `RequestsHTTPBackend` for making HTTP requests using the `requests` library.
|
|
14
|
+
- This backend implements synchronous HTTP requests only.
|
|
15
|
+
- The library `requests` is not installed by default. See the documentation for installation instructions.
|
|
16
|
+
- Introduce `AIOHTTPBackend` for making HTTP requests using the `aiohttp` library.
|
|
17
|
+
- This backend implements asynchronous HTTP requests only.
|
|
18
|
+
- The library `aiohttp` is not installed by default. See the documentation for installation instructions.
|
|
19
|
+
- Introduce `NiquestsHTTPBackend` for making HTTP requests using the `niquests` library.
|
|
20
|
+
- This backend implements both synchronous and asynchronous HTTP requests.
|
|
21
|
+
- The library `niquests` is not installed by default. See the documentation for installation instructions.
|
|
22
|
+
|
|
3
23
|
## 2.0.0
|
|
4
24
|
|
|
5
25
|
This release includes many breaking changes, improved type support, and dependency changes.
|
|
@@ -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,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import aiohttp as aiohttp_lib
|
|
7
|
+
except ImportError as exc:
|
|
8
|
+
raise ImportError(
|
|
9
|
+
"The AiohttpHTTPBackend requires the 'aiohttp' library. Install it with: pip install aiohttp"
|
|
10
|
+
) from exc
|
|
11
|
+
|
|
12
|
+
from clientele.api import exceptions as api_exceptions
|
|
13
|
+
from clientele.api.stream import hydrate_content
|
|
14
|
+
from clientele.http import backends, response
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AiohttpHTTPBackend(backends.HTTPBackend):
|
|
18
|
+
"""Asynchronous-only HTTP backend using the aiohttp library.
|
|
19
|
+
|
|
20
|
+
Sync methods (build_client, send_sync_request, handle_sync_stream) raise
|
|
21
|
+
NotImplementedError. Use HttpxHTTPBackend for sync support.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_url: Base URL prepended to all relative request paths.
|
|
25
|
+
headers: Default headers applied to every request.
|
|
26
|
+
timeout: Request timeout in seconds. None means no timeout.
|
|
27
|
+
follow_redirects: Whether to follow HTTP redirects.
|
|
28
|
+
verify: SSL certificate verification. False to disable; str CA bundle paths
|
|
29
|
+
are not supported (use an ssl.SSLContext via a custom connector instead).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_async_client: aiohttp_lib.ClientSession | None = None
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
base_url: str = "",
|
|
37
|
+
headers: dict[str, str] | None = None,
|
|
38
|
+
timeout: float | None = 5.0,
|
|
39
|
+
follow_redirects: bool = False,
|
|
40
|
+
verify: bool | str = True,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.base_url = base_url.rstrip("/")
|
|
43
|
+
self.headers = headers or {}
|
|
44
|
+
self.aiohttp_timeout = (
|
|
45
|
+
aiohttp_lib.ClientTimeout(total=timeout) if timeout is not None else aiohttp_lib.ClientTimeout(total=None)
|
|
46
|
+
)
|
|
47
|
+
self.follow_redirects = follow_redirects
|
|
48
|
+
# aiohttp uses ssl=False to disable; str CA paths are not supported here
|
|
49
|
+
self._ssl: bool | None = None if verify else False
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_config(cls, config: typing.Any) -> "AiohttpHTTPBackend":
|
|
53
|
+
return cls(
|
|
54
|
+
base_url=config.base_url,
|
|
55
|
+
headers=config.headers,
|
|
56
|
+
timeout=config.timeout,
|
|
57
|
+
follow_redirects=config.follow_redirects,
|
|
58
|
+
verify=config.verify,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _full_url(self, url: str) -> str:
|
|
62
|
+
if url.startswith("http://") or url.startswith("https://"):
|
|
63
|
+
return url
|
|
64
|
+
return self.base_url + url
|
|
65
|
+
|
|
66
|
+
def build_client(self) -> typing.Any:
|
|
67
|
+
raise NotImplementedError("AiohttpHTTPBackend does not support sync requests. Use HttpxHTTPBackend instead.")
|
|
68
|
+
|
|
69
|
+
def build_async_client(self) -> aiohttp_lib.ClientSession:
|
|
70
|
+
if not self._async_client or self._async_client.closed:
|
|
71
|
+
self._async_client = aiohttp_lib.ClientSession(headers=self.headers)
|
|
72
|
+
return self._async_client
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def convert_to_response(native_response: typing.Any) -> response.Response:
|
|
76
|
+
# Body must be pre-read via await resp.read() before calling this
|
|
77
|
+
content: bytes = native_response._body or b""
|
|
78
|
+
return response.Response(
|
|
79
|
+
status_code=native_response.status,
|
|
80
|
+
headers=dict(native_response.headers),
|
|
81
|
+
content=content,
|
|
82
|
+
request_method=native_response.method.upper(),
|
|
83
|
+
request_url=str(native_response.url),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def send_sync_request(
|
|
87
|
+
self,
|
|
88
|
+
method: str,
|
|
89
|
+
url: str,
|
|
90
|
+
**kwargs: typing.Any,
|
|
91
|
+
) -> response.Response:
|
|
92
|
+
raise NotImplementedError("AiohttpHTTPBackend does not support sync requests. Use HttpxHTTPBackend instead.")
|
|
93
|
+
|
|
94
|
+
async def send_async_request(
|
|
95
|
+
self,
|
|
96
|
+
method: str,
|
|
97
|
+
url: str,
|
|
98
|
+
**kwargs: typing.Any,
|
|
99
|
+
) -> response.Response:
|
|
100
|
+
session = self.build_async_client()
|
|
101
|
+
async with session.request(
|
|
102
|
+
method,
|
|
103
|
+
self._full_url(url),
|
|
104
|
+
timeout=self.aiohttp_timeout,
|
|
105
|
+
allow_redirects=self.follow_redirects,
|
|
106
|
+
ssl=self._ssl,
|
|
107
|
+
**kwargs,
|
|
108
|
+
) as resp:
|
|
109
|
+
await resp.read()
|
|
110
|
+
return self.convert_to_response(resp)
|
|
111
|
+
|
|
112
|
+
def handle_sync_stream(
|
|
113
|
+
self,
|
|
114
|
+
method: str,
|
|
115
|
+
url: str,
|
|
116
|
+
inner_type: typing.Any,
|
|
117
|
+
response_parser: typing.Callable[[str], typing.Any] | None = None,
|
|
118
|
+
**kwargs: typing.Any,
|
|
119
|
+
) -> typing.Generator[typing.Any, None, None]:
|
|
120
|
+
raise NotImplementedError("AiohttpHTTPBackend does not support sync requests. Use HttpxHTTPBackend instead.")
|
|
121
|
+
yield # type: ignore[misc]
|
|
122
|
+
|
|
123
|
+
async def handle_async_stream(
|
|
124
|
+
self,
|
|
125
|
+
method: str,
|
|
126
|
+
url: str,
|
|
127
|
+
inner_type: typing.Any,
|
|
128
|
+
response_parser: typing.Callable[[str], typing.Any] | None = None,
|
|
129
|
+
**kwargs: typing.Any,
|
|
130
|
+
) -> typing.AsyncGenerator[typing.Any, None]:
|
|
131
|
+
session = self.build_async_client()
|
|
132
|
+
async with session.request(
|
|
133
|
+
method,
|
|
134
|
+
self._full_url(url),
|
|
135
|
+
timeout=self.aiohttp_timeout,
|
|
136
|
+
allow_redirects=self.follow_redirects,
|
|
137
|
+
ssl=self._ssl,
|
|
138
|
+
**kwargs,
|
|
139
|
+
) as resp:
|
|
140
|
+
if 400 <= resp.status < 600:
|
|
141
|
+
await resp.read()
|
|
142
|
+
generic_response = self.convert_to_response(resp)
|
|
143
|
+
raise api_exceptions.HTTPStatusError(
|
|
144
|
+
response=generic_response,
|
|
145
|
+
reason=f"HTTP {resp.status}",
|
|
146
|
+
)
|
|
147
|
+
while True:
|
|
148
|
+
line = await resp.content.readline()
|
|
149
|
+
if not line:
|
|
150
|
+
break
|
|
151
|
+
decoded = line.decode("utf-8").rstrip("\r\n")
|
|
152
|
+
if not decoded:
|
|
153
|
+
continue
|
|
154
|
+
if response_parser is not None:
|
|
155
|
+
yield response_parser(decoded)
|
|
156
|
+
else:
|
|
157
|
+
yield hydrate_content(decoded, inner_type)
|
|
158
|
+
|
|
159
|
+
def close(self) -> None:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
async def aclose(self) -> None:
|
|
163
|
+
if self._async_client and not self._async_client.closed:
|
|
164
|
+
await self._async_client.close()
|
|
165
|
+
self._async_client = None
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import niquests as niquests_lib
|
|
7
|
+
except ImportError as exc:
|
|
8
|
+
raise ImportError(
|
|
9
|
+
"The NiquestsHTTPBackend requires the 'niquests' library. Install it with: pip install niquests"
|
|
10
|
+
) from exc
|
|
11
|
+
|
|
12
|
+
from clientele.api import exceptions as api_exceptions
|
|
13
|
+
from clientele.api.stream import hydrate_content
|
|
14
|
+
from clientele.http import backends, response
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NiquestsHTTPBackend(backends.HTTPBackend):
|
|
18
|
+
"""HTTP backend using the niquests library, supporting both sync and async.
|
|
19
|
+
|
|
20
|
+
This backend supports both synchronous and asynchronous requests in a single instance.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
base_url: Base URL prepended to all relative request paths.
|
|
24
|
+
headers: Default headers applied to every request.
|
|
25
|
+
timeout: Request timeout in seconds. None means no timeout.
|
|
26
|
+
follow_redirects: Whether to follow HTTP redirects.
|
|
27
|
+
verify: SSL certificate verification. True to enable, False to disable.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_sync_client: niquests_lib.Session | None = None
|
|
31
|
+
_async_client: niquests_lib.AsyncSession | None = None
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
base_url: str = "",
|
|
36
|
+
headers: dict[str, str] | None = None,
|
|
37
|
+
timeout: float | None = 5.0,
|
|
38
|
+
follow_redirects: bool = False,
|
|
39
|
+
verify: bool = True,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.base_url = base_url.rstrip("/")
|
|
42
|
+
self.headers = headers or {}
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
self.follow_redirects = follow_redirects
|
|
45
|
+
self.verify = verify
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_config(cls, config: typing.Any) -> NiquestsHTTPBackend:
|
|
49
|
+
return cls(
|
|
50
|
+
base_url=config.base_url,
|
|
51
|
+
headers=config.headers,
|
|
52
|
+
timeout=config.timeout,
|
|
53
|
+
follow_redirects=config.follow_redirects,
|
|
54
|
+
verify=bool(config.verify),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _full_url(self, url: str) -> str:
|
|
58
|
+
if url.startswith("http://") or url.startswith("https://"):
|
|
59
|
+
return url
|
|
60
|
+
return self.base_url + url
|
|
61
|
+
|
|
62
|
+
def build_client(self) -> niquests_lib.Session:
|
|
63
|
+
if not self._sync_client:
|
|
64
|
+
session = niquests_lib.Session(headers=self.headers, timeout=self.timeout)
|
|
65
|
+
session.verify = self.verify
|
|
66
|
+
self._sync_client = session
|
|
67
|
+
return self._sync_client
|
|
68
|
+
|
|
69
|
+
def build_async_client(self) -> niquests_lib.AsyncSession:
|
|
70
|
+
if not self._async_client:
|
|
71
|
+
session = niquests_lib.AsyncSession(headers=self.headers, timeout=self.timeout)
|
|
72
|
+
session.verify = self.verify
|
|
73
|
+
self._async_client = session
|
|
74
|
+
return self._async_client
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def convert_to_response(native_response: typing.Any) -> response.Response:
|
|
78
|
+
# Accepts both sync Response and non-streaming async Response (same type)
|
|
79
|
+
return response.Response(
|
|
80
|
+
status_code=native_response.status_code,
|
|
81
|
+
headers=dict(native_response.headers),
|
|
82
|
+
content=native_response.content or b"",
|
|
83
|
+
text=native_response.text or "",
|
|
84
|
+
request_method=native_response.request.method,
|
|
85
|
+
request_url=native_response.request.url or "",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def send_sync_request(
|
|
89
|
+
self,
|
|
90
|
+
method: str,
|
|
91
|
+
url: str,
|
|
92
|
+
**kwargs: typing.Any,
|
|
93
|
+
) -> response.Response:
|
|
94
|
+
session = self.build_client()
|
|
95
|
+
resp = session.request(
|
|
96
|
+
method,
|
|
97
|
+
self._full_url(url),
|
|
98
|
+
timeout=self.timeout,
|
|
99
|
+
allow_redirects=self.follow_redirects,
|
|
100
|
+
**kwargs,
|
|
101
|
+
)
|
|
102
|
+
return self.convert_to_response(resp)
|
|
103
|
+
|
|
104
|
+
async def send_async_request(
|
|
105
|
+
self,
|
|
106
|
+
method: str,
|
|
107
|
+
url: str,
|
|
108
|
+
**kwargs: typing.Any,
|
|
109
|
+
) -> response.Response:
|
|
110
|
+
session = self.build_async_client()
|
|
111
|
+
# Non-streaming async returns a regular Response (sync content/text)
|
|
112
|
+
resp = await session.request(
|
|
113
|
+
method,
|
|
114
|
+
self._full_url(url),
|
|
115
|
+
timeout=self.timeout,
|
|
116
|
+
allow_redirects=self.follow_redirects,
|
|
117
|
+
**kwargs,
|
|
118
|
+
)
|
|
119
|
+
return self.convert_to_response(resp)
|
|
120
|
+
|
|
121
|
+
def handle_sync_stream(
|
|
122
|
+
self,
|
|
123
|
+
method: str,
|
|
124
|
+
url: str,
|
|
125
|
+
inner_type: typing.Any,
|
|
126
|
+
response_parser: typing.Callable[[str], typing.Any] | None = None,
|
|
127
|
+
**kwargs: typing.Any,
|
|
128
|
+
) -> typing.Generator[typing.Any, None, None]:
|
|
129
|
+
session = self.build_client()
|
|
130
|
+
resp = session.request(
|
|
131
|
+
method,
|
|
132
|
+
self._full_url(url),
|
|
133
|
+
stream=True,
|
|
134
|
+
timeout=self.timeout,
|
|
135
|
+
allow_redirects=self.follow_redirects,
|
|
136
|
+
**kwargs,
|
|
137
|
+
)
|
|
138
|
+
try:
|
|
139
|
+
if resp.status_code is not None and 400 <= resp.status_code < 600:
|
|
140
|
+
generic_response = self.convert_to_response(resp)
|
|
141
|
+
raise api_exceptions.HTTPStatusError(
|
|
142
|
+
response=generic_response,
|
|
143
|
+
reason=f"HTTP {resp.status_code}",
|
|
144
|
+
)
|
|
145
|
+
for line in resp.iter_lines(decode_unicode=True):
|
|
146
|
+
if not line:
|
|
147
|
+
continue
|
|
148
|
+
if response_parser is not None:
|
|
149
|
+
yield response_parser(line)
|
|
150
|
+
else:
|
|
151
|
+
yield hydrate_content(line, inner_type)
|
|
152
|
+
finally:
|
|
153
|
+
resp.close()
|
|
154
|
+
|
|
155
|
+
async def handle_async_stream(
|
|
156
|
+
self,
|
|
157
|
+
method: str,
|
|
158
|
+
url: str,
|
|
159
|
+
inner_type: typing.Any,
|
|
160
|
+
response_parser: typing.Callable[[str], typing.Any] | None = None,
|
|
161
|
+
**kwargs: typing.Any,
|
|
162
|
+
) -> typing.AsyncGenerator[typing.Any, None]:
|
|
163
|
+
session = self.build_async_client()
|
|
164
|
+
# stream=True returns AsyncResponse with async content/text/close
|
|
165
|
+
resp = await session.request(
|
|
166
|
+
method,
|
|
167
|
+
self._full_url(url),
|
|
168
|
+
stream=True,
|
|
169
|
+
timeout=self.timeout,
|
|
170
|
+
allow_redirects=self.follow_redirects,
|
|
171
|
+
**kwargs,
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
if resp.status_code is not None and 400 <= resp.status_code < 600:
|
|
175
|
+
content = await resp.content or b""
|
|
176
|
+
text = await resp.text or ""
|
|
177
|
+
headers: dict[str, str] = {
|
|
178
|
+
k: v if isinstance(v, str) else v.decode("latin-1") for k, v in resp.headers.items()
|
|
179
|
+
}
|
|
180
|
+
request_method = (resp.request.method or "GET") if resp.request is not None else "GET"
|
|
181
|
+
request_url = (resp.request.url or "") if resp.request is not None else ""
|
|
182
|
+
generic_response = response.Response(
|
|
183
|
+
status_code=resp.status_code,
|
|
184
|
+
headers=headers,
|
|
185
|
+
content=content,
|
|
186
|
+
text=text,
|
|
187
|
+
request_method=request_method,
|
|
188
|
+
request_url=request_url,
|
|
189
|
+
)
|
|
190
|
+
raise api_exceptions.HTTPStatusError(
|
|
191
|
+
response=generic_response,
|
|
192
|
+
reason=f"HTTP {resp.status_code}",
|
|
193
|
+
)
|
|
194
|
+
# AsyncResponse.iter_lines is annotated as a coroutine returning AsyncGenerator,
|
|
195
|
+
# but niquests implements it as an async generator function (runtime discrepancy).
|
|
196
|
+
# Assigning to Any lets us iterate correctly at runtime without misrepresenting types.
|
|
197
|
+
resp_any: typing.Any = resp
|
|
198
|
+
async for line in resp_any.iter_lines(decode_unicode=True):
|
|
199
|
+
if not line:
|
|
200
|
+
continue
|
|
201
|
+
if response_parser is not None:
|
|
202
|
+
yield response_parser(line)
|
|
203
|
+
else:
|
|
204
|
+
yield hydrate_content(line, inner_type)
|
|
205
|
+
finally:
|
|
206
|
+
await resp.close()
|
|
207
|
+
|
|
208
|
+
def close(self) -> None:
|
|
209
|
+
if self._sync_client is not None:
|
|
210
|
+
self._sync_client.close()
|
|
211
|
+
self._sync_client = None
|
|
212
|
+
|
|
213
|
+
async def aclose(self) -> None:
|
|
214
|
+
if self._async_client is not None:
|
|
215
|
+
await self._async_client.close()
|
|
216
|
+
self._async_client = None
|