openapi-python-client 0.28.4__py3-none-any.whl → 0.29.1__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 +26 -17
- openapi_python_client/cli.py +1 -2
- openapi_python_client/config.py +5 -3
- openapi_python_client/parser/bodies.py +14 -23
- openapi_python_client/parser/openapi.py +58 -48
- openapi_python_client/parser/properties/__init__.py +34 -33
- openapi_python_client/parser/properties/any.py +11 -10
- openapi_python_client/parser/properties/boolean.py +13 -13
- openapi_python_client/parser/properties/const.py +11 -10
- openapi_python_client/parser/properties/date.py +15 -14
- openapi_python_client/parser/properties/datetime.py +15 -14
- openapi_python_client/parser/properties/enum_property.py +23 -20
- openapi_python_client/parser/properties/file.py +11 -10
- openapi_python_client/parser/properties/float.py +13 -13
- openapi_python_client/parser/properties/int.py +11 -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 +34 -33
- openapi_python_client/parser/properties/none.py +11 -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 +10 -11
- openapi_python_client/parser/properties/union.py +24 -22
- openapi_python_client/parser/properties/uuid.py +11 -11
- openapi_python_client/parser/responses.py +30 -23
- openapi_python_client/schema/__init__.py +2 -1
- openapi_python_client/schema/data_type.py +2 -2
- 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 +7 -6
- 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/parameter_location.py +7 -22
- 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 +3 -3
- openapi_python_client/templates/client.py.jinja +5 -5
- openapi_python_client/templates/endpoint_macros.py.jinja +18 -18
- openapi_python_client/templates/endpoint_module.py.jinja +20 -14
- openapi_python_client/templates/helpers.jinja +3 -7
- openapi_python_client/templates/literal_enum.py.jinja +3 -3
- 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 +2 -2
- openapi_python_client/templates/property_templates/datetime_property.py.jinja +2 -2
- 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 +4 -5
- openapi_python_client/templates/pyproject_poetry.toml.jinja +4 -5
- openapi_python_client/templates/pyproject_uv.toml.jinja +5 -6
- openapi_python_client/templates/setup.py.jinja +4 -4
- openapi_python_client/templates/str_enum.py.jinja +2 -2
- {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/METADATA +6 -9
- openapi_python_client-0.29.1.dist-info/RECORD +115 -0
- {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/WHEEL +1 -1
- openapi_python_client/utils.py +0 -122
- openapi_python_client-0.28.4.dist-info/RECORD +0 -113
- {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/entry_points.txt +0 -0
- {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ssl
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, Self
|
|
3
3
|
|
|
4
4
|
from attrs import define, field, evolve
|
|
5
5
|
import httpx
|
|
@@ -98,7 +98,7 @@ class Client:
|
|
|
98
98
|
return evolve(self, timeout=timeout)
|
|
99
99
|
{% endmacro %}{{ builders("Client") }}
|
|
100
100
|
{% macro httpx_stuff(name, custom_constructor=None) %}
|
|
101
|
-
def set_httpx_client(self, client: httpx.Client) ->
|
|
101
|
+
def set_httpx_client(self, client: httpx.Client) -> Self:
|
|
102
102
|
"""Manually set the underlying httpx.Client
|
|
103
103
|
|
|
104
104
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
@@ -123,7 +123,7 @@ class Client:
|
|
|
123
123
|
)
|
|
124
124
|
return self._client
|
|
125
125
|
|
|
126
|
-
def __enter__(self) ->
|
|
126
|
+
def __enter__(self) -> Self:
|
|
127
127
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
|
128
128
|
self.get_httpx_client().__enter__()
|
|
129
129
|
return self
|
|
@@ -132,7 +132,7 @@ class Client:
|
|
|
132
132
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
|
133
133
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
|
134
134
|
|
|
135
|
-
def set_async_httpx_client(self, async_client: httpx.AsyncClient) ->
|
|
135
|
+
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
|
|
136
136
|
"""Manually set the underlying httpx.AsyncClient
|
|
137
137
|
|
|
138
138
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
|
@@ -157,7 +157,7 @@ class Client:
|
|
|
157
157
|
)
|
|
158
158
|
return self._async_client
|
|
159
159
|
|
|
160
|
-
async def __aenter__(self) ->
|
|
160
|
+
async def __aenter__(self) -> Self:
|
|
161
161
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
|
162
162
|
await self.get_async_httpx_client().__aenter__()
|
|
163
163
|
return self
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{% from "property_templates/helpers.jinja" import guarded_statement %}
|
|
2
|
-
{% from "helpers.jinja" import
|
|
2
|
+
{% from "helpers.jinja" import docstring as _docstring %}
|
|
3
3
|
|
|
4
4
|
{% macro header_params(endpoint) %}
|
|
5
5
|
{% if endpoint.header_parameters or endpoint.bodies | length > 0 %}
|
|
@@ -12,7 +12,7 @@ headers: dict[str, Any] = {}
|
|
|
12
12
|
{% else %}
|
|
13
13
|
{% set expression = parameter.python_name %}
|
|
14
14
|
{% endif %}
|
|
15
|
-
{% set statement = 'headers["' +
|
|
15
|
+
{% set statement = 'headers["' + (parameter.name | in_double_quote_literal) + '"]' + " = " + expression %}
|
|
16
16
|
{{ guarded_statement(parameter, parameter.python_name, statement) }}
|
|
17
17
|
{% endfor %}
|
|
18
18
|
{% endif %}
|
|
@@ -24,10 +24,10 @@ headers: dict[str, Any] = {}
|
|
|
24
24
|
cookies = {}
|
|
25
25
|
{% for parameter in endpoint.cookie_parameters %}
|
|
26
26
|
{% if parameter.required %}
|
|
27
|
-
cookies["{{ parameter.name}}"] = {{ parameter.python_name }}
|
|
27
|
+
cookies["{{ parameter.name | in_double_quote_literal }}"] = {{ parameter.python_name }}
|
|
28
28
|
{% else %}
|
|
29
29
|
if {{ parameter.python_name }} is not UNSET:
|
|
30
|
-
cookies["{{ parameter.name}}"] = {{ parameter.python_name }}
|
|
30
|
+
cookies["{{ parameter.name | in_double_quote_literal }}"] = {{ parameter.python_name }}
|
|
31
31
|
{% endif %}
|
|
32
32
|
|
|
33
33
|
{% endfor %}
|
|
@@ -47,7 +47,7 @@ params: dict[str, Any] = {}
|
|
|
47
47
|
{{ prop_template.transform(property, property.python_name, destination) }}
|
|
48
48
|
{% endif %}
|
|
49
49
|
{%- if not property.json_is_dict %}
|
|
50
|
-
params["{{ property.name }}"] = {{ destination }}
|
|
50
|
+
params["{{ property.name | in_double_quote_literal }}"] = {{ destination }}
|
|
51
51
|
{% else %}
|
|
52
52
|
{{ guarded_statement(property, destination, "params.update(" + destination + ")") }}
|
|
53
53
|
{% endif %}
|
|
@@ -105,7 +105,7 @@ if not isinstance({{property.python_name}}, Unset):
|
|
|
105
105
|
{% macro arguments(endpoint, include_client=True) %}
|
|
106
106
|
{# path parameters #}
|
|
107
107
|
{% for parameter in endpoint.path_parameters %}
|
|
108
|
-
{{ parameter.to_string() }},
|
|
108
|
+
{{ parameter.to_string() | as_unembedded_code }},
|
|
109
109
|
{% endfor %}
|
|
110
110
|
{% if include_client or ((endpoint.list_all_parameters() | length) > (endpoint.path_parameters | length)) %}
|
|
111
111
|
*,
|
|
@@ -120,24 +120,24 @@ client: AuthenticatedClient | Client,
|
|
|
120
120
|
{% endif %}
|
|
121
121
|
{# Any allowed bodies #}
|
|
122
122
|
{% if endpoint.bodies | length == 1 %}
|
|
123
|
-
body: {{ endpoint.bodies[0].prop.get_type_string() }}{% if not endpoint.bodies[0].prop.required %} = UNSET{% endif %},
|
|
123
|
+
body: {{ endpoint.bodies[0].prop.get_type_string() | as_unembedded_code }}{% if not endpoint.bodies[0].prop.required %} = UNSET{% endif %},
|
|
124
124
|
{% elif endpoint.bodies | length > 1 %}
|
|
125
125
|
body:
|
|
126
126
|
{%- for body in endpoint.bodies -%}{% set body_required = body_required and body.prop.required %}
|
|
127
|
-
{{ body.prop.get_type_string(no_optional=True) }} {% if not loop.last %} | {% endif %}
|
|
127
|
+
{{ body.prop.get_type_string(no_optional=True) | as_unembedded_code }} {% if not loop.last %} | {% endif %}
|
|
128
128
|
{%- endfor -%}{% if not body_required %} | Unset = UNSET{% endif %}
|
|
129
129
|
,
|
|
130
130
|
{% endif %}
|
|
131
131
|
{# query parameters #}
|
|
132
132
|
{% for parameter in endpoint.query_parameters %}
|
|
133
|
-
{{ parameter.to_string() }},
|
|
133
|
+
{{ parameter.to_string() | as_unembedded_code }},
|
|
134
134
|
{% endfor %}
|
|
135
135
|
{% for parameter in endpoint.header_parameters %}
|
|
136
|
-
{{ parameter.to_string() }},
|
|
136
|
+
{{ parameter.to_string() | as_unembedded_code }},
|
|
137
137
|
{% endfor %}
|
|
138
138
|
{# cookie parameters #}
|
|
139
139
|
{% for parameter in endpoint.cookie_parameters %}
|
|
140
|
-
{{ parameter.to_string() }},
|
|
140
|
+
{{ parameter.to_string() | as_unembedded_code }},
|
|
141
141
|
{% endfor %}
|
|
142
142
|
{% endmacro %}
|
|
143
143
|
|
|
@@ -164,10 +164,10 @@ body=body,
|
|
|
164
164
|
{% endmacro %}
|
|
165
165
|
|
|
166
166
|
{% macro docstring_content(endpoint, return_string, is_detailed) %}
|
|
167
|
-
{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}}
|
|
167
|
+
{% if endpoint.summary %}{{ endpoint.summary | safe_for_docstring | wordwrap(100)}}
|
|
168
168
|
|
|
169
169
|
{% endif -%}
|
|
170
|
-
{%- if endpoint.description %} {{ endpoint.description | wordwrap(100) }}
|
|
170
|
+
{%- if endpoint.description %} {{ endpoint.description | safe_for_docstring | wordwrap(100) }}
|
|
171
171
|
|
|
172
172
|
{% endif %}
|
|
173
173
|
{% if not endpoint.summary and not endpoint.description %}
|
|
@@ -188,24 +188,24 @@ Raises:
|
|
|
188
188
|
|
|
189
189
|
Returns:
|
|
190
190
|
{% if is_detailed %}
|
|
191
|
-
Response[{{ return_string }}]
|
|
191
|
+
Response[{{ return_string | safe_for_docstring }}]
|
|
192
192
|
{% else %}
|
|
193
|
-
{{ return_string }}
|
|
193
|
+
{{ return_string | safe_for_docstring }}
|
|
194
194
|
{% endif %}
|
|
195
195
|
{% endmacro %}
|
|
196
196
|
|
|
197
197
|
{% macro docstring(endpoint, return_string, is_detailed) %}
|
|
198
|
-
{{
|
|
198
|
+
{{ _docstring(docstring_content(endpoint, return_string, is_detailed)) }}
|
|
199
199
|
{% endmacro %}
|
|
200
200
|
|
|
201
201
|
{% macro parse_response(parsed_responses, response) %}
|
|
202
202
|
{% if parsed_responses %}{% import "property_templates/" + response.prop.template as prop_template %}
|
|
203
203
|
{% if prop_template.construct %}
|
|
204
204
|
{{ prop_template.construct(response.prop, response.source.attribute) }}
|
|
205
|
-
{% elif response.source.return_type == response.prop.get_type_string() %}
|
|
205
|
+
{% elif response.source.return_type == response.prop.get_type_string() | as_unembedded_code %}
|
|
206
206
|
{{ response.prop.python_name }} = {{ response.source.attribute }}
|
|
207
207
|
{% else %}
|
|
208
|
-
{{ response.prop.python_name }} = cast({{ response.prop.get_type_string() }}, {{ response.source.attribute }})
|
|
208
|
+
{{ response.prop.python_name }} = cast({{ response.prop.get_type_string() | as_unembedded_code }}, {{ response.source.attribute }})
|
|
209
209
|
{% endif %}
|
|
210
210
|
return {{ response.prop.python_name }}
|
|
211
211
|
{% else %}
|
|
@@ -16,7 +16,7 @@ from ... import errors
|
|
|
16
16
|
arguments, client, kwargs, parse_response, docstring, body_to_kwarg %}
|
|
17
17
|
|
|
18
18
|
{% set return_string = endpoint.response_type() %}
|
|
19
|
-
{% set parsed_responses = (endpoint.responses | length > 0) and return_string != "Any" %}
|
|
19
|
+
{% set parsed_responses = (endpoint.responses | length > 0) and return_string != PythonCode("Any") %}
|
|
20
20
|
|
|
21
21
|
def _get_kwargs(
|
|
22
22
|
{{ arguments(endpoint, include_client=False) | indent(4) }}
|
|
@@ -30,13 +30,13 @@ def _get_kwargs(
|
|
|
30
30
|
_kwargs: dict[str, Any] = {
|
|
31
31
|
"method": "{{ endpoint.method }}",
|
|
32
32
|
{% if endpoint.path_parameters %}
|
|
33
|
-
"url": "{{ endpoint.path }}".format(
|
|
33
|
+
"url": "{{ endpoint.path | in_double_quote_literal }}".format(
|
|
34
34
|
{%- for parameter in endpoint.path_parameters -%}
|
|
35
35
|
{{parameter.python_name}}=quote(str({{parameter.python_name}}), safe=""),
|
|
36
36
|
{%- endfor -%}
|
|
37
37
|
),
|
|
38
38
|
{% else %}
|
|
39
|
-
"url": "{{ endpoint.path }}",
|
|
39
|
+
"url": "{{ endpoint.path | in_double_quote_literal }}",
|
|
40
40
|
{% endif %}
|
|
41
41
|
{% if endpoint.query_parameters %}
|
|
42
42
|
"params": params,
|
|
@@ -48,15 +48,21 @@ def _get_kwargs(
|
|
|
48
48
|
|
|
49
49
|
{% if endpoint.bodies | length > 1 %}
|
|
50
50
|
{% for body in endpoint.bodies %}
|
|
51
|
-
if isinstance(body, {{body.prop.get_type_string(no_optional=True) }}):
|
|
51
|
+
if isinstance(body, {{body.prop.get_type_string(no_optional=True) | as_unembedded_code }}):
|
|
52
52
|
{{ body_to_kwarg(body) | indent(8) }}
|
|
53
|
-
|
|
53
|
+
{%- if body.content_type == "multipart/form-data" %}
|
|
54
|
+
headers["Content-Type"] = "multipart/form-data; boundary=+++"
|
|
55
|
+
{% else %}
|
|
56
|
+
headers["Content-Type"] = "{{ body.content_type | in_double_quote_literal }}"
|
|
57
|
+
{% endif %}
|
|
54
58
|
{% endfor %}
|
|
55
59
|
{% elif endpoint.bodies | length == 1 %}
|
|
56
60
|
{% set body = endpoint.bodies[0] %}
|
|
57
61
|
{{ body_to_kwarg(body) | indent(4) }}
|
|
58
|
-
{
|
|
59
|
-
headers["Content-Type"] = "
|
|
62
|
+
{%- if body.content_type == "multipart/form-data" %}
|
|
63
|
+
headers["Content-Type"] = "multipart/form-data; boundary=+++"
|
|
64
|
+
{% else %}
|
|
65
|
+
headers["Content-Type"] = "{{ body.content_type | in_double_quote_literal }}"
|
|
60
66
|
{% endif %}
|
|
61
67
|
{% endif %}
|
|
62
68
|
|
|
@@ -66,9 +72,9 @@ def _get_kwargs(
|
|
|
66
72
|
return _kwargs
|
|
67
73
|
|
|
68
74
|
{% if endpoint.responses.default %}
|
|
69
|
-
{% set return_type = return_string %}
|
|
75
|
+
{% set return_type = return_string | as_unembedded_code %}
|
|
70
76
|
{% else %}
|
|
71
|
-
{% set return_type = return_string + " | None" %}
|
|
77
|
+
{% set return_type = (return_string | as_unembedded_code) + " | None" %}
|
|
72
78
|
{% endif %}
|
|
73
79
|
|
|
74
80
|
|
|
@@ -92,7 +98,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
|
|
|
92
98
|
{% endif %}
|
|
93
99
|
|
|
94
100
|
|
|
95
|
-
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[{{ return_string }}]:
|
|
101
|
+
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[{{ return_string | as_unembedded_code }}]:
|
|
96
102
|
return Response(
|
|
97
103
|
status_code=HTTPStatus(response.status_code),
|
|
98
104
|
content=response.content,
|
|
@@ -103,7 +109,7 @@ def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Res
|
|
|
103
109
|
|
|
104
110
|
def sync_detailed(
|
|
105
111
|
{{ arguments(endpoint) | indent(4) }}
|
|
106
|
-
) -> Response[{{ return_string }}]:
|
|
112
|
+
) -> Response[{{ return_string | as_unembedded_code }}]:
|
|
107
113
|
{{ docstring(endpoint, return_string, is_detailed=true) | indent(4) }}
|
|
108
114
|
|
|
109
115
|
kwargs = _get_kwargs(
|
|
@@ -119,7 +125,7 @@ def sync_detailed(
|
|
|
119
125
|
{% if parsed_responses %}
|
|
120
126
|
def sync(
|
|
121
127
|
{{ arguments(endpoint) | indent(4) }}
|
|
122
|
-
) -> {{ return_string }} | None:
|
|
128
|
+
) -> {{ return_string | as_unembedded_code }} | None:
|
|
123
129
|
{{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }}
|
|
124
130
|
|
|
125
131
|
return sync_detailed(
|
|
@@ -129,7 +135,7 @@ def sync(
|
|
|
129
135
|
|
|
130
136
|
async def asyncio_detailed(
|
|
131
137
|
{{ arguments(endpoint) | indent(4) }}
|
|
132
|
-
) -> Response[{{ return_string }}]:
|
|
138
|
+
) -> Response[{{ return_string | as_unembedded_code }}]:
|
|
133
139
|
{{ docstring(endpoint, return_string, is_detailed=true) | indent(4) }}
|
|
134
140
|
|
|
135
141
|
kwargs = _get_kwargs(
|
|
@@ -145,7 +151,7 @@ async def asyncio_detailed(
|
|
|
145
151
|
{% if parsed_responses %}
|
|
146
152
|
async def asyncio(
|
|
147
153
|
{{ arguments(endpoint) | indent(4) }}
|
|
148
|
-
) -> {{ return_string }} | None:
|
|
154
|
+
) -> {{ return_string | as_unembedded_code }} | None:
|
|
149
155
|
{{ docstring(endpoint, return_string, is_detailed=false) | indent(4) }}
|
|
150
156
|
|
|
151
157
|
return (await asyncio_detailed(
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
{% macro
|
|
2
|
-
{# This macro returns the provided content as a docstring
|
|
3
|
-
{% if (
|
|
4
|
-
{% if '\\' in content -%}
|
|
5
|
-
r""" {{ content }} """
|
|
6
|
-
{%- else -%}
|
|
1
|
+
{% macro docstring(content) %}
|
|
2
|
+
{# This macro returns the provided content as a docstring unless it's empty, then it emits nothing. #}
|
|
3
|
+
{% if (content | trim) %}
|
|
7
4
|
""" {{ content }} """
|
|
8
5
|
{%- endif -%}
|
|
9
|
-
{% endif %}
|
|
10
6
|
{% endmacro %}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from typing import Literal
|
|
1
|
+
from typing import Literal
|
|
2
2
|
|
|
3
3
|
{{ enum.class_info.name }} = Literal{{ "%r" | format(enum.values|list|sort) }}
|
|
4
4
|
|
|
5
5
|
{{ enum.get_class_name_snake_case() | upper }}_VALUES: set[{{ enum.class_info.name }}] = { {% for v in enum.values|list|sort %}{{"%r"|format(v)}}, {% endfor %} }
|
|
6
6
|
|
|
7
|
-
def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() }}) -> {{ enum.class_info.name}}:
|
|
7
|
+
def check_{{ enum.get_class_name_snake_case() }}(value: {{ enum.get_instance_type_string() | as_unembedded_code }}) -> {{ enum.class_info.name}}:
|
|
8
8
|
if value in {{ enum.get_class_name_snake_case() | upper }}_VALUES:
|
|
9
|
-
return
|
|
9
|
+
return value
|
|
10
10
|
raise TypeError(f"Unexpected value {value!r}. Expected one of {{"{"}}{{ enum.get_class_name_snake_case() | upper }}_VALUES!r}")
|