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,427 @@
1
+ """Integration tests for module splitting end-to-end workflow.
2
+
3
+ This module tests the complete module splitting flow from configuration
4
+ through code generation, verifying that the generated code is correct
5
+ and importable.
6
+ """
7
+
8
+ import ast
9
+
10
+ from otterapi.codegen.splitting import (
11
+ ModuleMapResolver,
12
+ ModuleTreeBuilder,
13
+ )
14
+ from otterapi.codegen.types import Endpoint
15
+ from otterapi.config import ModuleDefinition, 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.Return(value=ast.Constant(value=None))],
36
+ decorator_list=[],
37
+ returns=ast.Constant(value=None),
38
+ )
39
+ async_ast = ast.AsyncFunctionDef(
40
+ name=f'a{name}',
41
+ args=ast.arguments(
42
+ posonlyargs=[],
43
+ args=[],
44
+ kwonlyargs=[],
45
+ kw_defaults=[],
46
+ defaults=[],
47
+ ),
48
+ body=[ast.Return(value=ast.Constant(value=None))],
49
+ decorator_list=[],
50
+ returns=ast.Constant(value=None),
51
+ )
52
+
53
+ return Endpoint(
54
+ sync_ast=sync_ast,
55
+ async_ast=async_ast,
56
+ sync_fn_name=name,
57
+ async_fn_name=f'a{name}',
58
+ name=name,
59
+ method=method,
60
+ path=path,
61
+ tags=tags,
62
+ )
63
+
64
+
65
+ class TestEndToEndTagStrategy:
66
+ """End-to-end tests using tag-based strategy."""
67
+
68
+ def test_tag_based_splitting(self):
69
+ """Test complete flow with tag-based splitting."""
70
+ config = ModuleSplitConfig(
71
+ enabled=True,
72
+ strategy='tag',
73
+ min_endpoints=1,
74
+ )
75
+
76
+ # Create test endpoints
77
+ endpoints = [
78
+ make_endpoint('list_users', '/users', tags=['Users']),
79
+ make_endpoint('get_user', '/users/{id}', tags=['Users']),
80
+ make_endpoint('list_orders', '/orders', tags=['Orders']),
81
+ make_endpoint('create_order', '/orders', method='POST', tags=['Orders']),
82
+ ]
83
+
84
+ # Build tree
85
+ builder = ModuleTreeBuilder(config)
86
+ tree = builder.build(endpoints)
87
+
88
+ # Verify tree structure
89
+ assert 'users' in tree.children
90
+ assert 'orders' in tree.children
91
+ assert len(tree.children['users'].endpoints) == 2
92
+ assert len(tree.children['orders'].endpoints) == 2
93
+
94
+
95
+ class TestEndToEndPathStrategy:
96
+ """End-to-end tests using path-based strategy."""
97
+
98
+ def test_path_based_splitting(self):
99
+ """Test complete flow with path-based splitting."""
100
+ config = ModuleSplitConfig(
101
+ enabled=True,
102
+ strategy='path',
103
+ path_depth=1,
104
+ global_strip_prefixes=['/api'],
105
+ min_endpoints=1,
106
+ )
107
+
108
+ endpoints = [
109
+ make_endpoint('list_users', '/api/users'),
110
+ make_endpoint('get_user', '/api/users/{id}'),
111
+ make_endpoint('list_products', '/api/products'),
112
+ ]
113
+
114
+ builder = ModuleTreeBuilder(config)
115
+ tree = builder.build(endpoints)
116
+
117
+ assert 'users' in tree.children
118
+ assert 'products' in tree.children
119
+ assert len(tree.children['users'].endpoints) == 2
120
+ assert len(tree.children['products'].endpoints) == 1
121
+
122
+
123
+ class TestEndToEndCustomStrategy:
124
+ """End-to-end tests using custom module_map strategy."""
125
+
126
+ def test_custom_module_map(self):
127
+ """Test complete flow with custom module_map."""
128
+ config = ModuleSplitConfig(
129
+ enabled=True,
130
+ strategy='custom',
131
+ global_strip_prefixes=[],
132
+ module_map={
133
+ 'identity': ['/users/*', '/auth/*'],
134
+ 'commerce': ['/orders/*', '/products/*'],
135
+ 'health': ['/health', '/ready'],
136
+ },
137
+ fallback_module='misc',
138
+ min_endpoints=1,
139
+ )
140
+
141
+ endpoints = [
142
+ make_endpoint('get_user', '/users/123'),
143
+ make_endpoint('login', '/auth/login'),
144
+ make_endpoint('list_orders', '/orders/list'),
145
+ make_endpoint('health_check', '/health'),
146
+ make_endpoint('unknown', '/unknown'),
147
+ ]
148
+
149
+ builder = ModuleTreeBuilder(config)
150
+ tree = builder.build(endpoints)
151
+
152
+ assert 'identity' in tree.children
153
+ assert 'commerce' in tree.children
154
+ assert 'health' in tree.children
155
+ assert 'misc' in tree.children
156
+
157
+ assert len(tree.children['identity'].endpoints) == 2
158
+ assert len(tree.children['commerce'].endpoints) == 1
159
+ assert len(tree.children['health'].endpoints) == 1
160
+ assert len(tree.children['misc'].endpoints) == 1
161
+
162
+
163
+ class TestEndToEndHybridStrategy:
164
+ """End-to-end tests using hybrid strategy."""
165
+
166
+ def test_hybrid_strategy_priority(self):
167
+ """Test that hybrid strategy respects priority order."""
168
+ config = ModuleSplitConfig(
169
+ enabled=True,
170
+ strategy='hybrid',
171
+ global_strip_prefixes=['/api'],
172
+ module_map={
173
+ 'special': ['/special/*'],
174
+ },
175
+ min_endpoints=1,
176
+ )
177
+
178
+ endpoints = [
179
+ # Should match custom module_map
180
+ make_endpoint('special_endpoint', '/special/path'),
181
+ # Should use tag (no custom match)
182
+ make_endpoint('tagged_endpoint', '/api/other', tags=['Tagged']),
183
+ # Should use path (no custom match, no tags)
184
+ make_endpoint('path_endpoint', '/api/users'),
185
+ ]
186
+
187
+ builder = ModuleTreeBuilder(config)
188
+ tree = builder.build(endpoints)
189
+
190
+ assert 'special' in tree.children
191
+ assert 'tagged' in tree.children
192
+ assert 'users' in tree.children
193
+
194
+
195
+ class TestConsolidationBehavior:
196
+ """Tests for module consolidation behavior."""
197
+
198
+ def test_small_modules_consolidated(self):
199
+ """Test that modules with few endpoints are consolidated."""
200
+ config = ModuleSplitConfig(
201
+ enabled=True,
202
+ strategy='tag',
203
+ min_endpoints=3,
204
+ fallback_module='common',
205
+ )
206
+
207
+ endpoints = [
208
+ # Module with only 1 endpoint - should be consolidated
209
+ make_endpoint('single', '/single', tags=['Single']),
210
+ # Module with 3 endpoints - should remain
211
+ make_endpoint('multi1', '/multi', tags=['Multi']),
212
+ make_endpoint('multi2', '/multi', method='POST', tags=['Multi']),
213
+ make_endpoint('multi3', '/multi/{id}', tags=['Multi']),
214
+ ]
215
+
216
+ builder = ModuleTreeBuilder(config)
217
+ tree = builder.build(endpoints)
218
+
219
+ # Single should be consolidated
220
+ assert 'single' not in tree.children or tree.children['single'].is_empty()
221
+
222
+ # Multi should remain
223
+ assert 'multi' in tree.children
224
+ assert len(tree.children['multi'].endpoints) == 3
225
+
226
+ # Common should have the consolidated endpoint
227
+ assert 'common' in tree.children
228
+ assert len(tree.children['common'].endpoints) == 1
229
+
230
+
231
+ class TestResolverStrategies:
232
+ """Tests for resolver strategy selection."""
233
+
234
+ def test_none_strategy(self):
235
+ """Test that 'none' strategy goes to fallback."""
236
+ config = ModuleSplitConfig(
237
+ enabled=True,
238
+ strategy='none',
239
+ fallback_module='all',
240
+ )
241
+
242
+ resolver = ModuleMapResolver(config)
243
+ result = resolver.resolve('/users/123', 'GET', tags=['Users'])
244
+
245
+ # Even with tags, should go to fallback
246
+ assert result.module_path == ['all']
247
+ assert result.resolution == 'fallback'
248
+
249
+
250
+ class TestComplexScenarios:
251
+ """Tests for complex real-world scenarios."""
252
+
253
+ def test_petstore_like_api(self):
254
+ """Test with a Petstore-like API structure."""
255
+ config = ModuleSplitConfig(
256
+ enabled=True,
257
+ strategy='tag',
258
+ min_endpoints=1,
259
+ )
260
+
261
+ endpoints = [
262
+ # Pet endpoints
263
+ make_endpoint('list_pets', '/pet', tags=['pet']),
264
+ make_endpoint('add_pet', '/pet', method='POST', tags=['pet']),
265
+ make_endpoint('get_pet_by_id', '/pet/{petId}', tags=['pet']),
266
+ make_endpoint('update_pet', '/pet', method='PUT', tags=['pet']),
267
+ make_endpoint('find_pets_by_status', '/pet/findByStatus', tags=['pet']),
268
+ # Store endpoints
269
+ make_endpoint('get_inventory', '/store/inventory', tags=['store']),
270
+ make_endpoint('place_order', '/store/order', method='POST', tags=['store']),
271
+ make_endpoint('get_order_by_id', '/store/order/{orderId}', tags=['store']),
272
+ # User endpoints
273
+ make_endpoint('create_user', '/user', method='POST', tags=['user']),
274
+ make_endpoint('login_user', '/user/login', tags=['user']),
275
+ make_endpoint('logout_user', '/user/logout', tags=['user']),
276
+ make_endpoint('get_user_by_name', '/user/{username}', tags=['user']),
277
+ ]
278
+
279
+ builder = ModuleTreeBuilder(config)
280
+ tree = builder.build(endpoints)
281
+
282
+ assert 'pet' in tree.children
283
+ assert 'store' in tree.children
284
+ assert 'user' in tree.children
285
+
286
+ assert len(tree.children['pet'].endpoints) == 5
287
+ assert len(tree.children['store'].endpoints) == 3
288
+ assert len(tree.children['user'].endpoints) == 4
289
+
290
+ def test_versioned_api_with_prefix_stripping(self):
291
+ """Test API with version prefix stripping."""
292
+ config = ModuleSplitConfig(
293
+ enabled=True,
294
+ strategy='path',
295
+ path_depth=1,
296
+ global_strip_prefixes=['/api/v1', '/api/v2'],
297
+ min_endpoints=1,
298
+ )
299
+
300
+ endpoints = [
301
+ make_endpoint('v1_list_users', '/api/v1/users'),
302
+ make_endpoint('v1_get_user', '/api/v1/users/{id}'),
303
+ make_endpoint('v2_list_users', '/api/v2/users'),
304
+ make_endpoint('v1_list_orders', '/api/v1/orders'),
305
+ ]
306
+
307
+ builder = ModuleTreeBuilder(config)
308
+ tree = builder.build(endpoints)
309
+
310
+ # All should be grouped by path after prefix stripping
311
+ assert 'users' in tree.children
312
+ assert 'orders' in tree.children
313
+
314
+ # v1 and v2 users should be in same module
315
+ assert len(tree.children['users'].endpoints) == 3
316
+ assert len(tree.children['orders'].endpoints) == 1
317
+
318
+ def test_microservice_with_nested_modules(self):
319
+ """Test microservice-style API with nested module structure."""
320
+ config = ModuleSplitConfig(
321
+ enabled=True,
322
+ strategy='custom',
323
+ global_strip_prefixes=[],
324
+ module_map={
325
+ 'identity': ModuleDefinition(
326
+ paths=['/identity/**'],
327
+ modules={
328
+ 'users': ModuleDefinition(paths=['/users/*']),
329
+ 'roles': ModuleDefinition(paths=['/roles/*']),
330
+ },
331
+ ),
332
+ 'billing': ModuleDefinition(
333
+ paths=['/billing/**'],
334
+ ),
335
+ },
336
+ min_endpoints=1,
337
+ )
338
+
339
+ # The nested paths under identity won't match /users/* directly
340
+ # because the resolver applies strip_prefix logic
341
+ endpoints = [
342
+ make_endpoint('billing_invoice', '/billing/invoices'),
343
+ make_endpoint('identity_profile', '/identity/profile'),
344
+ ]
345
+
346
+ builder = ModuleTreeBuilder(config)
347
+ tree = builder.build(endpoints)
348
+
349
+ # Verify the modules are created
350
+ assert 'billing' in tree.children or 'identity' in tree.children
351
+
352
+
353
+ class TestTreeWalking:
354
+ """Tests for tree walking functionality."""
355
+
356
+ def test_walk_all_nodes(self):
357
+ """Test walking all nodes in the tree."""
358
+ config = ModuleSplitConfig(
359
+ enabled=True,
360
+ strategy='tag',
361
+ min_endpoints=1,
362
+ )
363
+
364
+ endpoints = [
365
+ make_endpoint('users_ep', '/users', tags=['Users']),
366
+ make_endpoint('orders_ep', '/orders', tags=['Orders']),
367
+ ]
368
+
369
+ builder = ModuleTreeBuilder(config)
370
+ tree = builder.build(endpoints)
371
+
372
+ all_nodes = list(tree.walk())
373
+
374
+ # Root + users + orders = 3 nodes
375
+ assert len(all_nodes) == 3
376
+
377
+ def test_walk_leaves_only(self):
378
+ """Test walking only leaf nodes with endpoints."""
379
+ config = ModuleSplitConfig(
380
+ enabled=True,
381
+ strategy='tag',
382
+ min_endpoints=1,
383
+ )
384
+
385
+ endpoints = [
386
+ make_endpoint('users_ep', '/users', tags=['Users']),
387
+ make_endpoint('orders_ep', '/orders', tags=['Orders']),
388
+ ]
389
+
390
+ builder = ModuleTreeBuilder(config)
391
+ tree = builder.build(endpoints)
392
+
393
+ leaves = list(tree.walk_leaves())
394
+
395
+ # Only users and orders are leaves with endpoints
396
+ assert len(leaves) == 2
397
+ paths = [leaf[0] for leaf in leaves]
398
+ assert ['users'] in paths
399
+ assert ['orders'] in paths
400
+
401
+
402
+ class TestFlattenedOutput:
403
+ """Tests for flattened tree output."""
404
+
405
+ def test_flatten_to_dict(self):
406
+ """Test flattening tree to dictionary."""
407
+ config = ModuleSplitConfig(
408
+ enabled=True,
409
+ strategy='tag',
410
+ min_endpoints=1,
411
+ )
412
+
413
+ endpoints = [
414
+ make_endpoint('users_ep1', '/users', tags=['Users']),
415
+ make_endpoint('users_ep2', '/users/{id}', tags=['Users']),
416
+ make_endpoint('orders_ep', '/orders', tags=['Orders']),
417
+ ]
418
+
419
+ builder = ModuleTreeBuilder(config)
420
+ tree = builder.build(endpoints)
421
+
422
+ flattened = tree.flatten()
423
+
424
+ assert 'users' in flattened
425
+ assert 'orders' in flattened
426
+ assert len(flattened['users']) == 2
427
+ assert len(flattened['orders']) == 1