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,1101 @@
|
|
|
1
|
+
"""Tests for pagination configuration and code generation."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from otterapi.codegen.endpoints import (
|
|
10
|
+
build_standalone_paginated_fn,
|
|
11
|
+
build_standalone_paginated_iter_fn,
|
|
12
|
+
)
|
|
13
|
+
from otterapi.codegen.pagination import (
|
|
14
|
+
PAGINATION_MODULE_CONTENT,
|
|
15
|
+
PaginationMethodConfig,
|
|
16
|
+
endpoint_is_paginated,
|
|
17
|
+
generate_pagination_module,
|
|
18
|
+
get_pagination_config_for_endpoint,
|
|
19
|
+
)
|
|
20
|
+
from otterapi.config import (
|
|
21
|
+
EndpointPaginationConfig,
|
|
22
|
+
PaginationConfig,
|
|
23
|
+
PaginationStyle,
|
|
24
|
+
ResolvedPaginationConfig,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestEndpointPaginationConfig:
|
|
29
|
+
"""Tests for EndpointPaginationConfig model."""
|
|
30
|
+
|
|
31
|
+
def test_default_values(self):
|
|
32
|
+
"""Test that default values are None (inherit from parent)."""
|
|
33
|
+
config = EndpointPaginationConfig()
|
|
34
|
+
assert config.enabled is None
|
|
35
|
+
assert config.style is None
|
|
36
|
+
assert config.offset_param is None
|
|
37
|
+
assert config.limit_param is None
|
|
38
|
+
assert config.cursor_param is None
|
|
39
|
+
assert config.page_param is None
|
|
40
|
+
assert config.per_page_param is None
|
|
41
|
+
assert config.data_path is None
|
|
42
|
+
assert config.total_path is None
|
|
43
|
+
assert config.next_cursor_path is None
|
|
44
|
+
assert config.total_pages_path is None
|
|
45
|
+
assert config.default_page_size is None
|
|
46
|
+
assert config.max_page_size is None
|
|
47
|
+
|
|
48
|
+
def test_explicit_values(self):
|
|
49
|
+
"""Test setting explicit values."""
|
|
50
|
+
config = EndpointPaginationConfig(
|
|
51
|
+
enabled=True,
|
|
52
|
+
style='offset',
|
|
53
|
+
offset_param='skip',
|
|
54
|
+
limit_param='take',
|
|
55
|
+
data_path='data.items',
|
|
56
|
+
total_path='meta.total',
|
|
57
|
+
default_page_size=50,
|
|
58
|
+
)
|
|
59
|
+
assert config.enabled is True
|
|
60
|
+
assert config.style == PaginationStyle.OFFSET
|
|
61
|
+
assert config.offset_param == 'skip'
|
|
62
|
+
assert config.limit_param == 'take'
|
|
63
|
+
assert config.data_path == 'data.items'
|
|
64
|
+
assert config.total_path == 'meta.total'
|
|
65
|
+
assert config.default_page_size == 50
|
|
66
|
+
|
|
67
|
+
def test_style_normalization(self):
|
|
68
|
+
"""Test that style strings are normalized to enum."""
|
|
69
|
+
config = EndpointPaginationConfig(style='CURSOR')
|
|
70
|
+
assert config.style == PaginationStyle.CURSOR
|
|
71
|
+
|
|
72
|
+
config = EndpointPaginationConfig(style='page')
|
|
73
|
+
assert config.style == PaginationStyle.PAGE
|
|
74
|
+
|
|
75
|
+
def test_rejects_extra_fields(self):
|
|
76
|
+
"""Test that extra fields are rejected."""
|
|
77
|
+
with pytest.raises(Exception):
|
|
78
|
+
EndpointPaginationConfig(unknown_field='value')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestPaginationConfig:
|
|
82
|
+
"""Tests for PaginationConfig model."""
|
|
83
|
+
|
|
84
|
+
def test_default_values(self):
|
|
85
|
+
"""Test default configuration values."""
|
|
86
|
+
config = PaginationConfig()
|
|
87
|
+
assert config.enabled is False
|
|
88
|
+
assert config.default_style == PaginationStyle.OFFSET
|
|
89
|
+
assert config.default_page_size == 100
|
|
90
|
+
assert config.default_data_path is None
|
|
91
|
+
assert config.default_offset_param == 'offset'
|
|
92
|
+
assert config.default_limit_param == 'limit'
|
|
93
|
+
assert config.default_cursor_param == 'cursor'
|
|
94
|
+
assert config.default_page_param == 'page'
|
|
95
|
+
assert config.default_per_page_param == 'per_page'
|
|
96
|
+
assert config.endpoints == {}
|
|
97
|
+
|
|
98
|
+
def test_enabled_with_defaults(self):
|
|
99
|
+
"""Test enabled config uses correct defaults."""
|
|
100
|
+
config = PaginationConfig(enabled=True)
|
|
101
|
+
assert config.enabled is True
|
|
102
|
+
assert config.default_style == PaginationStyle.OFFSET
|
|
103
|
+
assert config.default_page_size == 100
|
|
104
|
+
|
|
105
|
+
def test_with_endpoints(self):
|
|
106
|
+
"""Test configuration with endpoint overrides."""
|
|
107
|
+
config = PaginationConfig(
|
|
108
|
+
enabled=True,
|
|
109
|
+
endpoints={
|
|
110
|
+
'list_users': EndpointPaginationConfig(
|
|
111
|
+
style='offset',
|
|
112
|
+
data_path='users',
|
|
113
|
+
),
|
|
114
|
+
'list_items': EndpointPaginationConfig(
|
|
115
|
+
style='cursor',
|
|
116
|
+
cursor_param='after',
|
|
117
|
+
next_cursor_path='meta.next',
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
assert 'list_users' in config.endpoints
|
|
122
|
+
assert config.endpoints['list_users'].data_path == 'users'
|
|
123
|
+
assert config.endpoints['list_items'].style == PaginationStyle.CURSOR
|
|
124
|
+
|
|
125
|
+
def test_style_normalization(self):
|
|
126
|
+
"""Test that default_style strings are normalized."""
|
|
127
|
+
config = PaginationConfig(default_style='cursor')
|
|
128
|
+
assert config.default_style == PaginationStyle.CURSOR
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestShouldGenerateForEndpoint:
|
|
132
|
+
"""Tests for PaginationConfig.should_generate_for_endpoint method."""
|
|
133
|
+
|
|
134
|
+
def test_disabled_returns_false(self):
|
|
135
|
+
"""Test that disabled config returns False."""
|
|
136
|
+
config = PaginationConfig(enabled=False)
|
|
137
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_users')
|
|
138
|
+
assert should_generate is False
|
|
139
|
+
assert resolved is None
|
|
140
|
+
|
|
141
|
+
def test_no_endpoint_config_returns_false(self):
|
|
142
|
+
"""Test that endpoints not configured return False."""
|
|
143
|
+
config = PaginationConfig(enabled=True)
|
|
144
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_users')
|
|
145
|
+
assert should_generate is False
|
|
146
|
+
assert resolved is None
|
|
147
|
+
|
|
148
|
+
def test_endpoint_explicitly_disabled(self):
|
|
149
|
+
"""Test that explicitly disabled endpoints return False."""
|
|
150
|
+
config = PaginationConfig(
|
|
151
|
+
enabled=True,
|
|
152
|
+
endpoints={
|
|
153
|
+
'list_users': EndpointPaginationConfig(enabled=False),
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_users')
|
|
157
|
+
assert should_generate is False
|
|
158
|
+
assert resolved is None
|
|
159
|
+
|
|
160
|
+
def test_endpoint_configured_offset(self):
|
|
161
|
+
"""Test configured endpoint with offset pagination."""
|
|
162
|
+
config = PaginationConfig(
|
|
163
|
+
enabled=True,
|
|
164
|
+
endpoints={
|
|
165
|
+
'list_users': EndpointPaginationConfig(
|
|
166
|
+
style='offset',
|
|
167
|
+
data_path='users',
|
|
168
|
+
total_path='total',
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_users')
|
|
173
|
+
assert should_generate is True
|
|
174
|
+
assert resolved is not None
|
|
175
|
+
assert resolved.style == PaginationStyle.OFFSET
|
|
176
|
+
assert resolved.data_path == 'users'
|
|
177
|
+
assert resolved.total_path == 'total'
|
|
178
|
+
assert resolved.offset_param == 'offset' # default
|
|
179
|
+
assert resolved.limit_param == 'limit' # default
|
|
180
|
+
|
|
181
|
+
def test_endpoint_configured_cursor(self):
|
|
182
|
+
"""Test configured endpoint with cursor pagination."""
|
|
183
|
+
config = PaginationConfig(
|
|
184
|
+
enabled=True,
|
|
185
|
+
endpoints={
|
|
186
|
+
'list_items': EndpointPaginationConfig(
|
|
187
|
+
style='cursor',
|
|
188
|
+
cursor_param='after',
|
|
189
|
+
next_cursor_path='meta.next_cursor',
|
|
190
|
+
data_path='items',
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_items')
|
|
195
|
+
assert should_generate is True
|
|
196
|
+
assert resolved is not None
|
|
197
|
+
assert resolved.style == PaginationStyle.CURSOR
|
|
198
|
+
assert resolved.cursor_param == 'after'
|
|
199
|
+
assert resolved.next_cursor_path == 'meta.next_cursor'
|
|
200
|
+
assert resolved.data_path == 'items'
|
|
201
|
+
|
|
202
|
+
def test_endpoint_configured_page(self):
|
|
203
|
+
"""Test configured endpoint with page pagination."""
|
|
204
|
+
config = PaginationConfig(
|
|
205
|
+
enabled=True,
|
|
206
|
+
endpoints={
|
|
207
|
+
'list_orders': EndpointPaginationConfig(
|
|
208
|
+
style='page',
|
|
209
|
+
page_param='p',
|
|
210
|
+
per_page_param='size',
|
|
211
|
+
total_pages_path='pagination.total_pages',
|
|
212
|
+
),
|
|
213
|
+
},
|
|
214
|
+
)
|
|
215
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_orders')
|
|
216
|
+
assert should_generate is True
|
|
217
|
+
assert resolved is not None
|
|
218
|
+
assert resolved.style == PaginationStyle.PAGE
|
|
219
|
+
assert resolved.page_param == 'p'
|
|
220
|
+
assert resolved.per_page_param == 'size'
|
|
221
|
+
assert resolved.total_pages_path == 'pagination.total_pages'
|
|
222
|
+
|
|
223
|
+
def test_inherits_defaults(self):
|
|
224
|
+
"""Test that endpoint config inherits defaults."""
|
|
225
|
+
config = PaginationConfig(
|
|
226
|
+
enabled=True,
|
|
227
|
+
default_style=PaginationStyle.OFFSET,
|
|
228
|
+
default_page_size=50,
|
|
229
|
+
default_data_path='data',
|
|
230
|
+
default_offset_param='skip',
|
|
231
|
+
default_limit_param='take',
|
|
232
|
+
endpoints={
|
|
233
|
+
'list_users': EndpointPaginationConfig(), # minimal config
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_users')
|
|
237
|
+
assert should_generate is True
|
|
238
|
+
assert resolved is not None
|
|
239
|
+
assert resolved.style == PaginationStyle.OFFSET
|
|
240
|
+
assert resolved.default_page_size == 50
|
|
241
|
+
assert resolved.data_path == 'data'
|
|
242
|
+
assert resolved.offset_param == 'skip'
|
|
243
|
+
assert resolved.limit_param == 'take'
|
|
244
|
+
|
|
245
|
+
def test_endpoint_overrides_defaults(self):
|
|
246
|
+
"""Test that endpoint config overrides defaults."""
|
|
247
|
+
config = PaginationConfig(
|
|
248
|
+
enabled=True,
|
|
249
|
+
default_page_size=100,
|
|
250
|
+
default_data_path='data',
|
|
251
|
+
endpoints={
|
|
252
|
+
'list_users': EndpointPaginationConfig(
|
|
253
|
+
default_page_size=25,
|
|
254
|
+
data_path='users',
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
should_generate, resolved = config.should_generate_for_endpoint('list_users')
|
|
259
|
+
assert should_generate is True
|
|
260
|
+
assert resolved is not None
|
|
261
|
+
assert resolved.default_page_size == 25
|
|
262
|
+
assert resolved.data_path == 'users'
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class TestPaginationMethodConfig:
|
|
266
|
+
"""Tests for PaginationMethodConfig dataclass."""
|
|
267
|
+
|
|
268
|
+
def test_default_values(self):
|
|
269
|
+
"""Test default values."""
|
|
270
|
+
config = PaginationMethodConfig()
|
|
271
|
+
assert config.style == 'offset'
|
|
272
|
+
assert config.offset_param == 'offset'
|
|
273
|
+
assert config.limit_param == 'limit'
|
|
274
|
+
assert config.cursor_param == 'cursor'
|
|
275
|
+
assert config.page_param == 'page'
|
|
276
|
+
assert config.per_page_param == 'per_page'
|
|
277
|
+
assert config.data_path is None
|
|
278
|
+
assert config.total_path is None
|
|
279
|
+
assert config.next_cursor_path is None
|
|
280
|
+
assert config.total_pages_path is None
|
|
281
|
+
assert config.default_page_size == 100
|
|
282
|
+
assert config.max_page_size is None
|
|
283
|
+
|
|
284
|
+
def test_custom_values(self):
|
|
285
|
+
"""Test setting custom values."""
|
|
286
|
+
config = PaginationMethodConfig(
|
|
287
|
+
style='cursor',
|
|
288
|
+
cursor_param='after',
|
|
289
|
+
data_path='items',
|
|
290
|
+
next_cursor_path='meta.next',
|
|
291
|
+
default_page_size=50,
|
|
292
|
+
)
|
|
293
|
+
assert config.style == 'cursor'
|
|
294
|
+
assert config.cursor_param == 'after'
|
|
295
|
+
assert config.data_path == 'items'
|
|
296
|
+
assert config.next_cursor_path == 'meta.next'
|
|
297
|
+
assert config.default_page_size == 50
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class TestEndpointIsPaginated:
|
|
301
|
+
"""Tests for endpoint_is_paginated function."""
|
|
302
|
+
|
|
303
|
+
def test_disabled_config(self):
|
|
304
|
+
"""Test with disabled pagination config."""
|
|
305
|
+
config = PaginationConfig(enabled=False)
|
|
306
|
+
assert endpoint_is_paginated('list_users', config) is False
|
|
307
|
+
|
|
308
|
+
def test_unconfigured_endpoint(self):
|
|
309
|
+
"""Test with endpoint not in config."""
|
|
310
|
+
config = PaginationConfig(enabled=True)
|
|
311
|
+
assert endpoint_is_paginated('list_users', config) is False
|
|
312
|
+
|
|
313
|
+
def test_configured_endpoint(self):
|
|
314
|
+
"""Test with configured endpoint."""
|
|
315
|
+
config = PaginationConfig(
|
|
316
|
+
enabled=True,
|
|
317
|
+
endpoints={
|
|
318
|
+
'list_users': EndpointPaginationConfig(style='offset'),
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
assert endpoint_is_paginated('list_users', config) is True
|
|
322
|
+
assert endpoint_is_paginated('get_user', config) is False
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class TestGetPaginationConfigForEndpoint:
|
|
326
|
+
"""Tests for get_pagination_config_for_endpoint function."""
|
|
327
|
+
|
|
328
|
+
def test_disabled_config(self):
|
|
329
|
+
"""Test with disabled pagination config."""
|
|
330
|
+
config = PaginationConfig(enabled=False)
|
|
331
|
+
result = get_pagination_config_for_endpoint('list_users', config)
|
|
332
|
+
assert result is None
|
|
333
|
+
|
|
334
|
+
def test_unconfigured_endpoint(self):
|
|
335
|
+
"""Test with endpoint not in config."""
|
|
336
|
+
config = PaginationConfig(enabled=True)
|
|
337
|
+
result = get_pagination_config_for_endpoint('list_users', config)
|
|
338
|
+
assert result is None
|
|
339
|
+
|
|
340
|
+
def test_configured_endpoint(self):
|
|
341
|
+
"""Test with configured endpoint."""
|
|
342
|
+
config = PaginationConfig(
|
|
343
|
+
enabled=True,
|
|
344
|
+
endpoints={
|
|
345
|
+
'list_users': EndpointPaginationConfig(
|
|
346
|
+
style='offset',
|
|
347
|
+
data_path='users',
|
|
348
|
+
total_path='total',
|
|
349
|
+
default_page_size=50,
|
|
350
|
+
),
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
result = get_pagination_config_for_endpoint('list_users', config)
|
|
354
|
+
assert result is not None
|
|
355
|
+
assert isinstance(result, PaginationMethodConfig)
|
|
356
|
+
assert result.style == 'offset'
|
|
357
|
+
assert result.data_path == 'users'
|
|
358
|
+
assert result.total_path == 'total'
|
|
359
|
+
assert result.default_page_size == 50
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class TestResolvedPaginationConfig:
|
|
363
|
+
"""Tests for ResolvedPaginationConfig model."""
|
|
364
|
+
|
|
365
|
+
def test_all_fields(self):
|
|
366
|
+
"""Test creating a resolved config with all fields."""
|
|
367
|
+
config = ResolvedPaginationConfig(
|
|
368
|
+
style=PaginationStyle.OFFSET,
|
|
369
|
+
offset_param='offset',
|
|
370
|
+
limit_param='limit',
|
|
371
|
+
cursor_param='cursor',
|
|
372
|
+
page_param='page',
|
|
373
|
+
per_page_param='per_page',
|
|
374
|
+
data_path='data.items',
|
|
375
|
+
total_path='meta.total',
|
|
376
|
+
next_cursor_path=None,
|
|
377
|
+
total_pages_path=None,
|
|
378
|
+
default_page_size=100,
|
|
379
|
+
max_page_size=1000,
|
|
380
|
+
)
|
|
381
|
+
assert config.style == PaginationStyle.OFFSET
|
|
382
|
+
assert config.offset_param == 'offset'
|
|
383
|
+
assert config.data_path == 'data.items'
|
|
384
|
+
assert config.default_page_size == 100
|
|
385
|
+
assert config.max_page_size == 1000
|
|
386
|
+
|
|
387
|
+
def test_rejects_extra_fields(self):
|
|
388
|
+
"""Test that extra fields are rejected."""
|
|
389
|
+
with pytest.raises(Exception):
|
|
390
|
+
ResolvedPaginationConfig(
|
|
391
|
+
style=PaginationStyle.OFFSET,
|
|
392
|
+
offset_param='offset',
|
|
393
|
+
limit_param='limit',
|
|
394
|
+
cursor_param='cursor',
|
|
395
|
+
page_param='page',
|
|
396
|
+
per_page_param='per_page',
|
|
397
|
+
data_path=None,
|
|
398
|
+
total_path=None,
|
|
399
|
+
next_cursor_path=None,
|
|
400
|
+
total_pages_path=None,
|
|
401
|
+
default_page_size=100,
|
|
402
|
+
max_page_size=None,
|
|
403
|
+
unknown_field='value',
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class TestPaginationStyleEnum:
|
|
408
|
+
"""Tests for PaginationStyle enum."""
|
|
409
|
+
|
|
410
|
+
def test_values(self):
|
|
411
|
+
"""Test enum values."""
|
|
412
|
+
assert PaginationStyle.OFFSET.value == 'offset'
|
|
413
|
+
assert PaginationStyle.CURSOR.value == 'cursor'
|
|
414
|
+
assert PaginationStyle.PAGE.value == 'page'
|
|
415
|
+
assert PaginationStyle.LINK.value == 'link'
|
|
416
|
+
|
|
417
|
+
def test_from_string(self):
|
|
418
|
+
"""Test creating from string."""
|
|
419
|
+
assert PaginationStyle('offset') == PaginationStyle.OFFSET
|
|
420
|
+
assert PaginationStyle('cursor') == PaginationStyle.CURSOR
|
|
421
|
+
assert PaginationStyle('page') == PaginationStyle.PAGE
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class TestPaginationModuleGeneration:
|
|
425
|
+
"""Tests for pagination module generation."""
|
|
426
|
+
|
|
427
|
+
def test_generate_pagination_module(self):
|
|
428
|
+
"""Test that the pagination module is generated correctly."""
|
|
429
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
430
|
+
output_path = generate_pagination_module(Path(tmpdir))
|
|
431
|
+
assert output_path.exists()
|
|
432
|
+
content = output_path.read_text()
|
|
433
|
+
assert 'paginate_offset' in content
|
|
434
|
+
assert 'paginate_cursor' in content
|
|
435
|
+
assert 'paginate_page' in content
|
|
436
|
+
assert 'iterate_offset' in content
|
|
437
|
+
assert 'iterate_cursor' in content
|
|
438
|
+
assert 'extract_path' in content
|
|
439
|
+
|
|
440
|
+
def test_pagination_module_content_is_valid_python(self):
|
|
441
|
+
"""Test that the pagination module content is valid Python."""
|
|
442
|
+
# Should not raise SyntaxError
|
|
443
|
+
ast.parse(PAGINATION_MODULE_CONTENT)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class TestPaginatedFunctionGeneration:
|
|
447
|
+
"""Tests for paginated function code generation."""
|
|
448
|
+
|
|
449
|
+
def test_build_offset_paginated_function(self):
|
|
450
|
+
"""Test generating an offset-paginated function."""
|
|
451
|
+
fn_ast, imports = build_standalone_paginated_fn(
|
|
452
|
+
fn_name='list_users',
|
|
453
|
+
method='get',
|
|
454
|
+
path='/users',
|
|
455
|
+
parameters=None,
|
|
456
|
+
request_body_info=None,
|
|
457
|
+
response_type=None,
|
|
458
|
+
pagination_style='offset',
|
|
459
|
+
pagination_config={
|
|
460
|
+
'offset_param': 'offset',
|
|
461
|
+
'limit_param': 'limit',
|
|
462
|
+
'data_path': 'users',
|
|
463
|
+
'total_path': 'total',
|
|
464
|
+
'default_page_size': 100,
|
|
465
|
+
},
|
|
466
|
+
item_type_ast=ast.Name(id='User', ctx=ast.Load()),
|
|
467
|
+
docs='List all users.',
|
|
468
|
+
is_async=False,
|
|
469
|
+
)
|
|
470
|
+
assert fn_ast.name == 'list_users'
|
|
471
|
+
assert isinstance(fn_ast, ast.FunctionDef)
|
|
472
|
+
|
|
473
|
+
# Check that the function has the expected parameters
|
|
474
|
+
arg_names = [arg.arg for arg in fn_ast.args.kwonlyargs]
|
|
475
|
+
assert 'offset' in arg_names
|
|
476
|
+
assert 'page_size' in arg_names
|
|
477
|
+
assert 'max_items' in arg_names
|
|
478
|
+
|
|
479
|
+
def test_build_cursor_paginated_function(self):
|
|
480
|
+
"""Test generating a cursor-paginated function."""
|
|
481
|
+
fn_ast, imports = build_standalone_paginated_fn(
|
|
482
|
+
fn_name='list_items',
|
|
483
|
+
method='get',
|
|
484
|
+
path='/items',
|
|
485
|
+
parameters=None,
|
|
486
|
+
request_body_info=None,
|
|
487
|
+
response_type=None,
|
|
488
|
+
pagination_style='cursor',
|
|
489
|
+
pagination_config={
|
|
490
|
+
'cursor_param': 'after',
|
|
491
|
+
'limit_param': 'limit',
|
|
492
|
+
'data_path': 'items',
|
|
493
|
+
'next_cursor_path': 'meta.next_cursor',
|
|
494
|
+
'default_page_size': 50,
|
|
495
|
+
},
|
|
496
|
+
item_type_ast=ast.Name(id='Item', ctx=ast.Load()),
|
|
497
|
+
docs='List all items.',
|
|
498
|
+
is_async=False,
|
|
499
|
+
)
|
|
500
|
+
assert fn_ast.name == 'list_items'
|
|
501
|
+
assert isinstance(fn_ast, ast.FunctionDef)
|
|
502
|
+
|
|
503
|
+
arg_names = [arg.arg for arg in fn_ast.args.kwonlyargs]
|
|
504
|
+
assert 'cursor' in arg_names
|
|
505
|
+
assert 'page_size' in arg_names
|
|
506
|
+
assert 'max_items' in arg_names
|
|
507
|
+
|
|
508
|
+
def test_build_async_paginated_function(self):
|
|
509
|
+
"""Test generating an async paginated function."""
|
|
510
|
+
fn_ast, imports = build_standalone_paginated_fn(
|
|
511
|
+
fn_name='alist_users',
|
|
512
|
+
method='get',
|
|
513
|
+
path='/users',
|
|
514
|
+
parameters=None,
|
|
515
|
+
request_body_info=None,
|
|
516
|
+
response_type=None,
|
|
517
|
+
pagination_style='offset',
|
|
518
|
+
pagination_config={
|
|
519
|
+
'offset_param': 'offset',
|
|
520
|
+
'limit_param': 'limit',
|
|
521
|
+
'data_path': 'users',
|
|
522
|
+
'default_page_size': 100,
|
|
523
|
+
},
|
|
524
|
+
item_type_ast=ast.Name(id='User', ctx=ast.Load()),
|
|
525
|
+
docs='List all users async.',
|
|
526
|
+
is_async=True,
|
|
527
|
+
)
|
|
528
|
+
assert fn_ast.name == 'alist_users'
|
|
529
|
+
assert isinstance(fn_ast, ast.AsyncFunctionDef)
|
|
530
|
+
|
|
531
|
+
def test_build_paginated_iter_function(self):
|
|
532
|
+
"""Test generating a paginated iterator function."""
|
|
533
|
+
fn_ast, imports = build_standalone_paginated_iter_fn(
|
|
534
|
+
fn_name='list_users_iter',
|
|
535
|
+
method='get',
|
|
536
|
+
path='/users',
|
|
537
|
+
parameters=None,
|
|
538
|
+
request_body_info=None,
|
|
539
|
+
response_type=None,
|
|
540
|
+
pagination_style='offset',
|
|
541
|
+
pagination_config={
|
|
542
|
+
'offset_param': 'offset',
|
|
543
|
+
'limit_param': 'limit',
|
|
544
|
+
'data_path': 'users',
|
|
545
|
+
'default_page_size': 100,
|
|
546
|
+
},
|
|
547
|
+
item_type_ast=ast.Name(id='User', ctx=ast.Load()),
|
|
548
|
+
docs='Iterate over users.',
|
|
549
|
+
is_async=False,
|
|
550
|
+
)
|
|
551
|
+
assert fn_ast.name == 'list_users_iter'
|
|
552
|
+
assert isinstance(fn_ast, ast.FunctionDef)
|
|
553
|
+
|
|
554
|
+
# Check that Iterator is in imports
|
|
555
|
+
assert 'collections.abc' in imports
|
|
556
|
+
assert 'Iterator' in imports['collections.abc']
|
|
557
|
+
|
|
558
|
+
def test_build_async_paginated_iter_function(self):
|
|
559
|
+
"""Test generating an async paginated iterator function."""
|
|
560
|
+
fn_ast, imports = build_standalone_paginated_iter_fn(
|
|
561
|
+
fn_name='alist_users_iter',
|
|
562
|
+
method='get',
|
|
563
|
+
path='/users',
|
|
564
|
+
parameters=None,
|
|
565
|
+
request_body_info=None,
|
|
566
|
+
response_type=None,
|
|
567
|
+
pagination_style='cursor',
|
|
568
|
+
pagination_config={
|
|
569
|
+
'cursor_param': 'after',
|
|
570
|
+
'limit_param': 'limit',
|
|
571
|
+
'data_path': 'users',
|
|
572
|
+
'next_cursor_path': 'next',
|
|
573
|
+
'default_page_size': 100,
|
|
574
|
+
},
|
|
575
|
+
item_type_ast=ast.Name(id='User', ctx=ast.Load()),
|
|
576
|
+
docs='Iterate over users async.',
|
|
577
|
+
is_async=True,
|
|
578
|
+
)
|
|
579
|
+
assert fn_ast.name == 'alist_users_iter'
|
|
580
|
+
assert isinstance(fn_ast, ast.AsyncFunctionDef)
|
|
581
|
+
|
|
582
|
+
# Check that AsyncIterator is in imports
|
|
583
|
+
assert 'collections.abc' in imports
|
|
584
|
+
assert 'AsyncIterator' in imports['collections.abc']
|
|
585
|
+
|
|
586
|
+
def test_generated_code_is_valid_python(self):
|
|
587
|
+
"""Test that generated paginated function code is valid Python."""
|
|
588
|
+
fn_ast, _ = build_standalone_paginated_fn(
|
|
589
|
+
fn_name='list_users',
|
|
590
|
+
method='get',
|
|
591
|
+
path='/users',
|
|
592
|
+
parameters=None,
|
|
593
|
+
request_body_info=None,
|
|
594
|
+
response_type=None,
|
|
595
|
+
pagination_style='offset',
|
|
596
|
+
pagination_config={
|
|
597
|
+
'offset_param': 'offset',
|
|
598
|
+
'limit_param': 'limit',
|
|
599
|
+
'data_path': 'users',
|
|
600
|
+
'default_page_size': 100,
|
|
601
|
+
},
|
|
602
|
+
item_type_ast=None,
|
|
603
|
+
docs='List users.',
|
|
604
|
+
is_async=False,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Wrap in a module and compile to verify it's valid Python
|
|
608
|
+
module = ast.Module(body=[fn_ast], type_ignores=[])
|
|
609
|
+
ast.fix_missing_locations(module)
|
|
610
|
+
# Should not raise SyntaxError
|
|
611
|
+
compile(module, '<test>', 'exec')
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class TestPaginationCodegenIntegration:
|
|
615
|
+
"""Integration tests for pagination code generation with Codegen."""
|
|
616
|
+
|
|
617
|
+
def test_codegen_with_offset_pagination(self):
|
|
618
|
+
"""Test code generation with offset-based pagination."""
|
|
619
|
+
import json
|
|
620
|
+
|
|
621
|
+
from otterapi.codegen.codegen import Codegen
|
|
622
|
+
from otterapi.config import DocumentConfig
|
|
623
|
+
|
|
624
|
+
openapi_spec = {
|
|
625
|
+
'openapi': '3.0.0',
|
|
626
|
+
'info': {'title': 'Test API', 'version': '1.0.0'},
|
|
627
|
+
'servers': [{'url': 'https://api.example.com'}],
|
|
628
|
+
'paths': {
|
|
629
|
+
'/users': {
|
|
630
|
+
'get': {
|
|
631
|
+
'operationId': 'listUsers',
|
|
632
|
+
'parameters': [
|
|
633
|
+
{
|
|
634
|
+
'name': 'offset',
|
|
635
|
+
'in': 'query',
|
|
636
|
+
'schema': {'type': 'integer'},
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
'name': 'limit',
|
|
640
|
+
'in': 'query',
|
|
641
|
+
'schema': {'type': 'integer'},
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
'responses': {
|
|
645
|
+
'200': {
|
|
646
|
+
'description': 'List of users',
|
|
647
|
+
'content': {
|
|
648
|
+
'application/json': {
|
|
649
|
+
'schema': {
|
|
650
|
+
'type': 'object',
|
|
651
|
+
'properties': {
|
|
652
|
+
'users': {
|
|
653
|
+
'type': 'array',
|
|
654
|
+
'items': {
|
|
655
|
+
'$ref': '#/components/schemas/User'
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
'total': {'type': 'integer'},
|
|
659
|
+
},
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
'components': {
|
|
669
|
+
'schemas': {
|
|
670
|
+
'User': {
|
|
671
|
+
'type': 'object',
|
|
672
|
+
'properties': {
|
|
673
|
+
'id': {'type': 'integer'},
|
|
674
|
+
'name': {'type': 'string'},
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
682
|
+
spec_path = Path(tmpdir) / 'openapi.json'
|
|
683
|
+
spec_path.write_text(json.dumps(openapi_spec))
|
|
684
|
+
output_path = Path(tmpdir) / 'client'
|
|
685
|
+
|
|
686
|
+
config = DocumentConfig(
|
|
687
|
+
source=str(spec_path),
|
|
688
|
+
output=str(output_path),
|
|
689
|
+
pagination=PaginationConfig(
|
|
690
|
+
enabled=True,
|
|
691
|
+
endpoints={
|
|
692
|
+
'list_users': EndpointPaginationConfig(
|
|
693
|
+
style='offset',
|
|
694
|
+
data_path='users',
|
|
695
|
+
total_path='total',
|
|
696
|
+
)
|
|
697
|
+
},
|
|
698
|
+
),
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
codegen = Codegen(config)
|
|
702
|
+
codegen.generate()
|
|
703
|
+
|
|
704
|
+
# Verify pagination module was generated
|
|
705
|
+
assert (output_path / '_pagination.py').exists()
|
|
706
|
+
|
|
707
|
+
# Verify endpoints.py was generated and is valid Python
|
|
708
|
+
endpoints_file = output_path / 'endpoints.py'
|
|
709
|
+
assert endpoints_file.exists()
|
|
710
|
+
content = endpoints_file.read_text()
|
|
711
|
+
ast.parse(content) # Should not raise
|
|
712
|
+
|
|
713
|
+
# Verify pagination functions are present
|
|
714
|
+
assert 'list_users_iter' in content
|
|
715
|
+
assert 'alist_users_iter' in content
|
|
716
|
+
assert 'paginate_offset' in content
|
|
717
|
+
|
|
718
|
+
# Verify no duplicate function definitions
|
|
719
|
+
assert content.count('def list_users(') == 1
|
|
720
|
+
assert content.count('async def alist_users(') == 1
|
|
721
|
+
|
|
722
|
+
def test_codegen_with_cursor_pagination(self):
|
|
723
|
+
"""Test code generation with cursor-based pagination."""
|
|
724
|
+
import json
|
|
725
|
+
|
|
726
|
+
from otterapi.codegen.codegen import Codegen
|
|
727
|
+
from otterapi.config import DocumentConfig
|
|
728
|
+
|
|
729
|
+
openapi_spec = {
|
|
730
|
+
'openapi': '3.0.0',
|
|
731
|
+
'info': {'title': 'Test API', 'version': '1.0.0'},
|
|
732
|
+
'servers': [{'url': 'https://api.example.com'}],
|
|
733
|
+
'paths': {
|
|
734
|
+
'/items': {
|
|
735
|
+
'get': {
|
|
736
|
+
'operationId': 'listItems',
|
|
737
|
+
'parameters': [
|
|
738
|
+
{
|
|
739
|
+
'name': 'cursor',
|
|
740
|
+
'in': 'query',
|
|
741
|
+
'schema': {'type': 'string'},
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
'name': 'limit',
|
|
745
|
+
'in': 'query',
|
|
746
|
+
'schema': {'type': 'integer'},
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
'responses': {
|
|
750
|
+
'200': {
|
|
751
|
+
'description': 'List of items',
|
|
752
|
+
'content': {
|
|
753
|
+
'application/json': {
|
|
754
|
+
'schema': {
|
|
755
|
+
'type': 'object',
|
|
756
|
+
'properties': {
|
|
757
|
+
'items': {
|
|
758
|
+
'type': 'array',
|
|
759
|
+
'items': {
|
|
760
|
+
'$ref': '#/components/schemas/Item'
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
'next_cursor': {'type': 'string'},
|
|
764
|
+
},
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
'components': {
|
|
774
|
+
'schemas': {
|
|
775
|
+
'Item': {
|
|
776
|
+
'type': 'object',
|
|
777
|
+
'properties': {
|
|
778
|
+
'id': {'type': 'integer'},
|
|
779
|
+
'name': {'type': 'string'},
|
|
780
|
+
},
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
787
|
+
spec_path = Path(tmpdir) / 'openapi.json'
|
|
788
|
+
spec_path.write_text(json.dumps(openapi_spec))
|
|
789
|
+
output_path = Path(tmpdir) / 'client'
|
|
790
|
+
|
|
791
|
+
config = DocumentConfig(
|
|
792
|
+
source=str(spec_path),
|
|
793
|
+
output=str(output_path),
|
|
794
|
+
pagination=PaginationConfig(
|
|
795
|
+
enabled=True,
|
|
796
|
+
endpoints={
|
|
797
|
+
'list_items': EndpointPaginationConfig(
|
|
798
|
+
style='cursor',
|
|
799
|
+
cursor_param='cursor',
|
|
800
|
+
data_path='items',
|
|
801
|
+
next_cursor_path='next_cursor',
|
|
802
|
+
)
|
|
803
|
+
},
|
|
804
|
+
),
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
codegen = Codegen(config)
|
|
808
|
+
codegen.generate()
|
|
809
|
+
|
|
810
|
+
# Verify endpoints.py was generated and is valid Python
|
|
811
|
+
endpoints_file = output_path / 'endpoints.py'
|
|
812
|
+
assert endpoints_file.exists()
|
|
813
|
+
content = endpoints_file.read_text()
|
|
814
|
+
ast.parse(content) # Should not raise
|
|
815
|
+
|
|
816
|
+
# Verify cursor-based pagination is used
|
|
817
|
+
assert 'cursor: str | None' in content
|
|
818
|
+
assert 'paginate_cursor' in content
|
|
819
|
+
|
|
820
|
+
def test_codegen_without_pagination(self):
|
|
821
|
+
"""Test that code generation works normally without pagination config."""
|
|
822
|
+
import json
|
|
823
|
+
|
|
824
|
+
from otterapi.codegen.codegen import Codegen
|
|
825
|
+
from otterapi.config import DocumentConfig
|
|
826
|
+
|
|
827
|
+
openapi_spec = {
|
|
828
|
+
'openapi': '3.0.0',
|
|
829
|
+
'info': {'title': 'Test API', 'version': '1.0.0'},
|
|
830
|
+
'servers': [{'url': 'https://api.example.com'}],
|
|
831
|
+
'paths': {
|
|
832
|
+
'/users': {
|
|
833
|
+
'get': {
|
|
834
|
+
'operationId': 'listUsers',
|
|
835
|
+
'responses': {
|
|
836
|
+
'200': {
|
|
837
|
+
'description': 'List of users',
|
|
838
|
+
'content': {
|
|
839
|
+
'application/json': {
|
|
840
|
+
'schema': {
|
|
841
|
+
'type': 'array',
|
|
842
|
+
'items': {
|
|
843
|
+
'$ref': '#/components/schemas/User'
|
|
844
|
+
},
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
'components': {
|
|
854
|
+
'schemas': {
|
|
855
|
+
'User': {
|
|
856
|
+
'type': 'object',
|
|
857
|
+
'properties': {
|
|
858
|
+
'id': {'type': 'integer'},
|
|
859
|
+
'name': {'type': 'string'},
|
|
860
|
+
},
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
867
|
+
spec_path = Path(tmpdir) / 'openapi.json'
|
|
868
|
+
spec_path.write_text(json.dumps(openapi_spec))
|
|
869
|
+
output_path = Path(tmpdir) / 'client'
|
|
870
|
+
|
|
871
|
+
config = DocumentConfig(
|
|
872
|
+
source=str(spec_path),
|
|
873
|
+
output=str(output_path),
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
codegen = Codegen(config)
|
|
877
|
+
codegen.generate()
|
|
878
|
+
|
|
879
|
+
# Verify pagination module was NOT generated
|
|
880
|
+
assert not (output_path / '_pagination.py').exists()
|
|
881
|
+
|
|
882
|
+
# Verify endpoints.py was generated normally
|
|
883
|
+
endpoints_file = output_path / 'endpoints.py'
|
|
884
|
+
assert endpoints_file.exists()
|
|
885
|
+
content = endpoints_file.read_text()
|
|
886
|
+
ast.parse(content) # Should not raise
|
|
887
|
+
|
|
888
|
+
# Verify no pagination imports
|
|
889
|
+
assert 'paginate_offset' not in content
|
|
890
|
+
assert 'paginate_cursor' not in content
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
class TestPaginationAutoDetect:
|
|
894
|
+
"""Tests for pagination auto-detection feature."""
|
|
895
|
+
|
|
896
|
+
def test_auto_detect_default_enabled(self):
|
|
897
|
+
"""Test that auto_detect is enabled by default."""
|
|
898
|
+
config = PaginationConfig(enabled=True)
|
|
899
|
+
assert config.auto_detect is True
|
|
900
|
+
|
|
901
|
+
def test_auto_detect_disabled(self):
|
|
902
|
+
"""Test that auto_detect can be disabled."""
|
|
903
|
+
config = PaginationConfig(enabled=True, auto_detect=False)
|
|
904
|
+
assert config.auto_detect is False
|
|
905
|
+
|
|
906
|
+
def test_auto_detect_offset_pagination(self):
|
|
907
|
+
"""Test auto-detection of offset pagination parameters."""
|
|
908
|
+
from dataclasses import dataclass
|
|
909
|
+
|
|
910
|
+
@dataclass
|
|
911
|
+
class MockParam:
|
|
912
|
+
name: str
|
|
913
|
+
|
|
914
|
+
config = PaginationConfig(enabled=True, auto_detect=True)
|
|
915
|
+
params = [
|
|
916
|
+
MockParam(name='offset'),
|
|
917
|
+
MockParam(name='limit'),
|
|
918
|
+
MockParam(name='query'),
|
|
919
|
+
]
|
|
920
|
+
|
|
921
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
922
|
+
'list_users', endpoint_parameters=params
|
|
923
|
+
)
|
|
924
|
+
assert should_generate is True
|
|
925
|
+
assert resolved is not None
|
|
926
|
+
assert resolved.style == PaginationStyle.OFFSET
|
|
927
|
+
assert resolved.offset_param == 'offset'
|
|
928
|
+
assert resolved.limit_param == 'limit'
|
|
929
|
+
|
|
930
|
+
def test_auto_detect_cursor_pagination(self):
|
|
931
|
+
"""Test auto-detection of cursor pagination parameters."""
|
|
932
|
+
from dataclasses import dataclass
|
|
933
|
+
|
|
934
|
+
@dataclass
|
|
935
|
+
class MockParam:
|
|
936
|
+
name: str
|
|
937
|
+
|
|
938
|
+
config = PaginationConfig(enabled=True, auto_detect=True)
|
|
939
|
+
params = [
|
|
940
|
+
MockParam(name='cursor'),
|
|
941
|
+
MockParam(name='limit'),
|
|
942
|
+
MockParam(name='query'),
|
|
943
|
+
]
|
|
944
|
+
|
|
945
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
946
|
+
'list_items', endpoint_parameters=params
|
|
947
|
+
)
|
|
948
|
+
assert should_generate is True
|
|
949
|
+
assert resolved is not None
|
|
950
|
+
assert resolved.style == PaginationStyle.CURSOR
|
|
951
|
+
assert resolved.cursor_param == 'cursor'
|
|
952
|
+
assert resolved.limit_param == 'limit'
|
|
953
|
+
|
|
954
|
+
def test_auto_detect_page_pagination(self):
|
|
955
|
+
"""Test auto-detection of page pagination parameters."""
|
|
956
|
+
from dataclasses import dataclass
|
|
957
|
+
|
|
958
|
+
@dataclass
|
|
959
|
+
class MockParam:
|
|
960
|
+
name: str
|
|
961
|
+
|
|
962
|
+
config = PaginationConfig(enabled=True, auto_detect=True)
|
|
963
|
+
params = [
|
|
964
|
+
MockParam(name='page'),
|
|
965
|
+
MockParam(name='per_page'),
|
|
966
|
+
MockParam(name='query'),
|
|
967
|
+
]
|
|
968
|
+
|
|
969
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
970
|
+
'list_orders', endpoint_parameters=params
|
|
971
|
+
)
|
|
972
|
+
assert should_generate is True
|
|
973
|
+
assert resolved is not None
|
|
974
|
+
assert resolved.style == PaginationStyle.PAGE
|
|
975
|
+
assert resolved.page_param == 'page'
|
|
976
|
+
assert resolved.per_page_param == 'per_page'
|
|
977
|
+
|
|
978
|
+
def test_auto_detect_no_pagination_params(self):
|
|
979
|
+
"""Test that endpoints without pagination params are not detected."""
|
|
980
|
+
from dataclasses import dataclass
|
|
981
|
+
|
|
982
|
+
@dataclass
|
|
983
|
+
class MockParam:
|
|
984
|
+
name: str
|
|
985
|
+
|
|
986
|
+
config = PaginationConfig(enabled=True, auto_detect=True)
|
|
987
|
+
params = [MockParam(name='query'), MockParam(name='filter')]
|
|
988
|
+
|
|
989
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
990
|
+
'search_users', endpoint_parameters=params
|
|
991
|
+
)
|
|
992
|
+
assert should_generate is False
|
|
993
|
+
assert resolved is None
|
|
994
|
+
|
|
995
|
+
def test_auto_detect_custom_param_names(self):
|
|
996
|
+
"""Test auto-detection with custom parameter names."""
|
|
997
|
+
from dataclasses import dataclass
|
|
998
|
+
|
|
999
|
+
@dataclass
|
|
1000
|
+
class MockParam:
|
|
1001
|
+
name: str
|
|
1002
|
+
|
|
1003
|
+
config = PaginationConfig(
|
|
1004
|
+
enabled=True,
|
|
1005
|
+
auto_detect=True,
|
|
1006
|
+
default_offset_param='skip',
|
|
1007
|
+
default_limit_param='take',
|
|
1008
|
+
)
|
|
1009
|
+
params = [
|
|
1010
|
+
MockParam(name='skip'),
|
|
1011
|
+
MockParam(name='take'),
|
|
1012
|
+
MockParam(name='query'),
|
|
1013
|
+
]
|
|
1014
|
+
|
|
1015
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
1016
|
+
'list_users', endpoint_parameters=params
|
|
1017
|
+
)
|
|
1018
|
+
assert should_generate is True
|
|
1019
|
+
assert resolved is not None
|
|
1020
|
+
assert resolved.style == PaginationStyle.OFFSET
|
|
1021
|
+
assert resolved.offset_param == 'skip'
|
|
1022
|
+
assert resolved.limit_param == 'take'
|
|
1023
|
+
|
|
1024
|
+
def test_auto_detect_disabled_no_detection(self):
|
|
1025
|
+
"""Test that auto_detect=False prevents auto-detection."""
|
|
1026
|
+
from dataclasses import dataclass
|
|
1027
|
+
|
|
1028
|
+
@dataclass
|
|
1029
|
+
class MockParam:
|
|
1030
|
+
name: str
|
|
1031
|
+
|
|
1032
|
+
config = PaginationConfig(enabled=True, auto_detect=False)
|
|
1033
|
+
params = [MockParam(name='offset'), MockParam(name='limit')]
|
|
1034
|
+
|
|
1035
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
1036
|
+
'list_users', endpoint_parameters=params
|
|
1037
|
+
)
|
|
1038
|
+
assert should_generate is False
|
|
1039
|
+
assert resolved is None
|
|
1040
|
+
|
|
1041
|
+
def test_auto_detect_no_params_passed(self):
|
|
1042
|
+
"""Test that auto-detection is skipped when no params passed."""
|
|
1043
|
+
config = PaginationConfig(enabled=True, auto_detect=True)
|
|
1044
|
+
|
|
1045
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
1046
|
+
'list_users', endpoint_parameters=None
|
|
1047
|
+
)
|
|
1048
|
+
assert should_generate is False
|
|
1049
|
+
assert resolved is None
|
|
1050
|
+
|
|
1051
|
+
def test_explicit_config_takes_precedence(self):
|
|
1052
|
+
"""Test that explicit endpoint config takes precedence over auto-detect."""
|
|
1053
|
+
from dataclasses import dataclass
|
|
1054
|
+
|
|
1055
|
+
@dataclass
|
|
1056
|
+
class MockParam:
|
|
1057
|
+
name: str
|
|
1058
|
+
|
|
1059
|
+
config = PaginationConfig(
|
|
1060
|
+
enabled=True,
|
|
1061
|
+
auto_detect=True,
|
|
1062
|
+
endpoints={
|
|
1063
|
+
'list_users': EndpointPaginationConfig(
|
|
1064
|
+
style='cursor',
|
|
1065
|
+
data_path='users',
|
|
1066
|
+
),
|
|
1067
|
+
},
|
|
1068
|
+
)
|
|
1069
|
+
# Even though params suggest offset, explicit config says cursor
|
|
1070
|
+
params = [MockParam(name='offset'), MockParam(name='limit')]
|
|
1071
|
+
|
|
1072
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
1073
|
+
'list_users', endpoint_parameters=params
|
|
1074
|
+
)
|
|
1075
|
+
assert should_generate is True
|
|
1076
|
+
assert resolved is not None
|
|
1077
|
+
assert resolved.style == PaginationStyle.CURSOR
|
|
1078
|
+
assert resolved.data_path == 'users'
|
|
1079
|
+
|
|
1080
|
+
def test_explicit_disabled_prevents_auto_detect(self):
|
|
1081
|
+
"""Test that explicitly disabled endpoint prevents auto-detect."""
|
|
1082
|
+
from dataclasses import dataclass
|
|
1083
|
+
|
|
1084
|
+
@dataclass
|
|
1085
|
+
class MockParam:
|
|
1086
|
+
name: str
|
|
1087
|
+
|
|
1088
|
+
config = PaginationConfig(
|
|
1089
|
+
enabled=True,
|
|
1090
|
+
auto_detect=True,
|
|
1091
|
+
endpoints={
|
|
1092
|
+
'list_users': EndpointPaginationConfig(enabled=False),
|
|
1093
|
+
},
|
|
1094
|
+
)
|
|
1095
|
+
params = [MockParam(name='offset'), MockParam(name='limit')]
|
|
1096
|
+
|
|
1097
|
+
should_generate, resolved = config.should_generate_for_endpoint(
|
|
1098
|
+
'list_users', endpoint_parameters=params
|
|
1099
|
+
)
|
|
1100
|
+
assert should_generate is False
|
|
1101
|
+
assert resolved is None
|