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.
Files changed (91) hide show
  1. openapi_python_client/__init__.py +30 -21
  2. openapi_python_client/cli.py +10 -3
  3. openapi_python_client/config.py +4 -1
  4. openapi_python_client/parser/bodies.py +8 -7
  5. openapi_python_client/parser/openapi.py +58 -48
  6. openapi_python_client/parser/properties/__init__.py +36 -33
  7. openapi_python_client/parser/properties/any.py +12 -10
  8. openapi_python_client/parser/properties/boolean.py +14 -13
  9. openapi_python_client/parser/properties/const.py +11 -10
  10. openapi_python_client/parser/properties/date.py +13 -11
  11. openapi_python_client/parser/properties/datetime.py +13 -11
  12. openapi_python_client/parser/properties/enum_property.py +23 -20
  13. openapi_python_client/parser/properties/file.py +12 -10
  14. openapi_python_client/parser/properties/float.py +14 -13
  15. openapi_python_client/parser/properties/int.py +12 -11
  16. openapi_python_client/parser/properties/list_property.py +22 -22
  17. openapi_python_client/parser/properties/literal_enum_property.py +20 -20
  18. openapi_python_client/parser/properties/merge_properties.py +9 -6
  19. openapi_python_client/parser/properties/model_property.py +35 -34
  20. openapi_python_client/parser/properties/none.py +12 -11
  21. openapi_python_client/parser/properties/protocol.py +42 -32
  22. openapi_python_client/parser/properties/schemas.py +30 -23
  23. openapi_python_client/parser/properties/string.py +11 -11
  24. openapi_python_client/parser/properties/union.py +24 -22
  25. openapi_python_client/parser/properties/uuid.py +12 -11
  26. openapi_python_client/parser/responses.py +31 -24
  27. openapi_python_client/schema/__init__.py +2 -1
  28. openapi_python_client/schema/openapi_schema_pydantic/callback.py +3 -1
  29. openapi_python_client/schema/openapi_schema_pydantic/components.py +10 -9
  30. openapi_python_client/schema/openapi_schema_pydantic/contact.py +5 -3
  31. openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +4 -2
  32. openapi_python_client/schema/openapi_schema_pydantic/encoding.py +4 -3
  33. openapi_python_client/schema/openapi_schema_pydantic/example.py +5 -3
  34. openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +4 -2
  35. openapi_python_client/schema/openapi_schema_pydantic/header.py +20 -6
  36. openapi_python_client/schema/openapi_schema_pydantic/info.py +5 -4
  37. openapi_python_client/schema/openapi_schema_pydantic/license.py +4 -2
  38. openapi_python_client/schema/openapi_schema_pydantic/link.py +5 -4
  39. openapi_python_client/schema/openapi_schema_pydantic/media_type.py +3 -2
  40. openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +6 -4
  41. openapi_python_client/schema/openapi_schema_pydantic/open_api.py +5 -3
  42. openapi_python_client/schema/openapi_schema_pydantic/operation.py +6 -5
  43. openapi_python_client/schema/openapi_schema_pydantic/parameter.py +6 -5
  44. openapi_python_client/schema/openapi_schema_pydantic/path_item.py +5 -3
  45. openapi_python_client/schema/openapi_schema_pydantic/paths.py +2 -1
  46. openapi_python_client/schema/openapi_schema_pydantic/reference.py +3 -1
  47. openapi_python_client/schema/openapi_schema_pydantic/request_body.py +3 -2
  48. openapi_python_client/schema/openapi_schema_pydantic/response.py +5 -4
  49. openapi_python_client/schema/openapi_schema_pydantic/responses.py +2 -1
  50. openapi_python_client/schema/openapi_schema_pydantic/schema.py +9 -8
  51. openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +3 -1
  52. openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +8 -7
  53. openapi_python_client/schema/openapi_schema_pydantic/server.py +4 -3
  54. openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +5 -3
  55. openapi_python_client/schema/openapi_schema_pydantic/tag.py +3 -2
  56. openapi_python_client/schema/openapi_schema_pydantic/xml.py +5 -3
  57. openapi_python_client/schema/ref.py +52 -0
  58. openapi_python_client/schema/untrusted_string.py +56 -0
  59. openapi_python_client/strings.py +245 -0
  60. openapi_python_client/templates/README.md.jinja +9 -9
  61. openapi_python_client/templates/client.py.jinja +31 -31
  62. openapi_python_client/templates/endpoint_macros.py.jinja +19 -19
  63. openapi_python_client/templates/endpoint_module.py.jinja +18 -21
  64. openapi_python_client/templates/helpers.jinja +3 -7
  65. openapi_python_client/templates/literal_enum.py.jinja +1 -1
  66. openapi_python_client/templates/model.py.jinja +18 -18
  67. openapi_python_client/templates/package_init.py.jinja +2 -2
  68. openapi_python_client/templates/property_templates/const_property.py.jinja +3 -3
  69. openapi_python_client/templates/property_templates/date_property.py.jinja +1 -1
  70. openapi_python_client/templates/property_templates/datetime_property.py.jinja +1 -1
  71. openapi_python_client/templates/property_templates/enum_property.py.jinja +1 -1
  72. openapi_python_client/templates/property_templates/file_property.py.jinja +1 -1
  73. openapi_python_client/templates/property_templates/list_property.py.jinja +3 -3
  74. openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +2 -2
  75. openapi_python_client/templates/property_templates/model_property.py.jinja +1 -1
  76. openapi_python_client/templates/property_templates/property_macros.py.jinja +1 -1
  77. openapi_python_client/templates/property_templates/union_property.py.jinja +9 -9
  78. openapi_python_client/templates/property_templates/uuid_property.py.jinja +1 -1
  79. openapi_python_client/templates/pyproject_pdm.toml.jinja +3 -3
  80. openapi_python_client/templates/pyproject_poetry.toml.jinja +3 -3
  81. openapi_python_client/templates/pyproject_uv.toml.jinja +4 -4
  82. openapi_python_client/templates/setup.py.jinja +3 -3
  83. openapi_python_client/templates/str_enum.py.jinja +2 -2
  84. openapi_python_client/templates/types.py.jinja +2 -2
  85. {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/METADATA +10 -9
  86. openapi_python_client-0.30.0.dist-info/RECORD +115 -0
  87. {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/WHEEL +1 -1
  88. openapi_python_client/utils.py +0 -122
  89. openapi_python_client-0.29.0.dist-info/RECORD +0 -113
  90. {openapi_python_client-0.29.0.dist-info → openapi_python_client-0.30.0.dist-info}/entry_points.txt +0 -0
  91. {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: str | None = None
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: str | None = None
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[str] | None = Field(default=None)
38
- enum: None | list[Any] = Field(default=None, min_length=1)
39
- const: None | StrictStr | StrictInt | StrictFloat | StrictBool = None
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[str, ReferenceOr["Schema"]] | None = None
48
+ properties: dict[UntrustedString, ReferenceOr["Schema"]] | None = None
48
49
  additionalProperties: bool | ReferenceOr["Schema"] | None = None
49
- description: str | None = None
50
- schema_format: str | None = Field(default=None, alias="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
- SecurityRequirement = dict[str, list[str]]
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: str
21
- description: str | None = None
22
- name: str | None = None
23
- security_scheme_in: str | None = Field(default=None, alias="in")
24
- scheme: str | None = None
25
- bearerFormat: str | None = None
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: str | None = None
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: str
15
- description: str | None = None
16
- variables: dict[str, ServerVariable] | None = None
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[str] | None = None
13
- default: str
14
- description: str | None = None
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: str
17
- description: str | None = None
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: str | None = None
18
- namespace: str | None = None
19
- prefix: str | None = None
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 `httpx.Client` or `httpx.AsyncClient` (depending on your use-case):
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 httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client()
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 httpx client directly, but beware that this will override any existing settings (e.g., base_url):
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 httpx
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(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030"))
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 %}