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
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"""Test suite for the Codegen and TypeGenerator classes.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive unit tests for the code generation
|
|
4
|
+
functionality, including type generation, endpoint generation, and
|
|
5
|
+
the full code generation pipeline.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import json
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from otterapi.codegen.codegen import Codegen
|
|
16
|
+
from otterapi.codegen.schema import SchemaLoader
|
|
17
|
+
from otterapi.codegen.types import Type, TypeGenerator
|
|
18
|
+
from otterapi.config import DocumentConfig
|
|
19
|
+
from otterapi.openapi.v3_2.v3_2 import OpenAPI, Schema
|
|
20
|
+
|
|
21
|
+
from .fixtures import (
|
|
22
|
+
MINIMAL_OPENAPI_SPEC,
|
|
23
|
+
PARAMETERS_SPEC,
|
|
24
|
+
PETSTORE_SPEC,
|
|
25
|
+
SIMPLE_API_SPEC,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestTypeGenerator:
|
|
30
|
+
"""Tests for the TypeGenerator class."""
|
|
31
|
+
|
|
32
|
+
def _create_type_generator(self, spec: dict) -> TypeGenerator:
|
|
33
|
+
"""Helper to create a TypeGenerator from a spec dict."""
|
|
34
|
+
from pydantic import TypeAdapter
|
|
35
|
+
|
|
36
|
+
from otterapi.openapi import UniversalOpenAPI
|
|
37
|
+
|
|
38
|
+
openapi = TypeAdapter(UniversalOpenAPI).validate_python(spec)
|
|
39
|
+
# Upgrade if needed
|
|
40
|
+
schema = openapi.root
|
|
41
|
+
while not isinstance(schema, OpenAPI):
|
|
42
|
+
schema, _ = schema.upgrade()
|
|
43
|
+
return TypeGenerator(schema)
|
|
44
|
+
|
|
45
|
+
def test_primitive_string_type(self):
|
|
46
|
+
"""Test generating a primitive string type."""
|
|
47
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
48
|
+
|
|
49
|
+
schema = Schema(type='string')
|
|
50
|
+
result = typegen.schema_to_type(schema, 'TestString')
|
|
51
|
+
|
|
52
|
+
assert result is not None
|
|
53
|
+
assert result.type == 'primitive'
|
|
54
|
+
|
|
55
|
+
def test_primitive_integer_type(self):
|
|
56
|
+
"""Test generating a primitive integer type."""
|
|
57
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
58
|
+
|
|
59
|
+
schema = Schema(type='integer')
|
|
60
|
+
result = typegen.schema_to_type(schema, 'TestInt')
|
|
61
|
+
|
|
62
|
+
assert result is not None
|
|
63
|
+
assert result.type == 'primitive'
|
|
64
|
+
|
|
65
|
+
def test_primitive_boolean_type(self):
|
|
66
|
+
"""Test generating a primitive boolean type."""
|
|
67
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
68
|
+
|
|
69
|
+
schema = Schema(type='boolean')
|
|
70
|
+
result = typegen.schema_to_type(schema, 'TestBool')
|
|
71
|
+
|
|
72
|
+
assert result is not None
|
|
73
|
+
assert result.type == 'primitive'
|
|
74
|
+
|
|
75
|
+
def test_primitive_number_type(self):
|
|
76
|
+
"""Test generating a primitive number type."""
|
|
77
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
78
|
+
|
|
79
|
+
schema = Schema(type='number')
|
|
80
|
+
result = typegen.schema_to_type(schema, 'TestNumber')
|
|
81
|
+
|
|
82
|
+
assert result is not None
|
|
83
|
+
assert result.type == 'primitive'
|
|
84
|
+
|
|
85
|
+
def test_array_of_strings(self):
|
|
86
|
+
"""Test generating an array of strings type."""
|
|
87
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
88
|
+
|
|
89
|
+
schema = Schema(type='array', items=Schema(type='string'))
|
|
90
|
+
result = typegen.schema_to_type(schema, 'StringArray')
|
|
91
|
+
|
|
92
|
+
assert result is not None
|
|
93
|
+
|
|
94
|
+
def test_object_type_creates_model(self):
|
|
95
|
+
"""Test that object types create Pydantic models."""
|
|
96
|
+
typegen = self._create_type_generator(PETSTORE_SPEC)
|
|
97
|
+
|
|
98
|
+
schema = Schema(
|
|
99
|
+
type='object',
|
|
100
|
+
properties={
|
|
101
|
+
'name': Schema(type='string'),
|
|
102
|
+
'age': Schema(type='integer'),
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
result = typegen.schema_to_type(schema, 'Person')
|
|
106
|
+
|
|
107
|
+
assert result is not None
|
|
108
|
+
# Should be a model type (not primitive)
|
|
109
|
+
assert result.type in ('model', 'root')
|
|
110
|
+
|
|
111
|
+
def test_enum_type(self):
|
|
112
|
+
"""Test generating an enum type."""
|
|
113
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
114
|
+
|
|
115
|
+
schema = Schema(type='string', enum=['active', 'inactive', 'pending'])
|
|
116
|
+
result = typegen.schema_to_type(schema, 'Status')
|
|
117
|
+
|
|
118
|
+
assert result is not None
|
|
119
|
+
|
|
120
|
+
def test_reference_resolution(self):
|
|
121
|
+
"""Test that $ref references are resolved."""
|
|
122
|
+
typegen = self._create_type_generator(PETSTORE_SPEC)
|
|
123
|
+
|
|
124
|
+
# Get the Pet schema
|
|
125
|
+
pet_schema = typegen.openapi.components.schemas.get('Pet')
|
|
126
|
+
assert pet_schema is not None
|
|
127
|
+
|
|
128
|
+
result = typegen.schema_to_type(pet_schema, 'Pet')
|
|
129
|
+
assert result is not None
|
|
130
|
+
|
|
131
|
+
def test_nested_object(self):
|
|
132
|
+
"""Test generating a nested object type."""
|
|
133
|
+
typegen = self._create_type_generator(PETSTORE_SPEC)
|
|
134
|
+
|
|
135
|
+
# Use a simpler nested object (Pet has Category reference)
|
|
136
|
+
pet_schema = typegen.openapi.components.schemas.get('Pet')
|
|
137
|
+
assert pet_schema is not None
|
|
138
|
+
|
|
139
|
+
result = typegen.schema_to_type(pet_schema, 'Pet')
|
|
140
|
+
assert result is not None
|
|
141
|
+
|
|
142
|
+
def test_optional_field(self):
|
|
143
|
+
"""Test that optional fields are handled correctly."""
|
|
144
|
+
typegen = self._create_type_generator(MINIMAL_OPENAPI_SPEC)
|
|
145
|
+
|
|
146
|
+
# Test with a simple string schema (nullable is not in v3.2 Schema)
|
|
147
|
+
schema = Schema(type='string')
|
|
148
|
+
result = typegen.schema_to_type(schema, 'OptionalString')
|
|
149
|
+
|
|
150
|
+
assert result is not None
|
|
151
|
+
|
|
152
|
+
def test_add_type_registers_type(self):
|
|
153
|
+
"""Test that add_type properly registers types."""
|
|
154
|
+
typegen = self._create_type_generator(PETSTORE_SPEC)
|
|
155
|
+
|
|
156
|
+
# Use schema_to_type which creates a Type object
|
|
157
|
+
schema = Schema(
|
|
158
|
+
type='object',
|
|
159
|
+
properties={'id': Schema(type='integer'), 'name': Schema(type='string')},
|
|
160
|
+
)
|
|
161
|
+
result = typegen.schema_to_type(schema, 'NewModel')
|
|
162
|
+
|
|
163
|
+
assert result is not None
|
|
164
|
+
# schema_to_type may or may not add to types depending on type
|
|
165
|
+
# Just verify we got a valid type back
|
|
166
|
+
assert hasattr(result, 'type')
|
|
167
|
+
|
|
168
|
+
def test_get_sorted_types(self):
|
|
169
|
+
"""Test that types are returned in dependency order."""
|
|
170
|
+
typegen = self._create_type_generator(PETSTORE_SPEC)
|
|
171
|
+
|
|
172
|
+
# Add types via schema_to_type which properly creates Type objects
|
|
173
|
+
schema = Schema(
|
|
174
|
+
type='object',
|
|
175
|
+
properties={'id': Schema(type='integer'), 'name': Schema(type='string')},
|
|
176
|
+
)
|
|
177
|
+
typegen.schema_to_type(schema, 'SimpleModel')
|
|
178
|
+
|
|
179
|
+
sorted_types = typegen.get_sorted_types()
|
|
180
|
+
|
|
181
|
+
# Should return a list of types
|
|
182
|
+
assert isinstance(sorted_types, list)
|
|
183
|
+
|
|
184
|
+
# All types should be Type objects
|
|
185
|
+
for t in sorted_types:
|
|
186
|
+
assert isinstance(t, Type)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestCodegen:
|
|
190
|
+
"""Tests for the Codegen class."""
|
|
191
|
+
|
|
192
|
+
@pytest.fixture
|
|
193
|
+
def temp_output_dir(self):
|
|
194
|
+
"""Create a temporary directory for test outputs."""
|
|
195
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
196
|
+
yield Path(tmpdir)
|
|
197
|
+
|
|
198
|
+
@pytest.fixture
|
|
199
|
+
def petstore_spec_file(self, temp_output_dir):
|
|
200
|
+
"""Create a temp file with the Petstore spec."""
|
|
201
|
+
spec_file = temp_output_dir / 'petstore.json'
|
|
202
|
+
spec_file.write_text(json.dumps(PETSTORE_SPEC))
|
|
203
|
+
return spec_file
|
|
204
|
+
|
|
205
|
+
@pytest.fixture
|
|
206
|
+
def simple_spec_file(self, temp_output_dir):
|
|
207
|
+
"""Create a temp file with the simple spec."""
|
|
208
|
+
spec_file = temp_output_dir / 'simple.json'
|
|
209
|
+
spec_file.write_text(json.dumps(SIMPLE_API_SPEC))
|
|
210
|
+
return spec_file
|
|
211
|
+
|
|
212
|
+
def test_codegen_init(self, temp_output_dir, petstore_spec_file):
|
|
213
|
+
"""Test Codegen initialization."""
|
|
214
|
+
config = DocumentConfig(
|
|
215
|
+
source=str(petstore_spec_file), output=str(temp_output_dir / 'output')
|
|
216
|
+
)
|
|
217
|
+
codegen = Codegen(config)
|
|
218
|
+
|
|
219
|
+
assert codegen.config == config
|
|
220
|
+
assert codegen.openapi is None # Not loaded yet
|
|
221
|
+
|
|
222
|
+
def test_load_schema(self, temp_output_dir, petstore_spec_file):
|
|
223
|
+
"""Test schema loading."""
|
|
224
|
+
config = DocumentConfig(
|
|
225
|
+
source=str(petstore_spec_file), output=str(temp_output_dir / 'output')
|
|
226
|
+
)
|
|
227
|
+
codegen = Codegen(config)
|
|
228
|
+
|
|
229
|
+
codegen._load_schema()
|
|
230
|
+
|
|
231
|
+
assert codegen.openapi is not None
|
|
232
|
+
assert codegen.openapi.info.title == 'Petstore API'
|
|
233
|
+
|
|
234
|
+
def test_generate_creates_files(self, temp_output_dir, petstore_spec_file):
|
|
235
|
+
"""Test that generate() creates the expected files."""
|
|
236
|
+
output_dir = temp_output_dir / 'output'
|
|
237
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
238
|
+
codegen = Codegen(config)
|
|
239
|
+
|
|
240
|
+
codegen.generate()
|
|
241
|
+
|
|
242
|
+
# Check that files were created
|
|
243
|
+
assert (output_dir / 'models.py').exists()
|
|
244
|
+
assert (output_dir / 'endpoints.py').exists()
|
|
245
|
+
assert (output_dir / '__init__.py').exists()
|
|
246
|
+
|
|
247
|
+
def test_generate_models_valid_python(self, temp_output_dir, petstore_spec_file):
|
|
248
|
+
"""Test that generated models are valid Python."""
|
|
249
|
+
output_dir = temp_output_dir / 'output'
|
|
250
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
251
|
+
codegen = Codegen(config)
|
|
252
|
+
|
|
253
|
+
codegen.generate()
|
|
254
|
+
|
|
255
|
+
# Read and compile the models file
|
|
256
|
+
models_content = (output_dir / 'models.py').read_text()
|
|
257
|
+
|
|
258
|
+
# Should not raise SyntaxError
|
|
259
|
+
compile(models_content, 'models.py', 'exec')
|
|
260
|
+
|
|
261
|
+
def test_generate_endpoints_valid_python(self, temp_output_dir, petstore_spec_file):
|
|
262
|
+
"""Test that generated endpoints are valid Python."""
|
|
263
|
+
output_dir = temp_output_dir / 'output'
|
|
264
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
265
|
+
codegen = Codegen(config)
|
|
266
|
+
|
|
267
|
+
codegen.generate()
|
|
268
|
+
|
|
269
|
+
# Read and compile the endpoints file
|
|
270
|
+
endpoints_content = (output_dir / 'endpoints.py').read_text()
|
|
271
|
+
|
|
272
|
+
# Should not raise SyntaxError
|
|
273
|
+
compile(endpoints_content, 'endpoints.py', 'exec')
|
|
274
|
+
|
|
275
|
+
def test_generate_with_simple_api(self, temp_output_dir, simple_spec_file):
|
|
276
|
+
"""Test generation with a simple API spec."""
|
|
277
|
+
output_dir = temp_output_dir / 'output'
|
|
278
|
+
config = DocumentConfig(source=str(simple_spec_file), output=str(output_dir))
|
|
279
|
+
codegen = Codegen(config)
|
|
280
|
+
|
|
281
|
+
codegen.generate()
|
|
282
|
+
|
|
283
|
+
assert (output_dir / 'models.py').exists()
|
|
284
|
+
assert (output_dir / 'endpoints.py').exists()
|
|
285
|
+
|
|
286
|
+
def test_generate_with_custom_filenames(self, temp_output_dir, petstore_spec_file):
|
|
287
|
+
"""Test generation with custom output filenames."""
|
|
288
|
+
output_dir = temp_output_dir / 'output'
|
|
289
|
+
config = DocumentConfig(
|
|
290
|
+
source=str(petstore_spec_file),
|
|
291
|
+
output=str(output_dir),
|
|
292
|
+
models_file='custom_models.py',
|
|
293
|
+
endpoints_file='custom_endpoints.py',
|
|
294
|
+
)
|
|
295
|
+
codegen = Codegen(config)
|
|
296
|
+
|
|
297
|
+
codegen.generate()
|
|
298
|
+
|
|
299
|
+
assert (output_dir / 'custom_models.py').exists()
|
|
300
|
+
assert (output_dir / 'custom_endpoints.py').exists()
|
|
301
|
+
|
|
302
|
+
def test_generate_models_contains_pet(self, temp_output_dir, petstore_spec_file):
|
|
303
|
+
"""Test that generated models contain the Pet class."""
|
|
304
|
+
output_dir = temp_output_dir / 'output'
|
|
305
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
306
|
+
codegen = Codegen(config)
|
|
307
|
+
|
|
308
|
+
codegen.generate()
|
|
309
|
+
|
|
310
|
+
models_content = (output_dir / 'models.py').read_text()
|
|
311
|
+
|
|
312
|
+
assert 'class Pet' in models_content or 'Pet' in models_content
|
|
313
|
+
|
|
314
|
+
def test_generate_endpoints_contains_operations(
|
|
315
|
+
self, temp_output_dir, petstore_spec_file
|
|
316
|
+
):
|
|
317
|
+
"""Test that generated endpoints contain the expected operations."""
|
|
318
|
+
output_dir = temp_output_dir / 'output'
|
|
319
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
320
|
+
codegen = Codegen(config)
|
|
321
|
+
|
|
322
|
+
codegen.generate()
|
|
323
|
+
|
|
324
|
+
endpoints_content = (output_dir / 'endpoints.py').read_text()
|
|
325
|
+
|
|
326
|
+
# Check for operation function names
|
|
327
|
+
assert 'listPets' in endpoints_content or 'list_pets' in endpoints_content
|
|
328
|
+
assert 'getPetById' in endpoints_content or 'get_pet_by_id' in endpoints_content
|
|
329
|
+
assert 'createPet' in endpoints_content or 'create_pet' in endpoints_content
|
|
330
|
+
|
|
331
|
+
def test_extract_response_info(self, temp_output_dir, petstore_spec_file):
|
|
332
|
+
"""Test the _extract_response_info method."""
|
|
333
|
+
output_dir = temp_output_dir / 'output'
|
|
334
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
335
|
+
codegen = Codegen(config)
|
|
336
|
+
|
|
337
|
+
codegen._load_schema()
|
|
338
|
+
|
|
339
|
+
# Get the listPets operation
|
|
340
|
+
list_pets_op = codegen.openapi.paths.root['/pets'].get
|
|
341
|
+
assert list_pets_op is not None
|
|
342
|
+
|
|
343
|
+
responses = codegen._extract_response_info(list_pets_op)
|
|
344
|
+
|
|
345
|
+
# Should have a 200 response
|
|
346
|
+
assert 200 in responses
|
|
347
|
+
|
|
348
|
+
def test_extract_operation_parameters(self, temp_output_dir, petstore_spec_file):
|
|
349
|
+
"""Test the _extract_operation_parameters method."""
|
|
350
|
+
output_dir = temp_output_dir / 'output'
|
|
351
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
352
|
+
codegen = Codegen(config)
|
|
353
|
+
|
|
354
|
+
codegen._load_schema()
|
|
355
|
+
|
|
356
|
+
# Get the listPets operation which has parameters
|
|
357
|
+
list_pets_op = codegen.openapi.paths.root['/pets'].get
|
|
358
|
+
assert list_pets_op is not None
|
|
359
|
+
|
|
360
|
+
params = codegen._extract_operation_parameters(list_pets_op)
|
|
361
|
+
|
|
362
|
+
# Should have limit and status parameters
|
|
363
|
+
param_names = [p.name for p in params]
|
|
364
|
+
assert 'limit' in param_names
|
|
365
|
+
assert 'status' in param_names
|
|
366
|
+
|
|
367
|
+
def test_resolve_base_url(self, temp_output_dir, petstore_spec_file):
|
|
368
|
+
"""Test the _resolve_base_url method."""
|
|
369
|
+
output_dir = temp_output_dir / 'output'
|
|
370
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
371
|
+
codegen = Codegen(config)
|
|
372
|
+
|
|
373
|
+
codegen._load_schema()
|
|
374
|
+
|
|
375
|
+
base_url = codegen._resolve_base_url()
|
|
376
|
+
|
|
377
|
+
assert base_url is not None
|
|
378
|
+
assert 'petstore.example.com' in base_url
|
|
379
|
+
|
|
380
|
+
def test_resolve_base_url_with_config_override(
|
|
381
|
+
self, temp_output_dir, petstore_spec_file
|
|
382
|
+
):
|
|
383
|
+
"""Test that base_url config overrides the spec."""
|
|
384
|
+
output_dir = temp_output_dir / 'output'
|
|
385
|
+
config = DocumentConfig(
|
|
386
|
+
source=str(petstore_spec_file),
|
|
387
|
+
output=str(output_dir),
|
|
388
|
+
base_url='https://custom.example.com',
|
|
389
|
+
)
|
|
390
|
+
codegen = Codegen(config)
|
|
391
|
+
|
|
392
|
+
codegen._load_schema()
|
|
393
|
+
|
|
394
|
+
base_url = codegen._resolve_base_url()
|
|
395
|
+
|
|
396
|
+
assert base_url == 'https://custom.example.com'
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TestCodegenWithComplexTypes:
|
|
400
|
+
"""Tests for Codegen with complex type specifications."""
|
|
401
|
+
|
|
402
|
+
@pytest.fixture
|
|
403
|
+
def temp_output_dir(self):
|
|
404
|
+
"""Create a temporary directory for test outputs."""
|
|
405
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
406
|
+
yield Path(tmpdir)
|
|
407
|
+
|
|
408
|
+
@pytest.fixture
|
|
409
|
+
def petstore_spec_file(self, temp_output_dir):
|
|
410
|
+
"""Create a temp file with the petstore spec (has complex types)."""
|
|
411
|
+
spec_file = temp_output_dir / 'petstore.json'
|
|
412
|
+
spec_file.write_text(json.dumps(PETSTORE_SPEC))
|
|
413
|
+
return spec_file
|
|
414
|
+
|
|
415
|
+
def test_generate_complex_types(self, temp_output_dir, petstore_spec_file):
|
|
416
|
+
"""Test generation with complex types spec."""
|
|
417
|
+
output_dir = temp_output_dir / 'output'
|
|
418
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
419
|
+
codegen = Codegen(config)
|
|
420
|
+
|
|
421
|
+
codegen.generate()
|
|
422
|
+
|
|
423
|
+
# Should generate without errors
|
|
424
|
+
assert (output_dir / 'models.py').exists()
|
|
425
|
+
|
|
426
|
+
# Check that it's valid Python
|
|
427
|
+
models_content = (output_dir / 'models.py').read_text()
|
|
428
|
+
compile(models_content, 'models.py', 'exec')
|
|
429
|
+
|
|
430
|
+
def test_complex_types_contains_all_fields(
|
|
431
|
+
self, temp_output_dir, petstore_spec_file
|
|
432
|
+
):
|
|
433
|
+
"""Test that complex type generation includes all field types."""
|
|
434
|
+
output_dir = temp_output_dir / 'output'
|
|
435
|
+
config = DocumentConfig(source=str(petstore_spec_file), output=str(output_dir))
|
|
436
|
+
codegen = Codegen(config)
|
|
437
|
+
|
|
438
|
+
codegen.generate()
|
|
439
|
+
|
|
440
|
+
models_content = (output_dir / 'models.py').read_text()
|
|
441
|
+
|
|
442
|
+
# Should contain Pet model
|
|
443
|
+
# These checks are flexible since exact output may vary
|
|
444
|
+
assert 'Pet' in models_content or 'pet' in models_content.lower()
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class TestCodegenWithParameters:
|
|
448
|
+
"""Tests for Codegen with various parameter types."""
|
|
449
|
+
|
|
450
|
+
@pytest.fixture
|
|
451
|
+
def temp_output_dir(self):
|
|
452
|
+
"""Create a temporary directory for test outputs."""
|
|
453
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
454
|
+
yield Path(tmpdir)
|
|
455
|
+
|
|
456
|
+
@pytest.fixture
|
|
457
|
+
def params_spec_file(self, temp_output_dir):
|
|
458
|
+
"""Create a temp file with the parameters spec."""
|
|
459
|
+
spec_file = temp_output_dir / 'params.json'
|
|
460
|
+
spec_file.write_text(json.dumps(PARAMETERS_SPEC))
|
|
461
|
+
return spec_file
|
|
462
|
+
|
|
463
|
+
def test_generate_with_various_parameters(self, temp_output_dir, params_spec_file):
|
|
464
|
+
"""Test generation with various parameter types."""
|
|
465
|
+
output_dir = temp_output_dir / 'output'
|
|
466
|
+
config = DocumentConfig(source=str(params_spec_file), output=str(output_dir))
|
|
467
|
+
codegen = Codegen(config)
|
|
468
|
+
|
|
469
|
+
codegen.generate()
|
|
470
|
+
|
|
471
|
+
assert (output_dir / 'endpoints.py').exists()
|
|
472
|
+
|
|
473
|
+
endpoints_content = (output_dir / 'endpoints.py').read_text()
|
|
474
|
+
compile(endpoints_content, 'endpoints.py', 'exec')
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class TestSchemaLoader:
|
|
478
|
+
"""Tests for the SchemaLoader class."""
|
|
479
|
+
|
|
480
|
+
@pytest.fixture
|
|
481
|
+
def temp_dir(self):
|
|
482
|
+
"""Create a temporary directory."""
|
|
483
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
484
|
+
yield Path(tmpdir)
|
|
485
|
+
|
|
486
|
+
def test_load_from_file(self, temp_dir):
|
|
487
|
+
"""Test loading a schema from a file."""
|
|
488
|
+
spec_file = temp_dir / 'api.json'
|
|
489
|
+
spec_file.write_text(json.dumps(MINIMAL_OPENAPI_SPEC))
|
|
490
|
+
|
|
491
|
+
loader = SchemaLoader()
|
|
492
|
+
schema = loader.load(str(spec_file))
|
|
493
|
+
|
|
494
|
+
assert schema is not None
|
|
495
|
+
assert schema.info.title == 'Minimal API'
|
|
496
|
+
|
|
497
|
+
def test_load_file_not_found(self, temp_dir):
|
|
498
|
+
"""Test that loading a non-existent file raises an error."""
|
|
499
|
+
from otterapi.exceptions import SchemaLoadError
|
|
500
|
+
|
|
501
|
+
loader = SchemaLoader()
|
|
502
|
+
|
|
503
|
+
with pytest.raises(SchemaLoadError):
|
|
504
|
+
loader.load(str(temp_dir / 'nonexistent.json'))
|
|
505
|
+
|
|
506
|
+
def test_load_invalid_json(self, temp_dir):
|
|
507
|
+
"""Test that loading invalid JSON raises an error."""
|
|
508
|
+
spec_file = temp_dir / 'invalid.json'
|
|
509
|
+
spec_file.write_text('not valid json {{{')
|
|
510
|
+
|
|
511
|
+
loader = SchemaLoader()
|
|
512
|
+
|
|
513
|
+
with pytest.raises(Exception): # Could be JSONDecodeError or ValidationError
|
|
514
|
+
loader.load(str(spec_file))
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class TestEndToEndGeneration:
|
|
518
|
+
"""End-to-end tests for the complete generation pipeline."""
|
|
519
|
+
|
|
520
|
+
@pytest.fixture
|
|
521
|
+
def temp_output_dir(self):
|
|
522
|
+
"""Create a temporary directory for test outputs."""
|
|
523
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
524
|
+
yield Path(tmpdir)
|
|
525
|
+
|
|
526
|
+
def test_full_generation_pipeline(self, temp_output_dir):
|
|
527
|
+
"""Test the complete generation pipeline."""
|
|
528
|
+
# Create spec file
|
|
529
|
+
spec_file = temp_output_dir / 'petstore.json'
|
|
530
|
+
spec_file.write_text(json.dumps(PETSTORE_SPEC))
|
|
531
|
+
|
|
532
|
+
# Configure and generate
|
|
533
|
+
output_dir = temp_output_dir / 'client'
|
|
534
|
+
config = DocumentConfig(source=str(spec_file), output=str(output_dir))
|
|
535
|
+
codegen = Codegen(config)
|
|
536
|
+
|
|
537
|
+
codegen.generate()
|
|
538
|
+
|
|
539
|
+
# Verify all files exist
|
|
540
|
+
assert (output_dir / 'models.py').exists()
|
|
541
|
+
assert (output_dir / 'endpoints.py').exists()
|
|
542
|
+
assert (output_dir / '__init__.py').exists()
|
|
543
|
+
|
|
544
|
+
# Verify files are valid Python
|
|
545
|
+
for py_file in ['models.py', 'endpoints.py']:
|
|
546
|
+
content = (output_dir / py_file).read_text()
|
|
547
|
+
compile(content, py_file, 'exec')
|
|
548
|
+
|
|
549
|
+
def test_generated_code_can_be_parsed_as_ast(self, temp_output_dir):
|
|
550
|
+
"""Test that generated code can be parsed as AST."""
|
|
551
|
+
spec_file = temp_output_dir / 'petstore.json'
|
|
552
|
+
spec_file.write_text(json.dumps(PETSTORE_SPEC))
|
|
553
|
+
|
|
554
|
+
output_dir = temp_output_dir / 'client'
|
|
555
|
+
config = DocumentConfig(source=str(spec_file), output=str(output_dir))
|
|
556
|
+
codegen = Codegen(config)
|
|
557
|
+
|
|
558
|
+
codegen.generate()
|
|
559
|
+
|
|
560
|
+
# Parse as AST
|
|
561
|
+
models_content = (output_dir / 'models.py').read_text()
|
|
562
|
+
tree = ast.parse(models_content)
|
|
563
|
+
|
|
564
|
+
# Should have class definitions
|
|
565
|
+
class_defs = [node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]
|
|
566
|
+
assert len(class_defs) > 0
|
|
567
|
+
|
|
568
|
+
def test_generated_endpoints_have_functions(self, temp_output_dir):
|
|
569
|
+
"""Test that generated endpoints have function definitions."""
|
|
570
|
+
spec_file = temp_output_dir / 'petstore.json'
|
|
571
|
+
spec_file.write_text(json.dumps(PETSTORE_SPEC))
|
|
572
|
+
|
|
573
|
+
output_dir = temp_output_dir / 'client'
|
|
574
|
+
config = DocumentConfig(source=str(spec_file), output=str(output_dir))
|
|
575
|
+
codegen = Codegen(config)
|
|
576
|
+
|
|
577
|
+
codegen.generate()
|
|
578
|
+
|
|
579
|
+
endpoints_content = (output_dir / 'endpoints.py').read_text()
|
|
580
|
+
tree = ast.parse(endpoints_content)
|
|
581
|
+
|
|
582
|
+
# Should have function definitions
|
|
583
|
+
func_defs = [
|
|
584
|
+
node
|
|
585
|
+
for node in ast.walk(tree)
|
|
586
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
587
|
+
]
|
|
588
|
+
assert len(func_defs) > 0
|
|
589
|
+
|
|
590
|
+
def test_multiple_generations_overwrite(self, temp_output_dir):
|
|
591
|
+
"""Test that generating twice overwrites the files."""
|
|
592
|
+
spec_file = temp_output_dir / 'petstore.json'
|
|
593
|
+
spec_file.write_text(json.dumps(PETSTORE_SPEC))
|
|
594
|
+
|
|
595
|
+
output_dir = temp_output_dir / 'client'
|
|
596
|
+
config = DocumentConfig(source=str(spec_file), output=str(output_dir))
|
|
597
|
+
|
|
598
|
+
# Generate twice
|
|
599
|
+
codegen1 = Codegen(config)
|
|
600
|
+
codegen1.generate()
|
|
601
|
+
|
|
602
|
+
first_content = (output_dir / 'models.py').read_text()
|
|
603
|
+
|
|
604
|
+
codegen2 = Codegen(config)
|
|
605
|
+
codegen2.generate()
|
|
606
|
+
|
|
607
|
+
second_content = (output_dir / 'models.py').read_text()
|
|
608
|
+
|
|
609
|
+
# Content should be the same (deterministic generation)
|
|
610
|
+
assert first_content == second_content
|