diff --git a/generators/python/core_utilities/shared/jsonable_encoder.py b/generators/python/core_utilities/shared/jsonable_encoder.py index ee2b36b91d33..460d3737c617 100644 --- a/generators/python/core_utilities/shared/jsonable_encoder.py +++ b/generators/python/core_utilities/shared/jsonable_encoder.py @@ -13,6 +13,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -116,3 +117,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/generators/python/sdk/changes/unreleased/encode-path-parameters.yml b/generators/python/sdk/changes/unreleased/encode-path-parameters.yml new file mode 100644 index 000000000000..6768d4106e9f --- /dev/null +++ b/generators/python/sdk/changes/unreleased/encode-path-parameters.yml @@ -0,0 +1,7 @@ +- summary: | + Add an `encode_path_params` config option (default `false`). When enabled, path parameter + values are percent-encoded when substituted into the request path, so a value containing `/` + or `..` can no longer change which endpoint the request resolves to. The default `false` + preserves the existing (unencoded) behavior; TypeScript, Go, Java, and C# already encode path + params. + type: feat diff --git a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py index a4e5d64eef5a..f2680d1b81b2 100644 --- a/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py +++ b/generators/python/src/fern_python/generators/sdk/core_utilities/core_utilities.py @@ -44,6 +44,7 @@ def __init__( self._version = custom_config.pydantic_config.version self._project_module_path = project_module_path self._use_pydantic_field_aliases = custom_config.pydantic_config.use_pydantic_field_aliases + self._encode_path_params = custom_config.encode_path_params self._should_generate_websocket_clients = custom_config.should_generate_websocket_clients self._exclude_types_from_init_exports = custom_config.exclude_types_from_init_exports self._custom_pager_base_name = self._sanitize_pager_name(custom_config.custom_pager_name or "CustomPager") @@ -682,7 +683,7 @@ def encode_path_param(self, obj: AST.Expression) -> AST.Expression: qualified_name_excluding_import=(), import_=AST.ReferenceImport( module=AST.Module.local(*self._module_path, "jsonable_encoder"), - named_import="encode_path_param", + named_import="quote_path_param" if self._encode_path_params else "encode_path_param", ), ), args=[obj], diff --git a/generators/python/src/fern_python/generators/sdk/custom_config.py b/generators/python/src/fern_python/generators/sdk/custom_config.py index 87fb1c9dd814..a167d66cedc2 100644 --- a/generators/python/src/fern_python/generators/sdk/custom_config.py +++ b/generators/python/src/fern_python/generators/sdk/custom_config.py @@ -129,6 +129,11 @@ class SDKCustomConfig(pydantic.BaseModel): # If true, treats path parameters as named parameters in endpoint functions inline_path_params: bool = False + # If true, path parameter values are percent-encoded when substituted into the + # request path, so a value containing "/" or ".." cannot change which endpoint + # the request resolves to. Off by default so existing output is unchanged. + encode_path_params: bool = False + # Feature flag that enables generation of Python websocket clients should_generate_websocket_clients: bool = False diff --git a/generators/python/tests/sdk/test_jsonable_encoder.py b/generators/python/tests/sdk/test_jsonable_encoder.py index e60d81c92849..0e49c3c10aba 100644 --- a/generators/python/tests/sdk/test_jsonable_encoder.py +++ b/generators/python/tests/sdk/test_jsonable_encoder.py @@ -2,10 +2,31 @@ from fern.generator_exec.logging import GeneratorUpdate, InitUpdateV2 -from core_utilities.shared.jsonable_encoder import jsonable_encoder +from core_utilities.shared.jsonable_encoder import encode_path_param, jsonable_encoder, quote_path_param def test_jsonable_encoder() -> None: updates: List[GeneratorUpdate] = [GeneratorUpdate.factory.init_v_2(InitUpdateV2(publishing_to_registry=None))] serialized = jsonable_encoder(updates) assert serialized == [{"_type": "initV2", "publishingToRegistry": None}] + + +def test_encode_path_param() -> None: + assert encode_path_param("../connections") == "../connections" + assert encode_path_param("user_1") == "user_1" + assert encode_path_param(42) == "42" + assert encode_path_param(True) == "true" + assert encode_path_param(False) == "false" + + +def test_quote_path_param() -> None: + assert quote_path_param("../connections") == "..%2Fconnections" + assert quote_path_param("user id?") == "user%20id%3F" + assert quote_path_param("user_1") == "user_1" + assert quote_path_param(42) == "42" + assert quote_path_param(True) == "true" + assert quote_path_param(False) == "false" + # every "/" is encoded, so a multi-segment value cannot change the endpoint + assert quote_path_param("a/b/c") == "a%2Fb%2Fc" + # already-encoded input is encoded again rather than passed through + assert quote_path_param("a%2Fb") == "a%252Fb" diff --git a/seed/python-sdk/accept-header/src/seed/core/jsonable_encoder.py b/seed/python-sdk/accept-header/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/accept-header/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/accept-header/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/alias-extends/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/alias-extends/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/alias/src/seed/core/jsonable_encoder.py b/seed/python-sdk/alias/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/alias/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/alias/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/allof-inline/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/allof-inline/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/allof-inline/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/allof-inline/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/allof/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/allof/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/allof/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/allof/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/any-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/any-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/any-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/any-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/api-wide-base-path-with-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/api-wide-base-path-with-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/api-wide-base-path-with-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/api-wide-base-path-with-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/api-wide-base-path/src/seed/core/jsonable_encoder.py b/seed/python-sdk/api-wide-base-path/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/api-wide-base-path/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/api-wide-base-path/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/audiences/src/seed/core/jsonable_encoder.py b/seed/python-sdk/audiences/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/audiences/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/audiences/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/basic-auth-environment-variables/src/seed/core/jsonable_encoder.py b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/basic-auth-environment-variables/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/basic-auth-environment-variables/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/basic-auth-pw-omitted/with-wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/basic-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/basic-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/basic-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/basic-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/bearer-token-environment-variable/src/seed/core/jsonable_encoder.py b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/bearer-token-environment-variable/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/bearer-token-environment-variable/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/bytes-download/src/seed/core/jsonable_encoder.py b/seed/python-sdk/bytes-download/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/bytes-download/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/bytes-download/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/bytes-upload/src/seed/core/jsonable_encoder.py b/seed/python-sdk/bytes-upload/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/bytes-upload/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/bytes-upload/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/circular-references-advanced/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/circular-references-advanced/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/circular-references-advanced/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/circular-references-advanced/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/circular-references-advanced/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/circular-references-extends/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/circular-references-extends/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/circular-references-extends/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/circular-references-extends/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/circular-references/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/circular-references/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/circular-references/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/circular-references/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/client-side-params/src/seed/core/jsonable_encoder.py b/seed/python-sdk/client-side-params/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/client-side-params/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/client-side-params/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/content-type/src/seed/core/jsonable_encoder.py b/seed/python-sdk/content-type/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/content-type/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/content-type/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/cross-package-type-names/src/seed/core/jsonable_encoder.py b/seed/python-sdk/cross-package-type-names/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/cross-package-type-names/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/cross-package-type-names/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/discriminated-union-with-nested-oneof/src/seed/core/jsonable_encoder.py b/seed/python-sdk/discriminated-union-with-nested-oneof/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/discriminated-union-with-nested-oneof/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/discriminated-union-with-nested-oneof/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/dollar-string-examples/src/seed/core/jsonable_encoder.py b/seed/python-sdk/dollar-string-examples/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/dollar-string-examples/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/dollar-string-examples/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/empty-clients/src/seed/core/jsonable_encoder.py b/seed/python-sdk/empty-clients/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/empty-clients/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/empty-clients/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/endpoint-security-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/endpoint-security-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/endpoint-security-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/endpoint-security-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/enum/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/enum/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/enum/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/enum/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/enum/real-enum-forward-compat/src/seed/core/jsonable_encoder.py b/seed/python-sdk/enum/real-enum-forward-compat/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/enum/real-enum-forward-compat/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/enum/real-enum-forward-compat/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/enum/real-enum/src/seed/core/jsonable_encoder.py b/seed/python-sdk/enum/real-enum/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/enum/real-enum/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/enum/real-enum/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/enum/strenum/src/seed/core/jsonable_encoder.py b/seed/python-sdk/enum/strenum/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/enum/strenum/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/enum/strenum/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/error-property/src/seed/core/jsonable_encoder.py b/seed/python-sdk/error-property/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/error-property/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/error-property/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/errors/src/seed/core/jsonable_encoder.py b/seed/python-sdk/errors/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/errors/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/errors/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/additional_init_exports_with_duplicates/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/additional_init_exports_with_duplicates/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/additional_init_exports_with_duplicates/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/additional_init_exports_with_duplicates/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/client-filename/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/client-filename/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/client-filename/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/client-filename/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/include-platform-headers/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/include-platform-headers/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/include-platform-headers/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/include-platform-headers/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/legacy-wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/omit-fern-headers/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/omit-fern-headers/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/omit-fern-headers/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/omit-fern-headers/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/examples/readme/src/seed/core/jsonable_encoder.py b/seed/python-sdk/examples/readme/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/examples/readme/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/examples/readme/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/additional_init_exports/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/aliases_with_validation/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/aliases_without_validation/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/custom-transport/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/custom-transport/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/custom-transport/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/custom-transport/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/datetime-milliseconds/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/datetime-milliseconds/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/datetime-milliseconds/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/datetime-milliseconds/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/deps_with_min_python_version/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/eager-imports/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/eager-imports/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/eager-imports/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/extra_dependencies/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/extra_dev_dependencies/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/five-second-timeout/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/follow_redirects_by_default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/import-paths/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/import-paths/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/import-paths/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/import-paths/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/improved_imports/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/improved_imports/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/improved_imports/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/infinite-timeout/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/inline-path-params/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/inline_request_params/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/output-directory-project-root/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/output-directory-project-root/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/output-directory-project-root/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/output-directory-project-root/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/output-directory-source-root-no-package-root/sub/dir/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/output-directory-source-root-no-package-root/sub/dir/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/output-directory-source-root-no-package-root/sub/dir/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/output-directory-source-root-no-package-root/sub/dir/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/output-directory-source-root-with-package-path/seed/my_org/my_sdk/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/output-directory-source-root-with-package-path/seed/my_org/my_sdk/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/output-directory-source-root-with-package-path/seed/my_org/my_sdk/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/output-directory-source-root-with-package-path/seed/my_org/my_sdk/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/output-directory-source-root/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/output-directory-source-root/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/output-directory-source-root/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/output-directory-source-root/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/package-path/src/seed/matryoshka/doll/structure/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/package-path/src/seed/matryoshka/doll/structure/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/package-path/src/seed/matryoshka/doll/structure/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/package-path/src/seed/matryoshka/doll/structure/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pydantic-extra-fields/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pydantic-ignore-fields/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pydantic-v1-with-utils/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pydantic-v1-wrapped/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pydantic-v1/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pydantic-v2-wrapped/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/pyproject_extras/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/runtime-version/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/runtime-version/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/runtime-version/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/runtime-version/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/skip-pydantic-validation/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/tcp-keepalive/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/tcp-keepalive/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/tcp-keepalive/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/tcp-keepalive/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/union-utils/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/union-utils/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/union-utils/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/union-utils/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/exhaustive/wire-tests-custom-client-name/src/seed/core/jsonable_encoder.py b/seed/python-sdk/exhaustive/wire-tests-custom-client-name/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/exhaustive/wire-tests-custom-client-name/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/exhaustive/wire-tests-custom-client-name/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/extends/src/seed/core/jsonable_encoder.py b/seed/python-sdk/extends/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/extends/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/extends/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/extra-properties/src/seed/core/jsonable_encoder.py b/seed/python-sdk/extra-properties/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/extra-properties/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/extra-properties/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/file-download/default-chunk-size/src/seed/core/jsonable_encoder.py b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/file-download/default-chunk-size/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/file-download/default-chunk-size/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/file-download/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/file-download/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/file-download/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/file-download/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/file-upload-openapi/src/seed/core/jsonable_encoder.py b/seed/python-sdk/file-upload-openapi/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/file-upload-openapi/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/file-upload-openapi/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/file-upload/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/file-upload/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/file-upload/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/file-upload/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/file-upload/use_typeddict_requests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/folders/src/seed/core/jsonable_encoder.py b/seed/python-sdk/folders/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/folders/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/folders/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/header-auth-environment-variable/src/seed/core/jsonable_encoder.py b/seed/python-sdk/header-auth-environment-variable/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/header-auth-environment-variable/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/header-auth-environment-variable/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/header-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/header-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/header-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/header-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/http-head/src/seed/core/jsonable_encoder.py b/seed/python-sdk/http-head/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/http-head/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/http-head/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/idempotency-headers/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py b/seed/python-sdk/idempotency-headers/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/idempotency-headers/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/idempotency-headers/auto-generate-idempotency-key/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/idempotency-headers/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/idempotency-headers/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/idempotency-headers/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/idempotency-headers/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/imdb/src/seed/core/jsonable_encoder.py b/seed/python-sdk/imdb/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/imdb/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/imdb/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/inferred-auth-explicit/src/seed/core/jsonable_encoder.py b/seed/python-sdk/inferred-auth-explicit/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/inferred-auth-explicit/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/inferred-auth-explicit/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/inferred-auth-implicit-api-key/src/seed/core/jsonable_encoder.py b/seed/python-sdk/inferred-auth-implicit-api-key/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/inferred-auth-implicit-api-key/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/inferred-auth-implicit-api-key/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/jsonable_encoder.py b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/inferred-auth-implicit-no-expiry/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/inferred-auth-implicit-reference/src/seed/core/jsonable_encoder.py b/seed/python-sdk/inferred-auth-implicit-reference/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/inferred-auth-implicit-reference/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/inferred-auth-implicit-reference/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/inferred-auth-implicit/src/seed/core/jsonable_encoder.py b/seed/python-sdk/inferred-auth-implicit/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/inferred-auth-implicit/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/inferred-auth-implicit/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/inline-enum-type-name-override/src/seed/core/jsonable_encoder.py b/seed/python-sdk/inline-enum-type-name-override/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/inline-enum-type-name-override/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/inline-enum-type-name-override/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/license/src/seed/core/jsonable_encoder.py b/seed/python-sdk/license/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/license/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/license/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/literal-user-agent/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/literal-user-agent/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/literal-user-agent/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/literal-user-agent/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/literal/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/literal/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/literal/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/literal/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/literal/use_typeddict_requests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/literal/wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/literal/wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/literal/wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/literal/wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/literals-unions/src/seed/core/jsonable_encoder.py b/seed/python-sdk/literals-unions/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/literals-unions/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/literals-unions/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/mixed-case/src/seed/core/jsonable_encoder.py b/seed/python-sdk/mixed-case/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/mixed-case/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/mixed-case/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/mixed-file-directory/exclude_types_from_init_exports/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/mixed-file-directory/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/multi-content-type-examples/src/seed/core/jsonable_encoder.py b/seed/python-sdk/multi-content-type-examples/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/multi-content-type-examples/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/multi-content-type-examples/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/multi-line-docs/src/seed/core/jsonable_encoder.py b/seed/python-sdk/multi-line-docs/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/multi-line-docs/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/multi-line-docs/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/multi-url-environment-no-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/multi-url-environment-no-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/multi-url-environment-no-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/multi-url-environment-reference/src/seed/core/jsonable_encoder.py b/seed/python-sdk/multi-url-environment-reference/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/multi-url-environment-reference/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/multi-url-environment-reference/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/multi-url-environment/src/seed/core/jsonable_encoder.py b/seed/python-sdk/multi-url-environment/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/multi-url-environment/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/multi-url-environment/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/multiple-request-bodies/src/seed/core/jsonable_encoder.py b/seed/python-sdk/multiple-request-bodies/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/multiple-request-bodies/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/multiple-request-bodies/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/no-content-response/src/seed/core/jsonable_encoder.py b/seed/python-sdk/no-content-response/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/no-content-response/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/no-content-response/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/no-environment/src/seed/core/jsonable_encoder.py b/seed/python-sdk/no-environment/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/no-environment/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/no-environment/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/no-retries/src/seed/core/jsonable_encoder.py b/seed/python-sdk/no-retries/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/no-retries/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/no-retries/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/null-type/src/seed/core/jsonable_encoder.py b/seed/python-sdk/null-type/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/null-type/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/null-type/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/nullable-allof-extends/src/seed/core/jsonable_encoder.py b/seed/python-sdk/nullable-allof-extends/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/nullable-allof-extends/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/nullable-allof-extends/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/nullable-optional/src/seed/core/jsonable_encoder.py b/seed/python-sdk/nullable-optional/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/nullable-optional/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/nullable-optional/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/nullable-request-body/src/seed/core/jsonable_encoder.py b/seed/python-sdk/nullable-request-body/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/nullable-request-body/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/nullable-request-body/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/nullable/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/nullable/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/nullable/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/nullable/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/nullable/use-typeddict-requests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-custom/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-environment-variables/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-mandatory-auth/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-nested-root/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-openapi/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-openapi/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-openapi/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-openapi/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-reference/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-reference/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-reference/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-reference/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials-with-variables/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-client-credentials/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-client-credentials/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-client-credentials/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-client-credentials/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/oauth-pkce/wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/oauth-pkce/wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/oauth-pkce/wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/oauth-pkce/wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/object/src/seed/core/jsonable_encoder.py b/seed/python-sdk/object/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/object/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/object/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/objects-with-imports/src/seed/core/jsonable_encoder.py b/seed/python-sdk/objects-with-imports/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/objects-with-imports/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/objects-with-imports/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/openapi-path-param-body-collision/src/seed/core/jsonable_encoder.py b/seed/python-sdk/openapi-path-param-body-collision/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/openapi-path-param-body-collision/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/openapi-path-param-body-collision/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/openapi-request-body-ref/src/seed/core/jsonable_encoder.py b/seed/python-sdk/openapi-request-body-ref/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/openapi-request-body-ref/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/openapi-request-body-ref/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/openapi-subtitle/src/seed/core/jsonable_encoder.py b/seed/python-sdk/openapi-subtitle/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/openapi-subtitle/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/openapi-subtitle/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/optional/src/seed/core/jsonable_encoder.py b/seed/python-sdk/optional/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/optional/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/optional/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/package-yml/src/seed/core/jsonable_encoder.py b/seed/python-sdk/package-yml/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/package-yml/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/package-yml/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/pagination-custom/src/seed/core/jsonable_encoder.py b/seed/python-sdk/pagination-custom/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/pagination-custom/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/pagination-custom/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/pagination-uri-path/src/seed/core/jsonable_encoder.py b/seed/python-sdk/pagination-uri-path/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/pagination-uri-path/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/pagination-uri-path/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/pagination/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/pagination/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/pagination/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/pagination/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/pagination/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/pagination/page-index-semantics/src/seed/core/jsonable_encoder.py b/seed/python-sdk/pagination/page-index-semantics/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/pagination/page-index-semantics/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/pagination/page-index-semantics/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/path-parameters/encode-path-params/.fern/metadata.json b/seed/python-sdk/path-parameters/encode-path-params/.fern/metadata.json new file mode 100644 index 000000000000..9e57d4571337 --- /dev/null +++ b/seed/python-sdk/path-parameters/encode-path-params/.fern/metadata.json @@ -0,0 +1,13 @@ +{ + "cliVersion": "DUMMY", + "generatorName": "fernapi/fern-python-sdk", + "generatorVersion": "local", + "generatorConfig": { + "encode_path_params": true + }, + "originGitCommit": "DUMMY", + "invokedBy": "ci", + "requestedVersion": "0.0.1", + "ciProvider": "github", + "sdkVersion": "0.0.1" +} \ No newline at end of file diff --git a/seed/python-sdk/path-parameters/.github/workflows/ci.yml b/seed/python-sdk/path-parameters/encode-path-params/.github/workflows/ci.yml similarity index 100% rename from seed/python-sdk/path-parameters/.github/workflows/ci.yml rename to seed/python-sdk/path-parameters/encode-path-params/.github/workflows/ci.yml diff --git a/seed/python-sdk/path-parameters/.gitignore b/seed/python-sdk/path-parameters/encode-path-params/.gitignore similarity index 100% rename from seed/python-sdk/path-parameters/.gitignore rename to seed/python-sdk/path-parameters/encode-path-params/.gitignore diff --git a/seed/python-sdk/path-parameters/CONTRIBUTING.md b/seed/python-sdk/path-parameters/encode-path-params/CONTRIBUTING.md similarity index 100% rename from seed/python-sdk/path-parameters/CONTRIBUTING.md rename to seed/python-sdk/path-parameters/encode-path-params/CONTRIBUTING.md diff --git a/seed/python-sdk/path-parameters/README.md b/seed/python-sdk/path-parameters/encode-path-params/README.md similarity index 100% rename from seed/python-sdk/path-parameters/README.md rename to seed/python-sdk/path-parameters/encode-path-params/README.md diff --git a/seed/python-sdk/path-parameters/poetry.lock b/seed/python-sdk/path-parameters/encode-path-params/poetry.lock similarity index 100% rename from seed/python-sdk/path-parameters/poetry.lock rename to seed/python-sdk/path-parameters/encode-path-params/poetry.lock diff --git a/seed/python-sdk/path-parameters/pyproject.toml b/seed/python-sdk/path-parameters/encode-path-params/pyproject.toml similarity index 100% rename from seed/python-sdk/path-parameters/pyproject.toml rename to seed/python-sdk/path-parameters/encode-path-params/pyproject.toml diff --git a/seed/python-sdk/path-parameters/reference.md b/seed/python-sdk/path-parameters/encode-path-params/reference.md similarity index 100% rename from seed/python-sdk/path-parameters/reference.md rename to seed/python-sdk/path-parameters/encode-path-params/reference.md diff --git a/seed/python-sdk/path-parameters/requirements.txt b/seed/python-sdk/path-parameters/encode-path-params/requirements.txt similarity index 100% rename from seed/python-sdk/path-parameters/requirements.txt rename to seed/python-sdk/path-parameters/encode-path-params/requirements.txt diff --git a/seed/python-sdk/path-parameters/snippet.json b/seed/python-sdk/path-parameters/encode-path-params/snippet.json similarity index 100% rename from seed/python-sdk/path-parameters/snippet.json rename to seed/python-sdk/path-parameters/encode-path-params/snippet.json diff --git a/seed/python-sdk/path-parameters/src/seed/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/_default_clients.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/_default_clients.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/_default_clients.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/_default_clients.py diff --git a/seed/python-sdk/path-parameters/src/seed/client.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/client.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/client.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/client.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/api_error.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/api_error.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/api_error.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/api_error.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/client_wrapper.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/client_wrapper.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/client_wrapper.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/client_wrapper.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/datetime_utils.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/datetime_utils.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/datetime_utils.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/datetime_utils.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/file.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/file.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/file.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/file.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/force_multipart.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/force_multipart.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/force_multipart.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/force_multipart.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_client.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_client.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_client.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_client.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_response.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_response.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_response.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_response.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_sse/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/_api.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_api.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_sse/_api.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_api.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/_decoders.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_decoders.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_sse/_decoders.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_decoders.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/_exceptions.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_exceptions.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_sse/_exceptions.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_exceptions.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/http_sse/_models.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_models.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/http_sse/_models.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/http_sse/_models.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/jsonable_encoder.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/jsonable_encoder.py similarity index 90% rename from seed/python-sdk/path-parameters/src/seed/core/jsonable_encoder.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/path-parameters/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/path-parameters/src/seed/core/logging.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/logging.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/logging.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/logging.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/parse_error.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/parse_error.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/parse_error.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/parse_error.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/pydantic_utilities.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/pydantic_utilities.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/pydantic_utilities.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/pydantic_utilities.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/query_encoder.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/query_encoder.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/query_encoder.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/query_encoder.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/remove_none_from_dict.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/remove_none_from_dict.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/remove_none_from_dict.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/remove_none_from_dict.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/request_options.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/request_options.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/request_options.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/request_options.py diff --git a/seed/python-sdk/path-parameters/src/seed/core/serialization.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/core/serialization.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/core/serialization.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/core/serialization.py diff --git a/seed/python-sdk/path-parameters/src/seed/organizations/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/organizations/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/organizations/client.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/client.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/organizations/client.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/client.py diff --git a/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/raw_client.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/raw_client.py new file mode 100644 index 000000000000..af29884eac12 --- /dev/null +++ b/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/raw_client.py @@ -0,0 +1,281 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import quote_path_param +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..user.types.user import User +from .types.organization import Organization +from pydantic import ValidationError + + +class RawOrganizationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_organization( + self, organization_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[Organization]: + """ + Parameters + ---------- + organization_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[Organization] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/organizations/{quote_path_param(organization_id)}/", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Organization, + parse_obj_as( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_organization_user( + self, organization_id: str, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[User]: + """ + Parameters + ---------- + organization_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[User] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/organizations/{quote_path_param(organization_id)}/users/{quote_path_param(user_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def search_organizations( + self, + organization_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[typing.List[Organization]]: + """ + Parameters + ---------- + organization_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[typing.List[Organization]] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/organizations/{quote_path_param(organization_id)}/search", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[Organization], + parse_obj_as( + type_=typing.List[Organization], # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawOrganizationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_organization( + self, organization_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[Organization]: + """ + Parameters + ---------- + organization_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[Organization] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/organizations/{quote_path_param(organization_id)}/", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + Organization, + parse_obj_as( + type_=Organization, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_organization_user( + self, organization_id: str, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[User]: + """ + Parameters + ---------- + organization_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[User] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/organizations/{quote_path_param(organization_id)}/users/{quote_path_param(user_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def search_organizations( + self, + organization_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[typing.List[Organization]]: + """ + Parameters + ---------- + organization_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[typing.List[Organization]] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/organizations/{quote_path_param(organization_id)}/search", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[Organization], + parse_obj_as( + type_=typing.List[Organization], # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/seed/python-sdk/path-parameters/src/seed/organizations/types/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/types/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/organizations/types/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/types/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/organizations/types/organization.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/types/organization.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/organizations/types/organization.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/organizations/types/organization.py diff --git a/seed/python-sdk/path-parameters/src/seed/py.typed b/seed/python-sdk/path-parameters/encode-path-params/src/seed/py.typed similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/py.typed rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/py.typed diff --git a/seed/python-sdk/path-parameters/src/seed/user/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/user/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/user/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/user/client.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/client.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/user/client.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/user/client.py diff --git a/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/raw_client.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/raw_client.py new file mode 100644 index 000000000000..0b3cac2fa846 --- /dev/null +++ b/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/raw_client.py @@ -0,0 +1,573 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import quote_path_param +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .types.user import User +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawUserClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_user(self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[User]: + """ + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[User] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def create_user( + self, *, name: str, tags: typing.Sequence[str], request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[User]: + """ + Parameters + ---------- + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[User] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/", + method="POST", + json={ + "name": name, + "tags": tags, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update_user( + self, + user_id: str, + *, + name: str, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[User]: + """ + Parameters + ---------- + user_id : str + + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[User] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}", + method="PATCH", + json={ + "name": name, + "tags": tags, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def search_users( + self, + user_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[typing.List[User]]: + """ + Parameters + ---------- + user_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[typing.List[User]] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}/search", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[User], + parse_obj_as( + type_=typing.List[User], # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_user_metadata( + self, user_id: str, version: int, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[User]: + """ + Test endpoint with path parameter that has a text prefix (v{version}) + + Parameters + ---------- + user_id : str + + version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[User] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}/metadata/v{quote_path_param(version)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get_user_specifics( + self, user_id: str, version: int, thought: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[User]: + """ + Test endpoint with path parameters listed in different order than found in path + + Parameters + ---------- + user_id : str + + version : int + + thought : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[User] + """ + _response = self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}/specifics/{quote_path_param(version)}/{quote_path_param(thought)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawUserClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_user( + self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[User]: + """ + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[User] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def create_user( + self, *, name: str, tags: typing.Sequence[str], request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[User]: + """ + Parameters + ---------- + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[User] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/", + method="POST", + json={ + "name": name, + "tags": tags, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update_user( + self, + user_id: str, + *, + name: str, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[User]: + """ + Parameters + ---------- + user_id : str + + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[User] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}", + method="PATCH", + json={ + "name": name, + "tags": tags, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def search_users( + self, + user_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[typing.List[User]]: + """ + Parameters + ---------- + user_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[typing.List[User]] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}/search", + method="GET", + params={ + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.List[User], + parse_obj_as( + type_=typing.List[User], # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_user_metadata( + self, user_id: str, version: int, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[User]: + """ + Test endpoint with path parameter that has a text prefix (v{version}) + + Parameters + ---------- + user_id : str + + version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[User] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}/metadata/v{quote_path_param(version)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get_user_specifics( + self, user_id: str, version: int, thought: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[User]: + """ + Test endpoint with path parameters listed in different order than found in path + + Parameters + ---------- + user_id : str + + version : int + + thought : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[User] + """ + _response = await self._client_wrapper.httpx_client.request( + f"{quote_path_param(self._client_wrapper._tenant_id)}/user/{quote_path_param(user_id)}/specifics/{quote_path_param(version)}/{quote_path_param(thought)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + User, + parse_obj_as( + type_=User, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/seed/python-sdk/path-parameters/src/seed/user/types/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/types/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/user/types/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/user/types/__init__.py diff --git a/seed/python-sdk/path-parameters/src/seed/user/types/user.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/user/types/user.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/user/types/user.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/user/types/user.py diff --git a/seed/python-sdk/path-parameters/src/seed/version.py b/seed/python-sdk/path-parameters/encode-path-params/src/seed/version.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/version.py rename to seed/python-sdk/path-parameters/encode-path-params/src/seed/version.py diff --git a/seed/python-sdk/path-parameters/tests/conftest.py b/seed/python-sdk/path-parameters/encode-path-params/tests/conftest.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/conftest.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/conftest.py diff --git a/seed/python-sdk/path-parameters/tests/custom/test_client.py b/seed/python-sdk/path-parameters/encode-path-params/tests/custom/test_client.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/custom/test_client.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/custom/test_client.py diff --git a/seed/python-sdk/path-parameters/tests/test_aiohttp_autodetect.py b/seed/python-sdk/path-parameters/encode-path-params/tests/test_aiohttp_autodetect.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/test_aiohttp_autodetect.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/test_aiohttp_autodetect.py diff --git a/seed/python-sdk/path-parameters/tests/utils/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/__init__.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/__init__.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/__init__.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/__init__.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/__init__.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/circle.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/circle.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/circle.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/circle.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/color.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/color.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/color.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/color.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/object_with_defaults.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/object_with_defaults.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/object_with_defaults.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/object_with_defaults.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/object_with_optional_field.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/object_with_optional_field.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/object_with_optional_field.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/object_with_optional_field.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/shape.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/shape.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/shape.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/shape.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/square.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/square.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/square.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/square.py diff --git a/seed/python-sdk/path-parameters/tests/utils/assets/models/undiscriminated_shape.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/undiscriminated_shape.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/assets/models/undiscriminated_shape.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/assets/models/undiscriminated_shape.py diff --git a/seed/python-sdk/path-parameters/tests/utils/test_http_client.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/test_http_client.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/test_http_client.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/test_http_client.py diff --git a/seed/python-sdk/path-parameters/tests/utils/test_query_encoding.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/test_query_encoding.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/test_query_encoding.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/test_query_encoding.py diff --git a/seed/python-sdk/path-parameters/tests/utils/test_serialization.py b/seed/python-sdk/path-parameters/encode-path-params/tests/utils/test_serialization.py similarity index 100% rename from seed/python-sdk/path-parameters/tests/utils/test_serialization.py rename to seed/python-sdk/path-parameters/encode-path-params/tests/utils/test_serialization.py diff --git a/seed/python-sdk/path-parameters/.fern/metadata.json b/seed/python-sdk/path-parameters/no-custom-config/.fern/metadata.json similarity index 100% rename from seed/python-sdk/path-parameters/.fern/metadata.json rename to seed/python-sdk/path-parameters/no-custom-config/.fern/metadata.json diff --git a/seed/python-sdk/path-parameters/no-custom-config/.github/workflows/ci.yml b/seed/python-sdk/path-parameters/no-custom-config/.github/workflows/ci.yml new file mode 100644 index 000000000000..4464e975b7ba --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: ci +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + + - name: Test + run: poetry run pytest -rP -n auto . + + - name: Install aiohttp extra + run: poetry install --extras aiohttp + + - name: Test (aiohttp) + run: poetry run pytest -rP -n auto -m aiohttp . diff --git a/seed/python-sdk/path-parameters/no-custom-config/.gitignore b/seed/python-sdk/path-parameters/no-custom-config/.gitignore new file mode 100644 index 000000000000..d2e4ca808d21 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/.gitignore @@ -0,0 +1,5 @@ +.mypy_cache/ +.ruff_cache/ +__pycache__/ +dist/ +poetry.toml diff --git a/seed/python-sdk/path-parameters/no-custom-config/CONTRIBUTING.md b/seed/python-sdk/path-parameters/no-custom-config/CONTRIBUTING.md new file mode 100644 index 000000000000..af948ce72556 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing + +Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. + +## Getting Started + +### Prerequisites + +- Python 3.9+ +- pip +- poetry + +### Installation + +Install the project dependencies: + +```bash +poetry install +``` + +### Building + +Build the project: + +```bash +poetry build +``` + +### Testing + +Run the test suite: + +```bash +poetry run pytest +``` + +### Linting and Formatting + +Check code style: + +```bash +poetry run ruff check . +poetry run ruff format . +``` + +### Type Checking + +Run the type checker: + +```bash +poetry run mypy . +``` + +## About Generated Code + +**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. + +### Generated Files + +The following directories contain generated code: +- `src/` - API client classes and types +- Most Python files in the project + +### How to Customize + +If you need to customize the SDK, you have two options: + +#### Option 1: Use `.fernignore` + +For custom code that should persist across SDK regenerations: + +1. Create a `.fernignore` file in the project root +2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) +3. Add your custom code to those files + +Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. + +For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). + +#### Option 2: Contribute to the Generator + +If you want to change how code is generated for all users of this SDK: + +1. The Python SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) +2. Generator code is located at `generators/python-v2/` +3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) +4. Submit a pull request with your changes to the generator + +This approach is best for: +- Bug fixes in generated code +- New features that would benefit all users +- Improvements to code generation patterns + +## Making Changes + +### Workflow + +1. Create a new branch for your changes +2. Make your modifications +3. Run tests to ensure nothing breaks: `poetry run pytest` +4. Run linting and formatting: `poetry run ruff check .` and `poetry run ruff format .` +5. Run type checking: `poetry run mypy .` +6. Build the project: `poetry build` +7. Commit your changes with a clear commit message +8. Push your branch and create a pull request + +### Commit Messages + +Write clear, descriptive commit messages that explain what changed and why. + +### Code Style + +This project uses automated code formatting and linting. Run `poetry run ruff format .` and `poetry run ruff check .` before committing to ensure your code meets the project's style guidelines. + +## Questions or Issues? + +If you have questions or run into issues: + +1. Check the [Fern documentation](https://buildwithfern.com) +2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) +3. Open a new issue if your question hasn't been addressed + +## License + +By contributing to this project, you agree that your contributions will be licensed under the same license as the project. diff --git a/seed/python-sdk/path-parameters/no-custom-config/README.md b/seed/python-sdk/path-parameters/no-custom-config/README.md new file mode 100644 index 000000000000..3ec71131bf96 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/README.md @@ -0,0 +1,184 @@ +# Seed Python Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Seed%2FPython) +[![pypi](https://img.shields.io/pypi/v/fern_path-parameters)](https://pypi.python.org/pypi/fern_path-parameters) + +The Seed Python library provides convenient access to the Seed APIs from Python. + +## Table of Contents + +- [Installation](#installation) +- [Reference](#reference) +- [Usage](#usage) +- [Async Client](#async-client) +- [Exception Handling](#exception-handling) +- [Advanced](#advanced) + - [Access Raw Response Data](#access-raw-response-data) + - [Retries](#retries) + - [Timeouts](#timeouts) + - [Custom Client](#custom-client) +- [Contributing](#contributing) + +## Installation + +```sh +pip install fern_path-parameters +``` + +## Reference + +A full reference for this library is available [here](./reference.md). + +## Usage + +Instantiate and use the client with the following: + +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.create_user( + tenant_id="tenant_id", + name="name", + tags=[ + "tags", + "tags" + ], +) +``` + +## Async Client + +The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client). + +```python +import asyncio + +from seed import AsyncSeedPathParameters + +client = AsyncSeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + + +async def main() -> None: + await client.user.create_user( + tenant_id="tenant_id", + name="name", + tags=[ + "tags", + "tags" + ], + ) + + +asyncio.run(main()) +``` + +## Exception Handling + +When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error +will be thrown. + +```python +from seed.core.api_error import ApiError + +try: + client.user.create_user(...) +except ApiError as e: + print(e.status_code) + print(e.body) +``` + +## Advanced + +### Access Raw Response Data + +The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. +The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. + +```python +from seed import SeedPathParameters + +client = SeedPathParameters(...) +response = client.user.with_raw_response.create_user(...) +print(response.headers) # access the response headers +print(response.status_code) # access the response status code +print(response.data) # access the underlying object +``` + +### Retries + +The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long +as the request is deemed retryable and the number of retry attempts has not grown larger than the configured +retry limit (default: 2). + +Which status codes are retried depends on the `retryStatusCodes` generator configuration: + +**`legacy`** (current default): retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) + +**`recommended`**: retries on +- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) +- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) +- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) +- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) +- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) +- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) + +Use the `max_retries` request option to configure this behavior. + +```python +client.user.create_user(..., request_options={ + "max_retries": 1 +}) +``` + +### Timeouts + +The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. + +```python +from seed import SeedPathParameters + +client = SeedPathParameters(..., timeout=20.0) + +# Override timeout for a specific method +client.user.create_user(..., request_options={ + "timeout": 1 +}) +``` + +### Custom Client + +You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies +and transports. + +```python +import httpx +from seed import SeedPathParameters + +client = SeedPathParameters( + ..., + httpx_client=httpx.Client( + proxy="http://my.test.proxy.example.com", + transport=httpx.HTTPTransport(local_address="0.0.0.0"), + ), +) +``` + +## Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as +a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! diff --git a/seed/python-sdk/path-parameters/no-custom-config/poetry.lock b/seed/python-sdk/path-parameters/no-custom-config/poetry.lock new file mode 100644 index 000000000000..8c2d5982fa31 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/poetry.lock @@ -0,0 +1,1469 @@ +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +description = "Happy Eyeballs for asyncio" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +description = "Async http client/server framework (asyncio)" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "annotated-types" +version = "0.8.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, +] + +[[package]] +name = "anyio" +version = "4.14.2" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, + {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\" and extra == \"aiohttp\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "26.1.0" +description = "Classes Without Boilerplate" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "2.1.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, + {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "httpx-aiohttp" +version = "0.1.12" +description = "Aiohttp transport for HTTPX" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141"}, + {file = "httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c"}, +] + +[package.dependencies] +aiohttp = ">=3.10.0,<4" +httpx = ">=0.27.0" + +[[package]] +name = "idna" +version = "3.18" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, +] + +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "26.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, + {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.4" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, + {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, + {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, + {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, + {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, + {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, + {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, + {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, + {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, + {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, + {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, + {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, + {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, + {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, + {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, + {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, + {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, + {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, + {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, + {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, + {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, + {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, + {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, + {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, + {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, + {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, + {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, + {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, + {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "ruff" +version = "0.11.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, + {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, + {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, + {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, + {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, + {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, + {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, + {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, + {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260716" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_python_dateutil-2.9.0.20260716-py3-none-any.whl", hash = "sha256:1ae41d51a5c5f6bbeeb7f1f34df7086d55ff1605cf88d2ed71a7a276e1a7794e"}, + {file = "types_python_dateutil-2.9.0.20260716.tar.gz", hash = "sha256:1d55d1c3024bdb4861bb6a6622c9ec800c433d87bdc5b16fb84cdd0eed4ef2cb"}, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.7.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "yarl" +version = "1.24.5" +description = "Yet another URL library" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"aiohttp\"" +files = [ + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[extras] +aiohttp = ["aiohttp", "httpx-aiohttp"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.10" +content-hash = "4eaeff06fb8e558535aa3729ed6f99c2398eaaba665bcdf9c096bb08d9cb5fc6" diff --git a/seed/python-sdk/path-parameters/no-custom-config/pyproject.toml b/seed/python-sdk/path-parameters/no-custom-config/pyproject.toml new file mode 100644 index 000000000000..095249318c30 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/pyproject.toml @@ -0,0 +1,102 @@ +[project] +name = "fern_path-parameters" +dynamic = ["version"] + +[tool.poetry] +name = "fern_path-parameters" +version = "0.0.1" +description = "" +readme = "README.md" +authors = [] +keywords = [ + "fern", + "test" +] + +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed" +] +packages = [ + { include = "seed", from = "src"} +] + +[tool.poetry.urls] +Documentation = 'https://buildwithfern.com/learn' +Homepage = 'https://buildwithfern.com/' +Repository = 'https://github.com/path-parameters/fern' + +[tool.poetry.dependencies] +python = "^3.10" +aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"} +httpx = ">=0.21.2" +httpx-aiohttp = { version = "^0.1.8", optional = true, python = ">=3.10"} +pydantic = ">= 1.9.2" +pydantic-core = ">=2.18.2,<3.0.0" +typing_extensions = ">= 4.0.0" + +[tool.poetry.group.dev.dependencies] +mypy = "==1.13.0" +pytest = "^9.0.3" +pytest-asyncio = "^1.0.0" +pytest-xdist = "^3.6.1" +python-dateutil = "^2.9.0" +types-python-dateutil = "^2.9.0.20240316" +urllib3 = ">=2.6.3,<3.0.0" +ruff = "==0.11.5" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" +norecursedirs = [ "src" ] +markers = [ + "aiohttp: tests that require httpx_aiohttp to be installed", +] + +[tool.mypy] +plugins = ["pydantic.mypy"] + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort +] +ignore = [ + "E402", # Module level import not at top of file + "E501", # Line too long + "E711", # Comparison to `None` should be `cond is not None` + "E712", # Avoid equality comparisons to `True`; use `if ...:` checks + "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks + "E722", # Do not use bare `except` + "E731", # Do not assign a `lambda` expression, use a `def` + "F821", # Undefined name + "F841" # Local variable ... is assigned to but never used +] + +[tool.ruff.lint.isort] +section-order = ["future", "standard-library", "third-party", "first-party"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.extras] +aiohttp=["aiohttp", "httpx-aiohttp"] diff --git a/seed/python-sdk/path-parameters/no-custom-config/reference.md b/seed/python-sdk/path-parameters/no-custom-config/reference.md new file mode 100644 index 000000000000..39537e6d0b63 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/reference.md @@ -0,0 +1,696 @@ +# Reference +## Organizations +
client.organizations.get_organization(...) -> Organization +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.organizations.get_organization( + tenant_id="tenant_id", + organization_id="organization_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**organization_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.organizations.get_organization_user(...) -> User +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.organizations.get_organization_user( + tenant_id="tenant_id", + organization_id="organization_id", + user_id="user_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**organization_id:** `str` + +
+
+ +
+
+ +**user_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.organizations.search_organizations(...) -> typing.List[Organization] +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.organizations.search_organizations( + tenant_id="tenant_id", + organization_id="organization_id", + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**organization_id:** `str` + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +## User +
client.user.get_user(...) -> User +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.get_user( + tenant_id="tenant_id", + user_id="user_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**user_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.user.create_user(...) -> User +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.create_user( + tenant_id="tenant_id", + name="name", + tags=[ + "tags", + "tags" + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**request:** `User` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.user.update_user(...) -> User +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.update_user( + tenant_id="tenant_id", + user_id="user_id", + name="name", + tags=[ + "tags", + "tags" + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**user_id:** `str` + +
+
+ +
+
+ +**request:** `User` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.user.search_users(...) -> typing.List[User] +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.search_users( + tenant_id="tenant_id", + user_id="user_id", + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**user_id:** `str` + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.user.get_user_metadata(...) -> User +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Test endpoint with path parameter that has a text prefix (v{version}) +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.get_user_metadata( + tenant_id="tenant_id", + user_id="user_id", + version=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**user_id:** `str` + +
+
+ +
+
+ +**version:** `int` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.user.get_user_specifics(...) -> User +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Test endpoint with path parameters listed in different order than found in path +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from seed import SeedPathParameters + +client = SeedPathParameters( + base_url="https://yourhost.com/path/to/api", +) + +client.user.get_user_specifics( + tenant_id="tenant_id", + user_id="user_id", + version=1, + thought="thought", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**tenant_id:** `str` + +
+
+ +
+
+ +**user_id:** `str` + +
+
+ +
+
+ +**version:** `int` + +
+
+ +
+
+ +**thought:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ diff --git a/seed/python-sdk/path-parameters/no-custom-config/requirements.txt b/seed/python-sdk/path-parameters/no-custom-config/requirements.txt new file mode 100644 index 000000000000..443b1af40e27 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/requirements.txt @@ -0,0 +1,4 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2,<3.0.0 +typing_extensions>= 4.0.0 diff --git a/seed/python-sdk/path-parameters/no-custom-config/snippet.json b/seed/python-sdk/path-parameters/no-custom-config/snippet.json new file mode 100644 index 000000000000..c33c5d96b140 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/snippet.json @@ -0,0 +1,122 @@ +{ + "types": {}, + "endpoints": [ + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/organizations/{organization_id}/", + "method": "GET", + "identifier_override": "endpoint_organizations.getOrganization" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.organizations.get_organization(\n organization_id=\"organization_id\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.organizations.get_organization(\n organization_id=\"organization_id\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/organizations/{organization_id}/users/{user_id}", + "method": "GET", + "identifier_override": "endpoint_organizations.getOrganizationUser" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.organizations.get_organization_user(\n organization_id=\"organization_id\",\n user_id=\"user_id\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.organizations.get_organization_user(\n organization_id=\"organization_id\",\n user_id=\"user_id\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/organizations/{organization_id}/search", + "method": "GET", + "identifier_override": "endpoint_organizations.searchOrganizations" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.organizations.search_organizations(\n organization_id=\"organization_id\",\n limit=1,\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.organizations.search_organizations(\n organization_id=\"organization_id\",\n limit=1,\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/user/{user_id}", + "method": "GET", + "identifier_override": "endpoint_user.getUser" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.get_user(\n user_id=\"user_id\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.get_user(\n user_id=\"user_id\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/user/", + "method": "POST", + "identifier_override": "endpoint_user.createUser" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.create_user(\n name=\"name\",\n tags=[\"tags\", \"tags\"],\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.create_user(\n name=\"name\",\n tags=[\"tags\", \"tags\"],\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/user/{user_id}", + "method": "PATCH", + "identifier_override": "endpoint_user.updateUser" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.update_user(\n user_id=\"user_id\",\n name=\"name\",\n tags=[\"tags\", \"tags\"],\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.update_user(\n user_id=\"user_id\",\n name=\"name\",\n tags=[\"tags\", \"tags\"],\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/user/{user_id}/search", + "method": "GET", + "identifier_override": "endpoint_user.searchUsers" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.search_users(\n user_id=\"user_id\",\n limit=1,\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.search_users(\n user_id=\"user_id\",\n limit=1,\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/user/{user_id}/metadata/v{version}", + "method": "GET", + "identifier_override": "endpoint_user.getUserMetadata" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.get_user_metadata(\n user_id=\"user_id\",\n version=1,\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.get_user_metadata(\n user_id=\"user_id\",\n version=1,\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + }, + { + "example_identifier": "default", + "id": { + "path": "/{tenant_id}/user/{user_id}/specifics/{version}/{thought}", + "method": "GET", + "identifier_override": "endpoint_user.getUserSpecifics" + }, + "snippet": { + "sync_client": "from seed import SeedPathParameters\n\nclient = SeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\nclient.user.get_user_specifics(\n user_id=\"user_id\",\n version=1,\n thought=\"thought\",\n)\n", + "async_client": "import asyncio\n\nfrom seed import AsyncSeedPathParameters\n\nclient = AsyncSeedPathParameters(\n tenant_id=\"YOUR_TENANT_ID\",\n base_url=\"https://yourhost.com/path/to/api\",\n)\n\n\nasync def main() -> None:\n await client.user.get_user_specifics(\n user_id=\"user_id\",\n version=1,\n thought=\"thought\",\n )\n\n\nasyncio.run(main())\n", + "type": "python" + } + } + ] +} \ No newline at end of file diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/__init__.py new file mode 100644 index 000000000000..540aa71bdd60 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/__init__.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from . import organizations, user + from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient + from .client import AsyncSeedPathParameters, SeedPathParameters + from .organizations import Organization + from .user import User + from .version import __version__ +_dynamic_imports: typing.Dict[str, str] = { + "AsyncSeedPathParameters": ".client", + "DefaultAioHttpClient": "._default_clients", + "DefaultAsyncHttpxClient": "._default_clients", + "Organization": ".organizations", + "SeedPathParameters": ".client", + "User": ".user", + "__version__": ".version", + "organizations": ".organizations", + "user": ".user", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "AsyncSeedPathParameters", + "DefaultAioHttpClient", + "DefaultAsyncHttpxClient", + "Organization", + "SeedPathParameters", + "User", + "__version__", + "organizations", + "user", +] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/_default_clients.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/_default_clients.py new file mode 100644 index 000000000000..46c945b83183 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/_default_clients.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +SDK_DEFAULT_TIMEOUT = 60 + +try: + import httpx_aiohttp # type: ignore[import-not-found] +except ImportError: + + class DefaultAioHttpClient(httpx.AsyncClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + raise RuntimeError( + "To use the aiohttp client, install the aiohttp extra: pip install fern_path-parameters[aiohttp]" + ) + +else: + + class DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +class DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: typing.Any) -> None: + kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/client.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/client.py new file mode 100644 index 000000000000..db50211b5008 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/client.py @@ -0,0 +1,219 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import httpx +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.logging import LogConfig, Logger + +if typing.TYPE_CHECKING: + from .organizations.client import AsyncOrganizationsClient, OrganizationsClient + from .user.client import AsyncUserClient, UserClient + + +class SeedPathParameters: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + tenant_id : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + tenant_id: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_max_retries = max_retries if max_retries is not None else 2 + self._client_wrapper = SyncClientWrapper( + base_url=base_url, + tenant_id=tenant_id, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._organizations: typing.Optional[OrganizationsClient] = None + self._user: typing.Optional[UserClient] = None + + @property + def organizations(self): + if self._organizations is None: + from .organizations.client import OrganizationsClient # noqa: E402 + + self._organizations = OrganizationsClient(client_wrapper=self._client_wrapper) + return self._organizations + + @property + def user(self): + if self._user is None: + from .user.client import UserClient # noqa: E402 + + self._user = UserClient(client_wrapper=self._client_wrapper) + return self._user + + +def _make_default_async_client( + timeout: typing.Optional[float], + follow_redirects: typing.Optional[bool], +) -> httpx.AsyncClient: + try: + import httpx_aiohttp # type: ignore[import-not-found] + except ImportError: + pass + else: + if follow_redirects is not None: + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout) + + if follow_redirects is not None: + return httpx.AsyncClient(timeout=timeout, follow_redirects=follow_redirects) + return httpx.AsyncClient(timeout=timeout) + + +class AsyncSeedPathParameters: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + tenant_id : str + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + max_retries : typing.Optional[int] + The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + logging : typing.Optional[typing.Union[LogConfig, Logger]] + Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. + + Examples + -------- + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + tenant_id: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + _defaulted_max_retries = max_retries if max_retries is not None else 2 + self._client_wrapper = AsyncClientWrapper( + base_url=base_url, + tenant_id=tenant_id, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), + timeout=_defaulted_timeout, + max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._organizations: typing.Optional[AsyncOrganizationsClient] = None + self._user: typing.Optional[AsyncUserClient] = None + + @property + def organizations(self): + if self._organizations is None: + from .organizations.client import AsyncOrganizationsClient # noqa: E402 + + self._organizations = AsyncOrganizationsClient(client_wrapper=self._client_wrapper) + return self._organizations + + @property + def user(self): + if self._user is None: + from .user.client import AsyncUserClient # noqa: E402 + + self._user = AsyncUserClient(client_wrapper=self._client_wrapper) + return self._user diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/__init__.py new file mode 100644 index 000000000000..5bc159a110f2 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/__init__.py @@ -0,0 +1,127 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .api_error import ApiError + from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper + from .datetime_utils import Rfc2822DateTime, parse_rfc2822_datetime, serialize_datetime + from .file import File, convert_file_dict_to_httpx_tuples, with_content_type + from .http_client import AsyncHttpClient, HttpClient + from .http_response import AsyncHttpResponse, HttpResponse + from .jsonable_encoder import encode_path_param, jsonable_encoder + from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger + from .parse_error import ParsingError + from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, + ) + from .query_encoder import encode_query + from .remove_none_from_dict import remove_none_from_dict + from .request_options import RequestOptions + from .serialization import FieldMetadata, convert_and_respect_annotation_metadata +_dynamic_imports: typing.Dict[str, str] = { + "ApiError": ".api_error", + "AsyncClientWrapper": ".client_wrapper", + "AsyncHttpClient": ".http_client", + "AsyncHttpResponse": ".http_response", + "BaseClientWrapper": ".client_wrapper", + "ConsoleLogger": ".logging", + "FieldMetadata": ".serialization", + "File": ".file", + "HttpClient": ".http_client", + "HttpResponse": ".http_response", + "ILogger": ".logging", + "IS_PYDANTIC_V2": ".pydantic_utilities", + "LogConfig": ".logging", + "LogLevel": ".logging", + "Logger": ".logging", + "ParsingError": ".parse_error", + "RequestOptions": ".request_options", + "Rfc2822DateTime": ".datetime_utils", + "SyncClientWrapper": ".client_wrapper", + "UniversalBaseModel": ".pydantic_utilities", + "UniversalRootModel": ".pydantic_utilities", + "convert_and_respect_annotation_metadata": ".serialization", + "convert_file_dict_to_httpx_tuples": ".file", + "create_logger": ".logging", + "encode_path_param": ".jsonable_encoder", + "encode_query": ".query_encoder", + "jsonable_encoder": ".jsonable_encoder", + "parse_obj_as": ".pydantic_utilities", + "parse_rfc2822_datetime": ".datetime_utils", + "remove_none_from_dict": ".remove_none_from_dict", + "serialize_datetime": ".datetime_utils", + "universal_field_validator": ".pydantic_utilities", + "universal_root_validator": ".pydantic_utilities", + "update_forward_refs": ".pydantic_utilities", + "with_content_type": ".file", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "AsyncHttpResponse", + "BaseClientWrapper", + "ConsoleLogger", + "FieldMetadata", + "File", + "HttpClient", + "HttpResponse", + "ILogger", + "IS_PYDANTIC_V2", + "LogConfig", + "LogLevel", + "Logger", + "ParsingError", + "RequestOptions", + "Rfc2822DateTime", + "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "create_logger", + "encode_path_param", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "parse_rfc2822_datetime", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", +] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/api_error.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/api_error.py new file mode 100644 index 000000000000..6f850a60cba3 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/client_wrapper.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/client_wrapper.py new file mode 100644 index 000000000000..36c4267995d7 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/client_wrapper.py @@ -0,0 +1,140 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient +from .logging import LogConfig, Logger + + +class BaseClientWrapper: + def __init__( + self, + *, + tenant_id: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self._tenant_id = tenant_id + self._headers = headers + self._base_url = base_url + self._timeout = timeout + self._max_retries = max_retries + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._logging = logging + + def get_headers(self) -> typing.Dict[str, str]: + import platform + + headers: typing.Dict[str, str] = { + "User-Agent": "fern_path-parameters/0.0.1", + "X-Fern-Language": "Python", + "X-Fern-Runtime": f"python/{platform.python_version()}", + "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", + "X-Fern-SDK-Name": "fern_path-parameters", + "X-Fern-SDK-Version": "0.0.1", + **(self.get_custom_headers() or {}), + } + return headers + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + def get_max_retries(self) -> int: + return self._max_retries + + def get_stream_reconnection_enabled(self) -> bool: + return self._stream_reconnection_enabled if self._stream_reconnection_enabled is not None else True + + def get_max_stream_reconnection_attempts(self) -> typing.Optional[int]: + return self._max_stream_reconnection_attempts + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + tenant_id: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + httpx_client: httpx.Client, + ): + super().__init__( + tenant_id=tenant_id, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + logging_config=self._logging, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + tenant_id: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, + logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, + async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__( + tenant_id=tenant_id, + headers=headers, + base_url=base_url, + timeout=timeout, + max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, + logging=logging, + ) + self._async_token = async_token + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + base_max_retries=self.get_max_retries(), + async_base_headers=self.async_get_headers, + logging_config=self._logging, + ) + + async def async_get_headers(self) -> typing.Dict[str, str]: + headers = self.get_headers() + if self._async_token is not None: + token = await self._async_token() + headers["Authorization"] = f"Bearer {token}" + return headers diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/datetime_utils.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/datetime_utils.py new file mode 100644 index 000000000000..a12b2ad03c53 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/datetime_utils.py @@ -0,0 +1,70 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +from email.utils import parsedate_to_datetime +from typing import Any + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + + +def parse_rfc2822_datetime(v: Any) -> dt.datetime: + """ + Parse an RFC 2822 datetime string (e.g., "Wed, 02 Oct 2002 13:00:00 GMT") + into a datetime object. If the value is already a datetime, return it as-is. + Falls back to ISO 8601 parsing if RFC 2822 parsing fails. + """ + if isinstance(v, dt.datetime): + return v + if isinstance(v, str): + try: + return parsedate_to_datetime(v) + except Exception: + pass + # Fallback to ISO 8601 parsing + return dt.datetime.fromisoformat(v.replace("Z", "+00:00")) + raise ValueError(f"Expected str or datetime, got {type(v)}") + + +class Rfc2822DateTime(dt.datetime): + """A datetime subclass that parses RFC 2822 date strings. + + On Pydantic V1, uses __get_validators__ for pre-validation. + On Pydantic V2, uses __get_pydantic_core_schema__ for BeforeValidator-style parsing. + """ + + @classmethod + def __get_validators__(cls): # type: ignore[no-untyped-def] + yield parse_rfc2822_datetime + + @classmethod + def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: # type: ignore[override] + from pydantic_core import core_schema + + return core_schema.no_info_before_validator_function(parse_rfc2822_datetime, core_schema.datetime_schema()) + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/file.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/file.py new file mode 100644 index 000000000000..44b0d27c0895 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/force_multipart.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/force_multipart.py new file mode 100644 index 000000000000..5440913fd4bc --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/force_multipart.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict + + +class ForceMultipartDict(Dict[str, Any]): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self) -> bool: + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_client.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_client.py new file mode 100644 index 000000000000..409d2670933f --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_client.py @@ -0,0 +1,885 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import socket +import time +import typing +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .logging import LogConfig, Logger, create_logger +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 1.0 +MAX_RETRY_DELAY_SECONDS = 60.0 +JITTER_FACTOR = 0.2 # 20% random jitter + + +def get_keepalive_socket_options( + idle: int = 60, + intvl: int = 30, + cnt: int = 5, +) -> typing.List[typing.Tuple[int, int, int]]: + """ + Build TCP keepalive socket options for the current platform. + + Keepalive probes keep otherwise-idle connections alive so that long, + non-streaming requests survive idle-connection reaping by a firewall, + load balancer, or NAT. The available socket constants are OS-dependent, + so each option is guarded and only emitted when the platform defines it: + + - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows). + - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern + Windows, but ``TCP_KEEPALIVE`` on macOS. + - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows. + + Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` / + ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every + connection the transport opens. + """ + opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] + idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None) + if idle_const: + opts.append((socket.IPPROTO_TCP, idle_const, idle)) + if hasattr(socket, "TCP_KEEPINTVL"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl)) + if hasattr(socket, "TCP_KEEPCNT"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt)) + return opts + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _add_positive_jitter(delay: float) -> float: + """Add positive jitter (0-20%) to prevent thundering herd.""" + jitter_multiplier = 1 + random() * JITTER_FACTOR + return delay * jitter_multiplier + + +def _add_symmetric_jitter(delay: float) -> float: + """Add symmetric jitter (±10%) for exponential backoff.""" + jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR + return delay * jitter_multiplier + + +def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + Parse the X-RateLimit-Reset header (Unix timestamp in seconds). + Returns seconds to wait, or None if header is missing/invalid. + """ + reset_time_str = response_headers.get("x-ratelimit-reset") + if reset_time_str is None: + return None + + try: + reset_time = int(reset_time_str) + delay = reset_time - time.time() + if delay > 0: + return delay + except (ValueError, TypeError): + pass + + return None + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # 1. Check Retry-After header first + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after > 0: + return min(retry_after, MAX_RETRY_DELAY_SECONDS) + + # 2. Check X-RateLimit-Reset header (with positive jitter) + ratelimit_reset = _parse_x_ratelimit_reset(response.headers) + if ratelimit_reset is not None: + return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) + + # 3. Fall back to exponential backoff (with symmetric jitter) + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _retry_timeout_from_retries(retries: int) -> float: + """Determine retry timeout using exponential backoff when no response is available.""" + backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + return _add_symmetric_jitter(backoff) + + +def _should_retry(response: httpx.Response) -> bool: + return response.status_code >= 500 or response.status_code in [429, 408, 409] + + +_SENSITIVE_HEADERS = frozenset( + { + "authorization", + "www-authenticate", + "x-api-key", + "api-key", + "apikey", + "x-api-token", + "x-auth-token", + "auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "proxy-authenticate", + "x-csrf-token", + "x-xsrf-token", + "x-session-token", + "x-access-token", + } +) + + +def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]: + return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()} + + +def _build_url(base_url: str, path: typing.Optional[str]) -> str: + """ + Build a full URL by joining a base URL with a path. + + This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) + by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly + strip path components when the path starts with '/'. + + Example: + >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") + 'https://cloud.example.com/org/tenant/api/users' + + Args: + base_url: The base URL, which may contain path prefixes. + path: The path to append. Can be None or empty string. + + Returns: + The full URL with base_url and path properly joined. + """ + if not path: + return base_url + return f"{base_url.rstrip('/')}/{path.lstrip('/')}" + + +def _maybe_filter_none_from_multipart_data( + data: typing.Optional[typing.Any], + request_files: typing.Optional[RequestFiles], + force_multipart: typing.Optional[bool], +) -> typing.Optional[typing.Any]: + """ + Filter None values from data body for multipart/form requests. + This prevents httpx from converting None to empty strings in multipart encoding. + Only applies when files are present or force_multipart is True. + """ + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): + return remove_none_from_dict(data) + return data + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + has_additional_body_parameters = bool( + request_options is not None and request_options.get("additional_body_parameters") + ) + + # Only collapse empty dict to None when the body was not explicitly provided + # and there are no additional body parameters. This preserves explicit empty + # bodies (e.g., when an endpoint has a request body type but all fields are optional). + if json_body == {} and json is None and not has_additional_body_parameters: + json_body = None + if data_body == {} and data is None and not has_additional_body_parameters: + data_body = None + + return json_body, data_body + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + time.sleep(_retry_timeout_from_retries(retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + base_max_retries: int = 2, + async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, + logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.base_max_retries = base_max_retries + self.async_base_headers = async_base_headers + self.httpx_client = httpx_client + self.logger = create_logger(logging_config) + + async def _get_headers(self) -> typing.Dict[str, str]: + if self.async_base_headers is not None: + return await self.async_base_headers() + return self.base_headers() + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + has_body=json_body is not None or data_body is not None, + ) + + max_retries: int = ( + request_options.get("max_retries", self.base_max_retries) + if request_options is not None + else self.base_max_retries + ) + + try: + response = await self.httpx_client.request( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + except (httpx.ConnectError, httpx.RemoteProtocolError): + if retries < max_retries: + await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + raise + + if _should_retry(response=response): + if retries < max_retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + data=data, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + ) + + if self.logger.is_debug(): + if 200 <= response.status_code < 400: + self.logger.debug( + "HTTP request succeeded", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + if self.logger.is_error(): + if response.status_code >= 400: + self.logger.error( + "HTTP request failed with error status", + method=method, + url=_request_url, + status_code=response.status_code, + ) + + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 0, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) + + # Get headers (supports async token providers) + _headers = await self._get_headers() + + # Compute encoded params separately to avoid passing empty list to httpx + # (httpx strips existing query params from URL when params=[] is passed) + _encoded_params = encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ) + + _request_url = _build_url(base_url, path) + _request_headers = jsonable_encoder( + remove_none_from_dict( + { + **_headers, + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ) + + if self.logger.is_debug(): + self.logger.debug( + "Making streaming HTTP request", + method=method, + url=_request_url, + headers=_redact_headers(_request_headers), + ) + + async with self.httpx_client.stream( + method=method, + url=_request_url, + headers=_request_headers, + params=_encoded_params if _encoded_params else None, + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_response.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_response.py new file mode 100644 index 000000000000..00bb1096d2d0 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_response.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +# Generic to represent the underlying type of the data wrapped by the HTTP response. +T = TypeVar("T") + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers and status code.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + @property + def status_code(self) -> int: + return self._response.status_code + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/__init__.py new file mode 100644 index 000000000000..730e5a3382eb --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/__init__.py @@ -0,0 +1,42 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from ._api import EventSource, aconnect_sse, connect_sse + from ._exceptions import SSEError + from ._models import ServerSentEvent +_dynamic_imports: typing.Dict[str, str] = { + "EventSource": "._api", + "SSEError": "._exceptions", + "ServerSentEvent": "._models", + "aconnect_sse": "._api", + "connect_sse": "._api", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_api.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_api.py new file mode 100644 index 000000000000..9ca5602c2a74 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_api.py @@ -0,0 +1,455 @@ +# This file was auto-generated by Fern from our API Definition. + +import codecs +import re +import time +from contextlib import asynccontextmanager, contextmanager +from typing import ( + Any, + AsyncContextManager, + AsyncGenerator, + AsyncIterator, + Callable, + ContextManager, + Iterator, + Optional, +) + +import anyio +import httpx +from ._decoders import SSEDecoder +from ._exceptions import SSEError +from ._models import ServerSentEvent + +MAX_LINE_SIZE: int = 1_048_576 # 1 MiB + +# Reconnection defaults, mirroring the TypeScript SDK's Stream implementation. +DEFAULT_MAX_RECONNECTION_ATTEMPTS: int = 5 +DEFAULT_RECONNECT_DELAY_MS: int = 1_000 +MAX_RECONNECT_DELAY_MS: int = 30_000 + + +# A reconnect callback re-issues the original request (with a ``Last-Event-ID`` +# header set to the supplied event id) and returns a *context manager* yielding +# a fresh streaming ``httpx.Response``. Sync clients supply a sync context +# manager; async clients supply an async one. +class EventSource: + def __init__( + self, + response: httpx.Response, + *, + resumable: bool = False, + stream_reconnection_enabled: bool = True, + max_stream_reconnection_attempts: Optional[int] = None, + stream_terminator: Optional[str] = None, + reconnect: Optional[Callable[[str], Any]] = None, + ) -> None: + self._response = response + self._resumable = resumable + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._stream_terminator = stream_terminator + self._reconnect = reconnect + + @staticmethod + def _is_event_stream(response: httpx.Response) -> bool: + content_type = response.headers.get("content-type", "").partition(";")[0] + return "text/event-stream" in content_type + + def _check_content_type(self) -> None: + if not self._is_event_stream(self._response): + content_type = self._response.headers.get("content-type", "").partition(";")[0] + raise SSEError( + f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" + ) + + def _is_reconnect_response_usable(self, response: httpx.Response) -> bool: + """Whether a reconnected response can be resumed as an SSE stream. + + ``httpx.stream`` does not raise on non-success status, so a resume that + returns an error page (e.g. ``200 text/html`` or a ``500`` body) would + otherwise be parsed as SSE and yield garbage/zero events. Such a + response is treated as a failed attempt (back off and retry) instead. + """ + return response.status_code < 400 and self._is_event_stream(response) + + def _get_charset(self, response: Optional[httpx.Response] = None) -> str: + """Extract charset from Content-Type header, fallback to UTF-8.""" + resolved = response if response is not None else self._response + content_type = resolved.headers.get("content-type", "") + + # Parse charset parameter using regex + charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) + if charset_match: + charset = charset_match.group(1).strip("\"'") + # Validate that it's a known encoding + try: + # Test if the charset is valid by trying to encode/decode + "test".encode(charset).decode(charset) + return charset + except (LookupError, UnicodeError): + # If charset is invalid, fall back to UTF-8 + pass + + # Default to UTF-8 if no charset specified or invalid charset + return "utf-8" + + @property + def response(self) -> httpx.Response: + return self._response + + @staticmethod + def _normalize_sse_line_endings(buf: str) -> str: + """Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n). + + A trailing \\r is preserved because it may pair with a leading \\n in + the next chunk to form a single \\r\\n terminator. + """ + buf = buf.replace("\r\n", "\n") + if buf.endswith("\r"): + return buf[:-1].replace("\r", "\n") + "\r" + return buf.replace("\r", "\n") + + def _new_text_decoder(self, response: Optional[httpx.Response] = None) -> "codecs.IncrementalDecoder": + return codecs.getincrementaldecoder(self._get_charset(response))(errors="replace") + + def _reconnect_applicable(self) -> bool: + """Whether reconnection is configured for this stream at all. + + This is the terminator-gating half of the reconnect decision, kept + separate from :meth:`_should_reconnect` (which additionally requires a + last *dispatched* id and an unexhausted attempt budget). The split lets + a mid-stream transport error terminate consistently: + - a stream that can never reconnect (non-resumable, no terminator, + disabled, or no callback) must re-raise the error to the caller, so a + truncated stream is not mistaken for a clean completion; + - a resumable stream that has merely run out of attempts (or has no id + to resume from) ends cleanly — the same way an exhausted empty/error + -body resume already does, matching the TypeScript ``return``. + """ + return ( + self._resumable + and self._stream_terminator is not None + and self._stream_reconnection_enabled + and self._reconnect is not None + ) + + def _should_reconnect(self, last_dispatched_id: Optional[str], reconnect_attempts: int) -> bool: + """Decide whether a prematurely-ended stream should be reconnected. + + Mirrors the TypeScript ``shouldReconnect`` gating: + - only resumable SSE endpoints with a configured terminator, reconnect + enabled, and a reconnect callback are eligible (see + :meth:`_reconnect_applicable`); + - a last *dispatched* event id must exist to resume from; + - the consecutive-failed-attempt cap must not be exceeded. + """ + if not self._reconnect_applicable(): + return False + if not last_dispatched_id: + return False + max_attempts = ( + self._max_stream_reconnection_attempts + if self._max_stream_reconnection_attempts is not None + else DEFAULT_MAX_RECONNECTION_ATTEMPTS + ) + if reconnect_attempts >= max_attempts: + return False + return True + + def _reconnect_delay_seconds(self, last_retry: Optional[int]) -> float: + """Backoff before a reconnect. + + Uses the server's most recent ``retry:`` directive (milliseconds) when + present, otherwise a default of ``DEFAULT_RECONNECT_DELAY_MS``, clamped + to ``MAX_RECONNECT_DELAY_MS``. + """ + base_ms = last_retry if (last_retry is not None and last_retry > 0) else DEFAULT_RECONNECT_DELAY_MS + return min(base_ms, MAX_RECONNECT_DELAY_MS) / 1000.0 + + def _sleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``time.sleep`` blocks the calling thread but remains interruptible by + # signals (e.g. ``KeyboardInterrupt``), which propagate out and abort + # the reconnect without issuing another request. + time.sleep(self._reconnect_delay_seconds(last_retry)) + + async def _asleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``anyio.sleep`` is cancellation-aware: if the consumer cancels the task + # or closes the async generator mid-delay, this raises (and no further + # request is issued) instead of blocking for the whole interval. + await anyio.sleep(self._reconnect_delay_seconds(last_retry)) + + def _decode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: + buf = "" + for chunk in response.iter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + yield from self._flush_decoder(buf, decoder, text_decoder) + + async def _adecode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> AsyncGenerator[ServerSentEvent, None]: + buf = "" + async for chunk in response.aiter_bytes(): + buf += text_decoder.decode(chunk) + buf = self._normalize_sse_line_endings(buf) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + for sse in self._flush_decoder(buf, decoder, text_decoder): + yield sse + + def _flush_decoder( + self, + buf: str, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: + # Flush any remaining bytes from the incremental decoder + buf += text_decoder.decode(b"", final=True) + buf = buf.replace("\r\n", "\n").replace("\r", "\n") + + if len(buf) > MAX_LINE_SIZE: + raise SSEError( + f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" + ) + + while "\n" in buf: + line, buf = buf.split("\n", 1) + sse = decoder.decode(line) + if sse is not None: + yield sse + + if buf.strip(): + sse = decoder.decode(buf) + if sse is not None: + yield sse + + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + # Consecutive failed reconnection attempts. Reset to 0 whenever an event + # is successfully dispatched (reset-on-progress) — matching browser + # `EventSource` semantics: a server that emits >=1 event then drops on + # every connection can reconnect indefinitely. + reconnect_attempts = 0 + + # ``None`` means there is no live stream to read this iteration (e.g. a + # failed reconnect); the loop then re-evaluates the reconnect decision + # without re-reading an exhausted response. + response: Optional[httpx.Response] = self._response + # Context manager for a response we opened ourselves and must close. + # The initial response is owned by the caller, so it starts as None. + owned_cm: Optional[ContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._decode_response(response, decoder, text_decoder) + while True: + try: + sse = next(events) + except StopIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + # ``next`` is used rather than ``for`` so this cannot + # swallow a ``GeneratorExit`` raised at a ``yield``. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + self._sleep_before_reconnect(last_retry) + + # Close the previously-opened reconnect response before opening + # a new one so we never hold more than one extra connection. + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: ContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = cm.__enter__() + except Exception: + # A failed reconnect consumes an attempt; back off and retry. + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + # Null/empty body or a non-SSE/error response (e.g. 204/304, + # a 500, or an HTML error page): treat as a failed attempt. + response = None + continue + + response = new_response + # Drop any partial event left over from the dropped stream, but + # keep the last event id (per the SSE spec) and start a fresh + # incremental text decoder for the new connection. + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + reconnect_attempts = 0 + + response: Optional[httpx.Response] = self._response + owned_cm: Optional[AsyncContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._adecode_response(response, decoder, text_decoder) + while True: + try: + sse = await events.__anext__() + except StopAsyncIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + await self._asleep_before_reconnect(last_retry) + + if owned_cm is not None: + await owned_cm.__aexit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: AsyncContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = await cm.__aenter__() + except Exception: + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + response = None + continue + + response = new_response + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + # Shield the close so a cancellation delivered while reading a + # reconnected response still fully tears the connection down + # instead of leaking it until the client is closed. + with anyio.CancelScope(shield=True): + await owned_cm.__aexit__(None, None, None) + + +@contextmanager +def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) + + +@asynccontextmanager +async def aconnect_sse( + client: httpx.AsyncClient, + method: str, + url: str, + **kwargs: Any, +) -> AsyncIterator[EventSource]: + headers = kwargs.pop("headers", {}) + headers["Accept"] = "text/event-stream" + headers["Cache-Control"] = "no-store" + + async with client.stream(method, url, headers=headers, **kwargs) as response: + yield EventSource(response) diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_decoders.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_decoders.py new file mode 100644 index 000000000000..1f6b35ec0bdf --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_decoders.py @@ -0,0 +1,74 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import List, Optional + +from ._models import ServerSentEvent + + +class SSEDecoder: + def __init__(self) -> None: + self._event = "" + self._data: List[str] = [] + self._last_event_id = "" + self._retry: Optional[int] = None + + def reset_in_progress_event(self) -> None: + """Discard any partially-parsed (undispatched) event. + + Used when a stream ends mid-event before reconnecting: the buffered + ``event``/``data``/``retry`` fields of the never-dispatched event must + be dropped so they do not corrupt the first event of the reconnected + stream. Per the SSE spec the last event id is *not* reset here — it + persists across connections. + """ + self._event = "" + self._data = [] + self._retry = None + + def decode(self, line: str) -> Optional[ServerSentEvent]: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = "" + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_exceptions.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_exceptions.py new file mode 100644 index 000000000000..81605a8a65ed --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_exceptions.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import httpx + + +class SSEError(httpx.TransportError): + pass diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_models.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_models.py new file mode 100644 index 000000000000..1af57f8fd0d2 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/http_sse/_models.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +@dataclass(frozen=True) +class ServerSentEvent: + event: str = "message" + data: str = "" + id: str = "" + retry: Optional[int] = None + + def json(self) -> Any: + """Parse the data field as JSON.""" + return json.loads(self.data) diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/jsonable_encoder.py new file mode 100644 index 000000000000..f638cc9a4252 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/jsonable_encoder.py @@ -0,0 +1,133 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". + # OMIT values should be excluded from serialized payloads. + if obj is Ellipsis: + return None + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + if value is Ellipsis: + continue + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + if item is Ellipsis: + continue + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) + + +def encode_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment. + + Ensures proper string conversion for all types, including + booleans which need lowercase 'true'/'false' rather than + Python's 'True'/'False'. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/logging.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/logging.py new file mode 100644 index 000000000000..e5e572458bc8 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/logging.py @@ -0,0 +1,107 @@ +# This file was auto-generated by Fern from our API Definition. + +import logging +import typing + +LogLevel = typing.Literal["debug", "info", "warn", "error"] + +_LOG_LEVEL_MAP: typing.Dict[LogLevel, int] = { + "debug": 1, + "info": 2, + "warn": 3, + "error": 4, +} + + +class ILogger(typing.Protocol): + def debug(self, message: str, **kwargs: typing.Any) -> None: ... + def info(self, message: str, **kwargs: typing.Any) -> None: ... + def warn(self, message: str, **kwargs: typing.Any) -> None: ... + def error(self, message: str, **kwargs: typing.Any) -> None: ... + + +class ConsoleLogger: + _logger: logging.Logger + + def __init__(self) -> None: + self._logger = logging.getLogger("fern") + if not self._logger.handlers: + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) + self._logger.addHandler(handler) + self._logger.setLevel(logging.DEBUG) + + def debug(self, message: str, **kwargs: typing.Any) -> None: + self._logger.debug(message, extra=kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + self._logger.info(message, extra=kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + self._logger.warning(message, extra=kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + self._logger.error(message, extra=kwargs) + + +class LogConfig(typing.TypedDict, total=False): + level: LogLevel + logger: ILogger + silent: bool + + +class Logger: + _level: int + _logger: ILogger + _silent: bool + + def __init__(self, *, level: LogLevel, logger: ILogger, silent: bool) -> None: + self._level = _LOG_LEVEL_MAP[level] + self._logger = logger + self._silent = silent + + def _should_log(self, level: LogLevel) -> bool: + return not self._silent and self._level <= _LOG_LEVEL_MAP[level] + + def is_debug(self) -> bool: + return self._should_log("debug") + + def is_info(self) -> bool: + return self._should_log("info") + + def is_warn(self) -> bool: + return self._should_log("warn") + + def is_error(self) -> bool: + return self._should_log("error") + + def debug(self, message: str, **kwargs: typing.Any) -> None: + if self.is_debug(): + self._logger.debug(message, **kwargs) + + def info(self, message: str, **kwargs: typing.Any) -> None: + if self.is_info(): + self._logger.info(message, **kwargs) + + def warn(self, message: str, **kwargs: typing.Any) -> None: + if self.is_warn(): + self._logger.warn(message, **kwargs) + + def error(self, message: str, **kwargs: typing.Any) -> None: + if self.is_error(): + self._logger.error(message, **kwargs) + + +_default_logger: Logger = Logger(level="info", logger=ConsoleLogger(), silent=True) + + +def create_logger(config: typing.Optional[typing.Union[LogConfig, Logger]] = None) -> Logger: + if config is None: + return _default_logger + if isinstance(config, Logger): + return config + return Logger( + level=config.get("level", "info"), + logger=config.get("logger", ConsoleLogger()), + silent=config.get("silent", True), + ) diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/parse_error.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/parse_error.py new file mode 100644 index 000000000000..4527c6a8adec --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/parse_error.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ParsingError(Exception): + """ + Raised when the SDK fails to parse/validate a response from the server. + This typically indicates that the server returned a response whose shape + does not match the expected schema. + """ + + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + cause: Optional[Exception] + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + cause: Optional[Exception] = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + self.cause = cause + super().__init__() + if cause is not None: + self.__cause__ = cause + + def __str__(self) -> str: + cause_str = f", cause: {self.cause}" if self.cause is not None else "" + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}{cause_str}" diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/pydantic_utilities.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/pydantic_utilities.py new file mode 100644 index 000000000000..6587f5e1820f --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/pydantic_utilities.py @@ -0,0 +1,508 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +import inspect +import json +import logging +from collections import defaultdict +from dataclasses import asdict +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + Dict, + List, + Mapping, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +import pydantic +import typing_extensions +from pydantic.fields import FieldInfo as _FieldInfo + +_logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from .http_sse._models import ServerSentEvent + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + _datetime_adapter = pydantic.TypeAdapter(dt.datetime) # type: ignore[attr-defined] + _date_adapter = pydantic.TypeAdapter(dt.date) # type: ignore[attr-defined] + + def parse_datetime(value: Any) -> dt.datetime: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value + return _datetime_adapter.validate_python(value) + + def parse_date(value: Any) -> dt.date: # type: ignore[misc] + if isinstance(value, dt.datetime): + return value.date() + if isinstance(value, dt.date): + return value + return _date_adapter.validate_python(value) + + # Avoid importing from pydantic.v1 to maintain Python 3.14 compatibility. + from typing import get_args as get_args # type: ignore[assignment] + from typing import get_origin as get_origin # type: ignore[assignment] + + def is_literal_type(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return typing_extensions.get_origin(tp) is typing_extensions.Literal + + def is_union(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] + return tp is Union or typing_extensions.get_origin(tp) is Union # type: ignore[comparison-overlap] + + # Inline encoders_by_type to avoid importing from pydantic.v1.json + import re as _re + from collections import deque as _deque + from decimal import Decimal as _Decimal + from enum import Enum as _Enum + from ipaddress import ( + IPv4Address as _IPv4Address, + ) + from ipaddress import ( + IPv4Interface as _IPv4Interface, + ) + from ipaddress import ( + IPv4Network as _IPv4Network, + ) + from ipaddress import ( + IPv6Address as _IPv6Address, + ) + from ipaddress import ( + IPv6Interface as _IPv6Interface, + ) + from ipaddress import ( + IPv6Network as _IPv6Network, + ) + from pathlib import Path as _Path + from types import GeneratorType as _GeneratorType + from uuid import UUID as _UUID + + from pydantic.fields import FieldInfo as ModelField # type: ignore[no-redef, assignment] + + def _decimal_encoder(dec_value: Any) -> Any: + if dec_value.as_tuple().exponent >= 0: + return int(dec_value) + return float(dec_value) + + encoders_by_type: Dict[Type[Any], Callable[[Any], Any]] = { # type: ignore[no-redef] + bytes: lambda o: o.decode(), + dt.date: lambda o: o.isoformat(), + dt.datetime: lambda o: o.isoformat(), + dt.time: lambda o: o.isoformat(), + dt.timedelta: lambda td: td.total_seconds(), + _Decimal: _decimal_encoder, + _Enum: lambda o: o.value, + frozenset: list, + _deque: list, + _GeneratorType: list, + _IPv4Address: str, + _IPv4Interface: str, + _IPv4Network: str, + _IPv6Address: str, + _IPv6Interface: str, + _IPv6Network: str, + _Path: str, + _re.Pattern: lambda o: o.pattern, + set: list, + _UUID: str, + } +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef, assignment] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: + """ + Parse a ServerSentEvent into the appropriate type. + + This function handles data-level discrimination where the discriminator + (e.g., 'type') is inside the 'data' payload. It parses the SSE data field + as JSON and deserializes it into the target type. + + Note: Protocol-level discrimination (where the discriminator comes from + the SSE event: field) is handled at code-generation time and does not + use this function. + + Args: + sse: The ServerSentEvent object to parse + type_: The target type to deserialize into + + Returns: + The parsed object of type T + + Note: + This function is only available in SDK contexts where http_sse module exists. + """ + sse_event = asdict(sse) + data_value = sse_event.get("data") + if isinstance(data_value, str) and data_value: + try: + parsed_data = json.loads(data_value) + return parse_obj_as(type_, parsed_data) + except json.JSONDecodeError as e: + _logger.warning( + "Failed to parse SSE data field as JSON: %s, data: %s", + e, + data_value[:100] if len(data_value) > 100 else data_value, + ) + return parse_obj_as(type_, sse_event) + + +_type_adapter_cache: Dict[int, Any] = {} + + +def _get_type_adapter(type_: Type[Any]) -> Any: + key = id(type_) + adapter = _type_adapter_cache.get(key) + if adapter is None: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + _type_adapter_cache[key] = adapter + return adapter + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + # convert_and_respect_annotation_metadata is required for TypedDict aliasing. + # + # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: + # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through + # unchanged so Pydantic can validate them. + # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic + # will not recognize those aliases during validation. + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + has_pydantic_aliases = False + if IS_PYDANTIC_V2: + for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined] + alias = getattr(field_info, "alias", None) + if alias is not None and alias != field_name: + has_pydantic_aliases = True + break + else: + for field in getattr(type_, "__fields__", {}).values(): + alias = getattr(field, "alias", None) + name = getattr(field, "name", None) + if alias is not None and name is not None and alias != name: + has_pydantic_aliases = True + break + + dealiased_object = ( + object_ + if has_pydantic_aliases + else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + ) + else: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = _get_type_adapter(type_) + return adapter.validate_python(dealiased_object) # type: ignore[no-any-return] + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_validator(mode="before") # type: ignore[attr-defined] + @classmethod + def _coerce_field_names_to_aliases(cls, data: Any) -> Any: + """ + Accept Python field names in input by rewriting them to their Pydantic aliases, + while avoiding silent collisions when a key could refer to multiple fields. + """ + if not isinstance(data, Mapping): + return data + + fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined] + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field_info in fields.items(): + alias = getattr(field_info, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + # Detect ambiguous keys: a key that is an alias for one field and a name for another. + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in data and name_to_alias[key] not in data: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(data.keys()) + rewritten: Dict[str, Any] = dict(data) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.dict() # type: ignore[attr-defined] + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @pydantic.root_validator(pre=True) + def _coerce_field_names_to_aliases(cls, values: Any) -> Any: + """ + Pydantic v1 equivalent of _coerce_field_names_to_aliases. + """ + if not isinstance(values, Mapping): + return values + + fields = getattr(cls, "__fields__", {}) + name_to_alias: Dict[str, str] = {} + alias_to_name: Dict[str, str] = {} + + for name, field in fields.items(): + alias = getattr(field, "alias", None) or name + name_to_alias[name] = alias + if alias != name: + alias_to_name[alias] = name + + ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys())) + for key in ambiguous_keys: + if key in values and name_to_alias[key] not in values: + raise ValueError( + f"Ambiguous input key '{key}': it is both a field name and an alias. " + "Provide the explicit alias key to disambiguate." + ) + + original_keys = set(values.keys()) + rewritten: Dict[str, Any] = dict(values) + for name, alias in name_to_alias.items(): + if alias != name and name in original_keys and alias not in rewritten: + rewritten[alias] = rewritten.pop(name) + + return rewritten + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return cast( + Dict[str, Any], + convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"), + ) + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + # In Pydantic v2, for RootModel we always use "before" mode + # The custom validators transform the input value before the model is created + return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, _FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/query_encoder.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/query_encoder.py new file mode 100644 index 000000000000..3183001d4046 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/remove_none_from_dict.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/remove_none_from_dict.py new file mode 100644 index 000000000000..c2298143f14a --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/request_options.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/request_options.py new file mode 100644 index 000000000000..caa6f669b3e8 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/request_options.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout: float. The number of seconds to await an API call before timing out. + + - timeout_in_seconds: int. Deprecated alias for `timeout`; both are in seconds. Prefer `timeout`. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout: NotRequired[float] + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] + stream_reconnection_enabled: NotRequired[bool] + max_stream_reconnection_attempts: NotRequired[int] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/serialization.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/serialization.py new file mode 100644 index 000000000000..1d753e26f739 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/core/serialization.py @@ -0,0 +1,347 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles +# forward-reference annotations. The result is constant for a given type, so we cache it. +# This is critical for hot paths like SSE event parsing, where the same (often large +# discriminated-union) type is converted on every single event. +_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {} + + +def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + cached = _type_hints_cache.get(expected_type) + except TypeError: + # Unhashable type; resolve without caching. + return _resolve_type_hints(expected_type) + if cached is None: + cached = _resolve_type_hints(expected_type) + _type_hints_cache[expected_type] = cached + return cached + + +def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: + try: + return typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The type contains a circular reference, so we use the __annotations__ attribute directly. + return getattr(expected_type, "__annotations__", {}) + + +# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given +# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias. +# This is constant per type, so we cache it and use it to short-circuit the recursive walk. +_requires_conversion_cache: typing.Dict[typing.Any, bool] = {} + + +def _requires_conversion(type_: typing.Any) -> bool: + try: + cached = _requires_conversion_cache.get(type_) + except TypeError: + # Unhashable annotation; compute without caching. + return _compute_requires_conversion(type_, set()) + if cached is None: + cached = _compute_requires_conversion(type_, set()) + _requires_conversion_cache[type_] = cached + return cached + + +def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool: + clean_type = _remove_annotations(type_) + + try: + if clean_type in seen: + return False + seen = seen | {clean_type} + except TypeError: + # Unhashable type; skip cycle tracking (the type graph is finite in practice). + pass + + # Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields. + if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict( + clean_type + ): + annotations = _get_cached_type_hints(clean_type) + if _get_alias_to_field_name(annotations): + return True + return any(_compute_requires_conversion(hint, seen) for hint in annotations.values()) + + # Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.). + return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type)) + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + # The only thing this function ever rewrites is keys that carry a FieldMetadata + # alias. If nothing in the (cached) type graph has such an alias, the conversion is + # a content-identity transform, so we can skip the entire recursive walk. This is + # the hot path for SSE streaming, where a large discriminated union would otherwise + # be traversed on every single event. + if not _requires_conversion(annotation): + return object_ + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + annotations = _get_cached_type_hints(expected_type) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = _get_cached_type_hints(type_) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = _get_cached_type_hints(type_) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/__init__.py new file mode 100644 index 000000000000..fa16cbe223a2 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import Organization +_dynamic_imports: typing.Dict[str, str] = {"Organization": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["Organization"] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/client.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/client.py new file mode 100644 index 000000000000..18724a2a5e19 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/client.py @@ -0,0 +1,269 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..user.types.user import User +from .raw_client import AsyncRawOrganizationsClient, RawOrganizationsClient +from .types.organization import Organization + + +class OrganizationsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawOrganizationsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawOrganizationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawOrganizationsClient + """ + return self._raw_client + + def get_organization( + self, organization_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> Organization: + """ + Parameters + ---------- + organization_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.get_organization( + organization_id="organization_id", + ) + """ + _response = self._raw_client.get_organization(organization_id, request_options=request_options) + return _response.data + + def get_organization_user( + self, organization_id: str, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Parameters + ---------- + organization_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.get_organization_user( + organization_id="organization_id", + user_id="user_id", + ) + """ + _response = self._raw_client.get_organization_user(organization_id, user_id, request_options=request_options) + return _response.data + + def search_organizations( + self, + organization_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.List[Organization]: + """ + Parameters + ---------- + organization_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[Organization] + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.organizations.search_organizations( + organization_id="organization_id", + limit=1, + ) + """ + _response = self._raw_client.search_organizations(organization_id, limit=limit, request_options=request_options) + return _response.data + + +class AsyncOrganizationsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawOrganizationsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawOrganizationsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawOrganizationsClient + """ + return self._raw_client + + async def get_organization( + self, organization_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> Organization: + """ + Parameters + ---------- + organization_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + Organization + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.get_organization( + organization_id="organization_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_organization(organization_id, request_options=request_options) + return _response.data + + async def get_organization_user( + self, organization_id: str, user_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Parameters + ---------- + organization_id : str + + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.get_organization_user( + organization_id="organization_id", + user_id="user_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_organization_user( + organization_id, user_id, request_options=request_options + ) + return _response.data + + async def search_organizations( + self, + organization_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.List[Organization]: + """ + Parameters + ---------- + organization_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[Organization] + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.organizations.search_organizations( + organization_id="organization_id", + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.search_organizations( + organization_id, limit=limit, request_options=request_options + ) + return _response.data diff --git a/seed/python-sdk/path-parameters/src/seed/organizations/raw_client.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/raw_client.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/organizations/raw_client.py rename to seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/raw_client.py diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/types/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/types/__init__.py new file mode 100644 index 000000000000..dff8e04c10ce --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/types/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .organization import Organization +_dynamic_imports: typing.Dict[str, str] = {"Organization": ".organization"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["Organization"] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/types/organization.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/types/organization.py new file mode 100644 index 000000000000..57d98480bb13 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/organizations/types/organization.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class Organization(UniversalBaseModel): + name: str + tags: typing.List[str] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/py.typed b/seed/python-sdk/path-parameters/no-custom-config/src/seed/py.typed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/__init__.py new file mode 100644 index 000000000000..5f841626b151 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import User +_dynamic_imports: typing.Dict[str, str] = {"User": ".types"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["User"] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/client.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/client.py new file mode 100644 index 000000000000..122e175c6324 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/client.py @@ -0,0 +1,517 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawUserClient, RawUserClient +from .types.user import User + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class UserClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawUserClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawUserClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawUserClient + """ + return self._raw_client + + def get_user(self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> User: + """ + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.user.get_user( + user_id="user_id", + ) + """ + _response = self._raw_client.get_user(user_id, request_options=request_options) + return _response.data + + def create_user( + self, *, name: str, tags: typing.Sequence[str], request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Parameters + ---------- + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.user.create_user( + name="name", + tags=["tags", "tags"], + ) + """ + _response = self._raw_client.create_user(name=name, tags=tags, request_options=request_options) + return _response.data + + def update_user( + self, + user_id: str, + *, + name: str, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> User: + """ + Parameters + ---------- + user_id : str + + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.user.update_user( + user_id="user_id", + name="name", + tags=["tags", "tags"], + ) + """ + _response = self._raw_client.update_user(user_id, name=name, tags=tags, request_options=request_options) + return _response.data + + def search_users( + self, + user_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.List[User]: + """ + Parameters + ---------- + user_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[User] + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.user.search_users( + user_id="user_id", + limit=1, + ) + """ + _response = self._raw_client.search_users(user_id, limit=limit, request_options=request_options) + return _response.data + + def get_user_metadata( + self, user_id: str, version: int, *, request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Test endpoint with path parameter that has a text prefix (v{version}) + + Parameters + ---------- + user_id : str + + version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.user.get_user_metadata( + user_id="user_id", + version=1, + ) + """ + _response = self._raw_client.get_user_metadata(user_id, version, request_options=request_options) + return _response.data + + def get_user_specifics( + self, user_id: str, version: int, thought: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Test endpoint with path parameters listed in different order than found in path + + Parameters + ---------- + user_id : str + + version : int + + thought : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + from seed import SeedPathParameters + + client = SeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + client.user.get_user_specifics( + user_id="user_id", + version=1, + thought="thought", + ) + """ + _response = self._raw_client.get_user_specifics(user_id, version, thought, request_options=request_options) + return _response.data + + +class AsyncUserClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawUserClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawUserClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawUserClient + """ + return self._raw_client + + async def get_user(self, user_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> User: + """ + Parameters + ---------- + user_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.get_user( + user_id="user_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_user(user_id, request_options=request_options) + return _response.data + + async def create_user( + self, *, name: str, tags: typing.Sequence[str], request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Parameters + ---------- + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.create_user( + name="name", + tags=["tags", "tags"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_user(name=name, tags=tags, request_options=request_options) + return _response.data + + async def update_user( + self, + user_id: str, + *, + name: str, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> User: + """ + Parameters + ---------- + user_id : str + + name : str + + tags : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.update_user( + user_id="user_id", + name="name", + tags=["tags", "tags"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update_user(user_id, name=name, tags=tags, request_options=request_options) + return _response.data + + async def search_users( + self, + user_id: str, + *, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.List[User]: + """ + Parameters + ---------- + user_id : str + + limit : typing.Optional[int] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.List[User] + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.search_users( + user_id="user_id", + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.search_users(user_id, limit=limit, request_options=request_options) + return _response.data + + async def get_user_metadata( + self, user_id: str, version: int, *, request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Test endpoint with path parameter that has a text prefix (v{version}) + + Parameters + ---------- + user_id : str + + version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.get_user_metadata( + user_id="user_id", + version=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_user_metadata(user_id, version, request_options=request_options) + return _response.data + + async def get_user_specifics( + self, user_id: str, version: int, thought: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> User: + """ + Test endpoint with path parameters listed in different order than found in path + + Parameters + ---------- + user_id : str + + version : int + + thought : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + User + + Examples + -------- + import asyncio + + from seed import AsyncSeedPathParameters + + client = AsyncSeedPathParameters( + tenant_id="YOUR_TENANT_ID", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.user.get_user_specifics( + user_id="user_id", + version=1, + thought="thought", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_user_specifics( + user_id, version, thought, request_options=request_options + ) + return _response.data diff --git a/seed/python-sdk/path-parameters/src/seed/user/raw_client.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/raw_client.py similarity index 100% rename from seed/python-sdk/path-parameters/src/seed/user/raw_client.py rename to seed/python-sdk/path-parameters/no-custom-config/src/seed/user/raw_client.py diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/types/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/types/__init__.py new file mode 100644 index 000000000000..9a15de15fc1f --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/types/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .user import User +_dynamic_imports: typing.Dict[str, str] = {"User": ".user"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["User"] diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/types/user.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/types/user.py new file mode 100644 index 000000000000..344d574ea329 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/user/types/user.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class User(UniversalBaseModel): + name: str + tags: typing.List[str] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/seed/python-sdk/path-parameters/no-custom-config/src/seed/version.py b/seed/python-sdk/path-parameters/no-custom-config/src/seed/version.py new file mode 100644 index 000000000000..e59e6530060f --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/src/seed/version.py @@ -0,0 +1,3 @@ +from importlib import metadata + +__version__ = metadata.version("fern_path-parameters") diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/conftest.py b/seed/python-sdk/path-parameters/no-custom-config/tests/conftest.py new file mode 100644 index 000000000000..25710dbe6278 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def _has_httpx_aiohttp() -> bool: + """Check if httpx_aiohttp is importable.""" + try: + import httpx_aiohttp # type: ignore[import-not-found] # noqa: F401 + + return True + except ImportError: + return False + + +def pytest_collection_modifyitems(config: pytest.Config, items: list) -> None: + """Auto-skip @pytest.mark.aiohttp tests when httpx_aiohttp is not installed.""" + if _has_httpx_aiohttp(): + return + skip_aiohttp = pytest.mark.skip(reason="httpx_aiohttp not installed") + for item in items: + if "aiohttp" in item.keywords: + item.add_marker(skip_aiohttp) diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/custom/test_client.py b/seed/python-sdk/path-parameters/no-custom-config/tests/custom/test_client.py new file mode 100644 index 000000000000..ab04ce6393ef --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/custom/test_client.py @@ -0,0 +1,7 @@ +import pytest + + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/test_aiohttp_autodetect.py b/seed/python-sdk/path-parameters/no-custom-config/tests/test_aiohttp_autodetect.py new file mode 100644 index 000000000000..671c754de059 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/test_aiohttp_autodetect.py @@ -0,0 +1,116 @@ +import importlib +import sys +import unittest +from unittest import mock + +import httpx +import pytest + + +class TestMakeDefaultAsyncClientWithoutAiohttp(unittest.TestCase): + """Tests for _make_default_async_client when httpx_aiohttp is NOT installed.""" + + def test_returns_httpx_async_client(self) -> None: + """When httpx_aiohttp is not installed, returns plain httpx.AsyncClient.""" + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + from seed.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=True) + self.assertIsInstance(client, httpx.AsyncClient) + self.assertEqual(client.timeout.read, 60) + self.assertTrue(client.follow_redirects) + + def test_follow_redirects_none(self) -> None: + """When follow_redirects is None, omits it from httpx.AsyncClient.""" + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + from seed.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=None) + self.assertIsInstance(client, httpx.AsyncClient) + self.assertFalse(client.follow_redirects) + + def test_explicit_httpx_client_bypasses_autodetect(self) -> None: + """When user passes httpx_client explicitly, _make_default_async_client is not called.""" + + explicit_client = httpx.AsyncClient(timeout=120) + with mock.patch("seed.client._make_default_async_client") as mock_make: + # Replicate the generated conditional: httpx_client if httpx_client is not None else _make_default_async_client(...) + result = explicit_client if explicit_client is not None else mock_make(timeout=60, follow_redirects=True) + mock_make.assert_not_called() + self.assertIs(result, explicit_client) + + +@pytest.mark.aiohttp +class TestMakeDefaultAsyncClientWithAiohttp(unittest.TestCase): + """Tests for _make_default_async_client when httpx_aiohttp IS installed.""" + + def test_returns_aiohttp_client(self) -> None: + """When httpx_aiohttp is installed, returns HttpxAiohttpClient.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from seed.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=True) + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertEqual(client.timeout.read, 60) + self.assertTrue(client.follow_redirects) + + def test_follow_redirects_none(self) -> None: + """When httpx_aiohttp is installed and follow_redirects is None, omits it.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from seed.client import _make_default_async_client + + client = _make_default_async_client(timeout=60, follow_redirects=None) + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertFalse(client.follow_redirects) + + +class TestDefaultClientsWithoutAiohttp(unittest.TestCase): + """Tests for _default_clients.py convenience classes (no aiohttp).""" + + def test_default_async_httpx_client_defaults(self) -> None: + """DefaultAsyncHttpxClient applies SDK defaults.""" + from seed._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAsyncHttpxClient + + client = DefaultAsyncHttpxClient() + self.assertIsInstance(client, httpx.AsyncClient) + self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) + self.assertTrue(client.follow_redirects) + + def test_default_async_httpx_client_overrides(self) -> None: + """DefaultAsyncHttpxClient allows overriding defaults.""" + from seed._default_clients import DefaultAsyncHttpxClient + + client = DefaultAsyncHttpxClient(timeout=30, follow_redirects=False) + self.assertEqual(client.timeout.read, 30) + self.assertFalse(client.follow_redirects) + + def test_default_aiohttp_client_raises_without_package(self) -> None: + """DefaultAioHttpClient raises RuntimeError when httpx_aiohttp not installed.""" + import seed._default_clients + + with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): + importlib.reload(seed._default_clients) + + with self.assertRaises(RuntimeError) as ctx: + seed._default_clients.DefaultAioHttpClient() + self.assertIn("pip install fern_path-parameters[aiohttp]", str(ctx.exception)) + + importlib.reload(seed._default_clients) + + +@pytest.mark.aiohttp +class TestDefaultClientsWithAiohttp(unittest.TestCase): + """Tests for _default_clients.py when httpx_aiohttp IS installed.""" + + def test_default_aiohttp_client_defaults(self) -> None: + """DefaultAioHttpClient works when httpx_aiohttp is installed.""" + import httpx_aiohttp # type: ignore[import-not-found] + + from seed._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAioHttpClient + + client = DefaultAioHttpClient() + self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) + self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) + self.assertTrue(client.follow_redirects) diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/__init__.py new file mode 100644 index 000000000000..f3ea2659bb1c --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/__init__.py @@ -0,0 +1,2 @@ +# This file was auto-generated by Fern from our API Definition. + diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/__init__.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/__init__.py new file mode 100644 index 000000000000..2cf01263529d --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/__init__.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from .circle import CircleParams +from .object_with_defaults import ObjectWithDefaultsParams +from .object_with_optional_field import ObjectWithOptionalFieldParams +from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams +from .square import SquareParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +__all__ = [ + "CircleParams", + "ObjectWithDefaultsParams", + "ObjectWithOptionalFieldParams", + "ShapeParams", + "Shape_CircleParams", + "Shape_SquareParams", + "SquareParams", + "UndiscriminatedShapeParams", +] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/circle.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/circle.py new file mode 100644 index 000000000000..74ecf38c308b --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/circle.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from seed.core.serialization import FieldMetadata + + +class CircleParams(typing_extensions.TypedDict): + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/color.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/color.py new file mode 100644 index 000000000000..2aa2c4c52f0c --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/color.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/object_with_defaults.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/object_with_defaults.py new file mode 100644 index 000000000000..a977b1d2aa1c --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/object_with_defaults.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ObjectWithDefaultsParams(typing_extensions.TypedDict): + """ + Defines properties with default values and validation rules. + """ + + decimal: typing_extensions.NotRequired[float] + string: typing_extensions.NotRequired[str] + required_string: str diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/object_with_optional_field.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/object_with_optional_field.py new file mode 100644 index 000000000000..6b5608bc05b6 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/object_with_optional_field.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +import uuid + +import typing_extensions +from .color import Color +from .shape import ShapeParams +from .undiscriminated_shape import UndiscriminatedShapeParams + +from seed.core.serialization import FieldMetadata + + +class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): + literal: typing.Literal["lit_one"] + string: typing_extensions.NotRequired[str] + integer: typing_extensions.NotRequired[int] + long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] + double: typing_extensions.NotRequired[float] + bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] + datetime: typing_extensions.NotRequired[dt.datetime] + date: typing_extensions.NotRequired[dt.date] + uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] + base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] + list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] + set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] + map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] + enum: typing_extensions.NotRequired[Color] + union: typing_extensions.NotRequired[ShapeParams] + second_union: typing_extensions.NotRequired[ShapeParams] + undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] + any: typing.Optional[typing.Any] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/shape.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/shape.py new file mode 100644 index 000000000000..7e70010a251f --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/shape.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + +from seed.core.serialization import FieldMetadata + + +class Base(typing_extensions.TypedDict): + id: str + + +class Shape_CircleParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] + radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] + + +class Shape_SquareParams(Base): + shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] + + +ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/square.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/square.py new file mode 100644 index 000000000000..71c7d25fd4ad --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/square.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + +from seed.core.serialization import FieldMetadata + + +class SquareParams(typing_extensions.TypedDict): + length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/undiscriminated_shape.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/undiscriminated_shape.py new file mode 100644 index 000000000000..99f12b300d1d --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/assets/models/undiscriminated_shape.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .circle import CircleParams +from .square import SquareParams + +UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_http_client.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_http_client.py new file mode 100644 index 000000000000..d17928fb3019 --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_http_client.py @@ -0,0 +1,761 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Tuple, cast +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from seed.core.http_client import ( + AsyncHttpClient, + HttpClient, + _build_url, + _should_retry, + get_request_body, + remove_none_from_dict, +) +from seed.core.request_options import RequestOptions + + +# Stub clients for testing HttpClient and AsyncHttpClient +class _DummySyncClient: + """A minimal stub for httpx.Client that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyAsyncClient: + """A minimal stub for httpx.AsyncClient that records request arguments.""" + + def __init__(self) -> None: + self.last_request_kwargs: Dict[str, Any] = {} + + async def request(self, **kwargs: Any) -> "_DummyResponse": + self.last_request_kwargs = kwargs + return _DummyResponse() + + +class _DummyResponse: + """A minimal stub for httpx.Response.""" + + status_code = 200 + headers: Dict[str, str] = {} + + +def get_request_options() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later"}} + + +def get_request_options_with_none() -> RequestOptions: + return {"additional_body_parameters": {"see you": "later", "optional": None}} + + +def test_get_json_request_body() -> None: + json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) + assert json_body == {"hello": "world"} + assert data_body is None + + json_body_extras, data_body_extras = get_request_body( + json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"goodbye": "world", "see you": "later"} + assert data_body_extras is None + + +def test_get_files_request_body() -> None: + json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) + assert data_body == {"hello": "world"} + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None + ) + + assert data_body_extras == {"goodbye": "world", "see you": "later"} + assert json_body_extras is None + + +def test_get_none_request_body() -> None: + json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) + assert data_body is None + assert json_body is None + + json_body_extras, data_body_extras = get_request_body( + json=None, data=None, request_options=get_request_options(), omit=None + ) + + assert json_body_extras == {"see you": "later"} + assert data_body_extras is None + + +def test_get_empty_json_request_body() -> None: + """Test that implicit empty bodies (json=None) are collapsed to None.""" + unrelated_request_options: RequestOptions = {"max_retries": 3} + json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) + assert json_body is None + assert data_body is None + + +def test_explicit_empty_json_body_is_preserved() -> None: + """Test that explicit empty bodies (json={}) are preserved and sent as {}. + + This is important for endpoints where the request body is required but all + fields are optional. The server expects valid JSON ({}) not an empty body. + """ + unrelated_request_options: RequestOptions = {"max_retries": 3} + + # Explicit json={} should be preserved + json_body, data_body = get_request_body(json={}, data=None, request_options=unrelated_request_options, omit=None) + assert json_body == {} + assert data_body is None + + # Explicit data={} should also be preserved + json_body2, data_body2 = get_request_body(json=None, data={}, request_options=unrelated_request_options, omit=None) + assert json_body2 is None + assert data_body2 == {} + + +def test_json_body_preserves_none_values() -> None: + """Test that JSON bodies preserve None values (they become JSON null).""" + json_body, data_body = get_request_body( + json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None + ) + # JSON bodies should preserve None values + assert json_body == {"hello": "world", "optional": None} + assert data_body is None + + +def test_data_body_preserves_none_values_without_multipart() -> None: + """Test that data bodies preserve None values when not using multipart. + + The filtering of None values happens in HttpClient.request/stream methods, + not in get_request_body. This test verifies get_request_body doesn't filter None. + """ + json_body, data_body = get_request_body( + json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None + ) + # get_request_body should preserve None values in data body + # The filtering happens later in HttpClient.request when multipart is detected + assert data_body == {"hello": "world", "optional": None} + assert json_body is None + + +def test_remove_none_from_dict_filters_none_values() -> None: + """Test that remove_none_from_dict correctly filters out None values.""" + original = {"hello": "world", "optional": None, "another": "value", "also_none": None} + filtered = remove_none_from_dict(original) + assert filtered == {"hello": "world", "another": "value"} + # Original should not be modified + assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None} + + +def test_remove_none_from_dict_empty_dict() -> None: + """Test that remove_none_from_dict handles empty dict.""" + assert remove_none_from_dict({}) == {} + + +def test_remove_none_from_dict_all_none() -> None: + """Test that remove_none_from_dict handles dict with all None values.""" + assert remove_none_from_dict({"a": None, "b": None}) == {} + + +def test_http_client_does_not_pass_empty_params_list() -> None: + """Test that HttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + # Use a path with query params (e.g., pagination cursor URL) + http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +def test_http_client_passes_encoded_params_when_present() -> None: + """Test that HttpClient passes encoded params when params are provided.""" + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + ) + + http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +@pytest.mark.asyncio +async def test_async_http_client_does_not_pass_empty_params_list() -> None: + """Test that AsyncHttpClient passes params=None when params are empty. + + This prevents httpx from stripping existing query parameters from the URL, + which happens when params=[] or params={} is passed. + """ + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + # Use a path with query params (e.g., pagination cursor URL) + await http_client.request( + path="resource?after=123", + method="GET", + params=None, + request_options=None, + ) + + # We care that httpx receives params=None, not [] or {} + assert "params" in dummy_client.last_request_kwargs + assert dummy_client.last_request_kwargs["params"] is None + + # Verify the query string in the URL is preserved + url = str(dummy_client.last_request_kwargs["url"]) + assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" + + +@pytest.mark.asyncio +async def test_async_http_client_passes_encoded_params_when_present() -> None: + """Test that AsyncHttpClient passes encoded params when params are provided.""" + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com/resource", + async_base_headers=None, + ) + + await http_client.request( + path="", + method="GET", + params={"after": "456"}, + request_options=None, + ) + + params = dummy_client.last_request_kwargs["params"] + # For a simple dict, encode_query should give a single (key, value) tuple + assert params == [("after", "456")] + + +def test_basic_url_joining() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com", "/users") + assert result == "https://api.example.com/users" + + +def test_basic_url_joining_trailing_slash() -> None: + """Test basic URL joining with a simple base URL and path.""" + result = _build_url("https://api.example.com/", "/users") + assert result == "https://api.example.com/users" + + +def test_preserves_base_url_path_prefix() -> None: + """Test that path prefixes in base URL are preserved. + + This is the critical bug fix - urllib.parse.urljoin() would strip + the path prefix when the path starts with '/'. + """ + result = _build_url("https://cloud.example.com/org/tenant/api", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +def test_preserves_base_url_path_prefix_trailing_slash() -> None: + """Test that path prefixes in base URL are preserved.""" + result = _build_url("https://cloud.example.com/org/tenant/api/", "/users") + assert result == "https://cloud.example.com/org/tenant/api/users" + + +# --------------------------------------------------------------------------- +# Connection error retry tests +# --------------------------------------------------------------------------- + + +def _make_sync_http_client(mock_client: Any) -> HttpClient: + return HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + + +def _make_async_http_client(mock_client: Any) -> AsyncHttpClient: + return AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_retries_on_connect_error(mock_sleep: MagicMock) -> None: + """Sync: connection error retries on httpx.ConnectError.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + http_client = _make_sync_http_client(mock_client) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_retries_on_remote_protocol_error(mock_sleep: MagicMock) -> None: + """Sync: connection error retries on httpx.RemoteProtocolError.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.RemoteProtocolError("Remote end closed connection without response"), + _DummyResponse(), + ] + http_client = _make_sync_http_client(mock_client) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_connection_error_exhausts_retries(mock_sleep: MagicMock) -> None: + """Sync: connection error exhausts retries then raises.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = _make_sync_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + assert mock_sleep.call_count == 2 + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_connection_error_respects_max_retries_zero(mock_sleep: MagicMock) -> None: + """Sync: connection error respects max_retries=0.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = _make_sync_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 0}, + ) + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_retries_on_connect_error(mock_sleep: AsyncMock) -> None: + """Async: connection error retries on httpx.ConnectError.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + ) + http_client = _make_async_http_client(mock_client) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_retries_on_remote_protocol_error(mock_sleep: AsyncMock) -> None: + """Async: connection error retries on httpx.RemoteProtocolError.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.RemoteProtocolError("Remote end closed connection without response"), + _DummyResponse(), + ] + ) + http_client = _make_async_http_client(mock_client) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + assert mock_client.request.call_count == 2 + mock_sleep.assert_called_once() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_connection_error_exhausts_retries(mock_sleep: AsyncMock) -> None: + """Async: connection error exhausts retries then raises.""" + mock_client = MagicMock() + mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) + http_client = _make_async_http_client(mock_client) + + with pytest.raises(httpx.ConnectError): + await http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + assert mock_sleep.call_count == 2 + + +# --------------------------------------------------------------------------- +# base_max_retries constructor parameter tests +# --------------------------------------------------------------------------- + + +def test_sync_http_client_default_base_max_retries() -> None: + """HttpClient defaults to base_max_retries=2.""" + http_client = HttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + ) + assert http_client.base_max_retries == 2 + + +def test_async_http_client_default_base_max_retries() -> None: + """AsyncHttpClient defaults to base_max_retries=2.""" + http_client = AsyncHttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + ) + assert http_client.base_max_retries == 2 + + +def test_sync_http_client_custom_base_max_retries() -> None: + """HttpClient accepts a custom base_max_retries value.""" + http_client = HttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_max_retries=5, + ) + assert http_client.base_max_retries == 5 + + +def test_async_http_client_custom_base_max_retries() -> None: + """AsyncHttpClient accepts a custom base_max_retries value.""" + http_client = AsyncHttpClient( + httpx_client=MagicMock(), # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_max_retries=5, + ) + assert http_client.base_max_retries == 5 + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_base_max_retries_zero_disables_retries(mock_sleep: MagicMock) -> None: + """Sync: base_max_retries=0 disables retries when no request_options override.""" + mock_client = MagicMock() + mock_client.request.side_effect = httpx.ConnectError("connection failed") + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, + ) + + with pytest.raises(httpx.ConnectError): + http_client.request(path="/test", method="GET") + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_base_max_retries_zero_disables_retries(mock_sleep: AsyncMock) -> None: + """Async: base_max_retries=0 disables retries when no request_options override.""" + mock_client = MagicMock() + mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, + ) + + with pytest.raises(httpx.ConnectError): + await http_client.request(path="/test", method="GET") + + # No retries, just the initial attempt + assert mock_client.request.call_count == 1 + mock_sleep.assert_not_called() + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_request_options_override_base_max_retries(mock_sleep: MagicMock) -> None: + """Sync: request_options max_retries overrides base_max_retries.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("connection failed"), + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, # base says no retries + ) + + # But request_options overrides to allow 2 retries + response = http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + assert response.status_code == 200 + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_request_options_override_base_max_retries(mock_sleep: AsyncMock) -> None: + """Async: request_options max_retries overrides base_max_retries.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("connection failed"), + httpx.ConnectError("connection failed"), + _DummyResponse(), + ] + ) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=0, # base says no retries + ) + + # But request_options overrides to allow 2 retries + response = await http_client.request( + path="/test", + method="GET", + request_options={"max_retries": 2}, + ) + + assert response.status_code == 200 + # 1 initial + 2 retries = 3 total attempts + assert mock_client.request.call_count == 3 + + +@patch("seed.core.http_client.time.sleep", return_value=None) +def test_sync_base_max_retries_used_as_default(mock_sleep: MagicMock) -> None: + """Sync: base_max_retries is used when request_options has no max_retries.""" + mock_client = MagicMock() + mock_client.request.side_effect = [ + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + _DummyResponse(), + ] + http_client = HttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=3, + ) + + response = http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + # 1 initial + 3 retries = 4 total attempts + assert mock_client.request.call_count == 4 + + +@pytest.mark.asyncio +@patch("seed.core.http_client.asyncio.sleep", new_callable=AsyncMock) +async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) -> None: + """Async: base_max_retries is used when request_options has no max_retries.""" + mock_client = MagicMock() + mock_client.request = AsyncMock( + side_effect=[ + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + httpx.ConnectError("fail"), + _DummyResponse(), + ] + ) + http_client = AsyncHttpClient( + httpx_client=mock_client, # type: ignore[arg-type] + base_timeout=lambda: None, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + base_max_retries=3, + ) + + response = await http_client.request(path="/test", method="GET") + + assert response.status_code == 200 + # 1 initial + 3 retries = 4 total attempts + assert mock_client.request.call_count == 4 + + +# --------------------------------------------------------------------------- +# _should_retry unit tests +# --------------------------------------------------------------------------- + + +def _make_response(status_code: int) -> httpx.Response: + return httpx.Response(status_code=status_code, content=b"") + + +@pytest.mark.parametrize( + "status_code", + [408, 409, 429, 500, 501, 502, 503, 504, 599], +) +def test_should_retry_retryable_status_codes(status_code: int) -> None: + """Legacy mode: retries on 408, 409, 429, and all >= 500.""" + assert _should_retry(_make_response(status_code)) is True + + +@pytest.mark.parametrize( + "status_code", + [200, 201, 301, 400, 401, 403, 404], +) +def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None: + assert _should_retry(_make_response(status_code)) is False + + +def test_should_retry_599_upper_boundary() -> None: + """Legacy mode retries on >= 500, which includes 599.""" + assert _should_retry(_make_response(599)) is True + + +# --------------------------------------------------------------------------- +# RequestOptions timeout resolution tests (timeout / deprecated timeout_in_seconds) +# --------------------------------------------------------------------------- + + +def _sync_client_with_base_timeout(base_timeout: Any) -> Tuple[HttpClient, _DummySyncClient]: + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: base_timeout, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + return http_client, dummy_client + + +def test_sync_request_options_timeout_used() -> None: + """The new `timeout` request option is passed through to httpx (in seconds).""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options={"timeout": 30}) + assert dummy_client.last_request_kwargs["timeout"] == 30 + + +def test_sync_request_options_timeout_in_seconds_still_works() -> None: + """The deprecated `timeout_in_seconds` request option remains backwards compatible.""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options={"timeout_in_seconds": 45}) + assert dummy_client.last_request_kwargs["timeout"] == 45 + + +def test_sync_request_options_timeout_takes_precedence() -> None: + """When both are set, `timeout` wins over `timeout_in_seconds` (same seconds unit).""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45}) + assert dummy_client.last_request_kwargs["timeout"] == 30 + + +def test_sync_request_options_timeout_falls_back_to_base() -> None: + """When neither key is set, the client-level base timeout is used.""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options=None) + assert dummy_client.last_request_kwargs["timeout"] == 60 + + +def test_sync_request_options_timeout_none_falls_back_to_deprecated() -> None: + """An explicit `timeout=None` (dynamic caller) falls back to the deprecated `timeout_in_seconds`.""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + request_options = cast(RequestOptions, {"timeout": None, "timeout_in_seconds": 45}) + http_client.request(path="/test", method="GET", request_options=request_options) + assert dummy_client.last_request_kwargs["timeout"] == 45 + + +@pytest.mark.asyncio +async def test_async_request_options_timeout_takes_precedence() -> None: + """Async: `timeout` wins over the deprecated `timeout_in_seconds`.""" + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: 60, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + await http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45}) + assert dummy_client.last_request_kwargs["timeout"] == 30 diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_query_encoding.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_query_encoding.py new file mode 100644 index 000000000000..ef5fd7094f9b --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_query_encoding.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +from seed.core.query_encoder import encode_query + + +def test_query_encoding_deep_objects() -> None: + assert encode_query({"hello world": "hello world"}) == [("hello world", "hello world")] + assert encode_query({"hello_world": {"hello": "world"}}) == [("hello_world[hello]", "world")] + assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == [ + ("hello_world[hello][world]", "today"), + ("hello_world[test]", "this"), + ("hi", "there"), + ] + + +def test_query_encoding_deep_object_arrays() -> None: + assert encode_query({"objects": [{"key": "hello", "value": "world"}, {"key": "foo", "value": "bar"}]}) == [ + ("objects[key]", "hello"), + ("objects[value]", "world"), + ("objects[key]", "foo"), + ("objects[value]", "bar"), + ] + assert encode_query( + {"users": [{"name": "string", "tags": ["string"]}, {"name": "string2", "tags": ["string2", "string3"]}]} + ) == [ + ("users[name]", "string"), + ("users[tags]", "string"), + ("users[name]", "string2"), + ("users[tags]", "string2"), + ("users[tags]", "string3"), + ] + + +def test_encode_query_with_none() -> None: + encoded = encode_query(None) + assert encoded is None diff --git a/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_serialization.py b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_serialization.py new file mode 100644 index 000000000000..b298db89c4bd --- /dev/null +++ b/seed/python-sdk/path-parameters/no-custom-config/tests/utils/test_serialization.py @@ -0,0 +1,72 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, List + +from .assets.models import ObjectWithOptionalFieldParams, ShapeParams + +from seed.core.serialization import convert_and_respect_annotation_metadata + +UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} +UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} + + +def test_convert_and_respect_annotation_metadata() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "bool_": True, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} + + +def test_convert_and_respect_annotation_metadata_in_list() -> None: + data: List[ObjectWithOptionalFieldParams] = [ + {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, + ] + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=List[ObjectWithOptionalFieldParams], direction="write" + ) + + assert converted == [ + {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, + {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, + ] + + +def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: + data: ObjectWithOptionalFieldParams = { + "string": "string", + "long_": 12345, + "union": UNION_TEST, + "literal": "lit_one", + "any": "any", + } + converted = convert_and_respect_annotation_metadata( + object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" + ) + + assert converted == { + "string": "string", + "long": 12345, + "union": UNION_TEST_CONVERTED, + "literal": "lit_one", + "any": "any", + } + + +def test_convert_and_respect_annotation_metadata_in_union() -> None: + converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams, direction="write") + + assert converted == UNION_TEST_CONVERTED + + +def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: + data: Any = {} + converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams, direction="write") + assert converted == data diff --git a/seed/python-sdk/plain-text/src/seed/core/jsonable_encoder.py b/seed/python-sdk/plain-text/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/plain-text/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/plain-text/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/property-access/src/seed/core/jsonable_encoder.py b/seed/python-sdk/property-access/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/property-access/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/property-access/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/public-object/src/seed/core/jsonable_encoder.py b/seed/python-sdk/public-object/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/public-object/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/public-object/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-backslash-escape/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-backslash-escape/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-backslash-escape/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-backslash-escape/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-multi-env-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-multi-env-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-multi-env-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-multi-env-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-mypy-exclude/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-mypy-exclude/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-mypy-exclude/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-mypy-exclude/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-mypy-exclude/with-mypy-exclude/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-mypy-exclude/with-mypy-exclude/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-mypy-exclude/with-mypy-exclude/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-mypy-exclude/with-mypy-exclude/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-oauth-token-optional/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-oauth-token-optional/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-oauth-token-optional/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-oauth-token-optional/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-positional-single-property/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-positional-single-property/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-positional-single-property/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-positional-single-property/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-positional-single-property/with-positional-constructors/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-positional-single-property/with-positional-constructors/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-positional-single-property/with-positional-constructors/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-positional-single-property/with-positional-constructors/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-reserved-keyword-subpackages/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-reserved-keyword-subpackages/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-reserved-keyword-subpackages/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-reserved-keyword-subpackages/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-streaming-parameter-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/python-underscore-subpackages/src/seed/core/jsonable_encoder.py b/seed/python-sdk/python-underscore-subpackages/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/python-underscore-subpackages/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/python-underscore-subpackages/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/query-param-name-conflict/src/seed/core/jsonable_encoder.py b/seed/python-sdk/query-param-name-conflict/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/query-param-name-conflict/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/query-param-name-conflict/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/query-parameters-openapi-as-objects/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/query-parameters-openapi/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/query-parameters/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/request-parameters/src/seed/core/jsonable_encoder.py b/seed/python-sdk/request-parameters/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/request-parameters/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/request-parameters/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/required-nullable/src/seed/core/jsonable_encoder.py b/seed/python-sdk/required-nullable/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/required-nullable/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/required-nullable/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/reserved-keywords/src/seed/core/jsonable_encoder.py b/seed/python-sdk/reserved-keywords/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/reserved-keywords/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/reserved-keywords/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py b/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/response-property/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/schemaless-request-body-examples/src/seed/core/jsonable_encoder.py b/seed/python-sdk/schemaless-request-body-examples/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/schemaless-request-body-examples/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/schemaless-request-body-examples/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/seed.yml b/seed/python-sdk/seed.yml index ec9151894967..89255f7edec4 100644 --- a/seed/python-sdk/seed.yml +++ b/seed/python-sdk/seed.yml @@ -399,6 +399,12 @@ fixtures: - customConfig: offsetSemantics: "page-index" outputFolder: page-index-semantics + path-parameters: + - customConfig: null + outputFolder: no-custom-config + - customConfig: + encode_path_params: true + outputFolder: encode-path-params query-parameters: - customConfig: null outputFolder: no-custom-config diff --git a/seed/python-sdk/server-sent-event-examples/src/seed/core/jsonable_encoder.py b/seed/python-sdk/server-sent-event-examples/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/server-sent-event-examples/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/server-sent-event-examples/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/server-sent-events-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/server-sent-events-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/server-sent-events-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/server-sent-events-openapi/with-wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/server-sent-events-resumable/src/seed/core/jsonable_encoder.py b/seed/python-sdk/server-sent-events-resumable/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/server-sent-events-resumable/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/server-sent-events-resumable/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/server-sent-events/with-wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/server-sent-events/with-wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/server-sent-events/with-wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/server-sent-events/with-wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/server-url-templating-single-url/src/seed/core/jsonable_encoder.py b/seed/python-sdk/server-url-templating-single-url/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/server-url-templating-single-url/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/server-url-templating-single-url/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/server-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/server-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/server-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/server-url-templating/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/simple-api/optional-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/simple-api/optional-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/simple-api/optional-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/simple-api/optional-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/simple-api/src/seed/core/jsonable_encoder.py b/seed/python-sdk/simple-api/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/simple-api/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/simple-api/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/simple-fhir/no-inheritance-for-extended-models/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/single-url-environment-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/single-url-environment-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/single-url-environment-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/single-url-environment-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/single-url-environment-no-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/single-url-environment-no-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/single-url-environment-no-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/single-url-environment-no-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/streaming-parameter/src/seed/core/jsonable_encoder.py b/seed/python-sdk/streaming-parameter/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/streaming-parameter/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/streaming-parameter/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/streaming/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/streaming/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/streaming/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/streaming/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/jsonable_encoder.py b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/streaming/skip-pydantic-validation/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/trace/src/seed/core/jsonable_encoder.py b/seed/python-sdk/trace/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/trace/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/trace/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/undiscriminated-union-with-response-property/src/seed/core/jsonable_encoder.py b/seed/python-sdk/undiscriminated-union-with-response-property/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/undiscriminated-union-with-response-property/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/undiscriminated-union-with-response-property/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/undiscriminated-unions/src/seed/core/jsonable_encoder.py b/seed/python-sdk/undiscriminated-unions/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/undiscriminated-unions/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/undiscriminated-unions/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/union-query-parameters/src/seed/core/jsonable_encoder.py b/seed/python-sdk/union-query-parameters/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/union-query-parameters/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/union-query-parameters/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unions-with-local-date/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unions-with-local-date/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unions-with-local-date/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unions-with-local-date/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unions/flatten-union-request-bodies/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unions/flatten-union-request-bodies/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unions/flatten-union-request-bodies/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unions/flatten-union-request-bodies/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unions/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unions/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unions/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unions/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unions/union-naming-v1-wire-tests/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unions/union-naming-v1-wire-tests/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unions/union-naming-v1-wire-tests/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unions/union-naming-v1-wire-tests/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unions/union-naming-v1/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unions/union-naming-v1/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unions/union-naming-v1/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unions/union-naming-v1/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unions/union-utils/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unions/union-utils/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unions/union-utils/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unions/union-utils/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/unknown/src/seed/core/jsonable_encoder.py b/seed/python-sdk/unknown/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/unknown/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/unknown/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/url-form-encoded/src/seed/core/jsonable_encoder.py b/seed/python-sdk/url-form-encoded/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/url-form-encoded/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/url-form-encoded/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/validation/no-custom-config/src/seed/core/jsonable_encoder.py b/seed/python-sdk/validation/no-custom-config/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/validation/no-custom-config/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/validation/no-custom-config/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/validation/with-defaults-parameters/src/seed/core/jsonable_encoder.py b/seed/python-sdk/validation/with-defaults-parameters/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/validation/with-defaults-parameters/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/validation/with-defaults-parameters/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/validation/with-defaults/src/seed/core/jsonable_encoder.py b/seed/python-sdk/validation/with-defaults/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/validation/with-defaults/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/validation/with-defaults/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/variables/src/seed/core/jsonable_encoder.py b/seed/python-sdk/variables/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/variables/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/variables/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/version-no-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/version-no-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/version-no-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/version-no-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/version/src/seed/core/jsonable_encoder.py b/seed/python-sdk/version/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/version/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/version/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/webhook-audience/src/seed/core/jsonable_encoder.py b/seed/python-sdk/webhook-audience/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/webhook-audience/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/webhook-audience/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/webhooks/src/seed/core/jsonable_encoder.py b/seed/python-sdk/webhooks/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/webhooks/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/webhooks/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/websocket-bearer-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/websocket-bearer-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/websocket-bearer-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/websocket-bearer-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/websocket-inferred-auth/src/seed/core/jsonable_encoder.py b/seed/python-sdk/websocket-inferred-auth/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/websocket-inferred-auth/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/websocket-inferred-auth/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/websocket-multi-url/src/seed/core/jsonable_encoder.py b/seed/python-sdk/websocket-multi-url/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/websocket-multi-url/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/websocket-multi-url/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/websocket/websocket-base/src/seed/core/jsonable_encoder.py b/seed/python-sdk/websocket/websocket-base/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/websocket/websocket-base/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/websocket/websocket-base/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients-skip_validation/src/seed/core/jsonable_encoder.py b/seed/python-sdk/websocket/websocket-with_generated_clients-skip_validation/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/websocket/websocket-with_generated_clients-skip_validation/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/websocket/websocket-with_generated_clients-skip_validation/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/jsonable_encoder.py b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/websocket/websocket-with_generated_clients/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/x-fern-default/src/seed/core/jsonable_encoder.py b/seed/python-sdk/x-fern-default/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/x-fern-default/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/x-fern-default/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="") diff --git a/seed/python-sdk/x-fern-global-parameters/src/seed/core/jsonable_encoder.py b/seed/python-sdk/x-fern-global-parameters/src/seed/core/jsonable_encoder.py index 5b0902ebcde3..f638cc9a4252 100644 --- a/seed/python-sdk/x-fern-global-parameters/src/seed/core/jsonable_encoder.py +++ b/seed/python-sdk/x-fern-global-parameters/src/seed/core/jsonable_encoder.py @@ -15,6 +15,7 @@ from pathlib import PurePath from types import GeneratorType from typing import Any, Callable, Dict, List, Optional, Set, Union +from urllib.parse import quote import pydantic from .datetime_utils import serialize_datetime @@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str: if isinstance(obj, bool): return "true" if obj else "false" return str(jsonable_encoder(obj)) + + +def quote_path_param(obj: Any) -> str: + """Encode a value for use in a URL path segment, percent-encoding it. + + Same as encode_path_param, except the result is percent-encoded so + that a value containing "/" or ".." cannot change which endpoint + the request resolves to. + """ + if isinstance(obj, bool): + return "true" if obj else "false" + return quote(str(jsonable_encoder(obj)), safe="")