42api 0.1.1__tar.gz → 0.1.2__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 (80) hide show
  1. {42api-0.1.1 → 42api-0.1.2}/.gitignore +4 -1
  2. {42api-0.1.1 → 42api-0.1.2}/PKG-INFO +9 -7
  3. {42api-0.1.1 → 42api-0.1.2}/README.md +8 -6
  4. {42api-0.1.1 → 42api-0.1.2}/pyproject.toml +1 -1
  5. {42api-0.1.1 → 42api-0.1.2}/src/intra42/__init__.py +10 -2
  6. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/client.py +8 -0
  7. 42api-0.1.2/src/intra42/_async/resources/accreditations.py +16 -0
  8. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/cursus.py +4 -0
  9. 42api-0.1.2/src/intra42/_async/resources/exams.py +23 -0
  10. 42api-0.1.2/src/intra42/_async/resources/projects.py +19 -0
  11. 42api-0.1.2/src/intra42/_async/resources/projects_users.py +22 -0
  12. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/users.py +4 -0
  13. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/client.py +8 -0
  14. 42api-0.1.2/src/intra42/_sync/resources/accreditations.py +22 -0
  15. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/cursus.py +4 -0
  16. 42api-0.1.2/src/intra42/_sync/resources/exams.py +27 -0
  17. 42api-0.1.2/src/intra42/_sync/resources/projects.py +25 -0
  18. 42api-0.1.2/src/intra42/_sync/resources/projects_users.py +26 -0
  19. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/users.py +4 -0
  20. 42api-0.1.2/src/intra42/models/accreditation.py +29 -0
  21. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/cursus.py +6 -0
  22. 42api-0.1.2/src/intra42/models/exam.py +32 -0
  23. 42api-0.1.2/src/intra42/models/project.py +106 -0
  24. 42api-0.1.2/src/intra42/models/project_user.py +74 -0
  25. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/user.py +6 -0
  26. 42api-0.1.2/tests/integration/resources/test_resources_accreditations.py +78 -0
  27. 42api-0.1.2/tests/integration/resources/test_resources_exams.py +159 -0
  28. 42api-0.1.2/tests/integration/resources/test_resources_projects.py +193 -0
  29. 42api-0.1.2/tests/integration/resources/test_resources_projects_users.py +176 -0
  30. {42api-0.1.1 → 42api-0.1.2}/tests/integration/test_pagination_walk.py +3 -3
  31. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_public_api.py +8 -0
  32. {42api-0.1.1 → 42api-0.1.2}/uv.lock +1 -1
  33. {42api-0.1.1 → 42api-0.1.2}/.github/workflows/ci.yml +0 -0
  34. {42api-0.1.1 → 42api-0.1.2}/.github/workflows/publish.yml +0 -0
  35. {42api-0.1.1 → 42api-0.1.2}/.pre-commit-config.yaml +0 -0
  36. {42api-0.1.1 → 42api-0.1.2}/LICENSE +0 -0
  37. {42api-0.1.1 → 42api-0.1.2}/scripts/unasync_generate.py +0 -0
  38. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/__init__.py +0 -0
  39. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/query.py +0 -0
  40. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/__init__.py +0 -0
  41. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/base.py +0 -0
  42. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/campus_users.py +0 -0
  43. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/campuses.py +0 -0
  44. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/events.py +0 -0
  45. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_async/resources/locations.py +0 -0
  46. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_auth.py +0 -0
  47. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_config.py +0 -0
  48. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_pagination.py +0 -0
  49. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_query_params.py +0 -0
  50. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_rate_limit.py +0 -0
  51. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/__init__.py +0 -0
  52. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/query.py +0 -0
  53. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/__init__.py +0 -0
  54. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/base.py +0 -0
  55. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/campus_users.py +0 -0
  56. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/campuses.py +0 -0
  57. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/events.py +0 -0
  58. {42api-0.1.1 → 42api-0.1.2}/src/intra42/_sync/resources/locations.py +0 -0
  59. {42api-0.1.1 → 42api-0.1.2}/src/intra42/exceptions.py +0 -0
  60. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/__init__.py +0 -0
  61. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/base.py +0 -0
  62. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/campus.py +0 -0
  63. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/campus_user.py +0 -0
  64. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/event.py +0 -0
  65. {42api-0.1.1 → 42api-0.1.2}/src/intra42/models/location.py +0 -0
  66. {42api-0.1.1 → 42api-0.1.2}/src/intra42/py.typed +0 -0
  67. {42api-0.1.1 → 42api-0.1.2}/tests/conftest.py +0 -0
  68. {42api-0.1.1 → 42api-0.1.2}/tests/integration/resources/test_resources_campus_users.py +0 -0
  69. {42api-0.1.1 → 42api-0.1.2}/tests/integration/resources/test_resources_campuses.py +0 -0
  70. {42api-0.1.1 → 42api-0.1.2}/tests/integration/resources/test_resources_cursus.py +0 -0
  71. {42api-0.1.1 → 42api-0.1.2}/tests/integration/resources/test_resources_events.py +0 -0
  72. {42api-0.1.1 → 42api-0.1.2}/tests/integration/resources/test_resources_locations.py +0 -0
  73. {42api-0.1.1 → 42api-0.1.2}/tests/integration/test_client_integration.py +0 -0
  74. {42api-0.1.1 → 42api-0.1.2}/tests/integration/test_nested_access.py +0 -0
  75. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_auth.py +0 -0
  76. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_models_campus.py +0 -0
  77. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_models_user.py +0 -0
  78. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_pagination.py +0 -0
  79. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_query_builder.py +0 -0
  80. {42api-0.1.1 → 42api-0.1.2}/tests/unit/test_rate_limit.py +0 -0
