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,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'
|