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.
Files changed (81) hide show
  1. {42api-0.1.2 → 42api-0.1.3}/PKG-INFO +1 -1
  2. {42api-0.1.2 → 42api-0.1.3}/pyproject.toml +1 -1
  3. {42api-0.1.2 → 42api-0.1.3}/src/intra42/__init__.py +1 -1
  4. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/query.py +26 -3
  5. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/query.py +26 -3
  6. 42api-0.1.3/tests/integration/test_pagination_walk.py +141 -0
  7. {42api-0.1.2 → 42api-0.1.3}/uv.lock +2 -2
  8. 42api-0.1.2/tests/integration/test_pagination_walk.py +0 -82
  9. {42api-0.1.2 → 42api-0.1.3}/.github/workflows/ci.yml +0 -0
  10. {42api-0.1.2 → 42api-0.1.3}/.github/workflows/publish.yml +0 -0
  11. {42api-0.1.2 → 42api-0.1.3}/.gitignore +0 -0
  12. {42api-0.1.2 → 42api-0.1.3}/.pre-commit-config.yaml +0 -0
  13. {42api-0.1.2 → 42api-0.1.3}/LICENSE +0 -0
  14. {42api-0.1.2 → 42api-0.1.3}/README.md +0 -0
  15. {42api-0.1.2 → 42api-0.1.3}/scripts/unasync_generate.py +0 -0
  16. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/__init__.py +0 -0
  17. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/client.py +0 -0
  18. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/__init__.py +0 -0
  19. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/accreditations.py +0 -0
  20. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/base.py +0 -0
  21. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/campus_users.py +0 -0
  22. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/campuses.py +0 -0
  23. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/cursus.py +0 -0
  24. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/events.py +0 -0
  25. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/exams.py +0 -0
  26. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/locations.py +0 -0
  27. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/projects.py +0 -0
  28. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/projects_users.py +0 -0
  29. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_async/resources/users.py +0 -0
  30. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_auth.py +0 -0
  31. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_config.py +0 -0
  32. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_pagination.py +0 -0
  33. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_query_params.py +0 -0
  34. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_rate_limit.py +0 -0
  35. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/__init__.py +0 -0
  36. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/client.py +0 -0
  37. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/__init__.py +0 -0
  38. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/accreditations.py +0 -0
  39. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/base.py +0 -0
  40. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/campus_users.py +0 -0
  41. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/campuses.py +0 -0
  42. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/cursus.py +0 -0
  43. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/events.py +0 -0
  44. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/exams.py +0 -0
  45. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/locations.py +0 -0
  46. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/projects.py +0 -0
  47. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/projects_users.py +0 -0
  48. {42api-0.1.2 → 42api-0.1.3}/src/intra42/_sync/resources/users.py +0 -0
  49. {42api-0.1.2 → 42api-0.1.3}/src/intra42/exceptions.py +0 -0
  50. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/__init__.py +0 -0
  51. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/accreditation.py +0 -0
  52. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/base.py +0 -0
  53. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/campus.py +0 -0
  54. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/campus_user.py +0 -0
  55. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/cursus.py +0 -0
  56. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/event.py +0 -0
  57. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/exam.py +0 -0
  58. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/location.py +0 -0
  59. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/project.py +0 -0
  60. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/project_user.py +0 -0
  61. {42api-0.1.2 → 42api-0.1.3}/src/intra42/models/user.py +0 -0
  62. {42api-0.1.2 → 42api-0.1.3}/src/intra42/py.typed +0 -0
  63. {42api-0.1.2 → 42api-0.1.3}/tests/conftest.py +0 -0
  64. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_accreditations.py +0 -0
  65. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_campus_users.py +0 -0
  66. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_campuses.py +0 -0
  67. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_cursus.py +0 -0
  68. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_events.py +0 -0
  69. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_exams.py +0 -0
  70. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_locations.py +0 -0
  71. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_projects.py +0 -0
  72. {42api-0.1.2 → 42api-0.1.3}/tests/integration/resources/test_resources_projects_users.py +0 -0
  73. {42api-0.1.2 → 42api-0.1.3}/tests/integration/test_client_integration.py +0 -0
  74. {42api-0.1.2 → 42api-0.1.3}/tests/integration/test_nested_access.py +0 -0
  75. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_auth.py +0 -0
  76. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_models_campus.py +0 -0
  77. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_models_user.py +0 -0
  78. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_pagination.py +0 -0
  79. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_public_api.py +0 -0
  80. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_query_builder.py +0 -0
  81. {42api-0.1.2 → 42api-0.1.3}/tests/unit/test_rate_limit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: 42api
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Object-oriented Python client for the 42 School API (api.intra.42.fr)
5
5
  Project-URL: Homepage, https://github.com/Shadowaker/42api
6
6
  Author-email: Dan <danyridolfo98@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "42api"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Object-oriented Python client for the 42 School API (api.intra.42.fr)"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -60,4 +60,4 @@ __all__ = [
60
60
  "NetworkError",
61
61
  ]
62
62
 
63
- __version__ = "0.1.2"
63
+ __version__ = "0.1.3"
@@ -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 = self._initial_params()
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
- url = parse_link_header(response.headers.get("Link")).get("next")
91
- params = None # the next URL already carries its full query string
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 = self._initial_params()
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
- url = parse_link_header(response.headers.get("Link")).get("next")
97
- params = None # the next URL already carries its full query string
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.2"
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