openapi-python-client 0.29.0__py3-none-any.whl → 0.30.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openapi_python_client/__init__.py +30 -21
- openapi_python_client/cli.py +10 -3
- openapi_python_client/config.py +4 -1
- openapi_python_client/parser/bodies.py +8 -7
- openapi_python_client/parser/openapi.py +58 -48
- openapi_python_client/parser/properties/__init__.py +36 -33
- openapi_python_client/parser/properties/any.py +12 -10
- openapi_python_client/parser/properties/boolean.py +14 -13
- openapi_python_client/parser/properties/const.py +11 -10
- openapi_python_client/parser/properties/date.py +13 -11
- openapi_python_client/parser/properties/datetime.py +13 -11
- openapi_python_client/parser/properties/enum_property.py +23 -20
- openapi_python_client/parser/properties/file.py +12 -10
- openapi_python_client/parser/properties/float.py +14 -13
- openapi_python_client/parser/properties/int.py +12 -11
- openapi_python_client/parser/properties/list_property.py +22 -22
- openapi_python_client/parser/properties/literal_enum_property.py +20 -20
- openapi_python_client/parser/properties/merge_properties.py +9 -6
- openapi_python_client/parser/properties/model_property.py +35 -34
- openapi_python_client/parser/properties/none.py +12 -11
- openapi_python_client/parser/properties/protocol.py +42 -32
- openapi_python_client/parser/properties/schemas.py +30 -23
- openapi_python_client/parser/properties/string.py +11 -11
- openapi_python_client/parser/properties/union.py +24 -22
- openapi_python_client/parser/properties/uuid.py +12 -11
- openapi_python_client/parser/responses.py +31 -24
- openapi_python_client/schema/__init__.py +2 -1
- openapi_python_client/schema/openapi_schema_pydantic/callback.py +3 -1
- openapi_python_client/schema/openapi_schema_pydantic/components.py +10 -9
- openapi_python_client/schema/openapi_schema_pydantic/contact.py +5 -3
- openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +4 -2
- openapi_python_client/schema/openapi_schema_pydantic/encoding.py +4 -3
- openapi_python_client/schema/openapi_schema_pydantic/example.py +5 -3
- openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +4 -2
- openapi_python_client/schema/openapi_schema_pydantic/header.py +20 -6
- openapi_python_client/schema/openapi_schema_pydantic/info.py +5 -4
- openapi_python_client/schema/openapi_schema_pydantic/license.py +4 -2
- openapi_python_client/schema/openapi_schema_pydantic/link.py +5 -4
- openapi_python_client/schema/openapi_schema_pydantic/media_type.py +3 -2
- openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +6 -4
- openapi_python_client/schema/openapi_schema_pydantic/open_api.py +5 -3
- openapi_python_client/schema/openapi_schema_pydantic/operation.py +6 -5
- openapi_python_client/schema/openapi_schema_pydantic/parameter.py +6 -5
- openapi_python_client/schema/openapi_schema_pydantic/path_item.py +5 -3
- openapi_python_client/schema/openapi_schema_pydantic/paths.py +2 -1
- openapi_python_client/schema/openapi_schema_pydantic/reference.py +3 -1
- openapi_python_client/schema/openapi_schema_pydantic/request_body.py +3 -2
- openapi_python_client/schema/openapi_schema_pydantic/response.py +5 -4
- openapi_python_client/schema/openapi_schema_pydantic/responses.py +2 -1
- openapi_python_client/schema/openapi_schema_pydantic/schema.py +9 -8
- openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +3 -1
- openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +8 -7
- openapi_python_client/schema/openapi_schema_pydantic/server.py +4 -3
- openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +5 -3
- openapi_python_client/schema/openapi_schema_pydantic/tag.py +3 -2
- openapi_python_client/schema/openapi_schema_pydantic/xml.py +5 -3
- openapi_python_client/schema/ref.py +52 -0
- openapi_python_client/schema/untrusted_string.py +56 -0
- openapi_python_client/strings.py +245 -0
- openapi_python_client/templates/README.md.jinja +9 -9
- openapi_python_client/templates/client.py.jinja +31 -31
- openapi_python_client/templates/endpoint_macros.py.jinja +19 -19
- openapi_python_client/templates/endpoint_module.py.jinja +18 -21
- openapi_python_client/templates/helpers.jinja +3 -7
- openapi_python_client/templates/literal_enum.py.jinja +1 -1
- openapi_python_client/templates/model.py.jinja +18 -18
- openapi_python_client/templates/package_init.py.jinja +2 -2
- openapi_python_client/templates/property_templates/const_property.py.jinja +3 -3
- openapi_python_client/templates/property_templates/date_property.py.jinja +1 -1
- openapi_python_client/templates/property_templates/datetime_property.py.jinja +1 -1
- openapi_python_client/templates/property_templates/enum_property.py.jinja +1 -1
- openapi_python_client/templates/property_templates/file_property.py.jinja +1 -1
- openapi_python_client/templates/property_templates/list_property.py.jinja +3 -3
- openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +2 -2
- openapi_python_client/templates/property_templates/model_property.py.jinja +1 -1
- openapi_python_client/templates/property_templates/property_macros.py.jinja +1 -1
- openapi_python_client/templates/property_templates/union_property.py.jinja +9 -9
- openapi_python_client/templates/property_templates/uuid_property.py.jinja +1 -1
- openapi_python_client/templates/pyproject_pdm.toml.jinja +3 -3
- openapi_python_client/templates/pyproject_poetry.toml.jinja +3 -3
- openapi_python_client/templates/pyproject_uv.toml.jinja +4 -4
- openapi_python_client/templates/setup.py.jinja +3 -3
- openapi_python_client/templates/str_enum.py.jinja +2 -2
- openapi_python_client/templates/types.py.jinja +2 -2
- {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/METADATA +10 -9
- openapi_python_client-0.30.0.dist-info/RECORD +115 -0
- {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/WHEEL +1 -1
- openapi_python_client/utils.py +0 -122
- openapi_python_client-0.29.0.dist-info/RECORD +0 -113
- {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/entry_points.txt +0 -0
- {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,7 @@ from typing import Any
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, model_validator
|
|
4
4
|
|
|
5
5
|
from ..data_type import DataType
|
|
6
|
+
from ..untrusted_string import UntrustedString
|
|
6
7
|
from .discriminator import Discriminator
|
|
7
8
|
from .external_documentation import ExternalDocumentation
|
|
8
9
|
from .reference import ReferenceOr
|
|
@@ -20,7 +21,7 @@ class Schema(BaseModel):
|
|
|
20
21
|
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
|
-
title:
|
|
24
|
+
title: UntrustedString | None = None
|
|
24
25
|
multipleOf: float | None = Field(default=None, gt=0.0)
|
|
25
26
|
maximum: float | None = None
|
|
26
27
|
exclusiveMaximum: bool | float | None = None
|
|
@@ -28,15 +29,15 @@ class Schema(BaseModel):
|
|
|
28
29
|
exclusiveMinimum: bool | float | None = None
|
|
29
30
|
maxLength: int | None = Field(default=None, ge=0)
|
|
30
31
|
minLength: int | None = Field(default=None, ge=0)
|
|
31
|
-
pattern:
|
|
32
|
+
pattern: UntrustedString | None = None
|
|
32
33
|
maxItems: int | None = Field(default=None, ge=0)
|
|
33
34
|
minItems: int | None = Field(default=None, ge=0)
|
|
34
35
|
uniqueItems: bool | None = None
|
|
35
36
|
maxProperties: int | None = Field(default=None, ge=0)
|
|
36
37
|
minProperties: int | None = Field(default=None, ge=0)
|
|
37
|
-
required: list[
|
|
38
|
-
enum:
|
|
39
|
-
const:
|
|
38
|
+
required: list[UntrustedString] | None = Field(default=None)
|
|
39
|
+
enum: list[Any] | None = Field(default=None, min_length=1)
|
|
40
|
+
const: StrictStr | StrictInt | StrictFloat | StrictBool | None = None
|
|
40
41
|
type: DataType | list[DataType] | None = Field(default=None)
|
|
41
42
|
allOf: list[ReferenceOr["Schema"]] = Field(default_factory=list)
|
|
42
43
|
oneOf: list[ReferenceOr["Schema"]] = Field(default_factory=list)
|
|
@@ -44,10 +45,10 @@ class Schema(BaseModel):
|
|
|
44
45
|
schema_not: ReferenceOr["Schema"] | None = Field(default=None, alias="not")
|
|
45
46
|
items: ReferenceOr["Schema"] | None = None
|
|
46
47
|
prefixItems: list[ReferenceOr["Schema"]] = Field(default_factory=list)
|
|
47
|
-
properties: dict[
|
|
48
|
+
properties: dict[UntrustedString, ReferenceOr["Schema"]] | None = None
|
|
48
49
|
additionalProperties: bool | ReferenceOr["Schema"] | None = None
|
|
49
|
-
description:
|
|
50
|
-
schema_format:
|
|
50
|
+
description: UntrustedString | None = None
|
|
51
|
+
schema_format: UntrustedString | None = Field(default=None, alias="format")
|
|
51
52
|
default: Any | None = None
|
|
52
53
|
nullable: bool = Field(default=False)
|
|
53
54
|
discriminator: Discriminator | None = None
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
from ..untrusted_string import UntrustedString
|
|
2
|
+
|
|
3
|
+
SecurityRequirement = dict[UntrustedString, list[UntrustedString]]
|
|
2
4
|
"""
|
|
3
5
|
Lists the required security schemes to execute this operation.
|
|
4
6
|
The name used for each property MUST correspond to a security scheme declared in the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, ConfigDict, Field
|
|
2
2
|
|
|
3
|
+
from ..untrusted_string import UntrustedString
|
|
3
4
|
from .oauth_flows import OAuthFlows
|
|
4
5
|
|
|
5
6
|
|
|
@@ -17,14 +18,14 @@ class SecurityScheme(BaseModel):
|
|
|
17
18
|
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject
|
|
18
19
|
"""
|
|
19
20
|
|
|
20
|
-
type:
|
|
21
|
-
description:
|
|
22
|
-
name:
|
|
23
|
-
security_scheme_in:
|
|
24
|
-
scheme:
|
|
25
|
-
bearerFormat:
|
|
21
|
+
type: UntrustedString
|
|
22
|
+
description: UntrustedString | None = None
|
|
23
|
+
name: UntrustedString | None = None
|
|
24
|
+
security_scheme_in: UntrustedString | None = Field(default=None, alias="in")
|
|
25
|
+
scheme: UntrustedString | None = None
|
|
26
|
+
bearerFormat: UntrustedString | None = None
|
|
26
27
|
flows: OAuthFlows | None = None
|
|
27
|
-
openIdConnectUrl:
|
|
28
|
+
openIdConnectUrl: UntrustedString | None = None
|
|
28
29
|
model_config = ConfigDict(
|
|
29
30
|
extra="allow",
|
|
30
31
|
populate_by_name=True,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, ConfigDict
|
|
2
2
|
|
|
3
|
+
from ..untrusted_string import UntrustedString
|
|
3
4
|
from .server_variable import ServerVariable
|
|
4
5
|
|
|
5
6
|
|
|
@@ -11,9 +12,9 @@ class Server(BaseModel):
|
|
|
11
12
|
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
|
-
url:
|
|
15
|
-
description:
|
|
16
|
-
variables: dict[
|
|
15
|
+
url: UntrustedString
|
|
16
|
+
description: UntrustedString | None = None
|
|
17
|
+
variables: dict[UntrustedString, ServerVariable] | None = None
|
|
17
18
|
model_config = ConfigDict(
|
|
18
19
|
extra="allow",
|
|
19
20
|
json_schema_extra={
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from pydantic import BaseModel, ConfigDict
|
|
2
2
|
|
|
3
|
+
from ..untrusted_string import UntrustedString
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class ServerVariable(BaseModel):
|
|
5
7
|
"""An object representing a Server Variable for server URL template substitution.
|
|
@@ -9,7 +11,7 @@ class ServerVariable(BaseModel):
|
|
|
9
11
|
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverVariableObject
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
|
-
enum: list[
|
|
13
|
-
default:
|
|
14
|
-
description:
|
|
14
|
+
enum: list[UntrustedString] | None = None
|
|
15
|
+
default: UntrustedString
|
|
16
|
+
description: UntrustedString | None = None
|
|
15
17
|
model_config = ConfigDict(extra="allow")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from pydantic import BaseModel, ConfigDict
|
|
2
2
|
|
|
3
|
+
from ..untrusted_string import UntrustedString
|
|
3
4
|
from .external_documentation import ExternalDocumentation
|
|
4
5
|
|
|
5
6
|
|
|
@@ -13,8 +14,8 @@ class Tag(BaseModel):
|
|
|
13
14
|
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
|
-
name:
|
|
17
|
-
description:
|
|
17
|
+
name: UntrustedString
|
|
18
|
+
description: UntrustedString | None = None
|
|
18
19
|
externalDocs: ExternalDocumentation | None = None
|
|
19
20
|
model_config = ConfigDict(
|
|
20
21
|
extra="allow", json_schema_extra={"examples": [{"name": "pet", "description": "Pets operations"}]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from pydantic import BaseModel, ConfigDict
|
|
2
2
|
|
|
3
|
+
from ..untrusted_string import UntrustedString
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class XML(BaseModel):
|
|
5
7
|
"""
|
|
@@ -14,9 +16,9 @@ class XML(BaseModel):
|
|
|
14
16
|
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
|
-
name:
|
|
18
|
-
namespace:
|
|
19
|
-
prefix:
|
|
19
|
+
name: UntrustedString | None = None
|
|
20
|
+
namespace: UntrustedString | None = None
|
|
21
|
+
prefix: UntrustedString | None = None
|
|
20
22
|
attribute: bool = False
|
|
21
23
|
wrapped: bool = False
|
|
22
24
|
model_config = ConfigDict(
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
|
4
|
+
from pydantic_core import core_schema
|
|
5
|
+
|
|
6
|
+
from .untrusted_string import UntrustedString
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Ref:
|
|
10
|
+
"""A $ref in the schema, the raw value is protected to make sure we don't accidentally place it in the generated code."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, value: str | UntrustedString) -> None:
|
|
13
|
+
if isinstance(value, UntrustedString):
|
|
14
|
+
value = value.get_untrusted_value()
|
|
15
|
+
self._value = value
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
|
|
19
|
+
return core_schema.no_info_after_validator_function(
|
|
20
|
+
cls,
|
|
21
|
+
core_schema.str_schema(),
|
|
22
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
23
|
+
cls._serialize, info_arg=True, return_schema=core_schema.any_schema()
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _serialize(value: "Ref", info: core_schema.SerializationInfo) -> Any:
|
|
29
|
+
# Only unwrap in JSON mode (used for error messages); Python mode keeps the wrapper so it
|
|
30
|
+
# can't leak into generated code. Returning the wrapper also avoids serializer warnings.
|
|
31
|
+
if info.mode == "json":
|
|
32
|
+
return value.get_untrusted_value()
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def get_untrusted_value(self) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Get the raw $ref value.
|
|
38
|
+
|
|
39
|
+
DO NOT place this in generated code.
|
|
40
|
+
"""
|
|
41
|
+
return self._value
|
|
42
|
+
|
|
43
|
+
def endswith(self, suffix: str) -> bool:
|
|
44
|
+
return self._value.endswith(suffix)
|
|
45
|
+
|
|
46
|
+
def __hash__(self) -> int:
|
|
47
|
+
return hash(self._value)
|
|
48
|
+
|
|
49
|
+
def __eq__(self, other: object) -> bool:
|
|
50
|
+
if isinstance(other, Ref):
|
|
51
|
+
return self._value == other._value
|
|
52
|
+
return False # pragma: no cover
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import Any, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import GetCoreSchemaHandler
|
|
4
|
+
from pydantic_core import CoreSchema, core_schema
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UntrustedString:
|
|
10
|
+
"""A raw string in the OpenAPI document which should not be directly placed in the generated code."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, value: str) -> None:
|
|
13
|
+
self._value = value
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
17
|
+
return core_schema.no_info_after_validator_function(
|
|
18
|
+
cls,
|
|
19
|
+
core_schema.str_schema(),
|
|
20
|
+
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
21
|
+
cls._serialize, info_arg=True, return_schema=core_schema.any_schema()
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _serialize(value: "UntrustedString", info: core_schema.SerializationInfo) -> Any:
|
|
27
|
+
# Only unwrap in JSON mode (used for error messages); Python mode keeps the wrapper so it
|
|
28
|
+
# can't leak into generated code. Returning the wrapper also avoids serializer warnings.
|
|
29
|
+
if info.mode == "json":
|
|
30
|
+
return value.get_untrusted_value()
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
def get_untrusted_value(self) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Return the raw string with no transformations.
|
|
36
|
+
|
|
37
|
+
DO NOT use this for generating code directly. It can result in arbitrary code generation.
|
|
38
|
+
"""
|
|
39
|
+
return self._value
|
|
40
|
+
|
|
41
|
+
def startswith(self, prefix: str) -> bool:
|
|
42
|
+
return self._value.startswith(prefix)
|
|
43
|
+
|
|
44
|
+
def __bool__(self) -> bool:
|
|
45
|
+
"""Whether the raw value is non-empty. Emptiness is not sensitive, so this is safe to expose."""
|
|
46
|
+
return bool(self._value)
|
|
47
|
+
|
|
48
|
+
def __hash__(self) -> int:
|
|
49
|
+
return hash(self._value)
|
|
50
|
+
|
|
51
|
+
def __eq__(self, other: object) -> bool:
|
|
52
|
+
if isinstance(other, UntrustedString):
|
|
53
|
+
return self._value == other._value
|
|
54
|
+
if isinstance(other, str):
|
|
55
|
+
return self._value == other
|
|
56
|
+
return False
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from email.message import Message
|
|
9
|
+
from keyword import iskeyword
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .schema.untrusted_string import UntrustedString
|
|
14
|
+
|
|
15
|
+
DELIMITERS = r"\. _-"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, repr=False, slots=True)
|
|
19
|
+
class PythonCode:
|
|
20
|
+
"""
|
|
21
|
+
A snippet of Python source code generated from `UnstrustedString`s.
|
|
22
|
+
|
|
23
|
+
It's safe to treat as code, but _may not_ be safe to place in docstrings, f-strings, etc.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_code: str
|
|
27
|
+
|
|
28
|
+
def as_unembedded_code(self) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Return the raw source code with no transformations.
|
|
31
|
+
|
|
32
|
+
DO NOT use this inside a string context (docstrings, string literals, f-string literal text).
|
|
33
|
+
Pass this through the appropriate escaping function first (e.g. `safe_for_docstring`).
|
|
34
|
+
"""
|
|
35
|
+
return self._code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PythonIdentifier(str):
|
|
39
|
+
"""A snake_case string which has been validated / transformed into a valid identifier for Python"""
|
|
40
|
+
|
|
41
|
+
def __new__(cls, value: UntrustedString | str, prefix: str, skip_snake_case: bool = False) -> PythonIdentifier:
|
|
42
|
+
if isinstance(value, UntrustedString):
|
|
43
|
+
value = value.get_untrusted_value()
|
|
44
|
+
safe_value = sanitize(value)
|
|
45
|
+
leading_underscore = safe_value.startswith("_")
|
|
46
|
+
if skip_snake_case:
|
|
47
|
+
# Keep the name as close to the original as possible (to disambiguate conflicts);
|
|
48
|
+
# only strip the delimiters sanitize() preserved, as they aren't valid in identifiers
|
|
49
|
+
safe_value = re.sub(r"[^\w]+", "", safe_value)
|
|
50
|
+
else:
|
|
51
|
+
safe_value = snake_case(safe_value)
|
|
52
|
+
safe_value = fix_reserved_words(safe_value)
|
|
53
|
+
|
|
54
|
+
if not safe_value.isidentifier() or leading_underscore:
|
|
55
|
+
safe_value = f"{prefix}{safe_value}"
|
|
56
|
+
return str.__new__(cls, safe_value)
|
|
57
|
+
|
|
58
|
+
def __deepcopy__(self, _: Any) -> PythonIdentifier:
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ClassName(str):
|
|
63
|
+
"""A PascalCase string which has been validated / transformed into a valid class name for Python"""
|
|
64
|
+
|
|
65
|
+
def __new__(cls, value: UntrustedString | str, prefix: str) -> ClassName:
|
|
66
|
+
safe_value = fix_reserved_words(pascal_case(value))
|
|
67
|
+
|
|
68
|
+
if not safe_value.isidentifier():
|
|
69
|
+
safe_value = f"{prefix}{safe_value}"
|
|
70
|
+
safe_value = fix_reserved_words(pascal_case(safe_value))
|
|
71
|
+
return str.__new__(cls, safe_value)
|
|
72
|
+
|
|
73
|
+
def __deepcopy__(self, _: Any) -> ClassName:
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sanitize(value: str) -> str:
|
|
78
|
+
"""Removes every character that isn't 0-9, A-Z, a-z, or a known delimiter"""
|
|
79
|
+
return "".join(c for c in value if c.isidentifier() or c.isdecimal() or c in DELIMITERS)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def split_words(value: str) -> list[str]:
|
|
83
|
+
"""Split a string on words and known delimiters"""
|
|
84
|
+
if any(c.isupper() for c in value):
|
|
85
|
+
value = " ".join(re.split("([A-Z]?[a-z]+)", value))
|
|
86
|
+
return re.findall(rf"[^{re.escape(DELIMITERS)}]+", value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
RESERVED_WORDS = (set(dir(builtins)) | {"self", "true", "false", "datetime"}) - {
|
|
90
|
+
"id",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fix_reserved_words(value: str) -> str:
|
|
95
|
+
"""
|
|
96
|
+
Using reserved Python words as identifiers in generated code causes problems, so this function renames them.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
value: The identifier to-be that should be renamed if it's a reserved word.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
`value` suffixed with `_` if it was a reserved word.
|
|
103
|
+
"""
|
|
104
|
+
if value in RESERVED_WORDS or iskeyword(value):
|
|
105
|
+
return f"{value}_"
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def snake_case(value: str | UntrustedString) -> str:
|
|
110
|
+
"""Converts to snake_case identifier, stripping all non-alphanumeric characters"""
|
|
111
|
+
if isinstance(value, UntrustedString):
|
|
112
|
+
value = value.get_untrusted_value()
|
|
113
|
+
words = split_words(sanitize(value))
|
|
114
|
+
return "_".join(words).lower()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def pascal_case(value: str | UntrustedString) -> str:
|
|
118
|
+
"""Converts to PascalCase identifier, stripping all non-alphanumeric characters"""
|
|
119
|
+
if isinstance(value, UntrustedString):
|
|
120
|
+
value = value.get_untrusted_value()
|
|
121
|
+
words = split_words(sanitize(value))
|
|
122
|
+
capitalized_words = (word.capitalize() if not word.isupper() else word for word in words)
|
|
123
|
+
return "".join(capitalized_words)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def kebab_case(value: str | UntrustedString) -> str:
|
|
127
|
+
"""Converts to kebab-case identifier, stripping all non-alphanumeric characters"""
|
|
128
|
+
if isinstance(value, UntrustedString):
|
|
129
|
+
value = value.get_untrusted_value()
|
|
130
|
+
words = split_words(sanitize(value))
|
|
131
|
+
return "-".join(words).lower()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
#: Escaped words in docstring content are kept shorter than this so the ``wordwrap`` filter
|
|
135
|
+
#: never hard-splits inside an escape sequence (the narrowest wordwrap width in templates is 90).
|
|
136
|
+
_MAX_ESCAPED_WORD_LENGTH = 76
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _split_long_word_safely(word: str) -> str:
|
|
140
|
+
"""Insert newlines into an over-long escaped word where they can't interrupt an escape sequence.
|
|
141
|
+
|
|
142
|
+
wordwrap hard-splits words longer than its width at arbitrary positions. A split between the
|
|
143
|
+
backslashes of ``\\\\`` leaves a lone backslash at the start of the next line, where it forms an
|
|
144
|
+
invalid escape (``\\ `` warns today and will error in a future Python) or even a ``SyntaxError``
|
|
145
|
+
(``\\U`` + non-hex is a truncated escape). Splits right after an even number of backslashes are
|
|
146
|
+
always safe: the prefix ends with complete escapes and the suffix can't start a dangling one.
|
|
147
|
+
"""
|
|
148
|
+
chunks: list[str] = []
|
|
149
|
+
start = 0
|
|
150
|
+
while len(word) - start > _MAX_ESCAPED_WORD_LENGTH:
|
|
151
|
+
end = start + _MAX_ESCAPED_WORD_LENGTH
|
|
152
|
+
run = 0
|
|
153
|
+
while end > start and word[end - 1] == "\\":
|
|
154
|
+
end -= 1
|
|
155
|
+
run += 1
|
|
156
|
+
# Keep an even number of trailing backslashes in this chunk. Odd runs only occur right
|
|
157
|
+
# before an escaped quote, so the one left behind always forms a valid `\"`.
|
|
158
|
+
end += run - (run % 2)
|
|
159
|
+
chunks.append(word[start:end])
|
|
160
|
+
start = end
|
|
161
|
+
chunks.append(word[start:])
|
|
162
|
+
return "\n".join(chunks)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def safe_for_docstring(value: Any) -> str:
|
|
166
|
+
"""
|
|
167
|
+
Remove any risky escapes from docstring content.
|
|
168
|
+
|
|
169
|
+
The output is safe to run through the ``wordwrap`` filter at any width greater than
|
|
170
|
+
``_MAX_ESCAPED_WORD_LENGTH``: over-long words containing escape sequences are pre-split where
|
|
171
|
+
no escape sequence is interrupted, so wordwrap's hard-splitting can't create invalid escapes.
|
|
172
|
+
"""
|
|
173
|
+
if isinstance(value, UntrustedString):
|
|
174
|
+
value = value.get_untrusted_value()
|
|
175
|
+
if isinstance(value, PythonCode):
|
|
176
|
+
value = value.as_unembedded_code()
|
|
177
|
+
if not isinstance(value, str):
|
|
178
|
+
value = str(value)
|
|
179
|
+
# Strip control characters which are illegal in Python source (e.g. null bytes)
|
|
180
|
+
value = "".join(c for c in value if unicodedata.category(c) != "Cc" or c in "\n\r\t")
|
|
181
|
+
|
|
182
|
+
value = value.replace("\\", "\\\\").replace('"""', '\\"\\"\\"')
|
|
183
|
+
# Words without backslashes can be hard-split anywhere safely, so leave them alone
|
|
184
|
+
return re.sub(
|
|
185
|
+
r"\S+",
|
|
186
|
+
lambda match: _split_long_word_safely(match.group()) if "\\" in match.group() else match.group(),
|
|
187
|
+
value,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def in_double_quote_literal(value: UntrustedString | PythonCode | str) -> str:
|
|
192
|
+
"""Escape a string so it can be safely embedded inside a double-quoted string literal.
|
|
193
|
+
|
|
194
|
+
The result does NOT include surrounding quotes; the caller is expected to place it inside its own `"..."`
|
|
195
|
+
(e.g. a Python string literal or a TOML string in generated code). JSON encoding is used for the escaping,
|
|
196
|
+
which is valid inside both Python and TOML double-quoted strings.
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(value, UntrustedString):
|
|
199
|
+
value = value.get_untrusted_value()
|
|
200
|
+
if isinstance(value, PythonCode):
|
|
201
|
+
value = value.as_unembedded_code()
|
|
202
|
+
value = "".join(
|
|
203
|
+
character
|
|
204
|
+
for character in value
|
|
205
|
+
if (character in "\b\t\f" or not unicodedata.category(character).startswith("C"))
|
|
206
|
+
and unicodedata.category(character) not in {"Zl", "Zp"}
|
|
207
|
+
)
|
|
208
|
+
return json.dumps(value, ensure_ascii=False)[1:-1]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def in_f_string_literal(value: UntrustedString | PythonCode | str) -> str:
|
|
212
|
+
"""Escape a string so it can be safely embedded as literal text inside a double-quoted f-string.
|
|
213
|
+
|
|
214
|
+
In addition to the double-quoted-string escaping (quotes, backslashes), this escapes `{` and `}` as
|
|
215
|
+
`{{` and `}}` so the value cannot be interpreted as an f-string replacement field or format specifier.
|
|
216
|
+
Without this, an untrusted value could inject arbitrary expressions into the generated f-string.
|
|
217
|
+
"""
|
|
218
|
+
return in_double_quote_literal(value).replace("{", "{{").replace("}", "}}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def remove_string_escapes(value: str) -> str:
|
|
222
|
+
"""Used when parsing string-literal defaults to prevent escaping the string to write arbitrary Python
|
|
223
|
+
|
|
224
|
+
**REMOVING OR CHANGING THE USAGE OF THIS FUNCTION HAS SECURITY IMPLICATIONS**
|
|
225
|
+
|
|
226
|
+
See Also:
|
|
227
|
+
- https://github.com/openapi-generators/openapi-python-client/security/advisories/GHSA-9x4c-63pf-525f
|
|
228
|
+
"""
|
|
229
|
+
return value.replace("\\", "\\\\").replace('"', r"\"")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_content_type(content_type: str, config: Config) -> str | None:
|
|
233
|
+
"""
|
|
234
|
+
Given a string representing a content type with optional parameters, returns the content type only
|
|
235
|
+
"""
|
|
236
|
+
content_type = config.content_type_overrides.get(content_type, content_type)
|
|
237
|
+
message = Message()
|
|
238
|
+
message.add_header("Content-Type", content_type)
|
|
239
|
+
|
|
240
|
+
parsed_content_type = message.get_content_type()
|
|
241
|
+
if not content_type.startswith(parsed_content_type):
|
|
242
|
+
# Always defaults to `text/plain` if it's not recognized. We want to return an error, not default.
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
return parsed_content_type
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# {{ project_name }}
|
|
2
|
-
{{ package_description }}
|
|
2
|
+
{{ package_description.get_untrusted_value() }}
|
|
3
3
|
|
|
4
4
|
## Usage
|
|
5
5
|
First, create a client:
|
|
@@ -47,7 +47,7 @@ By default, when you're calling an HTTPS API it will attempt to verify that SSL
|
|
|
47
47
|
|
|
48
48
|
```python
|
|
49
49
|
client = AuthenticatedClient(
|
|
50
|
-
base_url="https://internal_api.example.com",
|
|
50
|
+
base_url="https://internal_api.example.com",
|
|
51
51
|
token="SuperSecretToken",
|
|
52
52
|
verify_ssl="/path/to/certificate_bundle.pem",
|
|
53
53
|
)
|
|
@@ -57,7 +57,7 @@ You can also disable certificate validation altogether, but beware that **this i
|
|
|
57
57
|
|
|
58
58
|
```python
|
|
59
59
|
client = AuthenticatedClient(
|
|
60
|
-
base_url="https://internal_api.example.com",
|
|
60
|
+
base_url="https://internal_api.example.com",
|
|
61
61
|
token="SuperSecretToken",
|
|
62
62
|
verify_ssl=False
|
|
63
63
|
)
|
|
@@ -76,7 +76,7 @@ Things to know:
|
|
|
76
76
|
|
|
77
77
|
## Advanced customizations
|
|
78
78
|
|
|
79
|
-
There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `
|
|
79
|
+
There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case):
|
|
80
80
|
|
|
81
81
|
```python
|
|
82
82
|
from {{ package_name }} import Client
|
|
@@ -93,20 +93,20 @@ client = Client(
|
|
|
93
93
|
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
# Or get the underlying
|
|
96
|
+
# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client()
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
You can even set the
|
|
99
|
+
You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url):
|
|
100
100
|
|
|
101
101
|
```python
|
|
102
|
-
import
|
|
102
|
+
import httpx2
|
|
103
103
|
from {{ package_name }} import Client
|
|
104
104
|
|
|
105
105
|
client = Client(
|
|
106
106
|
base_url="https://api.example.com",
|
|
107
107
|
)
|
|
108
108
|
# Note that base_url needs to be re-set, as would any shared cookies, headers, etc.
|
|
109
|
-
client.set_httpx_client(
|
|
109
|
+
client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030"))
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
{% if meta == "poetry" %}
|
|
@@ -136,4 +136,4 @@ If you want to install this client into another project without publishing it (e
|
|
|
136
136
|
1. If that project is not using uv:
|
|
137
137
|
1. Build a wheel with `uv build --wheel`.
|
|
138
138
|
1. Install that wheel from the other project `pip install <path-to-wheel>`.
|
|
139
|
-
{% endif %}
|
|
139
|
+
{% endif %}
|