classcharts-api 1.0.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.
@@ -0,0 +1,14 @@
1
+ """ClassCharts API Python wrapper."""
2
+
3
+ from .parent_client import ParentClient
4
+ from .student_client import StudentClient
5
+ from .exceptions import ClassChartsAuthError, ClassChartsApiError
6
+ from . import models
7
+
8
+ __all__ = [
9
+ "ParentClient",
10
+ "StudentClient",
11
+ "ClassChartsAuthError",
12
+ "ClassChartsApiError",
13
+ "models",
14
+ ]
@@ -0,0 +1,336 @@
1
+ """Base async client — shared logic for both ParentClient and StudentClient.
2
+
3
+ Mirrors src/core/baseClient.ts from classcharts-api-js.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import time
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any
12
+
13
+ import aiohttp
14
+
15
+ from .const import PING_INTERVAL
16
+ from .exceptions import ClassChartsApiError
17
+
18
+
19
+ class BaseClient(ABC):
20
+ """Shared async HTTP client for ClassCharts."""
21
+
22
+ def __init__(self, api_base: str) -> None:
23
+ self._api_base = api_base
24
+ self.student_id: int = 0
25
+ self.session_id: str = ""
26
+ self.auth_cookies: list[str] = []
27
+ self._last_ping: float = 0.0
28
+ self._session: aiohttp.ClientSession | None = None
29
+
30
+ # ------------------------------------------------------------------
31
+ # Session management
32
+ # ------------------------------------------------------------------
33
+
34
+ def _get_session(self) -> aiohttp.ClientSession:
35
+ if self._session is None or self._session.closed:
36
+ self._session = aiohttp.ClientSession()
37
+ return self._session
38
+
39
+ async def close(self) -> None:
40
+ """Close the underlying aiohttp session."""
41
+ if self._session and not self._session.closed:
42
+ await self._session.close()
43
+
44
+ async def __aenter__(self) -> "BaseClient":
45
+ return self
46
+
47
+ async def __aexit__(self, *_: Any) -> None:
48
+ await self.close()
49
+
50
+ @abstractmethod
51
+ async def login(self) -> None:
52
+ """Authenticate with ClassCharts and store session credentials."""
53
+
54
+ # ------------------------------------------------------------------
55
+ # Session revalidation (ping)
56
+ # ------------------------------------------------------------------
57
+
58
+ async def get_new_session_id(self) -> None:
59
+ """Revalidate the session ID.
60
+
61
+ Called automatically when the session is older than PING_INTERVAL
62
+ seconds, and immediately after login for student clients.
63
+ Mirrors getNewSessionId() in baseClient.ts.
64
+ """
65
+ data = aiohttp.FormData()
66
+ data.add_field("include_data", "true")
67
+ response = await self._make_authed_request(
68
+ f"{self._api_base}/ping",
69
+ method="POST",
70
+ data=data,
71
+ revalidate_token=False,
72
+ )
73
+ self.session_id = response["meta"]["session_id"]
74
+ self._last_ping = time.monotonic()
75
+
76
+ # ------------------------------------------------------------------
77
+ # Core request helper
78
+ # ------------------------------------------------------------------
79
+
80
+ async def _make_authed_request(
81
+ self,
82
+ url: str,
83
+ method: str = "GET",
84
+ data: Any = None,
85
+ params: dict[str, str] | None = None,
86
+ revalidate_token: bool = True,
87
+ ) -> dict[str, Any]:
88
+ """Make an authenticated request to the ClassCharts API.
89
+
90
+ Mirrors makeAuthedRequest() in baseClient.ts.
91
+ """
92
+ if not self.session_id:
93
+ raise ClassChartsApiError("No session ID — call login() first")
94
+
95
+ if revalidate_token and self._last_ping:
96
+ elapsed = time.monotonic() - self._last_ping
97
+ if elapsed + 5 > PING_INTERVAL:
98
+ await self.get_new_session_id()
99
+
100
+ headers = {
101
+ "Cookie": "; ".join(c.split(";")[0] for c in self.auth_cookies),
102
+ "Authorization": f"Basic {self.session_id}",
103
+ "User-Agent": "classcharts-api https://github.com/classchartsapi/classcharts-api-js",
104
+ }
105
+
106
+ session = self._get_session()
107
+ async with session.request(
108
+ method,
109
+ url,
110
+ headers=headers,
111
+ data=data,
112
+ params=params,
113
+ ) as resp:
114
+ try:
115
+ payload: dict[str, Any] = await resp.json(content_type=None)
116
+ except Exception as exc:
117
+ text = await resp.text()
118
+ raise ClassChartsApiError(
119
+ f"Error parsing JSON. Response: {text}"
120
+ ) from exc
121
+
122
+ if payload.get("success") == 0:
123
+ raise ClassChartsApiError(payload.get("error", "Unknown API error"))
124
+
125
+ return payload
126
+
127
+ # ------------------------------------------------------------------
128
+ # Shared endpoints
129
+ # ------------------------------------------------------------------
130
+
131
+ async def get_student_info(self) -> dict[str, Any]:
132
+ """Get general information about the current student.
133
+
134
+ Mirrors getStudentInfo() in baseClient.ts.
135
+ """
136
+ data = aiohttp.FormData()
137
+ data.add_field("include_data", "true")
138
+ return await self._make_authed_request(
139
+ f"{self._api_base}/ping", method="POST", data=data
140
+ )
141
+
142
+ async def get_activity(
143
+ self,
144
+ from_date: str | None = None,
145
+ to_date: str | None = None,
146
+ last_id: str | None = None,
147
+ ) -> dict[str, Any]:
148
+ """Get the current student's activity (paginated).
149
+
150
+ Mirrors getActivity() in baseClient.ts.
151
+ """
152
+ params: dict[str, str] = {}
153
+ if from_date:
154
+ params["from"] = from_date
155
+ if to_date:
156
+ params["to"] = to_date
157
+ if last_id:
158
+ params["last_id"] = last_id
159
+ return await self._make_authed_request(
160
+ f"{self._api_base}/activity/{self.student_id}", params=params
161
+ )
162
+
163
+ async def get_full_activity(
164
+ self, from_date: str, to_date: str
165
+ ) -> list[dict[str, Any]]:
166
+ """Get all activity between two dates, handling pagination automatically.
167
+
168
+ Mirrors getFullActivity() in baseClient.ts.
169
+ """
170
+ data: list[dict[str, Any]] = []
171
+ prev_last: str | None = None
172
+ while True:
173
+ response = await self.get_activity(
174
+ from_date=from_date, to_date=to_date, last_id=prev_last
175
+ )
176
+ fragment = response.get("data") or []
177
+ if not fragment:
178
+ break
179
+ data.extend(fragment)
180
+ prev_last = str(fragment[-1]["id"])
181
+ return data
182
+
183
+ async def get_behaviour(
184
+ self,
185
+ from_date: str | None = None,
186
+ to_date: str | None = None,
187
+ ) -> dict[str, Any]:
188
+ """Get the current student's behaviour.
189
+
190
+ Mirrors getBehaviour() in baseClient.ts.
191
+ """
192
+ params: dict[str, str] = {}
193
+ if from_date:
194
+ params["from"] = from_date
195
+ if to_date:
196
+ params["to"] = to_date
197
+ return await self._make_authed_request(
198
+ f"{self._api_base}/behaviour/{self.student_id}", params=params
199
+ )
200
+
201
+ async def get_homeworks(
202
+ self,
203
+ display_date: str | None = None,
204
+ from_date: str | None = None,
205
+ to_date: str | None = None,
206
+ ) -> dict[str, Any]:
207
+ """Get the current student's homeworks.
208
+
209
+ Mirrors getHomeworks() in baseClient.ts.
210
+
211
+ Args:
212
+ display_date: "due_date" or "issue_date" (default: "issue_date")
213
+ from_date: Start date in YYYY-MM-DD format
214
+ to_date: End date in YYYY-MM-DD format
215
+ """
216
+ params: dict[str, str] = {}
217
+ if display_date:
218
+ params["display_date"] = display_date
219
+ if from_date:
220
+ params["from"] = from_date
221
+ if to_date:
222
+ params["to"] = to_date
223
+ return await self._make_authed_request(
224
+ f"{self._api_base}/homeworks/{self.student_id}", params=params
225
+ )
226
+
227
+ async def get_lessons(self, date: str) -> dict[str, Any]:
228
+ """Get the current student's lessons for a given date.
229
+
230
+ Mirrors getLessons() in baseClient.ts.
231
+
232
+ Args:
233
+ date: Date in YYYY-MM-DD format
234
+ """
235
+ return await self._make_authed_request(
236
+ f"{self._api_base}/timetable/{self.student_id}",
237
+ params={"date": date},
238
+ )
239
+
240
+ async def get_badges(self) -> dict[str, Any]:
241
+ """Get the current student's earned badges.
242
+
243
+ Mirrors getBadges() in baseClient.ts.
244
+ """
245
+ return await self._make_authed_request(
246
+ f"{self._api_base}/eventbadges/{self.student_id}"
247
+ )
248
+
249
+ async def get_announcements(self) -> dict[str, Any]:
250
+ """Get the current student's announcements.
251
+
252
+ Mirrors getAnnouncements() in baseClient.ts.
253
+ """
254
+ return await self._make_authed_request(
255
+ f"{self._api_base}/announcements/{self.student_id}"
256
+ )
257
+
258
+ async def get_detentions(self) -> dict[str, Any]:
259
+ """Get the current student's detentions.
260
+
261
+ Mirrors getDetentions() in baseClient.ts.
262
+ """
263
+ return await self._make_authed_request(
264
+ f"{self._api_base}/detentions/{self.student_id}"
265
+ )
266
+
267
+ async def get_attendance(
268
+ self,
269
+ from_date: str | None = None,
270
+ to_date: str | None = None,
271
+ ) -> dict[str, Any]:
272
+ """Get the current student's attendance.
273
+
274
+ Mirrors getAttendance() in baseClient.ts.
275
+ """
276
+ params: dict[str, str] = {}
277
+ if from_date:
278
+ params["from"] = from_date
279
+ if to_date:
280
+ params["to"] = to_date
281
+ return await self._make_authed_request(
282
+ f"{self._api_base}/attendance/{self.student_id}", params=params
283
+ )
284
+
285
+ async def get_pupil_fields(self) -> dict[str, Any]:
286
+ """Get the current student's custom fields.
287
+
288
+ Mirrors getPupilFields() in baseClient.ts.
289
+ """
290
+ return await self._make_authed_request(
291
+ f"{self._api_base}/customfields/{self.student_id}"
292
+ )
293
+
294
+ async def get_classes(self) -> dict[str, Any]:
295
+ """Get the classes the current student is enrolled in."""
296
+ return await self._make_authed_request(
297
+ f"{self._api_base}/classes/{self.student_id}"
298
+ )
299
+
300
+ async def get_academic_reports(self) -> dict[str, Any]:
301
+ """List available academic reports for the current student."""
302
+ return await self._make_authed_request(
303
+ f"{self._api_base}/getacademicreports"
304
+ )
305
+
306
+ async def get_academic_report(self, report_id: int) -> dict[str, Any]:
307
+ """Get a specific academic report by ID."""
308
+ return await self._make_authed_request(
309
+ f"{self._api_base}/getacademicreport/{report_id}"
310
+ )
311
+
312
+ async def get_report_cards(self) -> dict[str, Any]:
313
+ """List on-report cards for the current student."""
314
+ data = aiohttp.FormData()
315
+ data.add_field("pupil_id", str(self.student_id))
316
+ return await self._make_authed_request(
317
+ f"{self._api_base}/getpupilreportcards", method="POST", data=data
318
+ )
319
+
320
+ async def get_report_card(self, report_card_id: int) -> dict[str, Any]:
321
+ """Get a specific on-report card by ID."""
322
+ return await self._make_authed_request(
323
+ f"{self._api_base}/getpupilreportcard/{report_card_id}"
324
+ )
325
+
326
+ async def get_report_card_summary_comment(self, report_card_id: int) -> dict[str, Any]:
327
+ """Get the summary comment for a specific on-report card."""
328
+ return await self._make_authed_request(
329
+ f"{self._api_base}/getpupilreportcardsummarycomment/{report_card_id}"
330
+ )
331
+
332
+ async def get_report_card_target(self, report_card_id: int) -> dict[str, Any]:
333
+ """Get the target for a specific on-report card."""
334
+ return await self._make_authed_request(
335
+ f"{self._api_base}/getpupilreportcardtarget/{report_card_id}"
336
+ )
@@ -0,0 +1,6 @@
1
+ BASE_URL = "https://www.classcharts.com"
2
+ API_BASE_STUDENT = f"{BASE_URL}/apiv2student"
3
+ API_BASE_PARENT = f"{BASE_URL}/apiv2parent"
4
+
5
+ # Session revalidation interval in seconds (3 minutes)
6
+ PING_INTERVAL = 60 * 3
@@ -0,0 +1,6 @@
1
+ class ClassChartsApiError(Exception):
2
+ """Raised when the ClassCharts API returns an error response."""
3
+
4
+
5
+ class ClassChartsAuthError(ClassChartsApiError):
6
+ """Raised when authentication with ClassCharts fails."""