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,319 @@
|
|
|
1
|
+
"""Tests for module splitting configuration models.
|
|
2
|
+
|
|
3
|
+
This module tests the configuration models used for module splitting:
|
|
4
|
+
- SplitStrategy enum
|
|
5
|
+
- ModuleDefinition model
|
|
6
|
+
- ModuleSplitConfig model
|
|
7
|
+
- Configuration normalization
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from otterapi.config import (
|
|
14
|
+
ModuleDefinition,
|
|
15
|
+
ModuleSplitConfig,
|
|
16
|
+
SplitStrategy,
|
|
17
|
+
_normalize_module_map,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestSplitStrategy:
|
|
22
|
+
"""Tests for the SplitStrategy enum."""
|
|
23
|
+
|
|
24
|
+
def test_enum_values(self):
|
|
25
|
+
"""Test that all expected strategy values exist."""
|
|
26
|
+
assert SplitStrategy.NONE.value == 'none'
|
|
27
|
+
assert SplitStrategy.PATH.value == 'path'
|
|
28
|
+
assert SplitStrategy.TAG.value == 'tag'
|
|
29
|
+
assert SplitStrategy.HYBRID.value == 'hybrid'
|
|
30
|
+
assert SplitStrategy.CUSTOM.value == 'custom'
|
|
31
|
+
|
|
32
|
+
def test_enum_from_string(self):
|
|
33
|
+
"""Test creating enum from string values."""
|
|
34
|
+
assert SplitStrategy('none') == SplitStrategy.NONE
|
|
35
|
+
assert SplitStrategy('path') == SplitStrategy.PATH
|
|
36
|
+
assert SplitStrategy('tag') == SplitStrategy.TAG
|
|
37
|
+
assert SplitStrategy('hybrid') == SplitStrategy.HYBRID
|
|
38
|
+
assert SplitStrategy('custom') == SplitStrategy.CUSTOM
|
|
39
|
+
|
|
40
|
+
def test_invalid_strategy(self):
|
|
41
|
+
"""Test that invalid strategy values raise an error."""
|
|
42
|
+
with pytest.raises(ValueError):
|
|
43
|
+
SplitStrategy('invalid')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestModuleDefinition:
|
|
47
|
+
"""Tests for the ModuleDefinition model."""
|
|
48
|
+
|
|
49
|
+
def test_empty_definition(self):
|
|
50
|
+
"""Test creating an empty ModuleDefinition."""
|
|
51
|
+
definition = ModuleDefinition()
|
|
52
|
+
assert definition.paths == []
|
|
53
|
+
assert definition.modules == {}
|
|
54
|
+
assert definition.strip_prefix is None
|
|
55
|
+
assert definition.package_prefix is None
|
|
56
|
+
assert definition.file_name is None
|
|
57
|
+
assert definition.description is None
|
|
58
|
+
|
|
59
|
+
def test_with_paths(self):
|
|
60
|
+
"""Test ModuleDefinition with paths."""
|
|
61
|
+
definition = ModuleDefinition(paths=['/users/*', '/user/*'])
|
|
62
|
+
assert definition.paths == ['/users/*', '/user/*']
|
|
63
|
+
|
|
64
|
+
def test_with_nested_modules(self):
|
|
65
|
+
"""Test ModuleDefinition with nested modules."""
|
|
66
|
+
child = ModuleDefinition(paths=['/child/*'])
|
|
67
|
+
definition = ModuleDefinition(modules={'child': child})
|
|
68
|
+
assert 'child' in definition.modules
|
|
69
|
+
assert definition.modules['child'].paths == ['/child/*']
|
|
70
|
+
|
|
71
|
+
def test_with_strip_prefix(self):
|
|
72
|
+
"""Test ModuleDefinition with strip_prefix."""
|
|
73
|
+
definition = ModuleDefinition(
|
|
74
|
+
paths=['/users/*'],
|
|
75
|
+
strip_prefix='/api/v1',
|
|
76
|
+
)
|
|
77
|
+
assert definition.strip_prefix == '/api/v1'
|
|
78
|
+
|
|
79
|
+
def test_with_description(self):
|
|
80
|
+
"""Test ModuleDefinition with description."""
|
|
81
|
+
definition = ModuleDefinition(
|
|
82
|
+
paths=['/users/*'],
|
|
83
|
+
description='User management endpoints',
|
|
84
|
+
)
|
|
85
|
+
assert definition.description == 'User management endpoints'
|
|
86
|
+
|
|
87
|
+
def test_full_definition(self):
|
|
88
|
+
"""Test ModuleDefinition with all fields."""
|
|
89
|
+
definition = ModuleDefinition(
|
|
90
|
+
paths=['/users/*'],
|
|
91
|
+
modules={'admin': ModuleDefinition(paths=['/admin/*'])},
|
|
92
|
+
strip_prefix='/api',
|
|
93
|
+
package_prefix='api.users',
|
|
94
|
+
file_name='user_endpoints.py',
|
|
95
|
+
description='User endpoints',
|
|
96
|
+
)
|
|
97
|
+
assert definition.paths == ['/users/*']
|
|
98
|
+
assert 'admin' in definition.modules
|
|
99
|
+
assert definition.strip_prefix == '/api'
|
|
100
|
+
assert definition.package_prefix == 'api.users'
|
|
101
|
+
assert definition.file_name == 'user_endpoints.py'
|
|
102
|
+
assert definition.description == 'User endpoints'
|
|
103
|
+
|
|
104
|
+
def test_extra_fields_forbidden(self):
|
|
105
|
+
"""Test that extra fields are not allowed."""
|
|
106
|
+
with pytest.raises(ValidationError):
|
|
107
|
+
ModuleDefinition(paths=['/users/*'], extra_field='not allowed')
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestModuleSplitConfig:
|
|
111
|
+
"""Tests for the ModuleSplitConfig model."""
|
|
112
|
+
|
|
113
|
+
def test_default_config(self):
|
|
114
|
+
"""Test default ModuleSplitConfig values."""
|
|
115
|
+
config = ModuleSplitConfig()
|
|
116
|
+
assert config.enabled is False
|
|
117
|
+
assert config.strategy == SplitStrategy.HYBRID
|
|
118
|
+
assert '/api' in config.global_strip_prefixes
|
|
119
|
+
assert config.path_depth == 1
|
|
120
|
+
assert config.min_endpoints == 2
|
|
121
|
+
assert config.fallback_module == 'common'
|
|
122
|
+
assert config.module_map == {}
|
|
123
|
+
assert config.flat_structure is False
|
|
124
|
+
assert config.split_models is False
|
|
125
|
+
assert config.shared_models_module == '_models'
|
|
126
|
+
|
|
127
|
+
def test_enabled_config(self):
|
|
128
|
+
"""Test enabled ModuleSplitConfig."""
|
|
129
|
+
config = ModuleSplitConfig(enabled=True, strategy='tag')
|
|
130
|
+
assert config.enabled is True
|
|
131
|
+
assert config.strategy == SplitStrategy.TAG
|
|
132
|
+
|
|
133
|
+
def test_strategy_normalization(self):
|
|
134
|
+
"""Test that string strategies are converted to enum."""
|
|
135
|
+
config = ModuleSplitConfig(strategy='path')
|
|
136
|
+
assert config.strategy == SplitStrategy.PATH
|
|
137
|
+
|
|
138
|
+
config = ModuleSplitConfig(strategy='CUSTOM')
|
|
139
|
+
assert config.strategy == SplitStrategy.CUSTOM
|
|
140
|
+
|
|
141
|
+
def test_path_depth_validation(self):
|
|
142
|
+
"""Test path_depth validation bounds."""
|
|
143
|
+
# Valid values
|
|
144
|
+
ModuleSplitConfig(path_depth=1)
|
|
145
|
+
ModuleSplitConfig(path_depth=5)
|
|
146
|
+
|
|
147
|
+
# Invalid values
|
|
148
|
+
with pytest.raises(ValidationError):
|
|
149
|
+
ModuleSplitConfig(path_depth=0)
|
|
150
|
+
with pytest.raises(ValidationError):
|
|
151
|
+
ModuleSplitConfig(path_depth=6)
|
|
152
|
+
|
|
153
|
+
def test_min_endpoints_validation(self):
|
|
154
|
+
"""Test min_endpoints validation."""
|
|
155
|
+
ModuleSplitConfig(min_endpoints=1)
|
|
156
|
+
ModuleSplitConfig(min_endpoints=100)
|
|
157
|
+
|
|
158
|
+
with pytest.raises(ValidationError):
|
|
159
|
+
ModuleSplitConfig(min_endpoints=0)
|
|
160
|
+
|
|
161
|
+
def test_custom_fallback_module(self):
|
|
162
|
+
"""Test custom fallback module name."""
|
|
163
|
+
config = ModuleSplitConfig(fallback_module='misc')
|
|
164
|
+
assert config.fallback_module == 'misc'
|
|
165
|
+
|
|
166
|
+
def test_flat_structure(self):
|
|
167
|
+
"""Test flat structure option."""
|
|
168
|
+
config = ModuleSplitConfig(flat_structure=True)
|
|
169
|
+
assert config.flat_structure is True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestModuleMapNormalization:
|
|
173
|
+
"""Tests for module_map normalization."""
|
|
174
|
+
|
|
175
|
+
def test_normalize_string_pattern(self):
|
|
176
|
+
"""Test normalizing a single string pattern."""
|
|
177
|
+
module_map = {'users': '/users/*'}
|
|
178
|
+
normalized = _normalize_module_map(module_map)
|
|
179
|
+
|
|
180
|
+
assert 'users' in normalized
|
|
181
|
+
assert isinstance(normalized['users'], ModuleDefinition)
|
|
182
|
+
assert normalized['users'].paths == ['/users/*']
|
|
183
|
+
|
|
184
|
+
def test_normalize_list_patterns(self):
|
|
185
|
+
"""Test normalizing a list of patterns."""
|
|
186
|
+
module_map = {'users': ['/users/*', '/user/*']}
|
|
187
|
+
normalized = _normalize_module_map(module_map)
|
|
188
|
+
|
|
189
|
+
assert 'users' in normalized
|
|
190
|
+
assert normalized['users'].paths == ['/users/*', '/user/*']
|
|
191
|
+
|
|
192
|
+
def test_normalize_module_definition(self):
|
|
193
|
+
"""Test that ModuleDefinition passes through."""
|
|
194
|
+
definition = ModuleDefinition(paths=['/users/*'], description='Users')
|
|
195
|
+
module_map = {'users': definition}
|
|
196
|
+
normalized = _normalize_module_map(module_map)
|
|
197
|
+
|
|
198
|
+
assert normalized['users'] == definition
|
|
199
|
+
|
|
200
|
+
def test_normalize_dict_as_definition(self):
|
|
201
|
+
"""Test normalizing a dict that should become ModuleDefinition."""
|
|
202
|
+
module_map = {
|
|
203
|
+
'users': {
|
|
204
|
+
'paths': ['/users/*'],
|
|
205
|
+
'description': 'User endpoints',
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
normalized = _normalize_module_map(module_map)
|
|
209
|
+
|
|
210
|
+
assert normalized['users'].paths == ['/users/*']
|
|
211
|
+
assert normalized['users'].description == 'User endpoints'
|
|
212
|
+
|
|
213
|
+
def test_normalize_nested_shorthand(self):
|
|
214
|
+
"""Test normalizing nested module shorthand."""
|
|
215
|
+
module_map = {
|
|
216
|
+
'identity': {
|
|
217
|
+
'users': ['/users/*'],
|
|
218
|
+
'auth': ['/auth/*', '/login'],
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
normalized = _normalize_module_map(module_map)
|
|
222
|
+
|
|
223
|
+
assert 'identity' in normalized
|
|
224
|
+
assert 'users' in normalized['identity'].modules
|
|
225
|
+
assert 'auth' in normalized['identity'].modules
|
|
226
|
+
assert normalized['identity'].modules['users'].paths == ['/users/*']
|
|
227
|
+
assert normalized['identity'].modules['auth'].paths == ['/auth/*', '/login']
|
|
228
|
+
|
|
229
|
+
def test_normalize_deeply_nested(self):
|
|
230
|
+
"""Test normalizing deeply nested modules."""
|
|
231
|
+
module_map = {
|
|
232
|
+
'api': {
|
|
233
|
+
'v1': {
|
|
234
|
+
'users': ['/users/*'],
|
|
235
|
+
'admin': {
|
|
236
|
+
'roles': ['/roles/*'],
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
normalized = _normalize_module_map(module_map)
|
|
242
|
+
|
|
243
|
+
assert 'api' in normalized
|
|
244
|
+
api_def = normalized['api']
|
|
245
|
+
assert 'v1' in api_def.modules
|
|
246
|
+
v1_def = api_def.modules['v1']
|
|
247
|
+
assert 'users' in v1_def.modules
|
|
248
|
+
assert 'admin' in v1_def.modules
|
|
249
|
+
admin_def = v1_def.modules['admin']
|
|
250
|
+
assert 'roles' in admin_def.modules
|
|
251
|
+
assert admin_def.modules['roles'].paths == ['/roles/*']
|
|
252
|
+
|
|
253
|
+
def test_config_normalizes_module_map(self):
|
|
254
|
+
"""Test that ModuleSplitConfig normalizes module_map on creation."""
|
|
255
|
+
config = ModuleSplitConfig(
|
|
256
|
+
enabled=True,
|
|
257
|
+
module_map={
|
|
258
|
+
'users': ['/users/*'],
|
|
259
|
+
'identity': {
|
|
260
|
+
'auth': ['/auth/*'],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
assert isinstance(config.module_map['users'], ModuleDefinition)
|
|
266
|
+
assert config.module_map['users'].paths == ['/users/*']
|
|
267
|
+
assert isinstance(config.module_map['identity'], ModuleDefinition)
|
|
268
|
+
assert 'auth' in config.module_map['identity'].modules
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TestDocumentConfigIntegration:
|
|
272
|
+
"""Tests for ModuleSplitConfig integration with DocumentConfig."""
|
|
273
|
+
|
|
274
|
+
def test_default_module_split(self):
|
|
275
|
+
"""Test that DocumentConfig has default ModuleSplitConfig."""
|
|
276
|
+
from otterapi.config import DocumentConfig
|
|
277
|
+
|
|
278
|
+
config = DocumentConfig(
|
|
279
|
+
source='https://example.com/openapi.json',
|
|
280
|
+
output='./client',
|
|
281
|
+
)
|
|
282
|
+
assert isinstance(config.module_split, ModuleSplitConfig)
|
|
283
|
+
assert config.module_split.enabled is False
|
|
284
|
+
|
|
285
|
+
def test_custom_module_split(self):
|
|
286
|
+
"""Test DocumentConfig with custom module split config."""
|
|
287
|
+
from otterapi.config import DocumentConfig
|
|
288
|
+
|
|
289
|
+
config = DocumentConfig(
|
|
290
|
+
source='https://example.com/openapi.json',
|
|
291
|
+
output='./client',
|
|
292
|
+
module_split=ModuleSplitConfig(
|
|
293
|
+
enabled=True,
|
|
294
|
+
strategy='tag',
|
|
295
|
+
fallback_module='misc',
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
assert config.module_split.enabled is True
|
|
299
|
+
assert config.module_split.strategy == SplitStrategy.TAG
|
|
300
|
+
assert config.module_split.fallback_module == 'misc'
|
|
301
|
+
|
|
302
|
+
def test_module_split_from_dict(self):
|
|
303
|
+
"""Test DocumentConfig with module_split as dict."""
|
|
304
|
+
from otterapi.config import DocumentConfig
|
|
305
|
+
|
|
306
|
+
config = DocumentConfig(
|
|
307
|
+
source='https://example.com/openapi.json',
|
|
308
|
+
output='./client',
|
|
309
|
+
module_split={
|
|
310
|
+
'enabled': True,
|
|
311
|
+
'strategy': 'custom',
|
|
312
|
+
'module_map': {
|
|
313
|
+
'users': ['/users/*'],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
assert config.module_split.enabled is True
|
|
318
|
+
assert config.module_split.strategy == SplitStrategy.CUSTOM
|
|
319
|
+
assert 'users' in config.module_split.module_map
|