From fce315588d68bca81ab70c225053c5cde8cf70e5 Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Thu, 1 May 2025 14:09:19 -0700 Subject: [PATCH 01/11] Updated version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1bb7e629b..cf55d5c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "benchling-openapi-python-client" -version = "2.0.0-alpha.2" +version = "2.0.0-alpha.dev2" description = "Generate modern Python clients from OpenAPI - Benchling fork" repository = "https://github.com/benchling/openapi-python-client" license = "MIT" From 4cd1a22dec706ee49b693a0ee894497fe3b9137d Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:14:06 -0700 Subject: [PATCH 02/11] Updated client to offer options to access v3 endpoints --- .../templates/client.py.jinja | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index cf0301a9a..6796cf6b8 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -3,6 +3,7 @@ from typing import Any, Union, Optional from attrs import define, field, evolve import httpx +import urllib.parse {% set attrs_info = { @@ -189,3 +190,26 @@ class AuthenticatedClient: {{ builders("AuthenticatedClient") }} {{ httpx_stuff("AuthenticatedClient", "self._headers[self.auth_header_name] = f\"{self.prefix} {self.token}\" if self.prefix else self.token") }} + +def replace_client_path(client: Client, base_path: str) -> Client: + """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" + parsed = urllib.parse.urlparse(client.base_url) + # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts + updated_url = parsed._replace(path=base_path) + return client.with_base_url(updated_url.geturl()) + + +def v3_stable_client(client: Client) -> Client: + """Override a client's base URL with a v2 stable path.""" + return replace_client_path(client, "api/v3-draft") + + +def v3_alpha_client(client: Client) -> Client: + """Override a client's base URL with a v2-alpha path.""" + return replace_client_path(client, "api/v3-alpha") + + +def v3_beta_client(client: Client) -> Client: + """Override a client's base URL with a v2-beta path.""" + return replace_client_path(client, "api/v3-beta") + From 42fcb187d7ea464515caa2203680c99a0043ba8a Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:23:01 -0700 Subject: [PATCH 03/11] Updated tests --- .../my_test_api_client/client.py | 23 ++++++++++++++++++ .../my_enum_api_client/client.py | 8 +++++++ .../test_3_1_features_client/client.py | 24 ++++++++++++++++++- integration-tests/integration_tests/client.py | 24 +++++++++++++++++++ .../templates/client.py.jinja | 1 - .../example.yml | 7 ++++++ .../example.json | 1 + .../example.yaml | 7 ++++++ .../example.json | 16 +++++++++++++ .../tmp/test_load_from_path_absolute_ecurrent | 1 + .../tmp/test_load_from_path_relative_ecurrent | 1 + 11 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 tests/tmp/test_load_from_path_absolute_e0/example.yml create mode 100644 tests/tmp/test_load_from_path_absolute_e1/example.json create mode 100644 tests/tmp/test_load_from_path_absolute_e2/example.yaml create mode 100644 tests/tmp/test_load_from_path_absolute_e3/example.json create mode 120000 tests/tmp/test_load_from_path_absolute_ecurrent create mode 120000 tests/tmp/test_load_from_path_relative_ecurrent diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-record/my_test_api_client/client.py index e80446f10..ab5ef0ae7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/client.py +++ b/end_to_end_tests/golden-record/my_test_api_client/client.py @@ -3,6 +3,7 @@ import httpx from attrs import define, evolve, field +import urllib.parse @define @@ -266,3 +267,25 @@ async def __aenter__(self) -> "AuthenticatedClient": async def __aexit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) + +def replace_client_path(client: Client, base_path: str) -> Client: + """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" + parsed = urllib.parse.urlparse(client.base_url) + # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts + updated_url = parsed._replace(path=base_path) + return client.with_base_url(updated_url.geturl()) + + +def v3_stable_client(client: Client) -> Client: + """Override a client's base URL with a v2 stable path.""" + return replace_client_path(client, "api/v3-draft") + + +def v3_alpha_client(client: Client) -> Client: + """Override a client's base URL with a v2-alpha path.""" + return replace_client_path(client, "api/v3-alpha") + + +def v3_beta_client(client: Client) -> Client: + """Override a client's base URL with a v2-beta path.""" + return replace_client_path(client, "api/v3-beta") diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py index e80446f10..aab8f4a0a 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py @@ -3,6 +3,7 @@ import httpx from attrs import define, evolve, field +import urllib.parse @define @@ -266,3 +267,10 @@ async def __aenter__(self) -> "AuthenticatedClient": async def __aexit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +def replace_client_path(client: Client, base_path: str) -> Client: + """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" + parsed = urllib.parse.urlparse(client.base_url) + # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts + updated_url = parsed._replace(path=base_path) diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py index e80446f10..5483b329f 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py @@ -3,7 +3,7 @@ import httpx from attrs import define, evolve, field - +import urllib.parse @define class Client: @@ -266,3 +266,25 @@ async def __aenter__(self) -> "AuthenticatedClient": async def __aexit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) + +def replace_client_path(client: Client, base_path: str) -> Client: + """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" + parsed = urllib.parse.urlparse(client.base_url) + # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts + updated_url = parsed._replace(path=base_path) + return client.with_base_url(updated_url.geturl()) + + +def v3_stable_client(client: Client) -> Client: + """Override a client's base URL with a v2 stable path.""" + return replace_client_path(client, "api/v3-draft") + + +def v3_alpha_client(client: Client) -> Client: + """Override a client's base URL with a v2-alpha path.""" + return replace_client_path(client, "api/v3-alpha") + + +def v3_beta_client(client: Client) -> Client: + """Override a client's base URL with a v2-beta path.""" + return replace_client_path(client, "api/v3-beta") diff --git a/integration-tests/integration_tests/client.py b/integration-tests/integration_tests/client.py index e80446f10..6f4d1157f 100644 --- a/integration-tests/integration_tests/client.py +++ b/integration-tests/integration_tests/client.py @@ -1,4 +1,5 @@ import ssl +import urllib.parse from typing import Any, Optional, Union import httpx @@ -266,3 +267,26 @@ async def __aenter__(self) -> "AuthenticatedClient": async def __aexit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +def replace_client_path(client: Client, base_path: str) -> Client: + """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" + parsed = urllib.parse.urlparse(client.base_url) + # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts + updated_url = parsed._replace(path=base_path) + return client.with_base_url(updated_url.geturl()) + + +def v3_stable_client(client: Client) -> Client: + """Override a client's base URL with a v2 stable path.""" + return replace_client_path(client, "api/v3-draft") + + +def v3_alpha_client(client: Client) -> Client: + """Override a client's base URL with a v2-alpha path.""" + return replace_client_path(client, "api/v3-alpha") + + +def v3_beta_client(client: Client) -> Client: + """Override a client's base URL with a v2-beta path.""" + return replace_client_path(client, "api/v3-beta") diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index 6796cf6b8..682f50a44 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -212,4 +212,3 @@ def v3_alpha_client(client: Client) -> Client: def v3_beta_client(client: Client) -> Client: """Override a client's base URL with a v2-beta path.""" return replace_client_path(client, "api/v3-beta") - diff --git a/tests/tmp/test_load_from_path_absolute_e0/example.yml b/tests/tmp/test_load_from_path_absolute_e0/example.yml new file mode 100644 index 000000000..9fda7bb4c --- /dev/null +++ b/tests/tmp/test_load_from_path_absolute_e0/example.yml @@ -0,0 +1,7 @@ +class_overrides: + Class1: {class_name: ExampleClass, module_name: example_module} + Class2: {class_name: DifferentClass, module_name: different_module} +field_prefix: blah +package_name_override: package_name +package_version_override: package_version +project_name_override: project-name \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_e1/example.json b/tests/tmp/test_load_from_path_absolute_e1/example.json new file mode 100644 index 000000000..1740d47f3 --- /dev/null +++ b/tests/tmp/test_load_from_path_absolute_e1/example.json @@ -0,0 +1 @@ +{"field_prefix": "blah", "class_overrides": {"Class1": {"class_name": "ExampleClass", "module_name": "example_module"}, "Class2": {"class_name": "DifferentClass", "module_name": "different_module"}}, "project_name_override": "project-name", "package_name_override": "package_name", "package_version_override": "package_version"} \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_e2/example.yaml b/tests/tmp/test_load_from_path_absolute_e2/example.yaml new file mode 100644 index 000000000..9fda7bb4c --- /dev/null +++ b/tests/tmp/test_load_from_path_absolute_e2/example.yaml @@ -0,0 +1,7 @@ +class_overrides: + Class1: {class_name: ExampleClass, module_name: example_module} + Class2: {class_name: DifferentClass, module_name: different_module} +field_prefix: blah +package_name_override: package_name +package_version_override: package_version +project_name_override: project-name \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_e3/example.json b/tests/tmp/test_load_from_path_absolute_e3/example.json new file mode 100644 index 000000000..e8b244366 --- /dev/null +++ b/tests/tmp/test_load_from_path_absolute_e3/example.json @@ -0,0 +1,16 @@ +{ + "field_prefix": "blah", + "class_overrides": { + "Class1": { + "class_name": "ExampleClass", + "module_name": "example_module" + }, + "Class2": { + "class_name": "DifferentClass", + "module_name": "different_module" + } + }, + "project_name_override": "project-name", + "package_name_override": "package_name", + "package_version_override": "package_version" +} \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_ecurrent b/tests/tmp/test_load_from_path_absolute_ecurrent new file mode 120000 index 000000000..e0d45a17c --- /dev/null +++ b/tests/tmp/test_load_from_path_absolute_ecurrent @@ -0,0 +1 @@ +/Users/tara.natarajan/openapi-python-client/tests/tmp/test_load_from_path_absolute_e3 \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_relative_ecurrent b/tests/tmp/test_load_from_path_relative_ecurrent new file mode 120000 index 000000000..d34e9c16d --- /dev/null +++ b/tests/tmp/test_load_from_path_relative_ecurrent @@ -0,0 +1 @@ +/Users/tara.natarajan/openapi-python-client/tests/tmp/test_load_from_path_relative_e3 \ No newline at end of file From b2ab7632ec8c1f5f832bc1f9de22e73f917a4208 Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:24:41 -0700 Subject: [PATCH 04/11] Update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cf55d5c7c..bbddc7727 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "benchling-openapi-python-client" -version = "2.0.0-alpha.dev2" +version = "2.0.0-alpha.dev3" description = "Generate modern Python clients from OpenAPI - Benchling fork" repository = "https://github.com/benchling/openapi-python-client" license = "MIT" From d81bfa8eb9b4d8a5b45b9cf468bc07224e0be61f Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:26:18 -0700 Subject: [PATCH 05/11] Update tests again for spaces --- .../my_test_api_client/client.py | 1 + .../my_enum_api_client/client.py | 308 ++++++++++++++++++ .../test_3_1_features_client/client.py | 2 + 3 files changed, 311 insertions(+) diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-record/my_test_api_client/client.py index ab5ef0ae7..1a07f304e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/client.py +++ b/end_to_end_tests/golden-record/my_test_api_client/client.py @@ -268,6 +268,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) + def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" parsed = urllib.parse.urlparse(client.base_url) diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py index aab8f4a0a..ddd83a51a 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py @@ -274,3 +274,311 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) + return client.with_base_url(updated_url.geturl()) + + +def v3_stable_client(client: Client) -> Client: + """Override a client's base URL with a v2 stable path.""" + return replace_client_path(client, "api/v3-draft") + + +def v3_alpha_client(client: Client) -> Client: + """Override a client's base URL with a v2-alpha path.""" + return replace_client_path(client, "api/v3-alpha") + + +def v3_beta_client(client: Client) -> Client: + """Override a client's base URL with a v2-beta path.""" + return replace_client_path(client, "api/v3-beta") +import ssl +from typing import Any, Optional, Union + +import httpx +from attrs import define, evolve, field +import urllib.parse + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: Optional[httpx.Client] = field(default=None, init=False) + _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout (in seconds)""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "Client": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "Client": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": + """Manually the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "Client": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: Optional[httpx.Client] = field(default=None, init=False) + _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout (in seconds)""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "AuthenticatedClient": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": + """Manually the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "AuthenticatedClient": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +def replace_client_path(client: Client, base_path: str) -> Client: + """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" + parsed = urllib.parse.urlparse(client.base_url) + # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts + updated_url = parsed._replace(path=base_path) + return client.with_base_url(updated_url.geturl()) + + +def v3_stable_client(client: Client) -> Client: + """Override a client's base URL with a v2 stable path.""" + return replace_client_path(client, "api/v3-draft") + + +def v3_alpha_client(client: Client) -> Client: + """Override a client's base URL with a v2-alpha path.""" + return replace_client_path(client, "api/v3-alpha") + + +def v3_beta_client(client: Client) -> Client: + """Override a client's base URL with a v2-beta path.""" + return replace_client_path(client, "api/v3-beta") diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py index 5483b329f..1a07f304e 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py @@ -5,6 +5,7 @@ from attrs import define, evolve, field import urllib.parse + @define class Client: """A class for keeping track of data related to the API @@ -267,6 +268,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" await self.get_async_httpx_client().__aexit__(*args, **kwargs) + def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" parsed = urllib.parse.urlparse(client.base_url) From c6a93b678b12e256af8b2e301336ee8d1ef8d9df Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:45:14 -0700 Subject: [PATCH 06/11] Update client file to include url --- .../my_test_api_client/client.py | 3 +- .../my_enum_api_client/client.py | 295 +----------------- .../test_3_1_features_client/client.py | 1 + .../templates/client.py.jinja | 1 + 4 files changed, 6 insertions(+), 294 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-record/my_test_api_client/client.py index 1a07f304e..78065228c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/client.py +++ b/end_to_end_tests/golden-record/my_test_api_client/client.py @@ -274,7 +274,8 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - return client.with_base_url(updated_url.geturl()) + client.base_url = updated_url.geturl() + return client def v3_stable_client(client: Client) -> Client: diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py index ddd83a51a..78065228c 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py @@ -274,299 +274,8 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - return client.with_base_url(updated_url.geturl()) - - -def v3_stable_client(client: Client) -> Client: - """Override a client's base URL with a v2 stable path.""" - return replace_client_path(client, "api/v3-draft") - - -def v3_alpha_client(client: Client) -> Client: - """Override a client's base URL with a v2-alpha path.""" - return replace_client_path(client, "api/v3-alpha") - - -def v3_beta_client(client: Client) -> Client: - """Override a client's base URL with a v2-beta path.""" - return replace_client_path(client, "api/v3-beta") -import ssl -from typing import Any, Optional, Union - -import httpx -from attrs import define, evolve, field -import urllib.parse - - -@define -class Client: - """A class for keeping track of data related to the API - - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: - - ``base_url``: The base URL for the API, all requests are made to a relative path to this URL - - ``cookies``: A dictionary of cookies to be sent with every request - - ``headers``: A dictionary of headers to be sent with every request - - ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. - - ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, - but can be set to False for testing purposes. - - ``follow_redirects``: Whether or not to follow redirects. Default value is False. - - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. - - - Attributes: - raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a - status code that was not documented in the source OpenAPI document. Can also be provided as a keyword - argument to the constructor. - """ - - raise_on_unexpected_status: bool = field(default=False, kw_only=True) - _base_url: str = field(alias="base_url") - _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") - _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") - _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") - _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) - - def with_headers(self, headers: dict[str, str]) -> "Client": - """Get a new client matching this one with additional headers""" - if self._client is not None: - self._client.headers.update(headers) - if self._async_client is not None: - self._async_client.headers.update(headers) - return evolve(self, headers={**self._headers, **headers}) - - def with_cookies(self, cookies: dict[str, str]) -> "Client": - """Get a new client matching this one with additional cookies""" - if self._client is not None: - self._client.cookies.update(cookies) - if self._async_client is not None: - self._async_client.cookies.update(cookies) - return evolve(self, cookies={**self._cookies, **cookies}) - - def with_timeout(self, timeout: httpx.Timeout) -> "Client": - """Get a new client matching this one with a new timeout (in seconds)""" - if self._client is not None: - self._client.timeout = timeout - if self._async_client is not None: - self._async_client.timeout = timeout - return evolve(self, timeout=timeout) - - def set_httpx_client(self, client: httpx.Client) -> "Client": - """Manually set the underlying httpx.Client - - **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. - """ - self._client = client - return self - - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" - if self._client is None: - self._client = httpx.Client( - base_url=self._base_url, - cookies=self._cookies, - headers=self._headers, - timeout=self._timeout, - verify=self._verify_ssl, - follow_redirects=self._follow_redirects, - **self._httpx_args, - ) - return self._client - - def __enter__(self) -> "Client": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" - self.get_httpx_client().__enter__() - return self - - def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" - self.get_httpx_client().__exit__(*args, **kwargs) - - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": - """Manually the underlying httpx.AsyncClient - - **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. - """ - self._async_client = async_client - return self - - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" - if self._async_client is None: - self._async_client = httpx.AsyncClient( - base_url=self._base_url, - cookies=self._cookies, - headers=self._headers, - timeout=self._timeout, - verify=self._verify_ssl, - follow_redirects=self._follow_redirects, - **self._httpx_args, - ) - return self._async_client - - async def __aenter__(self) -> "Client": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" - await self.get_async_httpx_client().__aenter__() - return self - - async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" - await self.get_async_httpx_client().__aexit__(*args, **kwargs) - - -@define -class AuthenticatedClient: - """A Client which has been authenticated for use on secured endpoints - - The following are accepted as keyword arguments and will be used to construct httpx Clients internally: - - ``base_url``: The base URL for the API, all requests are made to a relative path to this URL - - ``cookies``: A dictionary of cookies to be sent with every request - - ``headers``: A dictionary of headers to be sent with every request - - ``timeout``: The maximum amount of a time a request can take. API functions will raise - httpx.TimeoutException if this is exceeded. - - ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, - but can be set to False for testing purposes. - - ``follow_redirects``: Whether or not to follow redirects. Default value is False. - - ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. - - - Attributes: - raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a - status code that was not documented in the source OpenAPI document. Can also be provided as a keyword - argument to the constructor. - token: The token to use for authentication - prefix: The prefix to use for the Authorization header - auth_header_name: The name of the Authorization header - """ - - raise_on_unexpected_status: bool = field(default=False, kw_only=True) - _base_url: str = field(alias="base_url") - _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") - _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") - _timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout") - _verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl") - _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") - _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") - _client: Optional[httpx.Client] = field(default=None, init=False) - _async_client: Optional[httpx.AsyncClient] = field(default=None, init=False) - - token: str - prefix: str = "Bearer" - auth_header_name: str = "Authorization" - - def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": - """Get a new client matching this one with additional headers""" - if self._client is not None: - self._client.headers.update(headers) - if self._async_client is not None: - self._async_client.headers.update(headers) - return evolve(self, headers={**self._headers, **headers}) - - def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": - """Get a new client matching this one with additional cookies""" - if self._client is not None: - self._client.cookies.update(cookies) - if self._async_client is not None: - self._async_client.cookies.update(cookies) - return evolve(self, cookies={**self._cookies, **cookies}) - - def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": - """Get a new client matching this one with a new timeout (in seconds)""" - if self._client is not None: - self._client.timeout = timeout - if self._async_client is not None: - self._async_client.timeout = timeout - return evolve(self, timeout=timeout) - - def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": - """Manually set the underlying httpx.Client - - **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. - """ - self._client = client - return self - - def get_httpx_client(self) -> httpx.Client: - """Get the underlying httpx.Client, constructing a new one if not previously set""" - if self._client is None: - self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._client = httpx.Client( - base_url=self._base_url, - cookies=self._cookies, - headers=self._headers, - timeout=self._timeout, - verify=self._verify_ssl, - follow_redirects=self._follow_redirects, - **self._httpx_args, - ) - return self._client - - def __enter__(self) -> "AuthenticatedClient": - """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" - self.get_httpx_client().__enter__() - return self - - def __exit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for internal httpx.Client (see httpx docs)""" - self.get_httpx_client().__exit__(*args, **kwargs) - - def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": - """Manually the underlying httpx.AsyncClient - - **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. - """ - self._async_client = async_client - return self - - def get_async_httpx_client(self) -> httpx.AsyncClient: - """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" - if self._async_client is None: - self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token - self._async_client = httpx.AsyncClient( - base_url=self._base_url, - cookies=self._cookies, - headers=self._headers, - timeout=self._timeout, - verify=self._verify_ssl, - follow_redirects=self._follow_redirects, - **self._httpx_args, - ) - return self._async_client - - async def __aenter__(self) -> "AuthenticatedClient": - """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" - await self.get_async_httpx_client().__aenter__() - return self - - async def __aexit__(self, *args: Any, **kwargs: Any) -> None: - """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" - await self.get_async_httpx_client().__aexit__(*args, **kwargs) - - -def replace_client_path(client: Client, base_path: str) -> Client: - """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" - parsed = urllib.parse.urlparse(client.base_url) - # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts - updated_url = parsed._replace(path=base_path) - return client.with_base_url(updated_url.geturl()) + client.base_url = updated_url.geturl() + return client def v3_stable_client(client: Client) -> Client: diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py index 1a07f304e..05da32e8d 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py @@ -274,6 +274,7 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) + client.base_url = updated_url.geturl() return client.with_base_url(updated_url.geturl()) diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index 682f50a44..c46848c34 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -196,6 +196,7 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) + client.base_url = updated_url.geturl() return client.with_base_url(updated_url.geturl()) From edf009a7c8e004356183ec637ba7daa94ad7f90c Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:54:33 -0700 Subject: [PATCH 07/11] updated tests --- .../golden-record/my_test_api_client/client.py | 4 ++-- .../my_enum_api_client/client.py | 4 ++-- .../test_3_1_features_client/client.py | 6 +++--- integration-tests/integration_tests/client.py | 3 ++- openapi_python_client/templates/client.py.jinja | 7 +++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-record/my_test_api_client/client.py index 78065228c..91cb19d01 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/client.py +++ b/end_to_end_tests/golden-record/my_test_api_client/client.py @@ -1,9 +1,9 @@ import ssl +import urllib.parse from typing import Any, Optional, Union import httpx from attrs import define, evolve, field -import urllib.parse @define @@ -274,7 +274,7 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - client.base_url = updated_url.geturl() + client._base_url = updated_url.geturl() return client diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py index 78065228c..91cb19d01 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py @@ -1,9 +1,9 @@ import ssl +import urllib.parse from typing import Any, Optional, Union import httpx from attrs import define, evolve, field -import urllib.parse @define @@ -274,7 +274,7 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - client.base_url = updated_url.geturl() + client._base_url = updated_url.geturl() return client diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py index 05da32e8d..bab9f8d3b 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py @@ -1,9 +1,9 @@ import ssl +import urllib.parse from typing import Any, Optional, Union import httpx from attrs import define, evolve, field -import urllib.parse @define @@ -274,8 +274,8 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - client.base_url = updated_url.geturl() - return client.with_base_url(updated_url.geturl()) + client._base_url = updated_url.geturl() + return client def v3_stable_client(client: Client) -> Client: diff --git a/integration-tests/integration_tests/client.py b/integration-tests/integration_tests/client.py index 6f4d1157f..91cb19d01 100644 --- a/integration-tests/integration_tests/client.py +++ b/integration-tests/integration_tests/client.py @@ -274,7 +274,8 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - return client.with_base_url(updated_url.geturl()) + client._base_url = updated_url.geturl() + return client def v3_stable_client(client: Client) -> Client: diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index c46848c34..729c1e4fe 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -1,9 +1,9 @@ import ssl +import urllib.parse from typing import Any, Union, Optional from attrs import define, field, evolve import httpx -import urllib.parse {% set attrs_info = { @@ -196,9 +196,8 @@ def replace_client_path(client: Client, base_path: str) -> Client: parsed = urllib.parse.urlparse(client.base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - client.base_url = updated_url.geturl() - return client.with_base_url(updated_url.geturl()) - + client._base_url = updated_url.geturl() + return client def v3_stable_client(client: Client) -> Client: """Override a client's base URL with a v2 stable path.""" From 34991db87dd365a52c777bcc94f54a0981d4bac1 Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 15:59:03 -0700 Subject: [PATCH 08/11] Updated tests --- end_to_end_tests/golden-record/my_test_api_client/client.py | 2 +- .../literal-enums-golden-record/my_enum_api_client/client.py | 2 +- .../test_3_1_features_client/client.py | 5 ++--- integration-tests/integration_tests/client.py | 2 +- openapi_python_client/templates/client.py.jinja | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/client.py b/end_to_end_tests/golden-record/my_test_api_client/client.py index 91cb19d01..9b01a2795 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/client.py +++ b/end_to_end_tests/golden-record/my_test_api_client/client.py @@ -271,7 +271,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" - parsed = urllib.parse.urlparse(client.base_url) + parsed = urllib.parse.urlparse(client._base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) client._base_url = updated_url.geturl() diff --git a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py index 91cb19d01..9b01a2795 100644 --- a/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py +++ b/end_to_end_tests/literal-enums-golden-record/my_enum_api_client/client.py @@ -271,7 +271,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" - parsed = urllib.parse.urlparse(client.base_url) + parsed = urllib.parse.urlparse(client._base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) client._base_url = updated_url.geturl() diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py index bab9f8d3b..f0c5b0608 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py @@ -271,13 +271,12 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" - parsed = urllib.parse.urlparse(client.base_url) + parsed = urllib.parse.urlparse(client._base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) - client._base_url = updated_url.geturl() + client._base_url = updated_url.geturl() return client - def v3_stable_client(client: Client) -> Client: """Override a client's base URL with a v2 stable path.""" return replace_client_path(client, "api/v3-draft") diff --git a/integration-tests/integration_tests/client.py b/integration-tests/integration_tests/client.py index 91cb19d01..9b01a2795 100644 --- a/integration-tests/integration_tests/client.py +++ b/integration-tests/integration_tests/client.py @@ -271,7 +271,7 @@ async def __aexit__(self, *args: Any, **kwargs: Any) -> None: def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" - parsed = urllib.parse.urlparse(client.base_url) + parsed = urllib.parse.urlparse(client._base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) client._base_url = updated_url.geturl() diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index 729c1e4fe..2b9c31ba0 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -193,7 +193,7 @@ class AuthenticatedClient: def replace_client_path(client: Client, base_path: str) -> Client: """Override a client's base URL with a new path. Does not update scheme, host, or other URL parts.""" - parsed = urllib.parse.urlparse(client.base_url) + parsed = urllib.parse.urlparse(client._base_url) # _replace is not private, it's part of the NamedTuple API but prefixed _ to avoid conflicts updated_url = parsed._replace(path=base_path) client._base_url = updated_url.geturl() From f2a10d08c2c5db9b231a2707061b6e0b394caf22 Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 16:00:28 -0700 Subject: [PATCH 09/11] Missing line --- openapi_python_client/templates/client.py.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/openapi_python_client/templates/client.py.jinja b/openapi_python_client/templates/client.py.jinja index 2b9c31ba0..3a0df0a7b 100644 --- a/openapi_python_client/templates/client.py.jinja +++ b/openapi_python_client/templates/client.py.jinja @@ -199,6 +199,7 @@ def replace_client_path(client: Client, base_path: str) -> Client: client._base_url = updated_url.geturl() return client + def v3_stable_client(client: Client) -> Client: """Override a client's base URL with a v2 stable path.""" return replace_client_path(client, "api/v3-draft") From cf1df429b330f0d400275f6e6d2d5b392b6d275c Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 16:03:32 -0700 Subject: [PATCH 10/11] Missing line --- .../test-3-1-golden-record/test_3_1_features_client/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py index f0c5b0608..9b01a2795 100644 --- a/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py +++ b/end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/client.py @@ -277,6 +277,7 @@ def replace_client_path(client: Client, base_path: str) -> Client: client._base_url = updated_url.geturl() return client + def v3_stable_client(client: Client) -> Client: """Override a client's base URL with a v2 stable path.""" return replace_client_path(client, "api/v3-draft") From 3210556a381a79d68ff5c72b029a2bff9e69675f Mon Sep 17 00:00:00 2001 From: Tara Natarajan Date: Wed, 14 May 2025 16:07:09 -0700 Subject: [PATCH 11/11] reset version --- pyproject.toml | 2 +- .../test_load_from_path_absolute_e0/example.yml | 7 ------- .../test_load_from_path_absolute_e1/example.json | 1 - .../test_load_from_path_absolute_e2/example.yaml | 7 ------- .../test_load_from_path_absolute_e3/example.json | 16 ---------------- tests/tmp/test_load_from_path_absolute_ecurrent | 1 - tests/tmp/test_load_from_path_relative_ecurrent | 1 - 7 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 tests/tmp/test_load_from_path_absolute_e0/example.yml delete mode 100644 tests/tmp/test_load_from_path_absolute_e1/example.json delete mode 100644 tests/tmp/test_load_from_path_absolute_e2/example.yaml delete mode 100644 tests/tmp/test_load_from_path_absolute_e3/example.json delete mode 120000 tests/tmp/test_load_from_path_absolute_ecurrent delete mode 120000 tests/tmp/test_load_from_path_relative_ecurrent diff --git a/pyproject.toml b/pyproject.toml index bbddc7727..1bb7e629b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "benchling-openapi-python-client" -version = "2.0.0-alpha.dev3" +version = "2.0.0-alpha.2" description = "Generate modern Python clients from OpenAPI - Benchling fork" repository = "https://github.com/benchling/openapi-python-client" license = "MIT" diff --git a/tests/tmp/test_load_from_path_absolute_e0/example.yml b/tests/tmp/test_load_from_path_absolute_e0/example.yml deleted file mode 100644 index 9fda7bb4c..000000000 --- a/tests/tmp/test_load_from_path_absolute_e0/example.yml +++ /dev/null @@ -1,7 +0,0 @@ -class_overrides: - Class1: {class_name: ExampleClass, module_name: example_module} - Class2: {class_name: DifferentClass, module_name: different_module} -field_prefix: blah -package_name_override: package_name -package_version_override: package_version -project_name_override: project-name \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_e1/example.json b/tests/tmp/test_load_from_path_absolute_e1/example.json deleted file mode 100644 index 1740d47f3..000000000 --- a/tests/tmp/test_load_from_path_absolute_e1/example.json +++ /dev/null @@ -1 +0,0 @@ -{"field_prefix": "blah", "class_overrides": {"Class1": {"class_name": "ExampleClass", "module_name": "example_module"}, "Class2": {"class_name": "DifferentClass", "module_name": "different_module"}}, "project_name_override": "project-name", "package_name_override": "package_name", "package_version_override": "package_version"} \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_e2/example.yaml b/tests/tmp/test_load_from_path_absolute_e2/example.yaml deleted file mode 100644 index 9fda7bb4c..000000000 --- a/tests/tmp/test_load_from_path_absolute_e2/example.yaml +++ /dev/null @@ -1,7 +0,0 @@ -class_overrides: - Class1: {class_name: ExampleClass, module_name: example_module} - Class2: {class_name: DifferentClass, module_name: different_module} -field_prefix: blah -package_name_override: package_name -package_version_override: package_version -project_name_override: project-name \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_e3/example.json b/tests/tmp/test_load_from_path_absolute_e3/example.json deleted file mode 100644 index e8b244366..000000000 --- a/tests/tmp/test_load_from_path_absolute_e3/example.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "field_prefix": "blah", - "class_overrides": { - "Class1": { - "class_name": "ExampleClass", - "module_name": "example_module" - }, - "Class2": { - "class_name": "DifferentClass", - "module_name": "different_module" - } - }, - "project_name_override": "project-name", - "package_name_override": "package_name", - "package_version_override": "package_version" -} \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_absolute_ecurrent b/tests/tmp/test_load_from_path_absolute_ecurrent deleted file mode 120000 index e0d45a17c..000000000 --- a/tests/tmp/test_load_from_path_absolute_ecurrent +++ /dev/null @@ -1 +0,0 @@ -/Users/tara.natarajan/openapi-python-client/tests/tmp/test_load_from_path_absolute_e3 \ No newline at end of file diff --git a/tests/tmp/test_load_from_path_relative_ecurrent b/tests/tmp/test_load_from_path_relative_ecurrent deleted file mode 120000 index d34e9c16d..000000000 --- a/tests/tmp/test_load_from_path_relative_ecurrent +++ /dev/null @@ -1 +0,0 @@ -/Users/tara.natarajan/openapi-python-client/tests/tmp/test_load_from_path_relative_e3 \ No newline at end of file