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