otterapi 0.0.5__py3-none-any.whl → 0.0.6__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.
- README.md +581 -8
- otterapi/__init__.py +73 -0
- otterapi/cli.py +327 -29
- otterapi/codegen/__init__.py +115 -0
- otterapi/codegen/ast_utils.py +134 -5
- otterapi/codegen/client.py +1271 -0
- otterapi/codegen/codegen.py +1736 -0
- otterapi/codegen/dataframes.py +392 -0
- otterapi/codegen/emitter.py +473 -0
- otterapi/codegen/endpoints.py +2597 -343
- otterapi/codegen/pagination.py +1026 -0
- otterapi/codegen/schema.py +593 -0
- otterapi/codegen/splitting.py +1397 -0
- otterapi/codegen/types.py +1345 -0
- otterapi/codegen/utils.py +180 -1
- otterapi/config.py +1017 -24
- otterapi/exceptions.py +231 -0
- otterapi/openapi/__init__.py +46 -0
- otterapi/openapi/v2/__init__.py +86 -0
- otterapi/openapi/v2/spec.json +1607 -0
- otterapi/openapi/v2/v2.py +1776 -0
- otterapi/openapi/v3/__init__.py +131 -0
- otterapi/openapi/v3/spec.json +1651 -0
- otterapi/openapi/v3/v3.py +1557 -0
- otterapi/openapi/v3_1/__init__.py +133 -0
- otterapi/openapi/v3_1/spec.json +1411 -0
- otterapi/openapi/v3_1/v3_1.py +798 -0
- otterapi/openapi/v3_2/__init__.py +133 -0
- otterapi/openapi/v3_2/spec.json +1666 -0
- otterapi/openapi/v3_2/v3_2.py +777 -0
- otterapi/tests/__init__.py +3 -0
- otterapi/tests/fixtures/__init__.py +455 -0
- otterapi/tests/test_ast_utils.py +680 -0
- otterapi/tests/test_codegen.py +610 -0
- otterapi/tests/test_dataframe.py +1038 -0
- otterapi/tests/test_exceptions.py +493 -0
- otterapi/tests/test_openapi_support.py +616 -0
- otterapi/tests/test_openapi_upgrade.py +215 -0
- otterapi/tests/test_pagination.py +1101 -0
- otterapi/tests/test_splitting_config.py +319 -0
- otterapi/tests/test_splitting_integration.py +427 -0
- otterapi/tests/test_splitting_resolver.py +512 -0
- otterapi/tests/test_splitting_tree.py +525 -0
- otterapi-0.0.6.dist-info/METADATA +627 -0
- otterapi-0.0.6.dist-info/RECORD +48 -0
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
- otterapi/codegen/generator.py +0 -358
- otterapi/codegen/openapi_processor.py +0 -27
- otterapi/codegen/type_generator.py +0 -559
- otterapi-0.0.5.dist-info/METADATA +0 -54
- otterapi-0.0.5.dist-info/RECORD +0 -16
- {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/entry_points.txt +0 -0
|
@@ -1,559 +0,0 @@
|
|
|
1
|
-
import ast
|
|
2
|
-
import dataclasses
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
from typing import Any, Literal
|
|
5
|
-
from uuid import UUID
|
|
6
|
-
|
|
7
|
-
from openapi_pydantic import DataType, Reference, Schema
|
|
8
|
-
from openapi_pydantic.v3.parser import OpenAPIv3
|
|
9
|
-
from pydantic import BaseModel, Field, RootModel
|
|
10
|
-
|
|
11
|
-
from otterapi.codegen.ast_utils import _call, _name, _subscript, _union_expr
|
|
12
|
-
from otterapi.codegen.openapi_processor import OpenAPIProcessor
|
|
13
|
-
from otterapi.codegen.utils import sanitize_identifier, sanitize_parameter_field_name
|
|
14
|
-
|
|
15
|
-
_PRIMITIVE_TYPE_MAP = {
|
|
16
|
-
('string', None): str,
|
|
17
|
-
('string', 'date-time'): datetime,
|
|
18
|
-
('string', 'date'): datetime,
|
|
19
|
-
('string', 'uuid'): UUID,
|
|
20
|
-
('integer', None): int,
|
|
21
|
-
('integer', 'int32'): int,
|
|
22
|
-
('integer', 'int64'): int,
|
|
23
|
-
('number', None): float,
|
|
24
|
-
('number', 'float'): float,
|
|
25
|
-
('number', 'double'): float,
|
|
26
|
-
('boolean', None): bool,
|
|
27
|
-
('null', None): None,
|
|
28
|
-
(None, None): None,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclasses.dataclass
|
|
33
|
-
class Type:
|
|
34
|
-
reference: str | None # reference is None if type is 'primitive'
|
|
35
|
-
name: str | None
|
|
36
|
-
annotation_ast: ast.expr | ast.stmt
|
|
37
|
-
type: Literal['primitive', 'root', 'model']
|
|
38
|
-
implementation_ast: ast.expr | ast.stmt | None = dataclasses.field(
|
|
39
|
-
default_factory=None
|
|
40
|
-
)
|
|
41
|
-
dependencies: set[str] = dataclasses.field(default_factory=set)
|
|
42
|
-
implementation_imports: dict[str, set[str]] = dataclasses.field(
|
|
43
|
-
default_factory=dict
|
|
44
|
-
)
|
|
45
|
-
annotation_imports: dict[str, set[str]] = dataclasses.field(default_factory=dict)
|
|
46
|
-
|
|
47
|
-
def __eq__(self, other):
|
|
48
|
-
"""Deep comparison of Type objects, including AST nodes."""
|
|
49
|
-
if not isinstance(other, Type):
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
# Compare simple fields
|
|
53
|
-
if (
|
|
54
|
-
self.reference != other.reference
|
|
55
|
-
or self.name != other.name
|
|
56
|
-
or self.type != other.type
|
|
57
|
-
):
|
|
58
|
-
return False
|
|
59
|
-
|
|
60
|
-
# Compare AST nodes by dumping them to strings
|
|
61
|
-
if ast.dump(self.annotation_ast) != ast.dump(other.annotation_ast):
|
|
62
|
-
return False
|
|
63
|
-
|
|
64
|
-
# Compare implementation AST (can be None)
|
|
65
|
-
if self.implementation_ast is None and other.implementation_ast is None:
|
|
66
|
-
pass # Both None, equal
|
|
67
|
-
elif self.implementation_ast is None or other.implementation_ast is None:
|
|
68
|
-
return False # One is None, other isn't
|
|
69
|
-
else:
|
|
70
|
-
if ast.dump(self.implementation_ast) != ast.dump(other.implementation_ast):
|
|
71
|
-
return False
|
|
72
|
-
|
|
73
|
-
# Compare imports and dependencies
|
|
74
|
-
if (
|
|
75
|
-
self.dependencies != other.dependencies
|
|
76
|
-
or self.implementation_imports != other.implementation_imports
|
|
77
|
-
or self.annotation_imports != other.annotation_imports
|
|
78
|
-
):
|
|
79
|
-
return False
|
|
80
|
-
|
|
81
|
-
return True
|
|
82
|
-
|
|
83
|
-
def __hash__(self):
|
|
84
|
-
"""Make Type hashable based on its name (for use in sets/dicts)."""
|
|
85
|
-
# We only hash based on name since we use name as the key in the types dict
|
|
86
|
-
return hash(self.name)
|
|
87
|
-
|
|
88
|
-
def add_dependency(self, type_: 'Type') -> None:
|
|
89
|
-
self.dependencies.add(type_.name)
|
|
90
|
-
for dep in type_.dependencies:
|
|
91
|
-
self.dependencies.add(dep)
|
|
92
|
-
|
|
93
|
-
def copy_imports_from_sub_types(self, types: list['Type']):
|
|
94
|
-
for t in types:
|
|
95
|
-
for module, names in t.annotation_imports.items():
|
|
96
|
-
if module not in self.annotation_imports:
|
|
97
|
-
self.annotation_imports[module] = set()
|
|
98
|
-
self.annotation_imports[module].update(names)
|
|
99
|
-
|
|
100
|
-
for module, names in t.implementation_imports.items():
|
|
101
|
-
if module not in self.implementation_imports:
|
|
102
|
-
self.implementation_imports[module] = set()
|
|
103
|
-
self.implementation_imports[module].update(names)
|
|
104
|
-
|
|
105
|
-
def add_implementation_import(self, module: str, name: str | list[str]) -> None:
|
|
106
|
-
if isinstance(name, str):
|
|
107
|
-
name = [name]
|
|
108
|
-
|
|
109
|
-
if module not in self.implementation_imports:
|
|
110
|
-
self.implementation_imports[module] = set()
|
|
111
|
-
|
|
112
|
-
for n in name:
|
|
113
|
-
self.implementation_imports[module].add(n)
|
|
114
|
-
|
|
115
|
-
def add_annotation_import(self, module: str, name: str | list[str]) -> None:
|
|
116
|
-
if isinstance(name, str):
|
|
117
|
-
name = [name]
|
|
118
|
-
|
|
119
|
-
if module not in self.annotation_imports:
|
|
120
|
-
self.annotation_imports[module] = set()
|
|
121
|
-
|
|
122
|
-
for n in name:
|
|
123
|
-
self.annotation_imports[module].add(n)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@dataclasses.dataclass
|
|
127
|
-
class Parameter:
|
|
128
|
-
name: str
|
|
129
|
-
name_sanitized: str
|
|
130
|
-
location: str # query, path, header, cookie, body
|
|
131
|
-
required: bool
|
|
132
|
-
type: Type | None = None
|
|
133
|
-
description: str | None = None
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
@dataclasses.dataclass
|
|
137
|
-
class Endpoint:
|
|
138
|
-
sync_ast: ast.FunctionDef
|
|
139
|
-
async_ast: ast.AsyncFunctionDef
|
|
140
|
-
|
|
141
|
-
sync_fn_name: str
|
|
142
|
-
async_fn_name: str
|
|
143
|
-
|
|
144
|
-
name: str
|
|
145
|
-
imports: dict[str, set[str]] = dataclasses.field(default_factory=dict)
|
|
146
|
-
|
|
147
|
-
def add_imports(self, imports: list[dict[str, set[str]]]):
|
|
148
|
-
for imports_ in imports:
|
|
149
|
-
for module, names in imports_.items():
|
|
150
|
-
if module not in self.imports:
|
|
151
|
-
self.imports[module] = set()
|
|
152
|
-
self.imports[module].update(names)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class TypeGen(OpenAPIProcessor):
|
|
156
|
-
def __init__(self, openapi: OpenAPIv3):
|
|
157
|
-
super().__init__(openapi)
|
|
158
|
-
self.types: dict[str, Type] = {}
|
|
159
|
-
|
|
160
|
-
def add_type(self, type_: Type, base_name: str | None = None) -> Type:
|
|
161
|
-
"""Add a type to the registry. If a type with the same name but different definition
|
|
162
|
-
already exists, generate a unique name using the base_name prefix.
|
|
163
|
-
Returns the type (potentially with a modified name).
|
|
164
|
-
"""
|
|
165
|
-
# Skip types without names (primitive types, inline types, etc.)
|
|
166
|
-
if not type_.name:
|
|
167
|
-
return type_
|
|
168
|
-
|
|
169
|
-
# If type with same name and same definition exists, just return the existing one
|
|
170
|
-
if type_.name in self.types:
|
|
171
|
-
existing = self.types[type_.name]
|
|
172
|
-
if existing == type_:
|
|
173
|
-
# Same type already registered, return the existing one
|
|
174
|
-
# This avoids creating Detail20, Detail21 when they're identical
|
|
175
|
-
return existing
|
|
176
|
-
else:
|
|
177
|
-
# Different definition with same name - generate a unique name
|
|
178
|
-
if base_name:
|
|
179
|
-
# Use base_name as prefix for endpoint-specific types
|
|
180
|
-
unique_name = f'{base_name}{type_.name}'
|
|
181
|
-
if unique_name not in self.types:
|
|
182
|
-
type_.name = unique_name
|
|
183
|
-
type_.annotation_ast = _name(unique_name)
|
|
184
|
-
# Update the implementation_ast name if it's a ClassDef
|
|
185
|
-
if isinstance(type_.implementation_ast, ast.ClassDef):
|
|
186
|
-
type_.implementation_ast.name = unique_name
|
|
187
|
-
else:
|
|
188
|
-
# Check if even the base_name version is the same
|
|
189
|
-
if (
|
|
190
|
-
unique_name in self.types
|
|
191
|
-
and self.types[unique_name] == type_
|
|
192
|
-
):
|
|
193
|
-
return self.types[unique_name]
|
|
194
|
-
# If even that exists with different def, add a counter
|
|
195
|
-
counter = 1
|
|
196
|
-
while f'{unique_name}{counter}' in self.types:
|
|
197
|
-
candidate = f'{unique_name}{counter}'
|
|
198
|
-
if self.types[candidate] == type_:
|
|
199
|
-
return self.types[candidate]
|
|
200
|
-
counter += 1
|
|
201
|
-
unique_name = f'{unique_name}{counter}'
|
|
202
|
-
type_.name = unique_name
|
|
203
|
-
type_.annotation_ast = _name(unique_name)
|
|
204
|
-
if isinstance(type_.implementation_ast, ast.ClassDef):
|
|
205
|
-
type_.implementation_ast.name = unique_name
|
|
206
|
-
else:
|
|
207
|
-
# No base_name provided, just add a counter
|
|
208
|
-
counter = 1
|
|
209
|
-
original_name = type_.name
|
|
210
|
-
while f'{original_name}{counter}' in self.types:
|
|
211
|
-
candidate = f'{original_name}{counter}'
|
|
212
|
-
if self.types[candidate] == type_:
|
|
213
|
-
# Found identical type with numbered name
|
|
214
|
-
return self.types[candidate]
|
|
215
|
-
counter += 1
|
|
216
|
-
unique_name = f'{original_name}{counter}'
|
|
217
|
-
type_.name = unique_name
|
|
218
|
-
type_.annotation_ast = _name(unique_name)
|
|
219
|
-
if isinstance(type_.implementation_ast, ast.ClassDef):
|
|
220
|
-
type_.implementation_ast.name = unique_name
|
|
221
|
-
|
|
222
|
-
self.types[type_.name] = type_
|
|
223
|
-
return type_
|
|
224
|
-
|
|
225
|
-
def _resolve_reference(self, reference: Reference | Schema) -> tuple[Schema, str]:
|
|
226
|
-
if hasattr(reference, 'ref'):
|
|
227
|
-
if not reference.ref.startswith('#/components/schemas/'):
|
|
228
|
-
raise ValueError(f'Unsupported reference format: {reference.ref}')
|
|
229
|
-
|
|
230
|
-
schema_name = reference.ref.split('/')[-1]
|
|
231
|
-
schemas = self.openapi.components.schemas
|
|
232
|
-
|
|
233
|
-
if schema_name not in schemas:
|
|
234
|
-
raise ValueError(
|
|
235
|
-
f"Referenced schema '{schema_name}' not found in components.schemas"
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
return schemas[schema_name], sanitize_identifier(schema_name)
|
|
239
|
-
return reference, sanitize_identifier(
|
|
240
|
-
reference.title
|
|
241
|
-
) if reference.title else None
|
|
242
|
-
|
|
243
|
-
def _get_primitive_type_ast(self, schema: Schema) -> Type:
|
|
244
|
-
key = (schema.type, schema.schema_format or None)
|
|
245
|
-
mapped = _PRIMITIVE_TYPE_MAP.get(key, Any)
|
|
246
|
-
|
|
247
|
-
type_ = Type(
|
|
248
|
-
None,
|
|
249
|
-
sanitize_identifier(schema.title) if schema.title else None,
|
|
250
|
-
annotation_ast=_name(mapped.__name__ if mapped is not None else 'None'),
|
|
251
|
-
implementation_ast=None,
|
|
252
|
-
type='primitive',
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
if mapped is not None and mapped.__module__ != 'builtins':
|
|
256
|
-
type_.add_annotation_import(mapped.__module__, mapped.__name__)
|
|
257
|
-
return type_
|
|
258
|
-
|
|
259
|
-
def _create_pydantic_field(
|
|
260
|
-
self, field_name: str, field_schema: Schema, field_type: Type
|
|
261
|
-
) -> str:
|
|
262
|
-
if hasattr(field_schema, 'ref'):
|
|
263
|
-
field_schema, _ = self._resolve_reference(field_schema)
|
|
264
|
-
|
|
265
|
-
field_keywords = list()
|
|
266
|
-
|
|
267
|
-
sanitized_field_name = sanitize_parameter_field_name(field_name)
|
|
268
|
-
|
|
269
|
-
value = None
|
|
270
|
-
if field_schema.default is not None and isinstance(
|
|
271
|
-
field_schema.default, (str, int, float, bool)
|
|
272
|
-
):
|
|
273
|
-
field_keywords.append(
|
|
274
|
-
ast.keyword(arg='default', value=ast.Constant(field_schema.default))
|
|
275
|
-
)
|
|
276
|
-
elif field_schema.default is None and not field_schema.required:
|
|
277
|
-
field_keywords.append(ast.keyword(arg='default', value=ast.Constant(None)))
|
|
278
|
-
|
|
279
|
-
if sanitized_field_name != field_name:
|
|
280
|
-
field_keywords.append(
|
|
281
|
-
ast.keyword(
|
|
282
|
-
arg='alias',
|
|
283
|
-
value=ast.Constant(field_name), # original name before adding _
|
|
284
|
-
)
|
|
285
|
-
)
|
|
286
|
-
field_name = sanitized_field_name
|
|
287
|
-
|
|
288
|
-
if field_keywords:
|
|
289
|
-
value = _call(
|
|
290
|
-
func=_name(Field.__name__),
|
|
291
|
-
keywords=field_keywords,
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
field_type.add_implementation_import(
|
|
295
|
-
module=Field.__module__, name=Field.__name__
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
return ast.AnnAssign(
|
|
299
|
-
target=_name(field_name),
|
|
300
|
-
annotation=field_type.annotation_ast,
|
|
301
|
-
value=value,
|
|
302
|
-
simple=1,
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
def _create_pydantic_root_model(
|
|
306
|
-
self,
|
|
307
|
-
schema: Schema,
|
|
308
|
-
item_type: Type | None = None,
|
|
309
|
-
name: str | None = None,
|
|
310
|
-
base_name: str | None = None,
|
|
311
|
-
) -> Type:
|
|
312
|
-
name = (
|
|
313
|
-
name
|
|
314
|
-
or base_name
|
|
315
|
-
or (sanitize_identifier(schema.title) if schema.title else None)
|
|
316
|
-
)
|
|
317
|
-
if not name:
|
|
318
|
-
raise ValueError('Root model must have a name')
|
|
319
|
-
|
|
320
|
-
model = ast.ClassDef(
|
|
321
|
-
name=name,
|
|
322
|
-
bases=[_subscript(RootModel.__name__, item_type.annotation_ast)],
|
|
323
|
-
keywords=[],
|
|
324
|
-
body=[ast.Pass()],
|
|
325
|
-
decorator_list=[],
|
|
326
|
-
type_params=[],
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
type_ = Type(
|
|
330
|
-
reference=None,
|
|
331
|
-
name=name,
|
|
332
|
-
annotation_ast=_name(name),
|
|
333
|
-
implementation_ast=model,
|
|
334
|
-
type='root',
|
|
335
|
-
)
|
|
336
|
-
type_.add_implementation_import(
|
|
337
|
-
module=RootModel.__module__, name=RootModel.__name__
|
|
338
|
-
)
|
|
339
|
-
type_.copy_imports_from_sub_types([item_type] if item_type else [])
|
|
340
|
-
if item_type is not None:
|
|
341
|
-
type_.add_dependency(item_type)
|
|
342
|
-
type_ = self.add_type(type_, base_name=base_name)
|
|
343
|
-
|
|
344
|
-
return type_
|
|
345
|
-
|
|
346
|
-
def _create_pydantic_model(
|
|
347
|
-
self, schema: Schema, name: str | None = None, base_name: str | None = None
|
|
348
|
-
) -> Type:
|
|
349
|
-
base_bases = []
|
|
350
|
-
if schema.allOf:
|
|
351
|
-
for base_schema in schema.allOf:
|
|
352
|
-
base = self._create_object_type(schema=base_schema, base_name=base_name)
|
|
353
|
-
base_bases.append(base)
|
|
354
|
-
|
|
355
|
-
if schema.anyOf or schema.oneOf:
|
|
356
|
-
types_ = [
|
|
357
|
-
self._create_object_type(schema=t, base_name=base_name)
|
|
358
|
-
for t in (schema.anyOf or schema.oneOf)
|
|
359
|
-
]
|
|
360
|
-
|
|
361
|
-
union_type = Type(
|
|
362
|
-
reference=None,
|
|
363
|
-
name=None, # Union type doesn't need a name, it's used inline
|
|
364
|
-
annotation_ast=_union_expr(types=[t.annotation_ast for t in types_]),
|
|
365
|
-
implementation_ast=None,
|
|
366
|
-
type='primitive',
|
|
367
|
-
)
|
|
368
|
-
union_type.copy_imports_from_sub_types(types_)
|
|
369
|
-
union_type.add_annotation_import('typing', 'Union')
|
|
370
|
-
return union_type
|
|
371
|
-
|
|
372
|
-
name = name or (
|
|
373
|
-
sanitize_identifier(schema.title) if schema.title else 'UnnamedModel'
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
bases = [b.name for b in base_bases] or [BaseModel.__name__]
|
|
377
|
-
bases = [_name(base) for base in bases]
|
|
378
|
-
|
|
379
|
-
body = []
|
|
380
|
-
field_types = []
|
|
381
|
-
for property_name, property_schema in schema.properties.items():
|
|
382
|
-
type_ = self.schema_to_type(property_schema, base_name=base_name)
|
|
383
|
-
field = self._create_pydantic_field(property_name, property_schema, type_)
|
|
384
|
-
|
|
385
|
-
body.append(field)
|
|
386
|
-
field_types.append(type_)
|
|
387
|
-
|
|
388
|
-
model = ast.ClassDef(
|
|
389
|
-
name=name,
|
|
390
|
-
bases=bases,
|
|
391
|
-
keywords=[],
|
|
392
|
-
body=body or [ast.Pass()],
|
|
393
|
-
decorator_list=[],
|
|
394
|
-
type_params=[],
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
type_ = Type(
|
|
398
|
-
reference=None,
|
|
399
|
-
name=name,
|
|
400
|
-
annotation_ast=_name(name),
|
|
401
|
-
implementation_ast=model,
|
|
402
|
-
dependencies=set(),
|
|
403
|
-
type='model',
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
# Add base class dependencies
|
|
407
|
-
if base_bases:
|
|
408
|
-
for base in base_bases:
|
|
409
|
-
type_.add_dependency(base)
|
|
410
|
-
|
|
411
|
-
# Add field type dependencies
|
|
412
|
-
for field_type in field_types:
|
|
413
|
-
if field_type.name:
|
|
414
|
-
type_.dependencies.add(field_type.name)
|
|
415
|
-
type_.dependencies.update(field_type.dependencies)
|
|
416
|
-
|
|
417
|
-
type_.add_implementation_import(
|
|
418
|
-
module=BaseModel.__module__, name=BaseModel.__name__
|
|
419
|
-
)
|
|
420
|
-
type_.add_implementation_import(module=Field.__module__, name=Field.__name__)
|
|
421
|
-
type_.copy_imports_from_sub_types(field_types)
|
|
422
|
-
|
|
423
|
-
type_ = self.add_type(type_, base_name=base_name)
|
|
424
|
-
return type_
|
|
425
|
-
|
|
426
|
-
def _create_array_type(
|
|
427
|
-
self, schema: Schema, name: str | None = None, base_name: str | None = None
|
|
428
|
-
) -> Type:
|
|
429
|
-
if schema.type != DataType.ARRAY:
|
|
430
|
-
raise ValueError('Schema is not an array')
|
|
431
|
-
|
|
432
|
-
if not schema.items:
|
|
433
|
-
type_ = Type(
|
|
434
|
-
None,
|
|
435
|
-
None,
|
|
436
|
-
_subscript(
|
|
437
|
-
list.__name__,
|
|
438
|
-
ast.Name(id=Any.__name__, ctx=ast.Load()),
|
|
439
|
-
),
|
|
440
|
-
'primitive',
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
type_.add_annotation_import(module=list.__module__, name=list.__name__)
|
|
444
|
-
type_.add_annotation_import(module=Any.__module__, name=Any.__name__)
|
|
445
|
-
|
|
446
|
-
return type_
|
|
447
|
-
|
|
448
|
-
item_type = self.schema_to_type(schema.items, base_name=base_name)
|
|
449
|
-
|
|
450
|
-
type_ = Type(
|
|
451
|
-
None,
|
|
452
|
-
None,
|
|
453
|
-
annotation_ast=_subscript(
|
|
454
|
-
list.__name__,
|
|
455
|
-
item_type.annotation_ast,
|
|
456
|
-
),
|
|
457
|
-
implementation_ast=None,
|
|
458
|
-
type='primitive',
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
type_.add_annotation_import(list.__module__, list.__name__)
|
|
462
|
-
type_.copy_imports_from_sub_types([item_type])
|
|
463
|
-
|
|
464
|
-
if item_type:
|
|
465
|
-
type_.add_dependency(item_type)
|
|
466
|
-
|
|
467
|
-
return type_
|
|
468
|
-
|
|
469
|
-
def _create_object_type(
|
|
470
|
-
self,
|
|
471
|
-
schema: Schema | Reference,
|
|
472
|
-
name: str | None = None,
|
|
473
|
-
base_name: str | None = None,
|
|
474
|
-
) -> Type:
|
|
475
|
-
schema, schema_name = self._resolve_reference(schema)
|
|
476
|
-
if (
|
|
477
|
-
not schema.properties
|
|
478
|
-
and not schema.allOf
|
|
479
|
-
and not schema.anyOf
|
|
480
|
-
and not schema.oneOf
|
|
481
|
-
):
|
|
482
|
-
type_ = Type(
|
|
483
|
-
None,
|
|
484
|
-
None,
|
|
485
|
-
annotation_ast=_subscript(
|
|
486
|
-
dict.__name__,
|
|
487
|
-
ast.Tuple(
|
|
488
|
-
elts=[
|
|
489
|
-
ast.Name(id=str.__name__, ctx=ast.Load()),
|
|
490
|
-
ast.Name(id=Any.__name__, ctx=ast.Load()),
|
|
491
|
-
]
|
|
492
|
-
),
|
|
493
|
-
),
|
|
494
|
-
implementation_ast=None,
|
|
495
|
-
type='primitive',
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
type_.add_annotation_import(dict.__module__, dict.__name__)
|
|
499
|
-
type_.add_annotation_import(Any.__module__, Any.__name__)
|
|
500
|
-
|
|
501
|
-
return type_
|
|
502
|
-
|
|
503
|
-
return self._create_pydantic_model(
|
|
504
|
-
schema, schema_name or name, base_name=base_name
|
|
505
|
-
)
|
|
506
|
-
|
|
507
|
-
def schema_to_type(
|
|
508
|
-
self, schema: Schema | Reference, base_name: str | None = None
|
|
509
|
-
) -> Type:
|
|
510
|
-
if isinstance(schema, Reference):
|
|
511
|
-
ref_name = schema.ref.split('/')[-1]
|
|
512
|
-
sanitized_ref_name = sanitize_identifier(ref_name)
|
|
513
|
-
if sanitized_ref_name in self.types:
|
|
514
|
-
return self.types[sanitized_ref_name]
|
|
515
|
-
|
|
516
|
-
schema, schema_name = self._resolve_reference(schema)
|
|
517
|
-
|
|
518
|
-
if schema.type == DataType.ARRAY:
|
|
519
|
-
type_ = self._create_array_type(
|
|
520
|
-
schema=schema, name=schema_name, base_name=base_name
|
|
521
|
-
)
|
|
522
|
-
elif schema.type == DataType.OBJECT or schema.type is None:
|
|
523
|
-
type_ = self._create_object_type(
|
|
524
|
-
schema, name=schema_name, base_name=base_name
|
|
525
|
-
)
|
|
526
|
-
else:
|
|
527
|
-
type_ = self._get_primitive_type_ast(schema)
|
|
528
|
-
|
|
529
|
-
return type_
|
|
530
|
-
|
|
531
|
-
def get_sorted_types(self) -> list[Type]:
|
|
532
|
-
"""Returns the types sorted in dependency order using topological sort.
|
|
533
|
-
Types with no dependencies come first.
|
|
534
|
-
"""
|
|
535
|
-
sorted_types: list[Type] = []
|
|
536
|
-
temp_mark: set[str] = set()
|
|
537
|
-
perm_mark: set[str] = set()
|
|
538
|
-
|
|
539
|
-
def visit(type_: Type):
|
|
540
|
-
if type_.name in perm_mark:
|
|
541
|
-
return
|
|
542
|
-
if type_.name in temp_mark:
|
|
543
|
-
raise ValueError(f'Cyclic dependency detected for type: {type_.name}')
|
|
544
|
-
|
|
545
|
-
temp_mark.add(type_.name)
|
|
546
|
-
|
|
547
|
-
for dep_name in type_.dependencies:
|
|
548
|
-
if dep_name in self.types:
|
|
549
|
-
visit(self.types[dep_name])
|
|
550
|
-
|
|
551
|
-
perm_mark.add(type_.name)
|
|
552
|
-
temp_mark.remove(type_.name)
|
|
553
|
-
sorted_types.append(type_)
|
|
554
|
-
|
|
555
|
-
for type_ in self.types.values():
|
|
556
|
-
if type_.name not in perm_mark:
|
|
557
|
-
visit(type_)
|
|
558
|
-
|
|
559
|
-
return list(reversed(sorted_types))
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: otterapi
|
|
3
|
-
Version: 0.0.5
|
|
4
|
-
Summary: A cute little companion that generates type-safe clients from OpenAPI documents.
|
|
5
|
-
Project-URL: Source, https://github.com/danplischke/otter
|
|
6
|
-
Author: Dan Plischke
|
|
7
|
-
License-Expression: MIT
|
|
8
|
-
Classifier: Development Status :: 3 - Alpha
|
|
9
|
-
Classifier: Operating System :: MacOS
|
|
10
|
-
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
-
Classifier: Operating System :: POSIX
|
|
12
|
-
Classifier: Operating System :: Unix
|
|
13
|
-
Classifier: Programming Language :: Python
|
|
14
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Topic :: Utilities
|
|
20
|
-
Requires-Python: >=3.10
|
|
21
|
-
Requires-Dist: httpx>=0.28.1
|
|
22
|
-
Requires-Dist: openapi-pydantic>=0.5.1
|
|
23
|
-
Requires-Dist: pydantic-settings>=2.11.0
|
|
24
|
-
Requires-Dist: pyyaml>=6.0.3
|
|
25
|
-
Requires-Dist: typer>=0.20.0
|
|
26
|
-
Requires-Dist: universal-pathlib>=0.3.4
|
|
27
|
-
Description-Content-Type: text/markdown
|
|
28
|
-
|
|
29
|
-
# 🦦 OtterAPI
|
|
30
|
-
|
|
31
|
-
> *A cute and intelligent OpenAPI client generator that dives deep into your OpenAPIs*
|
|
32
|
-
|
|
33
|
-
**OtterAPI** is a sleek Python library that transforms OpenAPI specifications into clean, type-safe client code.
|
|
34
|
-
|
|
35
|
-
## 🚀 Quick Start
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# Generate from a pyproject.toml or any of the default config names (otter.yml, otter.yaml)
|
|
39
|
-
otter generate
|
|
40
|
-
|
|
41
|
-
# Generate from an otterapi config file
|
|
42
|
-
otter generate -c otter.yml
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## 📝 Example Config
|
|
46
|
-
|
|
47
|
-
```yaml
|
|
48
|
-
documents:
|
|
49
|
-
- source: https://petstore3.swagger.io/api/v3/openapi.json
|
|
50
|
-
output: petstore_client
|
|
51
|
-
|
|
52
|
-
- source: ./local-users-api.json
|
|
53
|
-
output: users_client
|
|
54
|
-
```
|
otterapi-0.0.5.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
README.md,sha256=ahWBisuW6qpU7SUSTe2gDZWTOVSFYqS2B4YYMjd0idM,625
|
|
2
|
-
otterapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
otterapi/__main__.py,sha256=jVpOa4WaSqM3912DD0GQ3gj7Jui_jgqo5zK7DnKUE58,67
|
|
4
|
-
otterapi/cli.py,sha256=dw-Pvcv5AqvJb8dWIg5-Wk4LBu6WRsxkkTYF3nxFUrA,2436
|
|
5
|
-
otterapi/config.py,sha256=eEVjHSJNDw1xsKicAySEOUdVHGxH9DEz5ZEoFEI2iEI,2098
|
|
6
|
-
otterapi/codegen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
otterapi/codegen/ast_utils.py,sha256=XYnu4WF-Ow1xjqVaeu_pKMKQ_FPOVBGPgUXrfPs-31o,2846
|
|
8
|
-
otterapi/codegen/endpoints.py,sha256=v4SseRv7bnMrzTJMbRf70wOCoI4ZOhItLW7F0iNg16M,15901
|
|
9
|
-
otterapi/codegen/generator.py,sha256=cRCWomte2zzShQfDuBeaAzsuFGPloYTWN4YU53-ct5g,13250
|
|
10
|
-
otterapi/codegen/openapi_processor.py,sha256=HtF4SGxmKor_vSRpN2zsjz3sAcj4oCHGJbZUBbOR3PM,1080
|
|
11
|
-
otterapi/codegen/type_generator.py,sha256=6U9OUBvBJ9lY2HjvAvJIRmsIpSu1V2D8DbZLpmIaigk,19997
|
|
12
|
-
otterapi/codegen/utils.py,sha256=s85oyOu9ZgDqDngoHWSpt5jQm_os2unZjx8TGi5fTpQ,2289
|
|
13
|
-
otterapi-0.0.5.dist-info/METADATA,sha256=P77pd8wmmmSX8kpISjSYRjeo5_dbLV4Z8JwugJqakFc,1686
|
|
14
|
-
otterapi-0.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
-
otterapi-0.0.5.dist-info/entry_points.txt,sha256=vRm02sAdS5QjrXjlKTIr7xKXw821MEQv2bRDp-gJuCE,48
|
|
16
|
-
otterapi-0.0.5.dist-info/RECORD,,
|
|
File without changes
|