librus-python-api 1.0.0rc1__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.
Files changed (46) hide show
  1. librus_python_api/__init__.py +243 -0
  2. librus_python_api/_notification_codec.py +310 -0
  3. librus_python_api/_storage.py +367 -0
  4. librus_python_api/announcements.py +159 -0
  5. librus_python_api/attachment_routes.py +114 -0
  6. librus_python_api/attachments.py +297 -0
  7. librus_python_api/attendance.py +182 -0
  8. librus_python_api/attendance_frequency.py +112 -0
  9. librus_python_api/budget.py +79 -0
  10. librus_python_api/checkpoint.py +61 -0
  11. librus_python_api/completed_lessons.py +216 -0
  12. librus_python_api/config.py +1410 -0
  13. librus_python_api/detail_fields.py +50 -0
  14. librus_python_api/diagnostics.py +25 -0
  15. librus_python_api/exceptions.py +172 -0
  16. librus_python_api/files.py +156 -0
  17. librus_python_api/grade_parsers.py +169 -0
  18. librus_python_api/grade_records.py +454 -0
  19. librus_python_api/homework_range.py +41 -0
  20. librus_python_api/lifecycle.py +24 -0
  21. librus_python_api/markup.py +147 -0
  22. librus_python_api/message_content.py +230 -0
  23. librus_python_api/messages.py +288 -0
  24. librus_python_api/models.py +1160 -0
  25. librus_python_api/modern_body.py +75 -0
  26. librus_python_api/modern_mailbox.py +459 -0
  27. librus_python_api/modern_messages.py +276 -0
  28. librus_python_api/notification_models.py +67 -0
  29. librus_python_api/notification_persistence.py +1044 -0
  30. librus_python_api/notification_workflow.py +303 -0
  31. librus_python_api/notifications.py +216 -0
  32. librus_python_api/parsers.py +232 -0
  33. librus_python_api/parsing.py +49 -0
  34. librus_python_api/persistence.py +405 -0
  35. librus_python_api/py.typed +0 -0
  36. librus_python_api/recipients.py +271 -0
  37. librus_python_api/scheduler.py +287 -0
  38. librus_python_api/school_reads.py +400 -0
  39. librus_python_api/sending.py +125 -0
  40. librus_python_api/service.py +2285 -0
  41. librus_python_api/timetable.py +261 -0
  42. librus_python_api/transport.py +956 -0
  43. librus_python_api-1.0.0rc1.dist-info/METADATA +254 -0
  44. librus_python_api-1.0.0rc1.dist-info/RECORD +46 -0
  45. librus_python_api-1.0.0rc1.dist-info/WHEEL +4 -0
  46. librus_python_api-1.0.0rc1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,243 @@
