calendry-client 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.
- calendry_client/__init__.py +180 -0
- calendry_client/_generic.py +64 -0
- calendry_client/_version.py +24 -0
- calendry_client/access_roles.py +56 -0
- calendry_client/calendar_periods.py +48 -0
- calendry_client/client.py +113 -0
- calendry_client/constraints.py +80 -0
- calendry_client/equipment.py +32 -0
- calendry_client/exceptions.py +26 -0
- calendry_client/groups.py +104 -0
- calendry_client/offerings.py +139 -0
- calendry_client/persons.py +100 -0
- calendry_client/roles.py +33 -0
- calendry_client/rooms.py +77 -0
- calendry_client/session_kinds.py +49 -0
- calendry_client/terms.py +49 -0
- calendry_client/time_grids.py +95 -0
- calendry_client-0.1.0.dist-info/METADATA +197 -0
- calendry_client-0.1.0.dist-info/RECORD +20 -0
- calendry_client-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Calendry API client library.
|
|
2
|
+
|
|
3
|
+
A thin, dependency-light client (:class:`CalendryClient`) plus a set of pure
|
|
4
|
+
functions per resource (persons, groups, rooms, offerings, ...) that build
|
|
5
|
+
request bodies matching the Calendry OpenAPI schema (see ``swagger.json`` in
|
|
6
|
+
the repository root) and call the generic CRUD/relation routes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .client import CalendryClient
|
|
10
|
+
from .exceptions import CalendryAPIError
|
|
11
|
+
|
|
12
|
+
from .access_roles import add_access_role, delete_access_role, get_access_role, list_access_roles, update_access_role
|
|
13
|
+
from .calendar_periods import add_calendar_period, delete_calendar_period, get_calendar_period, list_calendar_periods, update_calendar_period
|
|
14
|
+
from .constraints import (
|
|
15
|
+
add_constraint,
|
|
16
|
+
delete_constraint,
|
|
17
|
+
get_constraint,
|
|
18
|
+
get_constraint_scopes,
|
|
19
|
+
list_constraints,
|
|
20
|
+
set_constraint_scopes,
|
|
21
|
+
update_constraint,
|
|
22
|
+
)
|
|
23
|
+
from .equipment import add_equipment, delete_equipment, get_equipment, list_equipment, update_equipment
|
|
24
|
+
from .groups import (
|
|
25
|
+
add_group,
|
|
26
|
+
add_subgroup,
|
|
27
|
+
delete_group,
|
|
28
|
+
get_group,
|
|
29
|
+
get_group_availability,
|
|
30
|
+
get_group_sources,
|
|
31
|
+
get_group_terms,
|
|
32
|
+
list_groups,
|
|
33
|
+
set_group_availability,
|
|
34
|
+
set_group_sources,
|
|
35
|
+
set_group_terms,
|
|
36
|
+
update_group,
|
|
37
|
+
)
|
|
38
|
+
from .offerings import (
|
|
39
|
+
add_offering,
|
|
40
|
+
delete_offering,
|
|
41
|
+
get_offering,
|
|
42
|
+
get_offering_equipment,
|
|
43
|
+
get_offering_groups,
|
|
44
|
+
get_offering_lecturers,
|
|
45
|
+
list_offerings,
|
|
46
|
+
set_offering_equipment,
|
|
47
|
+
set_offering_groups,
|
|
48
|
+
set_offering_lecturers,
|
|
49
|
+
update_offering,
|
|
50
|
+
)
|
|
51
|
+
from .persons import (
|
|
52
|
+
add_lecturer,
|
|
53
|
+
add_person,
|
|
54
|
+
delete_person,
|
|
55
|
+
get_person,
|
|
56
|
+
get_person_access_roles,
|
|
57
|
+
get_person_groups,
|
|
58
|
+
get_person_roles,
|
|
59
|
+
list_persons,
|
|
60
|
+
set_person_access_roles,
|
|
61
|
+
set_person_groups,
|
|
62
|
+
set_person_roles,
|
|
63
|
+
update_person,
|
|
64
|
+
)
|
|
65
|
+
from .roles import add_role, delete_role, get_role, list_roles, update_role
|
|
66
|
+
from .rooms import add_room, delete_room, get_room, get_room_equipment, list_rooms, set_room_equipment, update_room
|
|
67
|
+
from .session_kinds import add_session_kind, delete_session_kind, get_session_kind, list_session_kinds, update_session_kind
|
|
68
|
+
from .terms import add_term, delete_term, get_term, list_terms, update_term
|
|
69
|
+
from .time_grids import (
|
|
70
|
+
add_time_grid,
|
|
71
|
+
delete_time_grid,
|
|
72
|
+
get_time_grid,
|
|
73
|
+
get_time_grid_breaks,
|
|
74
|
+
list_time_grids,
|
|
75
|
+
set_time_grid_breaks,
|
|
76
|
+
update_time_grid,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"CalendryClient",
|
|
81
|
+
"CalendryAPIError",
|
|
82
|
+
# access-roles
|
|
83
|
+
"add_access_role",
|
|
84
|
+
"delete_access_role",
|
|
85
|
+
"get_access_role",
|
|
86
|
+
"list_access_roles",
|
|
87
|
+
"update_access_role",
|
|
88
|
+
# calendar-periods
|
|
89
|
+
"add_calendar_period",
|
|
90
|
+
"delete_calendar_period",
|
|
91
|
+
"get_calendar_period",
|
|
92
|
+
"list_calendar_periods",
|
|
93
|
+
"update_calendar_period",
|
|
94
|
+
# constraints
|
|
95
|
+
"add_constraint",
|
|
96
|
+
"delete_constraint",
|
|
97
|
+
"get_constraint",
|
|
98
|
+
"get_constraint_scopes",
|
|
99
|
+
"list_constraints",
|
|
100
|
+
"set_constraint_scopes",
|
|
101
|
+
"update_constraint",
|
|
102
|
+
# equipment
|
|
103
|
+
"add_equipment",
|
|
104
|
+
"delete_equipment",
|
|
105
|
+
"get_equipment",
|
|
106
|
+
"list_equipment",
|
|
107
|
+
"update_equipment",
|
|
108
|
+
# groups
|
|
109
|
+
"add_group",
|
|
110
|
+
"add_subgroup",
|
|
111
|
+
"delete_group",
|
|
112
|
+
"get_group",
|
|
113
|
+
"get_group_availability",
|
|
114
|
+
"get_group_sources",
|
|
115
|
+
"get_group_terms",
|
|
116
|
+
"list_groups",
|
|
117
|
+
"set_group_availability",
|
|
118
|
+
"set_group_sources",
|
|
119
|
+
"set_group_terms",
|
|
120
|
+
"update_group",
|
|
121
|
+
# offerings
|
|
122
|
+
"add_offering",
|
|
123
|
+
"delete_offering",
|
|
124
|
+
"get_offering",
|
|
125
|
+
"get_offering_equipment",
|
|
126
|
+
"get_offering_groups",
|
|
127
|
+
"get_offering_lecturers",
|
|
128
|
+
"list_offerings",
|
|
129
|
+
"set_offering_equipment",
|
|
130
|
+
"set_offering_groups",
|
|
131
|
+
"set_offering_lecturers",
|
|
132
|
+
"update_offering",
|
|
133
|
+
# persons
|
|
134
|
+
"add_lecturer",
|
|
135
|
+
"add_person",
|
|
136
|
+
"delete_person",
|
|
137
|
+
"get_person",
|
|
138
|
+
"get_person_access_roles",
|
|
139
|
+
"get_person_groups",
|
|
140
|
+
"get_person_roles",
|
|
141
|
+
"list_persons",
|
|
142
|
+
"set_person_access_roles",
|
|
143
|
+
"set_person_groups",
|
|
144
|
+
"set_person_roles",
|
|
145
|
+
"update_person",
|
|
146
|
+
# roles
|
|
147
|
+
"add_role",
|
|
148
|
+
"delete_role",
|
|
149
|
+
"get_role",
|
|
150
|
+
"list_roles",
|
|
151
|
+
"update_role",
|
|
152
|
+
# rooms
|
|
153
|
+
"add_room",
|
|
154
|
+
"delete_room",
|
|
155
|
+
"get_room",
|
|
156
|
+
"get_room_equipment",
|
|
157
|
+
"list_rooms",
|
|
158
|
+
"set_room_equipment",
|
|
159
|
+
"update_room",
|
|
160
|
+
# session-kinds
|
|
161
|
+
"add_session_kind",
|
|
162
|
+
"delete_session_kind",
|
|
163
|
+
"get_session_kind",
|
|
164
|
+
"list_session_kinds",
|
|
165
|
+
"update_session_kind",
|
|
166
|
+
# terms
|
|
167
|
+
"add_term",
|
|
168
|
+
"delete_term",
|
|
169
|
+
"get_term",
|
|
170
|
+
"list_terms",
|
|
171
|
+
"update_term",
|
|
172
|
+
# time-grids
|
|
173
|
+
"add_time_grid",
|
|
174
|
+
"delete_time_grid",
|
|
175
|
+
"get_time_grid",
|
|
176
|
+
"get_time_grid_breaks",
|
|
177
|
+
"list_time_grids",
|
|
178
|
+
"set_time_grid_breaks",
|
|
179
|
+
"update_time_grid",
|
|
180
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Generic helpers shared by every resource module.
|
|
2
|
+
|
|
3
|
+
These mirror the generic ``/api/{resource}`` and ``/api/{resource}/{id}``
|
|
4
|
+
and ``/api/{resource}/{id}/{relation}`` routes documented in swagger.json.
|
|
5
|
+
Resource-specific modules build their pure functions on top of these.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Iterable, Optional
|
|
11
|
+
|
|
12
|
+
from .client import CalendryClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compact(**kwargs: Any) -> dict:
|
|
16
|
+
"""Drop keys whose value is None, so optional fields are simply omitted."""
|
|
17
|
+
return {k: v for k, v in kwargs.items() if v is not None}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_rows(
|
|
21
|
+
client: CalendryClient,
|
|
22
|
+
resource: str,
|
|
23
|
+
*,
|
|
24
|
+
limit: Optional[int] = None,
|
|
25
|
+
offset: Optional[int] = None,
|
|
26
|
+
q: Optional[str] = None,
|
|
27
|
+
ids: Optional[Iterable[str]] = None,
|
|
28
|
+
**filters: Any,
|
|
29
|
+
) -> Any:
|
|
30
|
+
params = compact(
|
|
31
|
+
limit=limit,
|
|
32
|
+
offset=offset,
|
|
33
|
+
q=q,
|
|
34
|
+
ids=",".join(ids) if ids is not None else None,
|
|
35
|
+
**filters,
|
|
36
|
+
)
|
|
37
|
+
return client.get(f"/api/{resource}", params=params or None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_row(client: CalendryClient, resource: str, row_id: str) -> Any:
|
|
41
|
+
return client.get(f"/api/{resource}/{row_id}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_row(client: CalendryClient, resource: str, body: dict) -> Any:
|
|
45
|
+
return client.post(f"/api/{resource}", json=body)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def update_row(client: CalendryClient, resource: str, row_id: str, body: dict) -> Any:
|
|
49
|
+
return client.patch(f"/api/{resource}/{row_id}", json=body)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def delete_row(client: CalendryClient, resource: str, row_id: str) -> Any:
|
|
53
|
+
return client.delete(f"/api/{resource}/{row_id}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_relation(client: CalendryClient, resource: str, row_id: str, relation: str) -> Any:
|
|
57
|
+
return client.get(f"/api/{resource}/{row_id}/{relation}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def set_relation(
|
|
61
|
+
client: CalendryClient, resource: str, row_id: str, relation: str, items: list
|
|
62
|
+
) -> Any:
|
|
63
|
+
"""Replace the entire membership set of a relation (PUT, not additive)."""
|
|
64
|
+
return client.put(f"/api/{resource}/{row_id}/{relation}", json=items)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Pure functions for the ``access-roles`` resource (authorization roles, not scheduling roles)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from ._generic import compact, create_row, delete_row, get_row, list_rows, update_row
|
|
8
|
+
from .client import CalendryClient
|
|
9
|
+
|
|
10
|
+
RESOURCE = "access-roles"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_access_roles(client: CalendryClient, *, limit=None, offset=None, q=None, ids: Optional[Iterable[str]] = None) -> Any:
|
|
14
|
+
return list_rows(client, RESOURCE, limit=limit, offset=offset, q=q, ids=ids)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_access_role(client: CalendryClient, access_role_id: str) -> Any:
|
|
18
|
+
return get_row(client, RESOURCE, access_role_id)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_access_role(
|
|
22
|
+
client: CalendryClient,
|
|
23
|
+
key: str,
|
|
24
|
+
name: str,
|
|
25
|
+
permission_keys: Iterable[str],
|
|
26
|
+
*,
|
|
27
|
+
description: Optional[str] = None,
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Requires ``access_role.manage`` on the server side."""
|
|
30
|
+
body = {
|
|
31
|
+
"key": key,
|
|
32
|
+
"name": name,
|
|
33
|
+
"permissions": [{"permissionKey": p} for p in permission_keys],
|
|
34
|
+
}
|
|
35
|
+
body.update(compact(description=description))
|
|
36
|
+
return create_row(client, RESOURCE, body)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def update_access_role(
|
|
40
|
+
client: CalendryClient,
|
|
41
|
+
access_role_id: str,
|
|
42
|
+
*,
|
|
43
|
+
name: Optional[str] = None,
|
|
44
|
+
description: Optional[str] = None,
|
|
45
|
+
permission_keys: Optional[Iterable[str]] = None,
|
|
46
|
+
) -> Any:
|
|
47
|
+
body = compact(
|
|
48
|
+
name=name,
|
|
49
|
+
description=description,
|
|
50
|
+
permissions=[{"permissionKey": p} for p in permission_keys] if permission_keys is not None else None,
|
|
51
|
+
)
|
|
52
|
+
return update_row(client, RESOURCE, access_role_id, body)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def delete_access_role(client: CalendryClient, access_role_id: str) -> Any:
|
|
56
|
+
return delete_row(client, RESOURCE, access_role_id)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Pure functions for the ``calendar-periods`` resource (holidays, breaks, exam periods)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from ._generic import compact, create_row, delete_row, get_row, list_rows, update_row
|
|
8
|
+
from .client import CalendryClient
|
|
9
|
+
|
|
10
|
+
RESOURCE = "calendar-periods"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_calendar_periods(client: CalendryClient, *, limit=None, offset=None, q=None, ids: Optional[Iterable[str]] = None) -> Any:
|
|
14
|
+
return list_rows(client, RESOURCE, limit=limit, offset=offset, q=q, ids=ids)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_calendar_period(client: CalendryClient, period_id: str) -> Any:
|
|
18
|
+
return get_row(client, RESOURCE, period_id)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_calendar_period(
|
|
22
|
+
client: CalendryClient,
|
|
23
|
+
term_id: str,
|
|
24
|
+
kind: str,
|
|
25
|
+
name: str,
|
|
26
|
+
start_date: str,
|
|
27
|
+
end_date: str,
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""``kind`` is one of HOLIDAY, BREAK, EXAM. Dates are ISO 8601."""
|
|
30
|
+
body = {"termId": term_id, "kind": kind, "name": name, "startDate": start_date, "endDate": end_date}
|
|
31
|
+
return create_row(client, RESOURCE, body)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def update_calendar_period(
|
|
35
|
+
client: CalendryClient,
|
|
36
|
+
period_id: str,
|
|
37
|
+
*,
|
|
38
|
+
kind: Optional[str] = None,
|
|
39
|
+
name: Optional[str] = None,
|
|
40
|
+
start_date: Optional[str] = None,
|
|
41
|
+
end_date: Optional[str] = None,
|
|
42
|
+
) -> Any:
|
|
43
|
+
body = compact(kind=kind, name=name, startDate=start_date, endDate=end_date)
|
|
44
|
+
return update_row(client, RESOURCE, period_id, body)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def delete_calendar_period(client: CalendryClient, period_id: str) -> Any:
|
|
48
|
+
return delete_row(client, RESOURCE, period_id)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""HTTP client for the Calendry API.
|
|
2
|
+
|
|
3
|
+
The server URL and the authentication token are both configurable on the
|
|
4
|
+
client instance (constructor args) and, via :meth:`CalendryClient.from_env`,
|
|
5
|
+
through environment variables. Nothing about the target server or the
|
|
6
|
+
credentials is hard-coded.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from .exceptions import CalendryAPIError
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CalendryClient:
|
|
22
|
+
"""Thin HTTP wrapper around the Calendry REST API.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
base_url:
|
|
27
|
+
Root URL of the Calendry server, e.g. ``https://calendry.example.com``.
|
|
28
|
+
token:
|
|
29
|
+
Bearer token used for the ``Authorization`` header. ``None`` means
|
|
30
|
+
unauthenticated requests are sent (useful against a server that
|
|
31
|
+
relies on a different auth mechanism, or for the public routes).
|
|
32
|
+
timeout:
|
|
33
|
+
Per-request timeout in seconds.
|
|
34
|
+
session:
|
|
35
|
+
Optional pre-configured :class:`requests.Session` (mainly for tests).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
base_url: str,
|
|
41
|
+
token: Optional[str] = None,
|
|
42
|
+
*,
|
|
43
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
44
|
+
session: Optional[requests.Session] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
if not base_url:
|
|
47
|
+
raise ValueError("base_url is required")
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.token = token
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.session = session or requests.Session()
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_env(
|
|
55
|
+
cls,
|
|
56
|
+
*,
|
|
57
|
+
url_var: str = "CALENDRY_SERVER_URL",
|
|
58
|
+
token_var: str = "CALENDRY_API_TOKEN",
|
|
59
|
+
**kwargs: Any,
|
|
60
|
+
) -> "CalendryClient":
|
|
61
|
+
"""Build a client from environment variables.
|
|
62
|
+
|
|
63
|
+
Raises ``RuntimeError`` if the URL variable is not set.
|
|
64
|
+
"""
|
|
65
|
+
base_url = os.environ.get(url_var)
|
|
66
|
+
if not base_url:
|
|
67
|
+
raise RuntimeError(f"Environment variable {url_var} is not set")
|
|
68
|
+
token = os.environ.get(token_var)
|
|
69
|
+
return cls(base_url, token, **kwargs)
|
|
70
|
+
|
|
71
|
+
def _headers(self) -> dict:
|
|
72
|
+
headers = {"Accept": "application/json"}
|
|
73
|
+
if self.token:
|
|
74
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
75
|
+
return headers
|
|
76
|
+
|
|
77
|
+
def request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
*,
|
|
82
|
+
params: Optional[dict] = None,
|
|
83
|
+
json: Any = None,
|
|
84
|
+
) -> Any:
|
|
85
|
+
url = f"{self.base_url}{path}"
|
|
86
|
+
response = self.session.request(
|
|
87
|
+
method,
|
|
88
|
+
url,
|
|
89
|
+
params=params,
|
|
90
|
+
json=json,
|
|
91
|
+
headers=self._headers(),
|
|
92
|
+
timeout=self.timeout,
|
|
93
|
+
)
|
|
94
|
+
if not response.ok:
|
|
95
|
+
raise CalendryAPIError.from_response(response)
|
|
96
|
+
if response.status_code == 204 or not response.content:
|
|
97
|
+
return None
|
|
98
|
+
return response.json()
|
|
99
|
+
|
|
100
|
+
def get(self, path: str, *, params: Optional[dict] = None) -> Any:
|
|
101
|
+
return self.request("GET", path, params=params)
|
|
102
|
+
|
|
103
|
+
def post(self, path: str, *, json: Any = None) -> Any:
|
|
104
|
+
return self.request("POST", path, json=json)
|
|
105
|
+
|
|
106
|
+
def patch(self, path: str, *, json: Any = None) -> Any:
|
|
107
|
+
return self.request("PATCH", path, json=json)
|
|
108
|
+
|
|
109
|
+
def put(self, path: str, *, json: Any = None) -> Any:
|
|
110
|
+
return self.request("PUT", path, json=json)
|
|
111
|
+
|
|
112
|
+
def delete(self, path: str) -> Any:
|
|
113
|
+
return self.request("DELETE", path)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Pure functions for the ``constraints`` resource (solver rules)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from ._generic import compact, create_row, delete_row, get_relation, get_row, list_rows, set_relation, update_row
|
|
8
|
+
from .client import CalendryClient
|
|
9
|
+
|
|
10
|
+
RESOURCE = "constraints"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_constraints(client: CalendryClient, *, limit=None, offset=None, q=None, ids: Optional[Iterable[str]] = None) -> Any:
|
|
14
|
+
return list_rows(client, RESOURCE, limit=limit, offset=offset, q=q, ids=ids)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_constraint(client: CalendryClient, constraint_id: str) -> Any:
|
|
18
|
+
return get_row(client, RESOURCE, constraint_id)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_constraint(
|
|
22
|
+
client: CalendryClient,
|
|
23
|
+
constraint_type: str,
|
|
24
|
+
name: str,
|
|
25
|
+
severity: str,
|
|
26
|
+
*,
|
|
27
|
+
weight: Optional[int] = None,
|
|
28
|
+
params: Optional[dict] = None,
|
|
29
|
+
is_enabled: Optional[bool] = None,
|
|
30
|
+
time_grid_id: Optional[str] = None,
|
|
31
|
+
scopes: Optional[Iterable[str]] = None,
|
|
32
|
+
members: Optional[Iterable[str]] = None,
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""``severity`` is HARD or SOFT. ``scopes``/``members`` are id lists (kindIds / offeringIds)."""
|
|
35
|
+
body = {"type": constraint_type, "name": name, "severity": severity}
|
|
36
|
+
body.update(
|
|
37
|
+
compact(
|
|
38
|
+
weight=weight,
|
|
39
|
+
params=params,
|
|
40
|
+
isEnabled=is_enabled,
|
|
41
|
+
timeGridId=time_grid_id,
|
|
42
|
+
scopes=[{"kindId": k} for k in scopes] if scopes is not None else None,
|
|
43
|
+
members=[{"offeringId": o} for o in members] if members is not None else None,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
return create_row(client, RESOURCE, body)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def update_constraint(
|
|
50
|
+
client: CalendryClient,
|
|
51
|
+
constraint_id: str,
|
|
52
|
+
*,
|
|
53
|
+
name: Optional[str] = None,
|
|
54
|
+
severity: Optional[str] = None,
|
|
55
|
+
weight: Optional[int] = None,
|
|
56
|
+
params: Optional[dict] = None,
|
|
57
|
+
is_enabled: Optional[bool] = None,
|
|
58
|
+
time_grid_id: Optional[str] = None,
|
|
59
|
+
) -> Any:
|
|
60
|
+
body = compact(
|
|
61
|
+
name=name, severity=severity, weight=weight, params=params, isEnabled=is_enabled, timeGridId=time_grid_id
|
|
62
|
+
)
|
|
63
|
+
return update_row(client, RESOURCE, constraint_id, body)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def delete_constraint(client: CalendryClient, constraint_id: str) -> Any:
|
|
67
|
+
return delete_row(client, RESOURCE, constraint_id)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_constraint_scopes(client: CalendryClient, constraint_id: str) -> Any:
|
|
71
|
+
return get_relation(client, RESOURCE, constraint_id, "scopes")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def set_constraint_scopes(client: CalendryClient, constraint_id: str, scopes: Iterable[dict]) -> Any:
|
|
75
|
+
"""Replace the scopes narrowing this constraint.
|
|
76
|
+
|
|
77
|
+
Each item is a dict with optional ``offering_id`` and/or ``kind_id`` (at least one required).
|
|
78
|
+
"""
|
|
79
|
+
items = [compact(offeringId=s.get("offering_id"), kindId=s.get("kind_id")) for s in scopes]
|
|
80
|
+
return set_relation(client, RESOURCE, constraint_id, "scopes", items)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Pure functions for the ``equipment`` resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Iterable, Optional
|
|
6
|
+
|
|
7
|
+
from ._generic import compact, create_row, delete_row, get_row, list_rows, update_row
|
|
8
|
+
from .client import CalendryClient
|
|
9
|
+
|
|
10
|
+
RESOURCE = "equipment"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def list_equipment(client: CalendryClient, *, limit=None, offset=None, q=None, ids: Optional[Iterable[str]] = None) -> Any:
|
|
14
|
+
return list_rows(client, RESOURCE, limit=limit, offset=offset, q=q, ids=ids)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_equipment(client: CalendryClient, equipment_id: str) -> Any:
|
|
18
|
+
return get_row(client, RESOURCE, equipment_id)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_equipment(client: CalendryClient, key: str, name: str, *, description: Optional[str] = None) -> Any:
|
|
22
|
+
body = {"key": key, "name": name}
|
|
23
|
+
body.update(compact(description=description))
|
|
24
|
+
return create_row(client, RESOURCE, body)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def update_equipment(client: CalendryClient, equipment_id: str, *, name: Optional[str] = None, description: Optional[str] = None) -> Any:
|
|
28
|
+
return update_row(client, RESOURCE, equipment_id, compact(name=name, description=description))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def delete_equipment(client: CalendryClient, equipment_id: str) -> Any:
|
|
32
|
+
return delete_row(client, RESOURCE, equipment_id)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Exceptions raised by the Calendry client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CalendryAPIError(Exception):
|
|
9
|
+
"""Raised when the Calendry API returns a non-2xx response."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, status_code: int, message: str, payload: Any = None):
|
|
12
|
+
super().__init__(f"Calendry API error {status_code}: {message}")
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.payload = payload
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_response(cls, response) -> "CalendryAPIError":
|
|
18
|
+
message = response.text
|
|
19
|
+
payload = None
|
|
20
|
+
try:
|
|
21
|
+
payload = response.json()
|
|
22
|
+
if isinstance(payload, dict):
|
|
23
|
+
message = payload.get("message") or payload.get("error") or message
|
|
24
|
+
except ValueError:
|
|
25
|
+
pass
|
|
26
|
+
return cls(response.status_code, message, payload)
|