@@ -218,4 +218,7 @@ __marimo__/
218
218
  # Streamlit
219
219
  .streamlit/secrets.toml
220
220
 
221
- assets/
221
+ assets/
222
+
223
+ # agent
224
+ .claude
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: 42api
3
- Version: 0.1.1
3
+ Version: 0.1.2
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(campus_id=1).sort("-level"):
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(campus_id=1).sort("-level"):
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`, `campuses`, `campus_users`,
112
- `cursus`, `events`, and `locations` (including its `graph` analytics
113
- endpoint). More resources are added incrementally on top of the same
114
- client/model/query-builder pattern.
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(campus_id=1).sort("-level"):
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(campus_id=1).sort("-level"):
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`, `campuses`, `campus_users`,
86
- `cursus`, `events`, and `locations` (including its `graph` analytics
87
- endpoint). More resources are added incrementally on top of the same
88
- client/model/query-builder pattern.
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
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "42api"
3
- version = "0.1.1"
3
+ version = "0.1.2"
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" }
@@ -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(campus_id=1).sort("-level"):
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.1"
63
+ __version__ = "0.1.2"
@@ -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:
@@ -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:
@@ -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
+ )
@@ -23,3 +23,7 @@ class CursusResource(Resource[Cursus]):
23
23
  "events",
24
24
  lambda: self._client.events._queryset(f"/cursus/{instance.id}/events"),
25
25
  )
26
+ instance._bind_relation(
27
+ "projects",
28
+ lambda: self._client.projects._queryset(f"/cursus/{instance.id}/projects"),
29
+ )
@@ -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
@@ -0,0 +1,106 @@
1
+ """The ``Project`` resource."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from .base import FortyTwoModel
9
+ from .campus import Campus
10
+ from .cursus import Cursus
11
+
12
+ if TYPE_CHECKING:
13
+ from .._async.query import AsyncQuerySet
14
+ from .._sync.query import QuerySet
15
+ from .project_user import ProjectUser
16
+
17
+
18
+ class ProjectRef(FortyTwoModel):
19
+ """A lightweight project reference, used for ``parent`` and ``children``."""
20
+
21
+ id: int | None = None
22
+ name: str | None = None
23
+ slug: str | None = None
24
+ url: str | None = None
25
+
26
+
27
+ class ProjectSkill(FortyTwoModel):
28
+ id: int | None = None
29
+ name: str | None = None
30
+ created_at: datetime | None = None
31
+
32
+
33
+ class ProjectTag(FortyTwoModel):
34
+ id: int | None = None
35
+ name: str | None = None
36
+ kind: str | None = None
37
+
38
+
39
+ class ProjectSessionScale(FortyTwoModel):
40
+ id: int | None = None
41
+ correction_number: int | None = None
42
+ is_primary: bool | None = None
43
+
44
+
45
+ class ProjectSessionUpload(FortyTwoModel):
46
+ id: int | None = None
47
+ name: str | None = None
48
+
49
+
50
+ class ProjectSession(FortyTwoModel):
51
+ id: int | None = None
52
+ solo: bool | None = None
53
+ begin_at: datetime | None = None
54
+ end_at: datetime | None = None
55
+ difficulty: int | None = None
56
+ # Confirmed against a real account: not always an int (seconds) as the
57
+ # apidoc example showed (2592000) — also seen as a free-form duration
58
+ # string ("1 day").
59
+ estimate_time: int | str | None = None
60
+ duration_days: int | None = None
61
+ terminating_after: int | None = None
62
+ project_id: int | None = None
63
+ campus_id: int | None = None
64
+ cursus_id: int | None = None
65
+ created_at: datetime | None = None
66
+ updated_at: datetime | None = None
67
+ max_people: int | None = None
68
+ is_subscriptable: bool | None = None
69
+ scales: list[ProjectSessionScale] | None = None
70
+ uploads: list[ProjectSessionUpload] | None = None
71
+ team_behaviour: str | None = None
72
+
73
+
74
+ class Project(FortyTwoModel):
75
+ id: int
76
+ name: str | None = None
77
+ slug: str | None = None
78
+ difficulty: int | None = None
79
+ description: str | None = None
80
+ parent_id: int | None = None
81
+ parent: ProjectRef | None = None
82
+ children: list[ProjectRef] | None = None
83
+ objectives: list[str] | None = None
84
+ attachments: list[Any] | None = None
85
+ created_at: datetime | None = None
86
+ updated_at: datetime | None = None
87
+ exam: bool | None = None
88
+ cursus: list[Cursus] | None = None
89
+ campus: list[Campus] | None = None
90
+ skills: list[ProjectSkill] | None = None
91
+ videos: list[Any] | None = None
92
+ tags: list[ProjectTag] | None = None
93
+ project_sessions: list[ProjectSession] | None = None
94
+ visible: bool | None = None
95
+ inherited_team: bool | None = None
96
+ position: int | None = None
97
+ has_git: bool | None = None
98
+ has_mark: bool | None = None
99
+ repository: str | None = None
100
+ git_id: int | None = None
101
+ cached_repository_path: str | None = None
102
+
103
+ @property
104
+ def projects_users(self) -> AsyncQuerySet[ProjectUser] | QuerySet[ProjectUser]:
105
+ """Assignments of this project (``GET /projects/{id}/projects_users``)."""
106
+ return self._relation("projects_users")