1
+ """Independent bounded async client. General live compatibility is unqualified."""
2
+
3
+ from librus_python_api.attachments import AttachmentStream, ModernAttachmentStream
4
+ from librus_python_api.budget import RequestBudget
5
+ from librus_python_api.config import (
6
+ AccountCredentials,
7
+ ConnectionSettings,
8
+ OperationLimits,
9
+ SchedulerLimits,
10
+ TransportLimits,
11
+ )
12
+ from librus_python_api.models import (
13
+ AccountContext,
14
+ Agenda,
15
+ AgendaDay,
16
+ AgendaEvent,
17
+ Announcement,
18
+ Announcements,
19
+ AttachmentHeaders,
20
+ AttachmentMetadata,
21
+ Attendance,
22
+ AttendanceDetail,
23
+ AttendanceDetailContent,
24
+ AttendanceFrequency,
25
+ AttendanceKind,
26
+ AttendanceRecord,
27
+ AttendanceRecords,
28
+ AttendanceView,
29
+ AttendanceWindow,
30
+ Availability,
31
+ CompletedLesson,
32
+ CompletedLessons,
33
+ CompletedLessonsCursor,
34
+ CompletedLessonsPage,
35
+ DescriptiveGrade,
36
+ DescriptiveGradeSummary,
37
+ DetailField,
38
+ DetailFieldKey,
39
+ FinalGrades,
40
+ FrequencyMeasure,
41
+ GatewayAttendance,
42
+ GatewayAttendanceRecord,
43
+ GradeKind,
44
+ GradeRecords,
45
+ Grades,
46
+ GradeSummaryValue,
47
+ GradeView,
48
+ GradeWindow,
49
+ Homework,
50
+ HomeworkItem,
51
+ HomeworkRangeRequest,
52
+ Identity,
53
+ LuckyNumber,
54
+ MessageAttachment,
55
+ MessageAttachmentReference,
56
+ MessageContent,
57
+ MessageContentData,
58
+ MessageFolder,
59
+ MessageRecipientReceipt,
60
+ MessageReference,
61
+ Messages,
62
+ MessagesCursor,
63
+ MessagesPage,
64
+ MessageSummary,
65
+ MessageTimestamp,
66
+ MessagingBackend,
67
+ ModernAccountData,
68
+ ModernIdentity,
69
+ ModernMessageAttachment,
70
+ ModernMessageAttachmentReference,
71
+ ModernMessageContent,
72
+ ModernMessageRecipientReceipt,
73
+ ModernMessageReference,
74
+ ModernMessages,
75
+ ModernMessagesCursor,
76
+ ModernMessagesPage,
77
+ ModernMessageSummary,
78
+ ModernRecipient,
79
+ ModernRecipientReference,
80
+ ModernRecipients,
81
+ ModernRecipientType,
82
+ ModernRecipientTypeReference,
83
+ ModernRecipientTypes,
84
+ ModernSendSubmission,
85
+ NotificationCategory,
86
+ NotificationCount,
87
+ NotificationCounts,
88
+ NumericGrade,
89
+ Observation,
90
+ Person,
91
+ RecentScheduleEvent,
92
+ Recipient,
93
+ RecipientGroup,
94
+ RecipientGroupChoice,
95
+ RecipientGroupChoices,
96
+ RecipientGroupReference,
97
+ RecipientGroups,
98
+ RecipientReference,
99
+ Recipients,
100
+ ScheduleEventResponse,
101
+ ScheduleEvents,
102
+ ScheduleEventWire,
103
+ SchoolAverage,
104
+ SchoolDetail,
105
+ SchoolReference,
106
+ SendResult,
107
+ SendStatus,
108
+ SendSubmission,
109
+ StudentInformation,
110
+ SubjectFrequencies,
111
+ SubjectFrequency,
112
+ SubjectGradeSummary,
113
+ Timetable,
114
+ TimetableChange,
115
+ TimetableDay,
116
+ TimetableInterval,
117
+ TimetableLesson,
118
+ TimetablePeriod,
119
+ )
120
+ from librus_python_api.sending import SendAttempt
121
+ from librus_python_api.service import AccountClient, LibrusService
122
+
123
+ __version__ = "1.0.0rc1"
124
+
125
+ __all__ = [
126
+ "AccountContext",
127
+ "MessagingBackend",
128
+ "ModernAccountData",
129
+ "ModernIdentity",
130
+ "ModernMessageAttachment",
131
+ "ModernAttachmentStream",
132
+ "ModernMessageAttachmentReference",
133
+ "ModernMessageContent",
134
+ "ModernMessageReference",
135
+ "ModernMessageRecipientReceipt",
136
+ "ModernMessages",
137
+ "ModernMessagesCursor",
138
+ "ModernMessagesPage",
139
+ "ModernMessageSummary",
140
+ "ModernRecipient",
141
+ "ModernRecipientReference",
142
+ "ModernRecipientType",
143
+ "ModernRecipientTypeReference",
144
+ "ModernRecipientTypes",
145
+ "ModernRecipients",
146
+ "ModernSendSubmission",
147
+ "SendAttempt",
148
+ "SendResult",
149
+ "SendStatus",
150
+ "SendSubmission",
151
+ "RecipientGroupChoice",
152
+ "RecipientGroupChoices",
153
+ "MessageRecipientReceipt",
154
+ "NotificationCategory",
155
+ "NotificationCount",
156
+ "NotificationCounts",
157
+ "ScheduleEventWire",
158
+ "ScheduleEventResponse",
159
+ "RecentScheduleEvent",
160
+ "ScheduleEvents",
161
+ "AttachmentStream",
162
+ "AttachmentMetadata",
163
+ "AttachmentHeaders",
164
+ "MessageAttachment",
165
+ "MessageAttachmentReference",
166
+ "MessageContent",
167
+ "MessageContentData",
168
+ "Recipient",
169
+ "RecipientGroup",
170
+ "RecipientGroupReference",
171
+ "RecipientGroups",
172
+ "RecipientReference",
173
+ "Recipients",
174
+ "MessageFolder",
175
+ "MessageReference",
176
+ "MessageSummary",
177
+ "MessageTimestamp",
178
+ "Messages",
179
+ "MessagesCursor",
180
+ "MessagesPage",
181
+ "CompletedLesson",
182
+ "CompletedLessons",
183
+ "CompletedLessonsCursor",
184
+ "CompletedLessonsPage",
185
+ "AccountClient",
186
+ "AccountCredentials",
187
+ "Availability",
188
+ "Attendance",
189
+ "Announcement",
190
+ "Agenda",
191
+ "AgendaDay",
192
+ "AgendaEvent",
193
+ "Homework",
194
+ "HomeworkItem",
195
+ "HomeworkRangeRequest",
196
+ "SchoolDetail",
197
+ "DetailField",
198
+ "DetailFieldKey",
199
+ "SchoolReference",
200
+ "Announcements",
201
+ "AttendanceDetail",
202
+ "AttendanceDetailContent",
203
+ "AttendanceKind",
204
+ "AttendanceFrequency",
205
+ "FrequencyMeasure",
206
+ "GatewayAttendance",
207
+ "GatewayAttendanceRecord",
208
+ "SubjectFrequency",
209
+ "SubjectFrequencies",
210
+ "Timetable",
211
+ "TimetableChange",
212
+ "TimetableDay",
213
+ "TimetableInterval",
214
+ "TimetableLesson",
215
+ "TimetablePeriod",
216
+ "AttendanceRecord",
217
+ "AttendanceRecords",
218
+ "AttendanceView",
219
+ "AttendanceWindow",
220
+ "ConnectionSettings",
221
+ "DescriptiveGrade",
222
+ "DescriptiveGradeSummary",
223
+ "FinalGrades",
224
+ "GradeKind",
225
+ "GradeRecords",
226
+ "Grades",
227
+ "GradeSummaryValue",
228
+ "GradeView",
229
+ "GradeWindow",
230
+ "Identity",
231
+ "LibrusService",
232
+ "LuckyNumber",
233
+ "NumericGrade",
234
+ "Observation",
235
+ "OperationLimits",
236
+ "Person",
237
+ "RequestBudget",
238
+ "SchedulerLimits",
239
+ "SchoolAverage",
240
+ "StudentInformation",
241
+ "SubjectGradeSummary",
242
+ "TransportLimits",
243
+ ]
@@ -0,0 +1,310 @@
1
+ """Bounded original serialization of typed native records and raw envelopes."""
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from dataclasses import asdict, dataclass, fields
7
+ from datetime import date, datetime
8
+ from functools import lru_cache
9
+ from typing import Any, cast
10
+
11
+ from pydantic import TypeAdapter, ValidationError
12
+
13
+ from librus_python_api.exceptions import ErrorKind, LibrusError
14
+ from librus_python_api.models import (
15
+ Announcement,
16
+ AttendanceRecord,
17
+ DescriptiveGrade,
18
+ HomeworkItem,
19
+ Identity,
20
+ MessageSummary,
21
+ NotificationCategory,
22
+ NumericGrade,
23
+ Observation,
24
+ RecentScheduleEvent,
25
+ ScheduleEventResponse,
26
+ ScheduleEventWire,
27
+ )
28
+ from librus_python_api.notification_models import (
29
+ NotificationBatch,
30
+ NotificationItem,
31
+ NotificationSeen,
32
+ NotificationState,
33
+ NotificationValue,
34
+ )
35
+ from librus_python_api.notifications import validate_response
36
+
37
+ HEX = re.compile(r"[0-9a-f]{64}")
38
+ WIRE_BYTES = 4 * 1024 * 1024
39
+ META_BYTES = 4 * 1024 * 1024
40
+ ARCHIVE_BYTES = 48 * 1024 * 1024
41
+ _VALUE_TYPES: dict[str, type[Any]] = {
42
+ cls.__name__: cls
43
+ for cls in (
44
+ NumericGrade,
45
+ DescriptiveGrade,
46
+ AttendanceRecord,
47
+ MessageSummary,
48
+ Announcement,
49
+ HomeworkItem,
50
+ RecentScheduleEvent,
51
+ )
52
+ }
53
+ _CATEGORY_TYPES: dict[NotificationCategory, tuple[type[Any], ...]] = {
54
+ NotificationCategory.GRADES: (NumericGrade, DescriptiveGrade),
55
+ NotificationCategory.ATTENDANCE: (AttendanceRecord,),
56
+ NotificationCategory.MESSAGES: (MessageSummary,),
57
+ NotificationCategory.ANNOUNCEMENTS: (Announcement,),
58
+ NotificationCategory.HOMEWORK: (HomeworkItem,),
59
+ NotificationCategory.AGENDA: (RecentScheduleEvent,),
60
+ }
61
+
62
+
63
+ def _dates(value: Any) -> str:
64
+ if isinstance(value, (date, datetime)):
65
+ return value.isoformat()
66
+ raise ValueError("Unsupported value")
67
+
68
+
69
+ def dump(value: Any, maximum: int) -> bytes:
70
+ failed = False
71
+ payload = b""
72
+ try:
73
+ payload = json.dumps(
74
+ value,
75
+ default=_dates,
76
+ ensure_ascii=False,
77
+ sort_keys=True,
78
+ separators=(",", ":"),
79
+ allow_nan=False,
80
+ ).encode("utf-8")
81
+ except (TypeError, ValueError, UnicodeError, RecursionError):
82
+ failed = True
83
+ if failed:
84
+ raise LibrusError(ErrorKind.INVALID_INPUT)
85
+ if len(payload) > maximum:
86
+ raise LibrusError(ErrorKind.LIMIT)
87
+ return payload
88
+
89
+
90
+ def _pairs(values: list[tuple[str, Any]]) -> dict[str, Any]:
91
+ result: dict[str, Any] = {}
92
+ for key, value in values:
93
+ if key in result:
94
+ raise ValueError("Duplicate key")
95
+ result[key] = value
96
+ return result
97
+
98
+
99
+ def load(payload: bytes, maximum: int) -> Any:
100
+ if type(payload) is not bytes or len(payload) > maximum:
101
+ raise LibrusError(ErrorKind.LIMIT)
102
+ failed = False
103
+ result: Any = None
104
+ try:
105
+ result = json.loads(payload, object_pairs_hook=_pairs)
106
+ except (ValueError, UnicodeError, RecursionError):
107
+ failed = True
108
+ if failed:
109
+ raise LibrusError(ErrorKind.PARSE)
110
+ return result
111
+
112
+
113
+ @lru_cache(maxsize=16)
114
+ def _adapter(cls: type[Any]) -> TypeAdapter[Any]:
115
+ return TypeAdapter(cls)
116
+
117
+
118
+ def typed[T](cls: type[T], value: Any, maximum: int = META_BYTES) -> T:
119
+ if type(value) is not dict or set(value) != {
120
+ f.name for f in fields(cast(Any, cls))
121
+ }:
122
+ raise LibrusError(ErrorKind.PARSE)
123
+ adapter = _adapter(cast(Any, cls))
124
+ failed = False
125
+ result: T | None = None
126
+ try:
127
+ result = cast(T, adapter.validate_json(dump(value, maximum), strict=True))
128
+ except (ValidationError, ValueError, RecursionError):
129
+ failed = True
130
+ if failed:
131
+ raise LibrusError(ErrorKind.PARSE)
132
+ assert result is not None
133
+ return result
134
+
135
+
136
+ def canonical_notification_id(
137
+ category: NotificationCategory, value: NotificationValue
138
+ ) -> str:
139
+ if (
140
+ not isinstance(category, NotificationCategory)
141
+ or type(value) not in _CATEGORY_TYPES[category]
142
+ ):
143
+ raise LibrusError(ErrorKind.INVALID_INPUT)
144
+ if isinstance(value, RecentScheduleEvent):
145
+ # Independently specified three-field canonical identity, including data.
146
+ canonical: Any = asdict(value)
147
+ elif isinstance(value, MessageSummary):
148
+ canonical = [
149
+ 1,
150
+ category.value,
151
+ value.reference.folder.value,
152
+ value.reference.identifier,
153
+ ]
154
+ elif isinstance(value, Announcement):
155
+ canonical = [1, category.value, value.reference]
156
+ elif isinstance(value, HomeworkItem) and value.reference is not None:
157
+ canonical = [1, category.value, value.reference.identifier]
158
+ elif isinstance(value, AttendanceRecord) and value.detail_id is not None:
159
+ canonical = [1, category.value, value.detail_id]
160
+ else:
161
+ canonical = [1, category.value, type(value).__name__, asdict(value)]
162
+ return hashlib.sha256(dump(canonical, META_BYTES)).hexdigest()
163
+
164
+
165
+ def empty_state() -> NotificationState:
166
+ return NotificationState(
167
+ False,
168
+ tuple(NotificationSeen(category, ()) for category in NotificationCategory),
169
+ )
170
+
171
+
172
+ def state_bytes(state: NotificationState, per_category: int, maximum: int) -> bytes:
173
+ restored = typed(NotificationState, asdict(state))
174
+ if tuple(s.category for s in restored.seen) != tuple(NotificationCategory):
175
+ raise LibrusError(ErrorKind.PARSE)
176
+ for seen in restored.seen:
177
+ if len(seen.identifiers) > per_category:
178
+ raise LibrusError(ErrorKind.LIMIT)
179
+ if len(set(seen.identifiers)) != len(seen.identifiers) or any(
180
+ HEX.fullmatch(v) is None for v in seen.identifiers
181
+ ):
182
+ raise LibrusError(ErrorKind.PARSE)
183
+ if not restored.initialized and any(seen.identifiers for seen in restored.seen):
184
+ raise LibrusError(ErrorKind.PARSE)
185
+ return dump(asdict(restored), maximum)
186
+
187
+
188
+ def restore_state(payload: bytes, per_category: int, maximum: int) -> NotificationState:
189
+ state = typed(NotificationState, load(payload, maximum))
190
+ state_bytes(state, per_category, maximum)
191
+ return state
192
+
193
+
194
+ @dataclass(frozen=True, slots=True)
195
+ class _EnvelopeMetadata:
196
+ version: int
197
+ identity: Identity
198
+ observation: Observation
199
+ content_type: str | None
200
+ content_codings: tuple[str, ...]
201
+ transfer_codings: tuple[str, ...]
202
+
203
+
204
+ def encode_envelope(response: ScheduleEventResponse, alias: str) -> tuple[bytes, bytes]:
205
+ validate_response(response, alias, WIRE_BYTES)
206
+ wire = response.wire
207
+ metadata = _EnvelopeMetadata(
208
+ response.version,
209
+ response.identity,
210
+ response.observation,
211
+ wire.content_type,
212
+ wire.content_codings,
213
+ wire.transfer_codings,
214
+ )
215
+ payload = dump(asdict(metadata), META_BYTES)
216
+ restore_envelope(payload, wire.body, alias)
217
+ return payload, wire.body
218
+
219
+
220
+ def restore_envelope(metadata: bytes, body: bytes, alias: str) -> ScheduleEventResponse:
221
+ record = typed(_EnvelopeMetadata, load(metadata, META_BYTES))
222
+ response = ScheduleEventResponse(
223
+ record.version,
224
+ record.identity,
225
+ ScheduleEventWire(
226
+ body, record.content_type, record.content_codings, record.transfer_codings
227
+ ),
228
+ record.observation,
229
+ )
230
+ validate_response(response, alias, WIRE_BYTES)
231
+ for observation in (record.observation, record.identity.observation):
232
+ if (
233
+ observation.observed_at.tzinfo is None
234
+ or not 0 <= observation.session_generation <= 10**12
235
+ ):
236
+ raise LibrusError(ErrorKind.PARSE)
237
+ return response
238
+
239
+
240
+ def encode_batch(batch: NotificationBatch, maximum: int) -> bytes:
241
+ payload = asdict(batch)
242
+ payload["items"] = [
243
+ {"kind": type(item.value).__name__, "item": asdict(item)}
244
+ for item in batch.items
245
+ ]
246
+ return dump(payload, maximum)
247
+
248
+
249
+ def restore_batch(payload: bytes, maximum: int) -> NotificationBatch:
250
+ record = load(payload, maximum)
251
+ if (
252
+ type(record) is not dict
253
+ or set(record) != {f.name for f in fields(NotificationBatch)}
254
+ or type(record["items"]) is not list
255
+ ):
256
+ raise LibrusError(ErrorKind.PARSE)
257
+ items = []
258
+ for entry in record["items"]:
259
+ if (
260
+ type(entry) is not dict
261
+ or set(entry) != {"kind", "item"}
262
+ or type(entry["kind"]) is not str
263
+ or entry["kind"] not in _VALUE_TYPES
264
+ ):
265
+ raise LibrusError(ErrorKind.PARSE)
266
+ item = entry["item"]
267
+ if type(item) is not dict or set(item) != {
268
+ f.name for f in fields(NotificationItem)
269
+ }:
270
+ raise LibrusError(ErrorKind.PARSE)
271
+ value = typed(_VALUE_TYPES[entry["kind"]], item["value"])
272
+ # Validate union contents separately, then reconstruct the original type.
273
+ parsed = typed(NotificationItem, item)
274
+ if type(value) not in _CATEGORY_TYPES[
275
+ parsed.category
276
+ ] or parsed.identifier != canonical_notification_id(parsed.category, value):
277
+ raise LibrusError(ErrorKind.PARSE)
278
+ items.append(
279
+ NotificationItem(
280
+ parsed.category,
281
+ parsed.identifier,
282
+ value,
283
+ parsed.identity,
284
+ parsed.observation,
285
+ )
286
+ )
287
+ record["items"] = []
288
+ batch = typed(NotificationBatch, record, maximum)
289
+ if (
290
+ HEX.fullmatch(batch.receipt) is None
291
+ or HEX.fullmatch(batch.context.identifier) is None
292
+ or not batch.categories
293
+ or len(set(batch.categories)) != len(batch.categories)
294
+ ):
295
+ raise LibrusError(ErrorKind.PARSE)
296
+ if any(
297
+ item.category not in batch.categories
298
+ or item.observation.account != batch.context.alias
299
+ or item.identity.observation.account != batch.context.alias
300
+ for item in items
301
+ ):
302
+ raise LibrusError(ErrorKind.PARSE)
303
+ return NotificationBatch(
304
+ batch.receipt,
305
+ batch.context,
306
+ batch.first_run,
307
+ batch.categories,
308
+ tuple(items),
309
+ batch.has_more_schedule,
310
+ )