42api 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- {42api-0.1.2 → 42api-0.1.3}/PKG-INFO +1 -1
- {42api-0.1.2 → 42api-0.1.3}/pyproject.toml +1 -1
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/__init__.py +1 -1
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/query.py +26 -3
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/query.py +26 -3
- 42api-0.1.3/tests/integration/test_pagination_walk.py +141 -0
- {42api-0.1.2 → 42api-0.1.3}/uv.lock +2 -2
- 42api-0.1.2/tests/integration/test_pagination_walk.py +0 -82
- {42api-0.1.2 → 42api-0.1.3}/.github/workflows/ci.yml +0 -0
- {42api-0.1.2 → 42api-0.1.3}/.github/workflows/publish.yml +0 -0
- {42api-0.1.2 → 42api-0.1.3}/.gitignore +0 -0
- {42api-0.1.2 → 42api-0.1.3}/.pre-commit-config.yaml +0 -0
- {42api-0.1.2 → 42api-0.1.3}/LICENSE +0 -0
- {42api-0.1.2 → 42api-0.1.3}/README.md +0 -0
- {42api-0.1.2 → 42api-0.1.3}/scripts/unasync_generate.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/__init__.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/client.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/__init__.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/accreditations.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/base.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/campus_users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/campuses.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/cursus.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/events.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/exams.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/locations.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/projects.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/projects_users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_auth.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_config.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_pagination.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_query_params.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_rate_limit.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/__init__.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/client.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/__init__.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/accreditations.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/base.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/campus_users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/campuses.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/cursus.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/events.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/exams.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/locations.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/projects.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/projects_users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/exceptions.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/__init__.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/accreditation.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/base.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/campus.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/campus_user.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/cursus.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/event.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/exam.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/location.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/project.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/project_user.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/user.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/src/intra42/py.typed +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/conftest.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_accreditations.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_campus_users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_campuses.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_cursus.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_events.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_exams.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_locations.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_projects.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_projects_users.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/test_client_integration.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/integration/test_nested_access.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_auth.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_models_campus.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_models_user.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_pagination.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_public_api.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_query_builder.py +0 -0
- {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_rate_limit.py +0 -0
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from collections.abc import AsyncIterator, Callable
|
|
9
9
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
10
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
10
11
|
|
|
11
12
|
from .._pagination import parse_link_header
|
|
12
13
|
from .._query_params import build_query_params
|
|
@@ -78,8 +79,20 @@ class AsyncQuerySet(Generic[ModelT]):
|
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
async def __aiter__(self) -> AsyncIterator[ModelT]:
|
|
82
|
+
# `base_params` (filter/sort/page_size/range) is re-applied on every
|
|
83
|
+
# page, not just the first. Confirmed against the live API: the
|
|
84
|
+
# `next` link's own query string always uses a *bare* `page=N`
|
|
85
|
+
# cursor — a different, incompatible pagination scheme from the
|
|
86
|
+
# bracketed `page[size]`/`page[number]` this library sends, and the
|
|
87
|
+
# server returns 400 if both appear on the same request. So rather
|
|
88
|
+
# than forwarding the link's query string as-is (which either loses
|
|
89
|
+
# page_size or actively errors once you try to keep it), we extract
|
|
90
|
+
# just the page *number* from the link and reissue it as
|
|
91
|
+
# `page[number]`, combined with our own persisted page_size/filter/
|
|
92
|
+
# sort/range.
|
|
93
|
+
base_params = self._initial_params()
|
|
81
94
|
url: str | None = self._path
|
|
82
|
-
params: dict[str, str] | None =
|
|
95
|
+
params: dict[str, str] | None = base_params
|
|
83
96
|
while url is not None:
|
|
84
97
|
response = await self._client.request("GET", url, params=params)
|
|
85
98
|
for item in response.json():
|
|
@@ -87,8 +100,18 @@ class AsyncQuerySet(Generic[ModelT]):
|
|
|
87
100
|
if self._bind is not None:
|
|
88
101
|
self._bind(instance)
|
|
89
102
|
yield instance
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
|
|
104
|
+
next_url = parse_link_header(response.headers.get("Link")).get("next")
|
|
105
|
+
if next_url is None:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
split = urlsplit(next_url)
|
|
109
|
+
url = f"{split.scheme}://{split.netloc}{split.path}" if split.scheme else split.path
|
|
110
|
+
next_query = dict(parse_qsl(split.query))
|
|
111
|
+
page_number = next_query.pop("page[number]", None) or next_query.pop("page", None)
|
|
112
|
+
params = {**next_query, **base_params}
|
|
113
|
+
if page_number is not None:
|
|
114
|
+
params["page[number]"] = page_number
|
|
92
115
|
|
|
93
116
|
async def all(self) -> list[ModelT]:
|
|
94
117
|
return [item async for item in self]
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from collections.abc import Callable, Iterator
|
|
15
15
|
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
16
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
16
17
|
|
|
17
18
|
from .._pagination import parse_link_header
|
|
18
19
|
from .._query_params import build_query_params
|
|
@@ -84,8 +85,20 @@ class QuerySet(Generic[ModelT]):
|
|
|
84
85
|
)
|
|
85
86
|
|
|
86
87
|
def __iter__(self) -> Iterator[ModelT]:
|
|
88
|
+
# `base_params` (filter/sort/page_size/range) is re-applied on every
|
|
89
|
+
# page, not just the first. Confirmed against the live API: the
|
|
90
|
+
# `next` link's own query string always uses a *bare* `page=N`
|
|
91
|
+
# cursor — a different, incompatible pagination scheme from the
|
|
92
|
+
# bracketed `page[size]`/`page[number]` this library sends, and the
|
|
93
|
+
# server returns 400 if both appear on the same request. So rather
|
|
94
|
+
# than forwarding the link's query string as-is (which either loses
|
|
95
|
+
# page_size or actively errors once you try to keep it), we extract
|
|
96
|
+
# just the page *number* from the link and reissue it as
|
|
97
|
+
# `page[number]`, combined with our own persisted page_size/filter/
|
|
98
|
+
# sort/range.
|
|
99
|
+
base_params = self._initial_params()
|
|
87
100
|
url: str | None = self._path
|
|
88
|
-
params: dict[str, str] | None =
|
|
101
|
+
params: dict[str, str] | None = base_params
|
|
89
102
|
while url is not None:
|
|
90
103
|
response = self._client.request("GET", url, params=params)
|
|
91
104
|
for item in response.json():
|
|
@@ -93,8 +106,18 @@ class QuerySet(Generic[ModelT]):
|
|
|
93
106
|
if self._bind is not None:
|
|
94
107
|
self._bind(instance)
|
|
95
108
|
yield instance
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
|
|
110
|
+
next_url = parse_link_header(response.headers.get("Link")).get("next")
|
|
111
|
+
if next_url is None:
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
split = urlsplit(next_url)
|
|
115
|
+
url = f"{split.scheme}://{split.netloc}{split.path}" if split.scheme else split.path
|
|
116
|
+
next_query = dict(parse_qsl(split.query))
|
|
117
|
+
page_number = next_query.pop("page[number]", None) or next_query.pop("page", None)
|
|
118
|
+
params = {**next_query, **base_params}
|
|
119
|
+
if page_number is not None:
|
|
120
|
+
params["page[number]"] = page_number
|
|
98
121
|
|
|
99
122
|
def all(self) -> list[ModelT]:
|
|
100
123
|
return [item for item in self]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Integration tests for the auto-paginating query builder against respx."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from intra42._async.client import AsyncClient
|
|
6
|
+
from intra42._sync.client import Client
|
|
7
|
+
|
|
8
|
+
PAGE_1 = httpx.Response(
|
|
9
|
+
200,
|
|
10
|
+
json=[{"id": 1, "login": "a"}, {"id": 2, "login": "b"}],
|
|
11
|
+
headers={"Link": '<https://api.intra.42.fr/v2/users?page=2>; rel="next"'},
|
|
12
|
+
)
|
|
13
|
+
PAGE_2 = httpx.Response(200, json=[{"id": 3, "login": "c"}])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def test_all_walks_every_page_async(mock_token_route) -> None:
|
|
17
|
+
mock, _ = mock_token_route
|
|
18
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
19
|
+
|
|
20
|
+
client = AsyncClient("id", "secret")
|
|
21
|
+
try:
|
|
22
|
+
users = await client.users.all()
|
|
23
|
+
finally:
|
|
24
|
+
await client.aclose()
|
|
25
|
+
|
|
26
|
+
assert [u.id for u in users] == [1, 2, 3]
|
|
27
|
+
assert route.call_count == 2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def test_async_for_walks_every_page(mock_token_route) -> None:
|
|
31
|
+
mock, _ = mock_token_route
|
|
32
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
33
|
+
|
|
34
|
+
client = AsyncClient("id", "secret")
|
|
35
|
+
try:
|
|
36
|
+
seen = []
|
|
37
|
+
async for user in client.users:
|
|
38
|
+
seen.append(user.id)
|
|
39
|
+
finally:
|
|
40
|
+
await client.aclose()
|
|
41
|
+
|
|
42
|
+
assert seen == [1, 2, 3]
|
|
43
|
+
assert route.call_count == 2
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def test_filter_and_sort_are_sent_as_query_params(mock_token_route) -> None:
|
|
47
|
+
mock, _ = mock_token_route
|
|
48
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(
|
|
49
|
+
return_value=httpx.Response(200, json=[])
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
client = AsyncClient("id", "secret")
|
|
53
|
+
try:
|
|
54
|
+
await client.users.filter(primary_campus_id=1).sort("-pool_year").all()
|
|
55
|
+
finally:
|
|
56
|
+
await client.aclose()
|
|
57
|
+
|
|
58
|
+
request = route.calls.last.request
|
|
59
|
+
assert request.url.params["filter[primary_campus_id]"] == "1"
|
|
60
|
+
assert request.url.params["sort"] == "-pool_year"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_all_walks_every_page_sync(mock_token_route) -> None:
|
|
64
|
+
mock, _ = mock_token_route
|
|
65
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
66
|
+
|
|
67
|
+
with Client("id", "secret") as client:
|
|
68
|
+
users = client.users.all()
|
|
69
|
+
|
|
70
|
+
assert [u.id for u in users] == [1, 2, 3]
|
|
71
|
+
assert route.call_count == 2
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_for_loop_walks_every_page_sync(mock_token_route) -> None:
|
|
75
|
+
mock, _ = mock_token_route
|
|
76
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
77
|
+
|
|
78
|
+
with Client("id", "secret") as client:
|
|
79
|
+
seen = [user.id for user in client.users]
|
|
80
|
+
|
|
81
|
+
assert seen == [1, 2, 3]
|
|
82
|
+
assert route.call_count == 2
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def test_next_link_missing_page_size_is_restored(mock_token_route) -> None:
|
|
86
|
+
"""Regression test, confirmed against the live API: 42's `next` link
|
|
87
|
+
uses a bare `page=N` cursor — a different, incompatible pagination
|
|
88
|
+
scheme from the bracketed `page[size]` this library sends, and the
|
|
89
|
+
server returns 400 if both appear on the same request. Forwarding the
|
|
90
|
+
link's query string as-is therefore either silently drops page_size
|
|
91
|
+
(producing duplicate/missing rows) or, if merged naively, breaks the
|
|
92
|
+
request outright. The fix extracts just the page number from the link
|
|
93
|
+
and reissues it as `page[number]`, never forwarding the bare `page`
|
|
94
|
+
key, alongside page_size/filter/sort/range re-applied on every page."""
|
|
95
|
+
mock, _ = mock_token_route
|
|
96
|
+
page_1 = httpx.Response(
|
|
97
|
+
200,
|
|
98
|
+
json=[{"id": 1}, {"id": 2}],
|
|
99
|
+
headers={"Link": '<https://api.intra.42.fr/v2/users?page=2>; rel="next"'},
|
|
100
|
+
)
|
|
101
|
+
page_2 = httpx.Response(200, json=[{"id": 3}])
|
|
102
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[page_1, page_2])
|
|
103
|
+
|
|
104
|
+
client = AsyncClient("id", "secret")
|
|
105
|
+
try:
|
|
106
|
+
users = await client.users.filter(primary_campus_id=1).page_size(2).all()
|
|
107
|
+
finally:
|
|
108
|
+
await client.aclose()
|
|
109
|
+
|
|
110
|
+
assert [u.id for u in users] == [1, 2, 3]
|
|
111
|
+
assert route.call_count == 2
|
|
112
|
+
|
|
113
|
+
first_request, second_request = (call.request for call in route.calls)
|
|
114
|
+
assert first_request.url.params["page[size]"] == "2"
|
|
115
|
+
assert first_request.url.params["filter[primary_campus_id]"] == "1"
|
|
116
|
+
# The fix: these must survive onto the second page too, even though the
|
|
117
|
+
# `next` link's own query string didn't carry them.
|
|
118
|
+
assert second_request.url.params["page[size]"] == "2"
|
|
119
|
+
assert second_request.url.params["filter[primary_campus_id]"] == "1"
|
|
120
|
+
# The link's cursor (bare `page=2`) must come through as `page[number]`,
|
|
121
|
+
# not the bare key — mixing the two schemes is what causes the 400.
|
|
122
|
+
assert second_request.url.params["page[number]"] == "2"
|
|
123
|
+
assert "page" not in second_request.url.params
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_next_link_missing_page_size_is_restored_sync(mock_token_route) -> None:
|
|
127
|
+
mock, _ = mock_token_route
|
|
128
|
+
page_1 = httpx.Response(
|
|
129
|
+
200,
|
|
130
|
+
json=[{"id": 1}, {"id": 2}],
|
|
131
|
+
headers={"Link": '<https://api.intra.42.fr/v2/users?page=2>; rel="next"'},
|
|
132
|
+
)
|
|
133
|
+
page_2 = httpx.Response(200, json=[{"id": 3}])
|
|
134
|
+
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[page_1, page_2])
|
|
135
|
+
|
|
136
|
+
with Client("id", "secret") as client:
|
|
137
|
+
users = client.users.page_size(2).all()
|
|
138
|
+
|
|
139
|
+
assert [u.id for u in users] == [1, 2, 3]
|
|
140
|
+
second_request = route.calls[1].request
|
|
141
|
+
assert second_request.url.params["page[size]"] == "2"
|
|
@@ -8,7 +8,7 @@ resolution-markers = [
|
|
|
8
8
|
|
|
9
9
|
[[package]]
|
|
10
10
|
name = "42api"
|
|
11
|
-
version = "0.1.
|
|
11
|
+
version = "0.1.3"
|
|
12
12
|
source = { editable = "." }
|
|
13
13
|
dependencies = [
|
|
14
14
|
{ name = "httpx" },
|
|
@@ -180,7 +180,7 @@ name = "exceptiongroup"
|
|
|
180
180
|
version = "1.3.1"
|
|
181
181
|
source = { registry = "https://pypi.org/simple" }
|
|
182
182
|
dependencies = [
|
|
183
|
-
{ name = "typing-extensions" },
|
|
183
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
184
184
|
]
|
|
185
185
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
186
186
|
wheels = [
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Integration tests for the auto-paginating query builder against respx."""
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
|
|
5
|
-
from intra42._async.client import AsyncClient
|
|
6
|
-
from intra42._sync.client import Client
|
|
7
|
-
|
|
8
|
-
PAGE_1 = httpx.Response(
|
|
9
|
-
200,
|
|
10
|
-
json=[{"id": 1, "login": "a"}, {"id": 2, "login": "b"}],
|
|
11
|
-
headers={"Link": '<https://api.intra.42.fr/v2/users?page=2>; rel="next"'},
|
|
12
|
-
)
|
|
13
|
-
PAGE_2 = httpx.Response(200, json=[{"id": 3, "login": "c"}])
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
async def test_all_walks_every_page_async(mock_token_route) -> None:
|
|
17
|
-
mock, _ = mock_token_route
|
|
18
|
-
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
19
|
-
|
|
20
|
-
client = AsyncClient("id", "secret")
|
|
21
|
-
try:
|
|
22
|
-
users = await client.users.all()
|
|
23
|
-
finally:
|
|
24
|
-
await client.aclose()
|
|
25
|
-
|
|
26
|
-
assert [u.id for u in users] == [1, 2, 3]
|
|
27
|
-
assert route.call_count == 2
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async def test_async_for_walks_every_page(mock_token_route) -> None:
|
|
31
|
-
mock, _ = mock_token_route
|
|
32
|
-
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
33
|
-
|
|
34
|
-
client = AsyncClient("id", "secret")
|
|
35
|
-
try:
|
|
36
|
-
seen = []
|
|
37
|
-
async for user in client.users:
|
|
38
|
-
seen.append(user.id)
|
|
39
|
-
finally:
|
|
40
|
-
await client.aclose()
|
|
41
|
-
|
|
42
|
-
assert seen == [1, 2, 3]
|
|
43
|
-
assert route.call_count == 2
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
async def test_filter_and_sort_are_sent_as_query_params(mock_token_route) -> None:
|
|
47
|
-
mock, _ = mock_token_route
|
|
48
|
-
route = mock.get("https://api.intra.42.fr/v2/users").mock(
|
|
49
|
-
return_value=httpx.Response(200, json=[])
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
client = AsyncClient("id", "secret")
|
|
53
|
-
try:
|
|
54
|
-
await client.users.filter(primary_campus_id=1).sort("-pool_year").all()
|
|
55
|
-
finally:
|
|
56
|
-
await client.aclose()
|
|
57
|
-
|
|
58
|
-
request = route.calls.last.request
|
|
59
|
-
assert request.url.params["filter[primary_campus_id]"] == "1"
|
|
60
|
-
assert request.url.params["sort"] == "-pool_year"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_all_walks_every_page_sync(mock_token_route) -> None:
|
|
64
|
-
mock, _ = mock_token_route
|
|
65
|
-
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
66
|
-
|
|
67
|
-
with Client("id", "secret") as client:
|
|
68
|
-
users = client.users.all()
|
|
69
|
-
|
|
70
|
-
assert [u.id for u in users] == [1, 2, 3]
|
|
71
|
-
assert route.call_count == 2
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_for_loop_walks_every_page_sync(mock_token_route) -> None:
|
|
75
|
-
mock, _ = mock_token_route
|
|
76
|
-
route = mock.get("https://api.intra.42.fr/v2/users").mock(side_effect=[PAGE_1, PAGE_2])
|
|
77
|
-
|
|
78
|
-
with Client("id", "secret") as client:
|
|
79
|
-
seen = [user.id for user in client.users]
|
|
80
|
-
|
|
81
|
-
assert seen == [1, 2, 3]
|
|
82
|
-
assert route.call_count == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|