42api 0.1.1__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.1 → 42api-0.1.3}/.gitignore +4 -1
- {42api-0.1.1 → 42api-0.1.3}/PKG-INFO +9 -7
- {42api-0.1.1 → 42api-0.1.3}/README.md +8 -6
- {42api-0.1.1 → 42api-0.1.3}/pyproject.toml +1 -1
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/__init__.py +10 -2
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/client.py +8 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/query.py +26 -3
- 42api-0.1.3/src/intra42/_async/resources/accreditations.py +16 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/cursus.py +4 -0
- 42api-0.1.3/src/intra42/_async/resources/exams.py +23 -0
- 42api-0.1.3/src/intra42/_async/resources/projects.py +19 -0
- 42api-0.1.3/src/intra42/_async/resources/projects_users.py +22 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/users.py +4 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/client.py +8 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/query.py +26 -3
- 42api-0.1.3/src/intra42/_sync/resources/accreditations.py +22 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/cursus.py +4 -0
- 42api-0.1.3/src/intra42/_sync/resources/exams.py +27 -0
- 42api-0.1.3/src/intra42/_sync/resources/projects.py +25 -0
- 42api-0.1.3/src/intra42/_sync/resources/projects_users.py +26 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/users.py +4 -0
- 42api-0.1.3/src/intra42/models/accreditation.py +29 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/cursus.py +6 -0
- 42api-0.1.3/src/intra42/models/exam.py +32 -0
- 42api-0.1.3/src/intra42/models/project.py +106 -0
- 42api-0.1.3/src/intra42/models/project_user.py +74 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/user.py +6 -0
- 42api-0.1.3/tests/integration/resources/test_resources_accreditations.py +78 -0
- 42api-0.1.3/tests/integration/resources/test_resources_exams.py +159 -0
- 42api-0.1.3/tests/integration/resources/test_resources_projects.py +193 -0
- 42api-0.1.3/tests/integration/resources/test_resources_projects_users.py +176 -0
- 42api-0.1.3/tests/integration/test_pagination_walk.py +141 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/unit/test_public_api.py +8 -0
- {42api-0.1.1 → 42api-0.1.3}/uv.lock +2 -2
- 42api-0.1.1/tests/integration/test_pagination_walk.py +0 -82
- {42api-0.1.1 → 42api-0.1.3}/.github/workflows/ci.yml +0 -0
- {42api-0.1.1 → 42api-0.1.3}/.github/workflows/publish.yml +0 -0
- {42api-0.1.1 → 42api-0.1.3}/.pre-commit-config.yaml +0 -0
- {42api-0.1.1 → 42api-0.1.3}/LICENSE +0 -0
- {42api-0.1.1 → 42api-0.1.3}/scripts/unasync_generate.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/__init__.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/__init__.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/base.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/campus_users.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/campuses.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/events.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_async/resources/locations.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_auth.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_config.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_pagination.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_query_params.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_rate_limit.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/__init__.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/__init__.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/base.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/campus_users.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/campuses.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/events.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/_sync/resources/locations.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/exceptions.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/__init__.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/base.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/campus.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/campus_user.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/event.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/models/location.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/src/intra42/py.typed +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/conftest.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/resources/test_resources_campus_users.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/resources/test_resources_campuses.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/resources/test_resources_cursus.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/resources/test_resources_events.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/resources/test_resources_locations.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/test_client_integration.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/integration/test_nested_access.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/unit/test_auth.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/unit/test_models_campus.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/unit/test_models_user.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/unit/test_pagination.py +0 -0
- {42api-0.1.1 → 42api-0.1.3}/tests/unit/test_query_builder.py +0 -0
- {42api-0.1.1 → 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.
|
|
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>
|
|
@@ -55,7 +55,7 @@ with Client(client_id="...", client_secret="...") as client:
|
|
|
55
55
|
user = client.users.get("jdoe")
|
|
56
56
|
print(user.login, user.email)
|
|
57
57
|
|
|
58
|
-
for user in client.users.filter(
|
|
58
|
+
for user in client.users.filter(primary_campus_id=1).sort("-pool_year"):
|
|
59
59
|
print(user.login)
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ from intra42 import AsyncClient
|
|
|
69
69
|
async def main() -> None:
|
|
70
70
|
async with AsyncClient(client_id="...", client_secret="...") as client:
|
|
71
71
|
user = await client.users.get("jdoe")
|
|
72
|
-
async for user in client.users.filter(
|
|
72
|
+
async for user in client.users.filter(primary_campus_id=1).sort("-pool_year"):
|
|
73
73
|
print(user.login)
|
|
74
74
|
|
|
75
75
|
|
|
@@ -108,10 +108,12 @@ and `NetworkError` (connection/timeout failures).
|
|
|
108
108
|
|
|
109
109
|
## Status
|
|
110
110
|
|
|
111
|
-
Early-stage: currently covers `users`, `
|
|
112
|
-
`cursus`, `events`,
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
Early-stage: currently covers `users`, `accreditations`, `campuses`,
|
|
112
|
+
`campus_users`, `cursus`, `events`, `exams`, `locations`, `projects`, and
|
|
113
|
+
`projects_users` (`exams`, `locations`, and `projects_users` each include
|
|
114
|
+
their `graph` analytics endpoint). More resources are added incrementally
|
|
115
|
+
on top of the same client/model/query-builder pattern.
|
|
116
|
+
The api with restricted access are not implemented currently.
|
|
115
117
|
|
|
116
118
|
## Development
|
|
117
119
|
|
|
@@ -29,7 +29,7 @@ with Client(client_id="...", client_secret="...") as client:
|
|
|
29
29
|
user = client.users.get("jdoe")
|
|
30
30
|
print(user.login, user.email)
|
|
31
31
|
|
|
32
|
-
for user in client.users.filter(
|
|
32
|
+
for user in client.users.filter(primary_campus_id=1).sort("-pool_year"):
|
|
33
33
|
print(user.login)
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -43,7 +43,7 @@ from intra42 import AsyncClient
|
|
|
43
43
|
async def main() -> None:
|
|
44
44
|
async with AsyncClient(client_id="...", client_secret="...") as client:
|
|
45
45
|
user = await client.users.get("jdoe")
|
|
46
|
-
async for user in client.users.filter(
|
|
46
|
+
async for user in client.users.filter(primary_campus_id=1).sort("-pool_year"):
|
|
47
47
|
print(user.login)
|
|
48
48
|
|
|
49
49
|
|
|
@@ -82,10 +82,12 @@ and `NetworkError` (connection/timeout failures).
|
|
|
82
82
|
|
|
83
83
|
## Status
|
|
84
84
|
|
|
85
|
-
Early-stage: currently covers `users`, `
|
|
86
|
-
`cursus`, `events`,
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
Early-stage: currently covers `users`, `accreditations`, `campuses`,
|
|
86
|
+
`campus_users`, `cursus`, `events`, `exams`, `locations`, `projects`, and
|
|
87
|
+
`projects_users` (`exams`, `locations`, and `projects_users` each include
|
|
88
|
+
their `graph` analytics endpoint). More resources are added incrementally
|
|
89
|
+
on top of the same client/model/query-builder pattern.
|
|
90
|
+
The api with restricted access are not implemented currently.
|
|
89
91
|
|
|
90
92
|
## Development
|
|
91
93
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
with Client(client_id, client_secret) as client:
|
|
6
6
|
user = client.users.get("jdoe")
|
|
7
|
-
for user in client.users.filter(
|
|
7
|
+
for user in client.users.filter(primary_campus_id=1).sort("-pool_year"):
|
|
8
8
|
...
|
|
9
9
|
|
|
10
10
|
An async client with the same interface is available as ``AsyncClient``.
|
|
@@ -25,11 +25,15 @@ from .exceptions import (
|
|
|
25
25
|
ServerError,
|
|
26
26
|
ValidationError,
|
|
27
27
|
)
|
|
28
|
+
from .models.accreditation import Accreditation
|
|
28
29
|
from .models.campus import Campus
|
|
29
30
|
from .models.campus_user import CampusUser
|
|
30
31
|
from .models.cursus import Cursus
|
|
31
32
|
from .models.event import Event
|
|
33
|
+
from .models.exam import Exam
|
|
32
34
|
from .models.location import Location
|
|
35
|
+
from .models.project import Project
|
|
36
|
+
from .models.project_user import ProjectUser
|
|
33
37
|
from .models.user import User
|
|
34
38
|
|
|
35
39
|
__all__ = [
|
|
@@ -37,11 +41,15 @@ __all__ = [
|
|
|
37
41
|
"Client",
|
|
38
42
|
"ClientConfig",
|
|
39
43
|
"User",
|
|
44
|
+
"Accreditation",
|
|
40
45
|
"Campus",
|
|
41
46
|
"CampusUser",
|
|
42
47
|
"Cursus",
|
|
43
48
|
"Event",
|
|
49
|
+
"Exam",
|
|
44
50
|
"Location",
|
|
51
|
+
"Project",
|
|
52
|
+
"ProjectUser",
|
|
45
53
|
"FortyTwoAPIError",
|
|
46
54
|
"AuthenticationError",
|
|
47
55
|
"PermissionDeniedError",
|
|
@@ -52,4 +60,4 @@ __all__ = [
|
|
|
52
60
|
"NetworkError",
|
|
53
61
|
]
|
|
54
62
|
|
|
55
|
-
__version__ = "0.1.
|
|
63
|
+
__version__ = "0.1.3"
|
|
@@ -12,11 +12,15 @@ from .._auth import TokenManager
|
|
|
12
12
|
from .._config import DEFAULT_BASE_URL, ClientConfig
|
|
13
13
|
from .._rate_limit import RateLimiter
|
|
14
14
|
from ..exceptions import NetworkError, raise_for_status
|
|
15
|
+
from .resources.accreditations import AsyncAccreditationsResource
|
|
15
16
|
from .resources.campus_users import AsyncCampusUsersResource
|
|
16
17
|
from .resources.campuses import AsyncCampusesResource
|
|
17
18
|
from .resources.cursus import AsyncCursusResource
|
|
18
19
|
from .resources.events import AsyncEventsResource
|
|
20
|
+
from .resources.exams import AsyncExamsResource
|
|
19
21
|
from .resources.locations import AsyncLocationsResource
|
|
22
|
+
from .resources.projects import AsyncProjectsResource
|
|
23
|
+
from .resources.projects_users import AsyncProjectUsersResource
|
|
20
24
|
from .resources.users import AsyncUsersResource
|
|
21
25
|
|
|
22
26
|
|
|
@@ -58,11 +62,15 @@ class AsyncClient:
|
|
|
58
62
|
)
|
|
59
63
|
|
|
60
64
|
self.users = AsyncUsersResource(self)
|
|
65
|
+
self.accreditations = AsyncAccreditationsResource(self)
|
|
61
66
|
self.campuses = AsyncCampusesResource(self)
|
|
62
67
|
self.campus_users = AsyncCampusUsersResource(self)
|
|
63
68
|
self.cursus = AsyncCursusResource(self)
|
|
64
69
|
self.events = AsyncEventsResource(self)
|
|
70
|
+
self.exams = AsyncExamsResource(self)
|
|
65
71
|
self.locations = AsyncLocationsResource(self)
|
|
72
|
+
self.projects = AsyncProjectsResource(self)
|
|
73
|
+
self.projects_users = AsyncProjectUsersResource(self)
|
|
66
74
|
|
|
67
75
|
@property
|
|
68
76
|
def base_url(self) -> str:
|
|
@@ -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]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...models.accreditation import Accreditation
|
|
4
|
+
from .base import AsyncResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncAccreditationsResource(AsyncResource[Accreditation]):
|
|
8
|
+
path = "/accreditations"
|
|
9
|
+
model = Accreditation
|
|
10
|
+
|
|
11
|
+
def _bind_relations(self, instance: Accreditation) -> None:
|
|
12
|
+
# Referenced in the users apidoc page: GET /accreditations/:id/users.
|
|
13
|
+
instance._bind_relation(
|
|
14
|
+
"users",
|
|
15
|
+
lambda: self._client.users._queryset(f"/accreditations/{instance.id}/users"),
|
|
16
|
+
)
|
|
@@ -17,3 +17,7 @@ class AsyncCursusResource(AsyncResource[Cursus]):
|
|
|
17
17
|
"events",
|
|
18
18
|
lambda: self._client.events._queryset(f"/cursus/{instance.id}/events"),
|
|
19
19
|
)
|
|
20
|
+
instance._bind_relation(
|
|
21
|
+
"projects",
|
|
22
|
+
lambda: self._client.projects._queryset(f"/cursus/{instance.id}/projects"),
|
|
23
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...models.exam import Exam
|
|
4
|
+
from .base import AsyncResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncExamsResource(AsyncResource[Exam]):
|
|
8
|
+
path = "/exams"
|
|
9
|
+
model = Exam
|
|
10
|
+
|
|
11
|
+
async def graph(
|
|
12
|
+
self, *, field: str | None = None, interval: str | None = None
|
|
13
|
+
) -> dict[str, int]:
|
|
14
|
+
"""Grouped temporal counts (``GET /exams/graph...``)."""
|
|
15
|
+
path = "/exams/graph"
|
|
16
|
+
if interval is not None and field is None:
|
|
17
|
+
field = "created_at"
|
|
18
|
+
if field is not None:
|
|
19
|
+
path += f"/on/{field}"
|
|
20
|
+
if interval is not None:
|
|
21
|
+
path += f"/by/{interval}"
|
|
22
|
+
response = await self._client.request("GET", path)
|
|
23
|
+
return response.json()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...models.project import Project
|
|
4
|
+
from .base import AsyncResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncProjectsResource(AsyncResource[Project]):
|
|
8
|
+
path = "/projects"
|
|
9
|
+
model = Project
|
|
10
|
+
|
|
11
|
+
def _bind_relations(self, instance: Project) -> None:
|
|
12
|
+
# Referenced in the projects_users apidoc page:
|
|
13
|
+
# GET /projects/:id/projects_users.
|
|
14
|
+
instance._bind_relation(
|
|
15
|
+
"projects_users",
|
|
16
|
+
lambda: self._client.projects_users._queryset(
|
|
17
|
+
f"/projects/{instance.id}/projects_users"
|
|
18
|
+
),
|
|
19
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ...models.project_user import ProjectUser
|
|
4
|
+
from .base import AsyncResource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncProjectUsersResource(AsyncResource[ProjectUser]):
|
|
8
|
+
path = "/projects_users"
|
|
9
|
+
model = ProjectUser
|
|
10
|
+
|
|
11
|
+
async def graph(
|
|
12
|
+
self, *, field: str | None = None, interval: str | None = None
|
|
13
|
+
) -> dict[str, int]:
|
|
14
|
+
path = "/projects_users/graph"
|
|
15
|
+
if interval is not None and field is None:
|
|
16
|
+
field = "created_at"
|
|
17
|
+
if field is not None:
|
|
18
|
+
path += f"/on/{field}"
|
|
19
|
+
if interval is not None:
|
|
20
|
+
path += f"/by/{interval}"
|
|
21
|
+
response = await self._client.request("GET", path)
|
|
22
|
+
return response.json()
|
|
@@ -21,3 +21,7 @@ class AsyncUsersResource(AsyncResource[User]):
|
|
|
21
21
|
"locations",
|
|
22
22
|
lambda: self._client.locations._queryset(f"/users/{instance.id}/locations"),
|
|
23
23
|
)
|
|
24
|
+
instance._bind_relation(
|
|
25
|
+
"projects_users",
|
|
26
|
+
lambda: self._client.projects_users._queryset(f"/users/{instance.id}/projects_users"),
|
|
27
|
+
)
|
|
@@ -18,11 +18,15 @@ from .._auth import TokenManager
|
|
|
18
18
|
from .._config import DEFAULT_BASE_URL, ClientConfig
|
|
19
19
|
from .._rate_limit import RateLimiter
|
|
20
20
|
from ..exceptions import NetworkError, raise_for_status
|
|
21
|
+
from .resources.accreditations import AccreditationsResource
|
|
21
22
|
from .resources.campus_users import CampusUsersResource
|
|
22
23
|
from .resources.campuses import CampusesResource
|
|
23
24
|
from .resources.cursus import CursusResource
|
|
24
25
|
from .resources.events import EventsResource
|
|
26
|
+
from .resources.exams import ExamsResource
|
|
25
27
|
from .resources.locations import LocationsResource
|
|
28
|
+
from .resources.projects import ProjectsResource
|
|
29
|
+
from .resources.projects_users import ProjectUsersResource
|
|
26
30
|
from .resources.users import UsersResource
|
|
27
31
|
|
|
28
32
|
|
|
@@ -64,11 +68,15 @@ class Client:
|
|
|
64
68
|
)
|
|
65
69
|
|
|
66
70
|
self.users = UsersResource(self)
|
|
71
|
+
self.accreditations = AccreditationsResource(self)
|
|
67
72
|
self.campuses = CampusesResource(self)
|
|
68
73
|
self.campus_users = CampusUsersResource(self)
|
|
69
74
|
self.cursus = CursusResource(self)
|
|
70
75
|
self.events = EventsResource(self)
|
|
76
|
+
self.exams = ExamsResource(self)
|
|
71
77
|
self.locations = LocationsResource(self)
|
|
78
|
+
self.projects = ProjectsResource(self)
|
|
79
|
+
self.projects_users = ProjectUsersResource(self)
|
|
72
80
|
|
|
73
81
|
@property
|
|
74
82
|
def base_url(self) -> str:
|
|
@@ -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,22 @@
|
|
|
1
|
+
# GENERATED FILE — DO NOT EDIT BY HAND.
|
|
2
|
+
#
|
|
3
|
+
# Generated from the corresponding module under intra42._async by
|
|
4
|
+
# scripts/unasync_generate.py (via the `unasync` library). Edit the
|
|
5
|
+
# async source and re-run that script instead.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ...models.accreditation import Accreditation
|
|
10
|
+
from .base import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AccreditationsResource(Resource[Accreditation]):
|
|
14
|
+
path = "/accreditations"
|
|
15
|
+
model = Accreditation
|
|
16
|
+
|
|
17
|
+
def _bind_relations(self, instance: Accreditation) -> None:
|
|
18
|
+
# Referenced in the users apidoc page: GET /accreditations/:id/users.
|
|
19
|
+
instance._bind_relation(
|
|
20
|
+
"users",
|
|
21
|
+
lambda: self._client.users._queryset(f"/accreditations/{instance.id}/users"),
|
|
22
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# GENERATED FILE — DO NOT EDIT BY HAND.
|
|
2
|
+
#
|
|
3
|
+
# Generated from the corresponding module under intra42._async by
|
|
4
|
+
# scripts/unasync_generate.py (via the `unasync` library). Edit the
|
|
5
|
+
# async source and re-run that script instead.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ...models.exam import Exam
|
|
10
|
+
from .base import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExamsResource(Resource[Exam]):
|
|
14
|
+
path = "/exams"
|
|
15
|
+
model = Exam
|
|
16
|
+
|
|
17
|
+
def graph(self, *, field: str | None = None, interval: str | None = None) -> dict[str, int]:
|
|
18
|
+
"""Grouped temporal counts (``GET /exams/graph...``)."""
|
|
19
|
+
path = "/exams/graph"
|
|
20
|
+
if interval is not None and field is None:
|
|
21
|
+
field = "created_at"
|
|
22
|
+
if field is not None:
|
|
23
|
+
path += f"/on/{field}"
|
|
24
|
+
if interval is not None:
|
|
25
|
+
path += f"/by/{interval}"
|
|
26
|
+
response = self._client.request("GET", path)
|
|
27
|
+
return response.json()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# GENERATED FILE — DO NOT EDIT BY HAND.
|
|
2
|
+
#
|
|
3
|
+
# Generated from the corresponding module under intra42._async by
|
|
4
|
+
# scripts/unasync_generate.py (via the `unasync` library). Edit the
|
|
5
|
+
# async source and re-run that script instead.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ...models.project import Project
|
|
10
|
+
from .base import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProjectsResource(Resource[Project]):
|
|
14
|
+
path = "/projects"
|
|
15
|
+
model = Project
|
|
16
|
+
|
|
17
|
+
def _bind_relations(self, instance: Project) -> None:
|
|
18
|
+
# Referenced in the projects_users apidoc page:
|
|
19
|
+
# GET /projects/:id/projects_users.
|
|
20
|
+
instance._bind_relation(
|
|
21
|
+
"projects_users",
|
|
22
|
+
lambda: self._client.projects_users._queryset(
|
|
23
|
+
f"/projects/{instance.id}/projects_users"
|
|
24
|
+
),
|
|
25
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# GENERATED FILE — DO NOT EDIT BY HAND.
|
|
2
|
+
#
|
|
3
|
+
# Generated from the corresponding module under intra42._async by
|
|
4
|
+
# scripts/unasync_generate.py (via the `unasync` library). Edit the
|
|
5
|
+
# async source and re-run that script instead.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ...models.project_user import ProjectUser
|
|
10
|
+
from .base import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProjectUsersResource(Resource[ProjectUser]):
|
|
14
|
+
path = "/projects_users"
|
|
15
|
+
model = ProjectUser
|
|
16
|
+
|
|
17
|
+
def graph(self, *, field: str | None = None, interval: str | None = None) -> dict[str, int]:
|
|
18
|
+
path = "/projects_users/graph"
|
|
19
|
+
if interval is not None and field is None:
|
|
20
|
+
field = "created_at"
|
|
21
|
+
if field is not None:
|
|
22
|
+
path += f"/on/{field}"
|
|
23
|
+
if interval is not None:
|
|
24
|
+
path += f"/by/{interval}"
|
|
25
|
+
response = self._client.request("GET", path)
|
|
26
|
+
return response.json()
|
|
@@ -27,3 +27,7 @@ class UsersResource(Resource[User]):
|
|
|
27
27
|
"locations",
|
|
28
28
|
lambda: self._client.locations._queryset(f"/users/{instance.id}/locations"),
|
|
29
29
|
)
|
|
30
|
+
instance._bind_relation(
|
|
31
|
+
"projects_users",
|
|
32
|
+
lambda: self._client.projects_users._queryset(f"/users/{instance.id}/projects_users"),
|
|
33
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The ``Accreditation`` resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from .base import FortyTwoModel
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .._async.query import AsyncQuerySet
|
|
12
|
+
from .._sync.query import QuerySet
|
|
13
|
+
from .user import User
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Accreditation(FortyTwoModel):
|
|
17
|
+
id: int
|
|
18
|
+
name: str | None = None
|
|
19
|
+
user_id: int | None = None
|
|
20
|
+
cursus_id: int | None = None
|
|
21
|
+
difficulty: int | None = None
|
|
22
|
+
validated: bool | None = None
|
|
23
|
+
created_at: datetime | None = None
|
|
24
|
+
updated_at: datetime | None = None
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def users(self) -> AsyncQuerySet[User] | QuerySet[User]:
|
|
28
|
+
"""Users holding this accreditation (``GET /accreditations/{id}/users``)."""
|
|
29
|
+
return self._relation("users")
|
|
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
from .._async.query import AsyncQuerySet
|
|
12
12
|
from .._sync.query import QuerySet
|
|
13
13
|
from .event import Event
|
|
14
|
+
from .project import Project
|
|
14
15
|
from .user import User
|
|
15
16
|
|
|
16
17
|
|
|
@@ -33,3 +34,8 @@ class Cursus(FortyTwoModel):
|
|
|
33
34
|
def events(self) -> AsyncQuerySet[Event] | QuerySet[Event]:
|
|
34
35
|
"""Events for this cursus (``GET /cursus/{id}/events``)."""
|
|
35
36
|
return self._relation("events")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def projects(self) -> AsyncQuerySet[Project] | QuerySet[Project]:
|
|
40
|
+
"""Projects belonging to this cursus (``GET /cursus/{id}/projects``)."""
|
|
41
|
+
return self._relation("projects")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The ``Exam`` resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from .base import FortyTwoModel
|
|
8
|
+
from .campus import Campus
|
|
9
|
+
from .cursus import Cursus
|
|
10
|
+
from .project import Project
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Exam(FortyTwoModel):
|
|
14
|
+
id: int
|
|
15
|
+
name: str | None = None
|
|
16
|
+
ip_range: str | None = None
|
|
17
|
+
begin_at: datetime | None = None
|
|
18
|
+
end_at: datetime | None = None
|
|
19
|
+
location: str | None = None
|
|
20
|
+
max_people: int | None = None
|
|
21
|
+
nbr_subscribers: int | None = None
|
|
22
|
+
created_at: datetime | None = None
|
|
23
|
+
updated_at: datetime | None = None
|
|
24
|
+
campus_id: int | None = None
|
|
25
|
+
campus: Campus | None = None
|
|
26
|
+
cursus: list[Cursus] | None = None
|
|
27
|
+
projects: list[Project] | None = None
|
|
28
|
+
visible: bool | None = None
|
|
29
|
+
validated: bool | None = None
|
|
30
|
+
validated_at: datetime | None = None
|
|
31
|
+
validator_id: int | None = None
|
|
32
|
+
prohibition_of_cancellation: int | None = None
|