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,525 @@
1
+ """Tests for the module tree builder.
2
+
3
+ This module tests the ModuleTree dataclass and ModuleTreeBuilder class
4
+ used for organizing endpoints into a hierarchical structure.
5
+ """
6
+
7
+ import ast
8
+
9
+ from otterapi.codegen.splitting import (
10
+ ModuleTree,
11
+ ModuleTreeBuilder,
12
+ build_module_tree,
13
+ )
14
+ from otterapi.codegen.types import Endpoint
15
+ from otterapi.config import ModuleSplitConfig
16
+
17
+
18
+ def make_endpoint(
19
+ name: str,
20
+ path: str,
21
+ method: str = 'GET',
22
+ tags: list[str] | None = None,
23
+ ) -> Endpoint:
24
+ """Create a minimal Endpoint for testing."""
25
+ # Create minimal AST nodes
26
+ sync_ast = ast.FunctionDef(
27
+ name=name,
28
+ args=ast.arguments(
29
+ posonlyargs=[],
30
+ args=[],
31
+ kwonlyargs=[],
32
+ kw_defaults=[],
33
+ defaults=[],
34
+ ),
35
+ body=[ast.Pass()],
36
+ decorator_list=[],
37
+ )
38
+ async_ast = ast.AsyncFunctionDef(
39
+ name=f'a{name}',
40
+ args=ast.arguments(
41
+ posonlyargs=[],
42
+ args=[],
43
+ kwonlyargs=[],
44
+ kw_defaults=[],
45
+ defaults=[],
46
+ ),
47
+ body=[ast.Pass()],
48
+ decorator_list=[],
49
+ )
50
+
51
+ return Endpoint(
52
+ sync_ast=sync_ast,
53
+ async_ast=async_ast,
54
+ sync_fn_name=name,
55
+ async_fn_name=f'a{name}',
56
+ name=name,
57
+ method=method,
58
+ path=path,
59
+ tags=tags,
60
+ )
61
+
62
+
63
+ class TestModuleTree:
64
+ """Tests for the ModuleTree dataclass."""
65
+
66
+ def test_empty_tree(self):
67
+ """Test creating an empty tree."""
68
+ tree = ModuleTree(name='root')
69
+ assert tree.name == 'root'
70
+ assert tree.endpoints == []
71
+ assert tree.children == {}
72
+ assert tree.definition is None
73
+ assert tree.description is None
74
+
75
+ def test_add_endpoint_to_root(self):
76
+ """Test adding an endpoint to the root."""
77
+ tree = ModuleTree(name='root')
78
+ endpoint = make_endpoint('get_users', '/users')
79
+
80
+ tree.add_endpoint([], endpoint)
81
+
82
+ assert len(tree.endpoints) == 1
83
+ assert tree.endpoints[0] == endpoint
84
+
85
+ def test_add_endpoint_single_level(self):
86
+ """Test adding an endpoint to a single-level path."""
87
+ tree = ModuleTree(name='root')
88
+ endpoint = make_endpoint('get_users', '/users')
89
+
90
+ tree.add_endpoint(['users'], endpoint)
91
+
92
+ assert len(tree.endpoints) == 0
93
+ assert 'users' in tree.children
94
+ assert len(tree.children['users'].endpoints) == 1
95
+
96
+ def test_add_endpoint_nested_path(self):
97
+ """Test adding an endpoint to a nested path."""
98
+ tree = ModuleTree(name='root')
99
+ endpoint = make_endpoint('get_user', '/api/v1/users/{id}')
100
+
101
+ tree.add_endpoint(['api', 'v1', 'users'], endpoint)
102
+
103
+ assert 'api' in tree.children
104
+ assert 'v1' in tree.children['api'].children
105
+ assert 'users' in tree.children['api'].children['v1'].children
106
+ users_node = tree.children['api'].children['v1'].children['users']
107
+ assert len(users_node.endpoints) == 1
108
+
109
+ def test_add_multiple_endpoints_same_path(self):
110
+ """Test adding multiple endpoints to the same path."""
111
+ tree = ModuleTree(name='root')
112
+ ep1 = make_endpoint('get_users', '/users', 'GET')
113
+ ep2 = make_endpoint('create_user', '/users', 'POST')
114
+
115
+ tree.add_endpoint(['users'], ep1)
116
+ tree.add_endpoint(['users'], ep2)
117
+
118
+ assert len(tree.children['users'].endpoints) == 2
119
+
120
+ def test_get_node_exists(self):
121
+ """Test getting an existing node."""
122
+ tree = ModuleTree(name='root')
123
+ endpoint = make_endpoint('get_users', '/users')
124
+ tree.add_endpoint(['api', 'users'], endpoint)
125
+
126
+ node = tree.get_node(['api', 'users'])
127
+
128
+ assert node is not None
129
+ assert len(node.endpoints) == 1
130
+
131
+ def test_get_node_not_exists(self):
132
+ """Test getting a non-existent node."""
133
+ tree = ModuleTree(name='root')
134
+
135
+ node = tree.get_node(['nonexistent'])
136
+
137
+ assert node is None
138
+
139
+ def test_get_node_empty_path(self):
140
+ """Test getting root node with empty path."""
141
+ tree = ModuleTree(name='root')
142
+
143
+ node = tree.get_node([])
144
+
145
+ assert node == tree
146
+
147
+ def test_walk_empty_tree(self):
148
+ """Test walking an empty tree."""
149
+ tree = ModuleTree(name='root')
150
+
151
+ paths = list(tree.walk())
152
+
153
+ assert len(paths) == 1
154
+ assert paths[0] == ([], tree)
155
+
156
+ def test_walk_with_children(self):
157
+ """Test walking a tree with children."""
158
+ tree = ModuleTree(name='root')
159
+ tree.add_endpoint(['users'], make_endpoint('get_users', '/users'))
160
+ tree.add_endpoint(['orders'], make_endpoint('get_orders', '/orders'))
161
+
162
+ paths = list(tree.walk())
163
+
164
+ # Root + users + orders
165
+ assert len(paths) == 3
166
+ path_names = ['/'.join(p[0]) for p in paths]
167
+ assert '' in path_names # root
168
+ assert 'orders' in path_names
169
+ assert 'users' in path_names
170
+
171
+ def test_walk_nested(self):
172
+ """Test walking a nested tree."""
173
+ tree = ModuleTree(name='root')
174
+ tree.add_endpoint(['api', 'v1', 'users'], make_endpoint('get_users', '/users'))
175
+
176
+ paths = list(tree.walk())
177
+
178
+ # Root + api + api/v1 + api/v1/users
179
+ assert len(paths) == 4
180
+
181
+ def test_walk_leaves_empty_tree(self):
182
+ """Test walk_leaves on empty tree."""
183
+ tree = ModuleTree(name='root')
184
+
185
+ leaves = list(tree.walk_leaves())
186
+
187
+ assert len(leaves) == 0
188
+
189
+ def test_walk_leaves_single_level(self):
190
+ """Test walk_leaves with endpoints at leaves."""
191
+ tree = ModuleTree(name='root')
192
+ tree.add_endpoint(['users'], make_endpoint('get_users', '/users'))
193
+ tree.add_endpoint(['orders'], make_endpoint('get_orders', '/orders'))
194
+
195
+ leaves = list(tree.walk_leaves())
196
+
197
+ assert len(leaves) == 2
198
+ leaf_names = ['/'.join(p[0]) for p in leaves]
199
+ assert 'users' in leaf_names
200
+ assert 'orders' in leaf_names
201
+
202
+ def test_walk_leaves_nested(self):
203
+ """Test walk_leaves with nested endpoints."""
204
+ tree = ModuleTree(name='root')
205
+ tree.add_endpoint(['api', 'users'], make_endpoint('get_users', '/users'))
206
+
207
+ leaves = list(tree.walk_leaves())
208
+
209
+ # Only api/users has endpoints
210
+ assert len(leaves) == 1
211
+ assert leaves[0][0] == ['api', 'users']
212
+
213
+ def test_count_endpoints_empty(self):
214
+ """Test counting endpoints in empty tree."""
215
+ tree = ModuleTree(name='root')
216
+
217
+ assert tree.count_endpoints() == 0
218
+
219
+ def test_count_endpoints_root_only(self):
220
+ """Test counting endpoints at root."""
221
+ tree = ModuleTree(name='root')
222
+ tree.add_endpoint([], make_endpoint('get_root', '/'))
223
+
224
+ assert tree.count_endpoints() == 1
225
+
226
+ def test_count_endpoints_nested(self):
227
+ """Test counting endpoints across tree."""
228
+ tree = ModuleTree(name='root')
229
+ tree.add_endpoint(['users'], make_endpoint('get_users', '/users'))
230
+ tree.add_endpoint(['users'], make_endpoint('create_user', '/users'))
231
+ tree.add_endpoint(['orders'], make_endpoint('get_orders', '/orders'))
232
+
233
+ assert tree.count_endpoints() == 3
234
+
235
+ def test_is_empty_true(self):
236
+ """Test is_empty on empty tree."""
237
+ tree = ModuleTree(name='root')
238
+
239
+ assert tree.is_empty() is True
240
+
241
+ def test_is_empty_false(self):
242
+ """Test is_empty on tree with endpoints."""
243
+ tree = ModuleTree(name='root')
244
+ tree.add_endpoint(['users'], make_endpoint('get_users', '/users'))
245
+
246
+ assert tree.is_empty() is False
247
+
248
+ def test_flatten_empty(self):
249
+ """Test flattening empty tree."""
250
+ tree = ModuleTree(name='root')
251
+
252
+ result = tree.flatten()
253
+
254
+ assert result == {}
255
+
256
+ def test_flatten_single_module(self):
257
+ """Test flattening tree with single module."""
258
+ tree = ModuleTree(name='root')
259
+ tree.add_endpoint(['users'], make_endpoint('get_users', '/users'))
260
+
261
+ result = tree.flatten()
262
+
263
+ assert 'users' in result
264
+ assert len(result['users']) == 1
265
+
266
+ def test_flatten_multiple_modules(self):
267
+ """Test flattening tree with multiple modules."""
268
+ tree = ModuleTree(name='root')
269
+ tree.add_endpoint(['users'], make_endpoint('get_users', '/users'))
270
+ tree.add_endpoint(['orders'], make_endpoint('get_orders', '/orders'))
271
+
272
+ result = tree.flatten()
273
+
274
+ assert len(result) == 2
275
+ assert 'users' in result
276
+ assert 'orders' in result
277
+
278
+ def test_flatten_nested(self):
279
+ """Test flattening nested tree."""
280
+ tree = ModuleTree(name='root')
281
+ tree.add_endpoint(['api', 'v1', 'users'], make_endpoint('get_users', '/users'))
282
+
283
+ result = tree.flatten()
284
+
285
+ assert 'api.v1.users' in result
286
+
287
+
288
+ class TestModuleTreeBuilder:
289
+ """Tests for the ModuleTreeBuilder class."""
290
+
291
+ def test_builder_initialization(self):
292
+ """Test builder initialization."""
293
+ config = ModuleSplitConfig(enabled=True)
294
+ builder = ModuleTreeBuilder(config)
295
+
296
+ assert builder.config == config
297
+ assert builder.resolver is not None
298
+
299
+ def test_build_empty_endpoints(self):
300
+ """Test building tree from empty endpoints list."""
301
+ config = ModuleSplitConfig(enabled=True)
302
+ builder = ModuleTreeBuilder(config)
303
+
304
+ tree = builder.build([])
305
+
306
+ assert tree.name == '__root__'
307
+ assert tree.is_empty()
308
+
309
+ def test_build_single_endpoint_tag_strategy(self):
310
+ """Test building tree with single endpoint using tag strategy."""
311
+ config = ModuleSplitConfig(
312
+ enabled=True,
313
+ strategy='tag',
314
+ min_endpoints=1, # Prevent consolidation
315
+ )
316
+ builder = ModuleTreeBuilder(config)
317
+ endpoint = make_endpoint('get_users', '/users', tags=['Users'])
318
+
319
+ tree = builder.build([endpoint])
320
+
321
+ assert 'users' in tree.children
322
+ assert len(tree.children['users'].endpoints) == 1
323
+
324
+ def test_build_single_endpoint_path_strategy(self):
325
+ """Test building tree with single endpoint using path strategy."""
326
+ config = ModuleSplitConfig(
327
+ enabled=True,
328
+ strategy='path',
329
+ global_strip_prefixes=[],
330
+ min_endpoints=1, # Prevent consolidation
331
+ )
332
+ builder = ModuleTreeBuilder(config)
333
+ endpoint = make_endpoint('get_users', '/users/123')
334
+
335
+ tree = builder.build([endpoint])
336
+
337
+ assert 'users' in tree.children
338
+ assert len(tree.children['users'].endpoints) == 1
339
+
340
+ def test_build_multiple_endpoints_same_module(self):
341
+ """Test building tree with multiple endpoints in same module."""
342
+ config = ModuleSplitConfig(
343
+ enabled=True,
344
+ strategy='tag',
345
+ min_endpoints=1, # Prevent consolidation
346
+ )
347
+ builder = ModuleTreeBuilder(config)
348
+ endpoints = [
349
+ make_endpoint('get_users', '/users', tags=['Users']),
350
+ make_endpoint('create_user', '/users', method='POST', tags=['Users']),
351
+ make_endpoint('get_user', '/users/{id}', tags=['Users']),
352
+ ]
353
+
354
+ tree = builder.build(endpoints)
355
+
356
+ assert 'users' in tree.children
357
+ assert len(tree.children['users'].endpoints) == 3
358
+
359
+ def test_build_multiple_modules(self):
360
+ """Test building tree with multiple modules."""
361
+ config = ModuleSplitConfig(
362
+ enabled=True,
363
+ strategy='tag',
364
+ min_endpoints=1, # Prevent consolidation
365
+ )
366
+ builder = ModuleTreeBuilder(config)
367
+ endpoints = [
368
+ make_endpoint('get_users', '/users', tags=['Users']),
369
+ make_endpoint('get_orders', '/orders', tags=['Orders']),
370
+ make_endpoint('get_products', '/products', tags=['Products']),
371
+ ]
372
+
373
+ tree = builder.build(endpoints)
374
+
375
+ assert len(tree.children) == 3
376
+ assert 'users' in tree.children
377
+ assert 'orders' in tree.children
378
+ assert 'products' in tree.children
379
+
380
+ def test_build_with_custom_module_map(self):
381
+ """Test building tree with custom module_map."""
382
+ config = ModuleSplitConfig(
383
+ enabled=True,
384
+ strategy='custom',
385
+ global_strip_prefixes=[],
386
+ module_map={
387
+ 'users': ['/users/*', '/users'],
388
+ 'orders': ['/orders/*'],
389
+ },
390
+ min_endpoints=1, # Prevent consolidation
391
+ )
392
+ builder = ModuleTreeBuilder(config)
393
+ endpoints = [
394
+ make_endpoint('get_users', '/users'),
395
+ make_endpoint('get_user', '/users/123'),
396
+ make_endpoint('get_orders', '/orders/456'),
397
+ ]
398
+
399
+ tree = builder.build(endpoints)
400
+
401
+ assert 'users' in tree.children
402
+ assert 'orders' in tree.children
403
+ assert len(tree.children['users'].endpoints) == 2
404
+ assert len(tree.children['orders'].endpoints) == 1
405
+
406
+ def test_build_with_fallback(self):
407
+ """Test that unmatched endpoints go to fallback module."""
408
+ config = ModuleSplitConfig(
409
+ enabled=True,
410
+ strategy='custom',
411
+ global_strip_prefixes=[],
412
+ module_map={'users': ['/users/*']},
413
+ fallback_module='misc',
414
+ min_endpoints=1, # Prevent consolidation
415
+ )
416
+ builder = ModuleTreeBuilder(config)
417
+ endpoints = [
418
+ make_endpoint('get_user', '/users/123'),
419
+ make_endpoint('get_health', '/health'),
420
+ ]
421
+
422
+ tree = builder.build(endpoints)
423
+
424
+ assert 'users' in tree.children
425
+ assert 'misc' in tree.children
426
+
427
+
428
+ class TestModuleConsolidation:
429
+ """Tests for small module consolidation."""
430
+
431
+ def test_consolidate_small_modules(self):
432
+ """Test that small modules are consolidated to fallback."""
433
+ config = ModuleSplitConfig(
434
+ enabled=True,
435
+ strategy='tag',
436
+ min_endpoints=3,
437
+ fallback_module='common',
438
+ )
439
+ builder = ModuleTreeBuilder(config)
440
+ endpoints = [
441
+ # Users has only 1 endpoint - should be consolidated
442
+ make_endpoint('get_users', '/users', tags=['Users']),
443
+ # Orders has 3 endpoints - should remain
444
+ make_endpoint('get_orders', '/orders', tags=['Orders']),
445
+ make_endpoint('create_order', '/orders', method='POST', tags=['Orders']),
446
+ make_endpoint('get_order', '/orders/{id}', tags=['Orders']),
447
+ ]
448
+
449
+ tree = builder.build(endpoints)
450
+
451
+ # Users should be consolidated into common
452
+ assert 'users' not in tree.children or tree.children['users'].is_empty()
453
+ assert 'orders' in tree.children
454
+ assert len(tree.children['orders'].endpoints) == 3
455
+ assert 'common' in tree.children
456
+ assert len(tree.children['common'].endpoints) == 1
457
+
458
+ def test_no_consolidation_when_min_endpoints_is_1(self):
459
+ """Test that consolidation is disabled when min_endpoints is 1."""
460
+ config = ModuleSplitConfig(
461
+ enabled=True,
462
+ strategy='tag',
463
+ min_endpoints=1,
464
+ )
465
+ builder = ModuleTreeBuilder(config)
466
+ endpoints = [
467
+ make_endpoint('get_users', '/users', tags=['Users']),
468
+ ]
469
+
470
+ tree = builder.build(endpoints)
471
+
472
+ assert 'users' in tree.children
473
+ assert len(tree.children['users'].endpoints) == 1
474
+
475
+
476
+ class TestBuildModuleTreeFunction:
477
+ """Tests for the convenience build_module_tree function."""
478
+
479
+ def test_build_module_tree_function(self):
480
+ """Test the convenience function."""
481
+ config = ModuleSplitConfig(
482
+ enabled=True,
483
+ strategy='tag',
484
+ min_endpoints=1, # Prevent consolidation
485
+ )
486
+ endpoints = [
487
+ make_endpoint('get_users', '/users', tags=['Users']),
488
+ ]
489
+
490
+ tree = build_module_tree(endpoints, config)
491
+
492
+ assert isinstance(tree, ModuleTree)
493
+ assert 'users' in tree.children
494
+
495
+
496
+ class TestTreeWithDefinitions:
497
+ """Tests for tree nodes with ModuleDefinitions attached."""
498
+
499
+ def test_definition_stored_on_node(self):
500
+ """Test that ModuleDefinition is stored on matching nodes."""
501
+ from otterapi.config import ModuleDefinition
502
+
503
+ config = ModuleSplitConfig(
504
+ enabled=True,
505
+ strategy='custom',
506
+ global_strip_prefixes=[],
507
+ module_map={
508
+ 'users': ModuleDefinition(
509
+ paths=['/users/*'],
510
+ description='User management endpoints',
511
+ ),
512
+ },
513
+ min_endpoints=1, # Prevent consolidation
514
+ )
515
+ builder = ModuleTreeBuilder(config)
516
+ endpoints = [
517
+ make_endpoint('get_user', '/users/123'),
518
+ ]
519
+
520
+ tree = builder.build(endpoints)
521
+
522
+ users_node = tree.children.get('users')
523
+ assert users_node is not None
524
+ assert users_node.definition is not None
525
+ assert users_node.description == 'User management endpoints'