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