Skip to content

Commit 3080bfa

Browse files
committed
enforce parameter validation
1 parent 5f376c9 commit 3080bfa

File tree

2 files changed

+166
-5
lines changed

2 files changed

+166
-5
lines changed

dlt/sources/helpers/rest_client/paginators.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ def __init__(
109109
For example, 'page'.
110110
param_body_path (str): The dot-separated path specifying
111111
where to place the numeric parameter in the request JSON body.
112-
Defaults to `None`.
112+
Defaults to `None`. Either `param_name` or `param_body_path`
113+
must be provided, not both.
113114
initial_value (int): The initial value of the numeric parameter.
114115
value_step (int): The step size to increment the numeric parameter.
115116
base_index (int, optional): The index of the initial element.
@@ -142,6 +143,10 @@ def __init__(
142143
"One of `total_path`, `maximum_value`, `has_more_path`, or `stop_after_empty_page`"
143144
" must be provided."
144145
)
146+
147+
if (not param_name and not param_body_path) or (param_name and param_body_path):
148+
raise ValueError("Either 'param_name' or 'param_body_path' must be provided, not both.")
149+
145150
self.param_name = param_name
146151
self.param_body_path = param_body_path
147152
self.initial_value = initial_value
@@ -240,6 +245,8 @@ def update_request(self, request: Request) -> None:
240245
def _update_request_with_param_name(
241246
request: Request, param_name: str, current_value: int
242247
) -> None:
248+
if not param_name:
249+
raise ValueError("`param_name` must not be empty.")
243250
if request.params is None:
244251
request.params = {}
245252
request.params[param_name] = current_value
@@ -248,6 +255,8 @@ def _update_request_with_param_name(
248255
def _update_request_with_body_path(
249256
request: Request, body_path: str, current_value: int
250257
) -> None:
258+
if not body_path:
259+
raise ValueError("`body_path` must not be empty.")
251260
if request.json is None:
252261
request.json = {}
253262
jsonpath.set_value_at_path(request.json, body_path, current_value)
@@ -342,7 +351,8 @@ def __init__(
342351
page (int): The page number for the first request. If not provided,
343352
the initial value will be set to `base_page`.
344353
page_param (str): The query parameter name for the page number.
345-
Defaults to 'page'.
354+
Defaults to 'page'. Either `page_param` or `page_body_path`
355+
must be provided, not both.
346356
page_body_path (str): A dot-separated path specifying where
347357
to place the page number in the request JSON body. Use this instead
348358
of `page_param` when sending the page number in the request body.
@@ -363,8 +373,9 @@ def __init__(
363373
if page_param is None and page_body_path is None:
364374
page_param = "page"
365375
# Check that only one of page_param or page_body_path is provided
366-
elif page_param is not None and page_body_path is not None:
376+
elif (not page_param and not page_body_path) or (page_param and page_body_path):
367377
raise ValueError("Either 'page_param' or 'page_body_path' must be provided, not both.")
378+
368379
if (
369380
total_path is None
370381
and maximum_page is None
@@ -523,15 +534,15 @@ def __init__(
523534
if offset_param is None and offset_body_path is None:
524535
offset_param = "offset"
525536
# Check that only one of offset_param or offset_body_path is provided
526-
if offset_param is not None and offset_body_path is not None:
537+
elif (not offset_param and not offset_body_path) or (offset_param and offset_body_path):
527538
raise ValueError(
528539
"Either 'offset_param' or 'offset_body_path' must be provided, not both."
529540
)
530541

531542
if limit_param is None and limit_body_path is None:
532543
limit_param = "limit"
533544
# Check that only one of limit_param or limit_body_path is provided
534-
if limit_param is not None and limit_body_path is not None:
545+
elif (not limit_param and not limit_body_path) or (limit_param and limit_body_path):
535546
raise ValueError(
536547
"Either 'limit_param' or 'limit_body_path' must be provided, not both."
537548
)

tests/sources/helpers/rest_client/test_paginators.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
SinglePagePaginator,
1111
OffsetPaginator,
1212
PageNumberPaginator,
13+
RangePaginator,
1314
HeaderLinkPaginator,
1415
JSONLinkPaginator,
1516
JSONResponseCursorPaginator,
@@ -264,6 +265,77 @@ def test_client_pagination(self, rest_client):
264265
assert_pagination(pages, total_pages=1)
265266

266267

268+
class TestRangePaginator:
269+
def test_init(self) -> None:
270+
# param_name provided, param_body_path not provided
271+
RangePaginator(
272+
param_name="page",
273+
initial_value=0,
274+
value_step=1,
275+
total_path="total",
276+
)
277+
278+
# param_name not provided, param_body_path provided
279+
RangePaginator(
280+
param_name=None,
281+
param_body_path="body.path",
282+
initial_value=0,
283+
value_step=1,
284+
total_path="total",
285+
)
286+
287+
# both param_name and param_body_path provided
288+
with pytest.raises(
289+
ValueError, match="Either 'param_name' or 'param_body_path' must be provided, not both"
290+
):
291+
RangePaginator(
292+
param_name="page",
293+
param_body_path="body.path",
294+
initial_value=0,
295+
value_step=1,
296+
total_path="total",
297+
)
298+
299+
# both param_name and param_body_path are None or empty strings
300+
for param_name, param_body_path in [
301+
(None, None),
302+
("", None),
303+
(None, ""),
304+
("", ""),
305+
]:
306+
with pytest.raises(
307+
ValueError,
308+
match="Either 'param_name' or 'param_body_path' must be provided, not both",
309+
):
310+
RangePaginator(
311+
param_name=param_name,
312+
param_body_path=param_body_path,
313+
initial_value=0,
314+
value_step=1,
315+
total_path="total",
316+
)
317+
318+
with pytest.raises(
319+
ValueError, match="Either 'param_name' or 'param_body_path' must be provided, not both"
320+
):
321+
RangePaginator(
322+
param_name=None,
323+
param_body_path=None,
324+
initial_value=0,
325+
value_step=1,
326+
total_path="total",
327+
)
328+
329+
def test_range_paginator_update(self) -> None:
330+
request = Mock(Request)
331+
request.json = {}
332+
with pytest.raises(ValueError, match="param_name.*must not be empty"):
333+
RangePaginator._update_request_with_param_name(request, "", 0)
334+
335+
with pytest.raises(ValueError, match="body_path.*must not be empty"):
336+
RangePaginator._update_request_with_body_path(request, "", 0)
337+
338+
267339
@pytest.mark.usefixtures("mock_api_server")
268340
class TestOffsetPaginator:
269341
def test_update_state(self):
@@ -499,6 +571,60 @@ def test_guarantee_termination(self):
499571
" must be provided"
500572
)
501573

574+
@pytest.mark.parametrize(
575+
"offset_param,offset_body_path",
576+
[
577+
(None, None),
578+
("", None),
579+
(None, ""),
580+
("", ""),
581+
],
582+
)
583+
def test_empty_offset_params(self, offset_param, offset_body_path) -> None:
584+
if offset_param is None and offset_body_path is None:
585+
# Fallback to default 'offset' param
586+
paginator = OffsetPaginator(
587+
limit=100, offset_param=offset_param, offset_body_path=offset_body_path
588+
)
589+
assert paginator.param_name == "offset"
590+
assert paginator.param_body_path is None
591+
else:
592+
with pytest.raises(
593+
ValueError, match="Either 'offset_param' or 'offset_body_path' must be provided.*"
594+
):
595+
OffsetPaginator(
596+
limit=100,
597+
offset_param=offset_param,
598+
offset_body_path=offset_body_path,
599+
)
600+
601+
@pytest.mark.parametrize(
602+
"limit_param,limit_body_path",
603+
[
604+
(None, None),
605+
("", None),
606+
(None, ""),
607+
("", ""),
608+
],
609+
)
610+
def test_empty_limit_params(self, limit_param, limit_body_path) -> None:
611+
if limit_param is None and limit_body_path is None:
612+
# Fallback to default 'limit' param
613+
paginator = OffsetPaginator(
614+
limit=100, limit_param=limit_param, limit_body_path=limit_body_path
615+
)
616+
assert paginator.limit_param == "limit"
617+
assert paginator.limit_body_path is None
618+
else:
619+
with pytest.raises(
620+
ValueError, match="Either 'limit_param' or 'limit_body_path' must be provided.*"
621+
):
622+
OffsetPaginator(
623+
limit=100,
624+
limit_param=limit_param,
625+
limit_body_path=limit_body_path,
626+
)
627+
502628

503629
@pytest.mark.usefixtures("mock_api_server")
504630
class TestPageNumberPaginator:
@@ -761,6 +887,30 @@ def test_guarantee_termination(self):
761887
" must be provided"
762888
)
763889

890+
@pytest.mark.parametrize(
891+
"page_param,page_body_path",
892+
[
893+
(None, None),
894+
("", None),
895+
(None, ""),
896+
("", ""),
897+
],
898+
)
899+
def test_empty_params(self, page_param, page_body_path) -> None:
900+
if page_param is None and page_body_path is None:
901+
# Fallback to default 'page' param
902+
paginator = PageNumberPaginator(page_param=page_param, page_body_path=page_body_path)
903+
assert paginator.param_name == "page"
904+
assert paginator.param_body_path is None
905+
else:
906+
with pytest.raises(
907+
ValueError, match="Either 'page_param' or 'page_body_path' must be provided.*"
908+
):
909+
PageNumberPaginator(
910+
page_param=page_param,
911+
page_body_path=page_body_path,
912+
)
913+
764914

765915
@pytest.mark.usefixtures("mock_api_server")
766916
class TestJSONResponseCursorPaginator:

0 commit comments

Comments
 (0)