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.
Files changed (52) hide show
  1. README.md +581 -8
  2. otterapi/__init__.py +73 -0
  3. otterapi/cli.py +327 -29
  4. otterapi/codegen/__init__.py +115 -0
  5. otterapi/codegen/ast_utils.py +134 -5
  6. otterapi/codegen/client.py +1271 -0
  7. otterapi/codegen/codegen.py +1736 -0
  8. otterapi/codegen/dataframes.py +392 -0
  9. otterapi/codegen/emitter.py +473 -0
  10. otterapi/codegen/endpoints.py +2597 -343
  11. otterapi/codegen/pagination.py +1026 -0
  12. otterapi/codegen/schema.py +593 -0
  13. otterapi/codegen/splitting.py +1397 -0
  14. otterapi/codegen/types.py +1345 -0
  15. otterapi/codegen/utils.py +180 -1
  16. otterapi/config.py +1017 -24
  17. otterapi/exceptions.py +231 -0
  18. otterapi/openapi/__init__.py +46 -0
  19. otterapi/openapi/v2/__init__.py +86 -0
  20. otterapi/openapi/v2/spec.json +1607 -0
  21. otterapi/openapi/v2/v2.py +1776 -0
  22. otterapi/openapi/v3/__init__.py +131 -0
  23. otterapi/openapi/v3/spec.json +1651 -0
  24. otterapi/openapi/v3/v3.py +1557 -0
  25. otterapi/openapi/v3_1/__init__.py +133 -0
  26. otterapi/openapi/v3_1/spec.json +1411 -0
  27. otterapi/openapi/v3_1/v3_1.py +798 -0
  28. otterapi/openapi/v3_2/__init__.py +133 -0
  29. otterapi/openapi/v3_2/spec.json +1666 -0
  30. otterapi/openapi/v3_2/v3_2.py +777 -0
  31. otterapi/tests/__init__.py +3 -0
  32. otterapi/tests/fixtures/__init__.py +455 -0
  33. otterapi/tests/test_ast_utils.py +680 -0
  34. otterapi/tests/test_codegen.py +610 -0
  35. otterapi/tests/test_dataframe.py +1038 -0
  36. otterapi/tests/test_exceptions.py +493 -0
  37. otterapi/tests/test_openapi_support.py +616 -0
  38. otterapi/tests/test_openapi_upgrade.py +215 -0
  39. otterapi/tests/test_pagination.py +1101 -0
  40. otterapi/tests/test_splitting_config.py +319 -0
  41. otterapi/tests/test_splitting_integration.py +427 -0
  42. otterapi/tests/test_splitting_resolver.py +512 -0
  43. otterapi/tests/test_splitting_tree.py +525 -0
  44. otterapi-0.0.6.dist-info/METADATA +627 -0
  45. otterapi-0.0.6.dist-info/RECORD +48 -0
  46. {otterapi-0.0.5.dist-info → otterapi-0.0.6.dist-info}/WHEEL +1 -1
  47. otterapi/codegen/generator.py +0 -358
  48. otterapi/codegen/openapi_processor.py +0 -27
  49. otterapi/codegen/type_generator.py +0 -559
  50. otterapi-0.0.5.dist-info/METADATA +0 -54
  51. otterapi-0.0.5.dist-info/RECORD +0 -16
  52. {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