42api 0.1.0__py3-none-any.whl
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.0.dist-info/METADATA +137 -0
- 42api-0.1.0.dist-info/RECORD +39 -0
- 42api-0.1.0.dist-info/WHEEL +4 -0
- 42api-0.1.0.dist-info/licenses/LICENSE +21 -0
- intra42/__init__.py +53 -0
- intra42/_async/__init__.py +0 -0
- intra42/_async/client.py +126 -0
- intra42/_async/query.py +99 -0
- intra42/_async/resources/__init__.py +0 -0
- intra42/_async/resources/base.py +77 -0
- intra42/_async/resources/campus_users.py +9 -0
- intra42/_async/resources/campuses.py +23 -0
- intra42/_async/resources/events.py +9 -0
- intra42/_async/resources/locations.py +30 -0
- intra42/_async/resources/users.py +23 -0
- intra42/_auth.py +103 -0
- intra42/_config.py +17 -0
- intra42/_pagination.py +23 -0
- intra42/_query_params.py +54 -0
- intra42/_rate_limit.py +102 -0
- intra42/_sync/__init__.py +0 -0
- intra42/_sync/client.py +132 -0
- intra42/_sync/query.py +105 -0
- intra42/_sync/resources/__init__.py +0 -0
- intra42/_sync/resources/base.py +83 -0
- intra42/_sync/resources/campus_users.py +15 -0
- intra42/_sync/resources/campuses.py +29 -0
- intra42/_sync/resources/events.py +15 -0
- intra42/_sync/resources/locations.py +34 -0
- intra42/_sync/resources/users.py +29 -0
- intra42/exceptions.py +88 -0
- intra42/models/__init__.py +0 -0
- intra42/models/base.py +47 -0
- intra42/models/campus.py +66 -0
- intra42/models/campus_user.py +16 -0
- intra42/models/event.py +41 -0
- intra42/models/location.py +26 -0
- intra42/models/user.py +73 -0
- intra42/py.typed +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
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.location import Location
|
|
10
|
+
from .base import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocationsResource(Resource[Location]):
|
|
14
|
+
path = "/locations"
|
|
15
|
+
model = Location
|
|
16
|
+
|
|
17
|
+
def graph(self, *, field: str | None = None, interval: str | None = None) -> dict[str, int]:
|
|
18
|
+
"""Grouped temporal counts (``GET /locations/graph...``).
|
|
19
|
+
|
|
20
|
+
Not a paginated list of ``Location`` objects — returns a raw
|
|
21
|
+
``{date_string: count}`` mapping, counting occurrences of `field`
|
|
22
|
+
(default ``begin_at``) bucketed by `interval` (default
|
|
23
|
+
``month_of_year``) from the first occurrence to now.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
path = "/locations/graph"
|
|
27
|
+
if interval is not None and field is None:
|
|
28
|
+
field = "begin_at"
|
|
29
|
+
if field is not None:
|
|
30
|
+
path += f"/on/{field}"
|
|
31
|
+
if interval is not None:
|
|
32
|
+
path += f"/by/{interval}"
|
|
33
|
+
response = self._client.request("GET", path)
|
|
34
|
+
return response.json()
|
|
@@ -0,0 +1,29 @@
|
|
|
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.user import User
|
|
10
|
+
from .base import Resource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UsersResource(Resource[User]):
|
|
14
|
+
path = "/users"
|
|
15
|
+
model = User
|
|
16
|
+
|
|
17
|
+
def _bind_relations(self, instance: User) -> None:
|
|
18
|
+
instance._bind_relation(
|
|
19
|
+
"events",
|
|
20
|
+
lambda: self._client.events._queryset(f"/users/{instance.id}/events"),
|
|
21
|
+
)
|
|
22
|
+
instance._bind_relation(
|
|
23
|
+
"campus_users",
|
|
24
|
+
lambda: self._client.campus_users._queryset(f"/users/{instance.id}/campus_users"),
|
|
25
|
+
)
|
|
26
|
+
instance._bind_relation(
|
|
27
|
+
"locations",
|
|
28
|
+
lambda: self._client.locations._queryset(f"/users/{instance.id}/locations"),
|
|
29
|
+
)
|
intra42/exceptions.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Exception hierarchy for the intra42 client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FortyTwoAPIError(Exception):
|
|
12
|
+
"""Base class for all errors raised by this library."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
*,
|
|
18
|
+
status_code: int | None = None,
|
|
19
|
+
response: httpx.Response | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.message = message
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.response = response
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return f"{type(self).__name__}(status_code={self.status_code!r}, message={self.message!r})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthenticationError(FortyTwoAPIError):
|
|
31
|
+
"""Raised on any 401"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PermissionDeniedError(FortyTwoAPIError):
|
|
35
|
+
"""Raised on any 403"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NotFoundError(FortyTwoAPIError):
|
|
39
|
+
"""Raised on any 404."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ValidationError(FortyTwoAPIError):
|
|
43
|
+
"""Raised on any 422"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RateLimitError(FortyTwoAPIError):
|
|
47
|
+
"""Raised on any 429 after the built-in retry/backoff budget is exhausted."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ServerError(FortyTwoAPIError):
|
|
51
|
+
"""Raised on any 5xx"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class NetworkError(FortyTwoAPIError):
|
|
55
|
+
"""Wraps a transport-level failure (DNS, connect, timeout) from httpx."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_STATUS_MAP: dict[int, type[FortyTwoAPIError]] = {
|
|
59
|
+
401: AuthenticationError,
|
|
60
|
+
403: PermissionDeniedError,
|
|
61
|
+
404: NotFoundError,
|
|
62
|
+
422: ValidationError,
|
|
63
|
+
429: RateLimitError,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def raise_for_status(response: httpx.Response) -> None:
|
|
68
|
+
"""Raise the mapped :class:`FortyTwoAPIError` subclass for a non-2xx response.
|
|
69
|
+
|
|
70
|
+
Does nothing for successful responses.
|
|
71
|
+
"""
|
|
72
|
+
if response.is_success:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
status = response.status_code
|
|
76
|
+
try:
|
|
77
|
+
payload = response.json()
|
|
78
|
+
detail = payload.get("error_description") or payload.get("message") or payload.get("error")
|
|
79
|
+
except Exception:
|
|
80
|
+
detail = None
|
|
81
|
+
message = detail or f"Request failed with status {status}"
|
|
82
|
+
|
|
83
|
+
if status >= 500:
|
|
84
|
+
error_cls: type[FortyTwoAPIError] = ServerError
|
|
85
|
+
else:
|
|
86
|
+
error_cls = _STATUS_MAP.get(status, FortyTwoAPIError)
|
|
87
|
+
|
|
88
|
+
raise error_cls(message, status_code=status, response=response)
|
|
File without changes
|
intra42/models/base.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared base model for all 42 API resource models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FortyTwoModel(BaseModel):
|
|
12
|
+
"""Base class for every typed resource returned by the API.
|
|
13
|
+
|
|
14
|
+
``extra="allow"`` is deliberate: 42's API has historically added fields
|
|
15
|
+
over time, so unknown keys are kept (inspectable via ``.model_extra``)
|
|
16
|
+
rather than raising a ``ValidationError`` or silently discarding them.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(extra="allow", populate_by_name=True)
|
|
20
|
+
|
|
21
|
+
# Nested-resource accessors (e.g. User.events) are not
|
|
22
|
+
# hardcoded, they're bound per-instance by whichever resource
|
|
23
|
+
# fetched this object, as a lazily invoked factory.
|
|
24
|
+
_relations: dict[str, Callable[[], Any]] = PrivateAttr(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def _bind_relation(self, name: str, factory: Callable[[], Any]) -> None:
|
|
27
|
+
"""Attach a lazily-invoked nested-resource accessor to this instance.
|
|
28
|
+
|
|
29
|
+
Called internally by Client/AsyncClient resources right after
|
|
30
|
+
parsing a response; not meant to be called directly.
|
|
31
|
+
"""
|
|
32
|
+
self._relations[name] = factory
|
|
33
|
+
|
|
34
|
+
def _relation(self, name: str) -> Any:
|
|
35
|
+
"""Resolve a nested-resource accessor bound via ``_bind_relation``.
|
|
36
|
+
|
|
37
|
+
Raises ``RuntimeError`` with a clear message if this instance wasn't
|
|
38
|
+
fetched through a Client/AsyncClient
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
factory = self._relations[name]
|
|
42
|
+
except KeyError:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
f"'{name}' is only available on a {type(self).__name__} instance "
|
|
45
|
+
"fetched via Client/AsyncClient, not one constructed manually."
|
|
46
|
+
) from None
|
|
47
|
+
return factory()
|
intra42/models/campus.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .base import FortyTwoModel
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .._async.query import AsyncQuerySet
|
|
9
|
+
from .._sync.query import QuerySet
|
|
10
|
+
from .event import Event
|
|
11
|
+
from .location import Location
|
|
12
|
+
from .user import User
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CampusLanguage(FortyTwoModel):
|
|
16
|
+
id: int
|
|
17
|
+
name: str | None = None
|
|
18
|
+
identifier: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CampusEndpoint(FortyTwoModel):
|
|
22
|
+
id: int
|
|
23
|
+
url: str | None = None
|
|
24
|
+
description: str | None = None
|
|
25
|
+
created_at: str | None = None
|
|
26
|
+
updated_at: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Campus(FortyTwoModel):
|
|
30
|
+
id: int
|
|
31
|
+
name: str
|
|
32
|
+
time_zone: str | None = None
|
|
33
|
+
language: CampusLanguage | None = None
|
|
34
|
+
users_count: int | None = None
|
|
35
|
+
vogsphere_id: int | None = None
|
|
36
|
+
country: str | None = None
|
|
37
|
+
address: str | None = None
|
|
38
|
+
zip: str | None = None
|
|
39
|
+
city: str | None = None
|
|
40
|
+
website: str | None = None
|
|
41
|
+
facebook: str | None = None
|
|
42
|
+
twitter: str | None = None
|
|
43
|
+
active: bool | None = None
|
|
44
|
+
public: bool | None = None
|
|
45
|
+
email_extension: str | None = None
|
|
46
|
+
default_hidden_phone: bool | None = None
|
|
47
|
+
endpoint: CampusEndpoint | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def users(self) -> AsyncQuerySet[User] | QuerySet[User]:
|
|
51
|
+
"""Users of this campus (``GET /campus/{id}/users``).
|
|
52
|
+
|
|
53
|
+
Lazily-paginating. Only available on instances fetched via
|
|
54
|
+
``Client``/``AsyncClient`` — see ``FortyTwoModel._relation``.
|
|
55
|
+
"""
|
|
56
|
+
return self._relation("users")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def events(self) -> AsyncQuerySet[Event] | QuerySet[Event]:
|
|
60
|
+
"""Events at this campus (``GET /campus/{id}/events``)."""
|
|
61
|
+
return self._relation("events")
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def locations(self) -> AsyncQuerySet[Location] | QuerySet[Location]:
|
|
65
|
+
"""Login sessions at this campus (``GET /campus/{id}/locations``)."""
|
|
66
|
+
return self._relation("locations")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The ``CampusUser`` resource: the join between a user and a campus."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from .base import FortyTwoModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CampusUser(FortyTwoModel):
|
|
11
|
+
id: int
|
|
12
|
+
user_id: int | None = None
|
|
13
|
+
campus_id: int | None = None
|
|
14
|
+
is_primary: bool | None = None
|
|
15
|
+
created_at: datetime | None = None
|
|
16
|
+
updated_at: datetime | None = None
|
intra42/models/event.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from .base import FortyTwoModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventTheme(FortyTwoModel):
|
|
9
|
+
id: int | None = None
|
|
10
|
+
name: str | None = None
|
|
11
|
+
created_at: datetime | None = None
|
|
12
|
+
updated_at: datetime | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventWaitlist(FortyTwoModel):
|
|
16
|
+
id: int | None = None
|
|
17
|
+
waitlistable_id: int | None = None
|
|
18
|
+
waitlistable_type: str | None = None
|
|
19
|
+
created_at: datetime | None = None
|
|
20
|
+
updated_at: datetime | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Event(FortyTwoModel):
|
|
24
|
+
id: int
|
|
25
|
+
name: str | None = None
|
|
26
|
+
description: str | None = None
|
|
27
|
+
location: str | None = None
|
|
28
|
+
kind: str | None = None
|
|
29
|
+
max_people: int | None = None
|
|
30
|
+
nbr_subscribers: int | None = None
|
|
31
|
+
begin_at: datetime | None = None
|
|
32
|
+
end_at: datetime | None = None
|
|
33
|
+
campus_ids: list[int] | None = None
|
|
34
|
+
cursus_ids: list[int] | None = None
|
|
35
|
+
themes: list[EventTheme] | None = None
|
|
36
|
+
waitlist: EventWaitlist | None = None
|
|
37
|
+
prohibition_of_cancellation: int | None = None
|
|
38
|
+
difficulty: int | None = None
|
|
39
|
+
remote: bool | None = None
|
|
40
|
+
created_at: datetime | None = None
|
|
41
|
+
updated_at: datetime | None = None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""The ``Location`` resource: a login session at a workstation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from .base import FortyTwoModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LocationUser(FortyTwoModel):
|
|
11
|
+
id: int | None = None
|
|
12
|
+
login: str | None = None
|
|
13
|
+
url: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Location(FortyTwoModel):
|
|
17
|
+
id: int
|
|
18
|
+
begin_at: datetime | None = None
|
|
19
|
+
end_at: datetime | None = None
|
|
20
|
+
primary: bool | None = None
|
|
21
|
+
floor: str | None = None
|
|
22
|
+
row: str | None = None
|
|
23
|
+
post: str | None = None
|
|
24
|
+
host: str | None = None
|
|
25
|
+
campus_id: int | None = None
|
|
26
|
+
user: LocationUser | None = None
|
intra42/models/user.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
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 .campus_user import CampusUser
|
|
14
|
+
from .event import Event
|
|
15
|
+
from .location import Location
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImageVersions(FortyTwoModel):
|
|
19
|
+
large: str | None = None
|
|
20
|
+
medium: str | None = None
|
|
21
|
+
small: str | None = None
|
|
22
|
+
micro: str | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Image(FortyTwoModel):
|
|
26
|
+
link: str | None = None
|
|
27
|
+
versions: ImageVersions | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class User(FortyTwoModel):
|
|
31
|
+
id: int
|
|
32
|
+
email: str | None = None
|
|
33
|
+
login: str | None = None
|
|
34
|
+
first_name: str | None = None
|
|
35
|
+
last_name: str | None = None
|
|
36
|
+
usual_full_name: str | None = None
|
|
37
|
+
usual_first_name: str | None = None
|
|
38
|
+
url: str | None = None
|
|
39
|
+
phone: str | None = None
|
|
40
|
+
displayname: str | None = None
|
|
41
|
+
kind: str | None = None
|
|
42
|
+
image: Image | None = None
|
|
43
|
+
staff: bool | None = Field(default=None, alias="staff?")
|
|
44
|
+
correction_point: int | None = None
|
|
45
|
+
pool_month: str | None = None
|
|
46
|
+
pool_year: str | None = None
|
|
47
|
+
location: str | None = None
|
|
48
|
+
wallet: int | None = None
|
|
49
|
+
anonymize_date: datetime | None = None
|
|
50
|
+
data_erasure_date: datetime | None = None
|
|
51
|
+
created_at: datetime | None = None
|
|
52
|
+
updated_at: datetime | None = None
|
|
53
|
+
alumnized_at: datetime | None = None
|
|
54
|
+
alumni: bool | None = Field(default=None, alias="alumni?")
|
|
55
|
+
active: bool | None = Field(default=None, alias="active?")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def events(self) -> AsyncQuerySet[Event] | QuerySet[Event]:
|
|
59
|
+
"""Events scoped to this user (``GET /users/{id}/events``)"""
|
|
60
|
+
return self._relation("events")
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def campus_users(self) -> AsyncQuerySet[CampusUser] | QuerySet[CampusUser]:
|
|
64
|
+
"""This user's campus memberships (``GET /users/{id}/campus_users``)."""
|
|
65
|
+
return self._relation("campus_users")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def locations(self) -> AsyncQuerySet[Location] | QuerySet[Location]:
|
|
69
|
+
"""This user's login sessions (``GET /users/{id}/locations``).
|
|
70
|
+
|
|
71
|
+
Not to be confused with the ``location`` string field.
|
|
72
|
+
"""
|
|
73
|
+
return self._relation("locations")
|
intra42/py.typed
ADDED
|
File without changes
|