myprepcourse 0.1.0__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.
- myprepcourse-0.1.0/.gitignore +22 -0
- myprepcourse-0.1.0/LICENSE +7 -0
- myprepcourse-0.1.0/PKG-INFO +518 -0
- myprepcourse-0.1.0/README.md +484 -0
- myprepcourse-0.1.0/pyproject.toml +51 -0
- myprepcourse-0.1.0/src/MyPrepCourse/MPCSession.py +308 -0
- myprepcourse-0.1.0/src/MyPrepCourse/MyPrepCourse.py +1844 -0
- myprepcourse-0.1.0/src/MyPrepCourse/__init__.py +112 -0
- myprepcourse-0.1.0/src/MyPrepCourse/exceptions.py +15 -0
- myprepcourse-0.1.0/src/MyPrepCourse/logging_utils.py +36 -0
- myprepcourse-0.1.0/src/MyPrepCourse/py.typed +0 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/__init__.py +116 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/base.py +118 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/classrooms.py +68 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/entities.py +182 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/enums.py +23 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/responses.py +89 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/scores.py +211 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/students.py +121 -0
- myprepcourse-0.1.0/src/MyPrepCourse/types/tests.py +360 -0
- myprepcourse-0.1.0/src/MyPrepCourse/utils/__init__.py +1 -0
- myprepcourse-0.1.0/src/MyPrepCourse/utils/students.py +176 -0
- myprepcourse-0.1.0/tests/README.md +179 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
**/*.env
|
|
2
|
+
**/*.env.*
|
|
3
|
+
.venv/
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
.eggs/
|
|
9
|
+
__pycache__/
|
|
10
|
+
**/__pycache__/
|
|
11
|
+
*.pyc
|
|
12
|
+
*.pyo
|
|
13
|
+
*.pyd
|
|
14
|
+
*.so
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.tox/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
.idea/
|
|
22
|
+
json_api_responses/
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 North Star Tutors Inc
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: myprepcourse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A tool for making API calls and programatic changes to MyPrepCourse
|
|
5
|
+
Project-URL: Homepage, https://github.com/north-star-tutors/MyPrepCourse
|
|
6
|
+
Project-URL: Repository, https://github.com/north-star-tutors/MyPrepCourse
|
|
7
|
+
Project-URL: Issues, https://github.com/north-star-tutors/MyPrepCourse/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/north-star-tutors/MyPrepCourse#readme
|
|
9
|
+
Author-email: Zachary Mason <zachary@northstar-tutors.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: python-dateutil
|
|
24
|
+
Requires-Dist: requests
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
31
|
+
Requires-Dist: twine; extra == 'dev'
|
|
32
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# MyPrepCourse SDK
|
|
36
|
+
|
|
37
|
+
A Python client for the [MyPrepCourse](https://myprepcourse.com) learning management API. Manage students, courses, test scores, and classrooms programmatically.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install myprepcourse
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Requirements:** Python 3.9+
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from MyPrepCourse import MyPrepCourse
|
|
51
|
+
|
|
52
|
+
client = MyPrepCourse(
|
|
53
|
+
email="admin@example.com",
|
|
54
|
+
password="your-password",
|
|
55
|
+
origin="https://yourorg.myprepcourse.com",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# ...or let the SDK read the same three values from the environment:
|
|
59
|
+
# client = MyPrepCourse()
|
|
60
|
+
|
|
61
|
+
# Look up a student
|
|
62
|
+
student = client.get_student_by_email("student@example.com")
|
|
63
|
+
print(student)
|
|
64
|
+
|
|
65
|
+
# Get test scores for a student's course
|
|
66
|
+
tests = client.get_student_tests_by_course(student)
|
|
67
|
+
for test in tests:
|
|
68
|
+
print(f"{test.test.name}: {test.total_score}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Authentication
|
|
72
|
+
|
|
73
|
+
The SDK supports three ways to configure a client:
|
|
74
|
+
|
|
75
|
+
**Credentials** -- the SDK handles login and token refresh automatically:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
client = MyPrepCourse(
|
|
79
|
+
email="admin@example.com",
|
|
80
|
+
password="your-password",
|
|
81
|
+
origin="https://yourorg.myprepcourse.com",
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Environment variables** -- omit an argument and the SDK reads it from the environment:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export MYPREPCOURSE_EMAIL="admin@example.com"
|
|
89
|
+
export MYPREPCOURSE_PASSWORD="your-password"
|
|
90
|
+
export MYPREPCOURSE_ORIGIN="https://yourorg.myprepcourse.com"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
client = MyPrepCourse() # email, password, and origin all come from the environment
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Explicit arguments take precedence over environment variables, so you can override a
|
|
98
|
+
single value without unsetting anything:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
# email and password still come from the environment
|
|
102
|
+
client = MyPrepCourse(origin="https://staging.myprepcourse.com")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See [Environment Variables](#environment-variables) for the full list.
|
|
106
|
+
|
|
107
|
+
**Existing token** -- bring your own bearer token via a pre-configured session:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from MyPrepCourse import MPCSession, MyPrepCourse
|
|
111
|
+
|
|
112
|
+
session = MPCSession(
|
|
113
|
+
token="your-bearer-token",
|
|
114
|
+
origin="https://yourorg.myprepcourse.com",
|
|
115
|
+
)
|
|
116
|
+
client = MyPrepCourse(session=session)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The `origin` parameter is required and must match your organization's MyPrepCourse
|
|
120
|
+
tenant URL. It may be supplied as an argument or via `MYPREPCOURSE_ORIGIN`, and applies
|
|
121
|
+
equally to `MyPrepCourse(...)` and to an `MPCSession` you build yourself:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
session = MPCSession(token="your-bearer-token") # origin from MYPREPCOURSE_ORIGIN
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Usage
|
|
128
|
+
|
|
129
|
+
### Students
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Get a student by email or ID
|
|
133
|
+
student = client.get_student_by_email("jane@example.com")
|
|
134
|
+
student = client.get_student_by_id("12345")
|
|
135
|
+
|
|
136
|
+
# Search students
|
|
137
|
+
results = client.search_students(search="Jane")
|
|
138
|
+
|
|
139
|
+
# Create a student
|
|
140
|
+
student = client.create_student(
|
|
141
|
+
first_name="Jane",
|
|
142
|
+
last_name="Doe",
|
|
143
|
+
email="jane@example.com",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Get or create (idempotent)
|
|
147
|
+
student = client.get_or_create_student(
|
|
148
|
+
first_name="Jane",
|
|
149
|
+
last_name="Doe",
|
|
150
|
+
email="jane@example.com",
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Courses
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Course IDs are loaded per account onto the client instance
|
|
158
|
+
client.course_options.ACT # the account's ACT course ID
|
|
159
|
+
client.course_options.available # {name: course_id} for what this account has
|
|
160
|
+
"eACT" in client.course_options # does this account have eACT?
|
|
161
|
+
|
|
162
|
+
# Select a course by name
|
|
163
|
+
course_name, course_id = client.select_course("ACT")
|
|
164
|
+
|
|
165
|
+
# Assign a course to a student
|
|
166
|
+
client.assign_course_to_student(student, course_id)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### The course context
|
|
170
|
+
|
|
171
|
+
Most endpoints are course-scoped: without an `X-Course-Id` header the API
|
|
172
|
+
answers **`401 Unauthenticated`**, which looks like a credentials problem but
|
|
173
|
+
isn't. The client therefore selects a course as soon as it has loaded the
|
|
174
|
+
catalog, the same way the web app always has one selected.
|
|
175
|
+
|
|
176
|
+
By default it picks `dSAT`, falling back to the first course your account has
|
|
177
|
+
from `DEFAULT_COURSE_PREFERENCE`. Override it per client or per environment:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
client = MyPrepCourse(email=..., password=..., origin=..., default_course="ACT")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
export MYPREPCOURSE_DEFAULT_COURSE="ACT"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Naming a course your account does not have raises `ValueError` at construction
|
|
188
|
+
rather than failing later. A session you built yourself with `course_id=` keeps
|
|
189
|
+
that context untouched, and `select_course()` still changes it at any time.
|
|
190
|
+
|
|
191
|
+
Course IDs differ per account, so they live on the client instance, never on
|
|
192
|
+
the class. Both of these raise `AttributeError` rather than quietly returning
|
|
193
|
+
an empty string that would be sent to the API as a course ID:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
client.CourseOptions.eACT # read off the class -- no IDs live there
|
|
197
|
+
client.course_options.eACT # when the account has no eACT course
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Use `hasattr` or `in` for a course the account may not have, and note the
|
|
201
|
+
names are case-sensitive (`eACT`, not `eact`).
|
|
202
|
+
|
|
203
|
+
### Test Scores
|
|
204
|
+
|
|
205
|
+
The SDK supports four test types: **dSAT**, **ACT**, **Old SAT**, and **eACT**. Each returns a typed score object with test-specific fields.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# Get tests for the currently selected course
|
|
209
|
+
client.select_course("ACT")
|
|
210
|
+
tests = client.get_student_tests_by_course(student)
|
|
211
|
+
|
|
212
|
+
# ...or every course at once, without managing the selection yourself
|
|
213
|
+
tests = client.get_all_student_tests(student)
|
|
214
|
+
|
|
215
|
+
# Get detailed score breakdown for a specific test
|
|
216
|
+
score = client.get_test_score_details(student, test_id="abc123")
|
|
217
|
+
|
|
218
|
+
# Access scores through a common interface
|
|
219
|
+
print(score.total_score)
|
|
220
|
+
print(score.section_scores) # {"Math": 720, "Reading & Writing": 680}
|
|
221
|
+
|
|
222
|
+
# Access test-type-specific fields
|
|
223
|
+
if isinstance(score, DSATTestScore):
|
|
224
|
+
for subject in score.subjects:
|
|
225
|
+
print(f"{subject.name}: {subject.current_score}")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Score report PDFs
|
|
229
|
+
|
|
230
|
+
`get_test_score_report` returns the rendered report as bytes. Writing or
|
|
231
|
+
uploading it is left to the caller, so the SDK does no filesystem work:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from pathlib import Path
|
|
235
|
+
|
|
236
|
+
pdf = client.get_test_score_report(student, test.test_id, notes="Great progress")
|
|
237
|
+
Path(f"{test.name}.pdf").write_bytes(pdf)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### eACT scores
|
|
241
|
+
|
|
242
|
+
The score endpoint returns no field distinguishing an eACT from a dSAT -- both
|
|
243
|
+
send an empty `sub_section_score` list -- so an eACT score is reported as a
|
|
244
|
+
`DSATTestScore` unless you say otherwise. The test type is on the student test
|
|
245
|
+
as `test.subtype`, so pass it through:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
for test in client.get_student_tests_by_course(student):
|
|
249
|
+
score = client.get_test_score_details(
|
|
250
|
+
student, test.test_id, test_type=test.test.subtype
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
eACT responses also differ structurally from the other test types:
|
|
255
|
+
`sequence`, `source_id`, `active` and `reference_id` arrive as integers rather
|
|
256
|
+
than strings. The domain models accept both forms.
|
|
257
|
+
|
|
258
|
+
**There are two kinds of eACT**, and they are not structurally identical. Both
|
|
259
|
+
report `test.subtype == "eACT"`; `test.type` tells them apart:
|
|
260
|
+
|
|
261
|
+
| | `DIGITAL` (software-authored) | `PAPER` (ACT-authored) |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| `test.course_id` | the ACT course | the eACT course |
|
|
264
|
+
| `test.paces` | populated | `null` |
|
|
265
|
+
| `section.abv` | `"ENG"`, `"MATH"`, ... | `null` |
|
|
266
|
+
| `section.description` | rich-text `introPages` dict | `null` |
|
|
267
|
+
| `section.sequence` | 0-based (`0..3`) | 1-based (`1..4`) |
|
|
268
|
+
|
|
269
|
+
Sections are not returned in the order they are taken, and the sequence base
|
|
270
|
+
differs between the two, so sort rather than assuming either:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
for student_section in sorted(test.sections, key=lambda s: s.section.sequence):
|
|
274
|
+
print(student_section.section.name, student_section.section.abv)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Because eACT sequences can start at 0, never test one for truthiness --
|
|
278
|
+
`if section.sequence:` skips the first section of a DIGITAL eACT.
|
|
279
|
+
|
|
280
|
+
#### eACT composite scores
|
|
281
|
+
|
|
282
|
+
On the new eACT, Science is scored separately and is **not** part of the
|
|
283
|
+
composite. `total_score` is the mean of English, Math and Reading, rounded.
|
|
284
|
+
The Science score is still reported in `scores[]` -- it just does not feed the
|
|
285
|
+
composite. Classic ACT composites do include Science, so the same arithmetic
|
|
286
|
+
does not carry across test types:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
scores = {s.section.name: s.section.subject_id for s in test.sections}
|
|
290
|
+
# eACT: total_score == round(mean(English, Math, Reading))
|
|
291
|
+
# classic ACT: total_score == round(mean(English, Math, Reading, Science))
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The SDK does not compute composites -- `total_score` comes from the API.
|
|
295
|
+
|
|
296
|
+
### Classrooms
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
# Fetch a classroom with its location, students and instructors
|
|
300
|
+
classroom = client.get_class("class-123")
|
|
301
|
+
|
|
302
|
+
print(classroom.name, classroom.active_students)
|
|
303
|
+
print(classroom.location.name)
|
|
304
|
+
|
|
305
|
+
for student in classroom.students:
|
|
306
|
+
print(student.first_name, student.last_name, student.email)
|
|
307
|
+
|
|
308
|
+
for instructor in classroom.instructors:
|
|
309
|
+
print(instructor.first_name, instructor.email)
|
|
310
|
+
|
|
311
|
+
# Assign/remove students from a classroom
|
|
312
|
+
client.assign_student_to_class(student, class_id="class-123")
|
|
313
|
+
client.remove_student_from_class(student_id="12345", class_id="class-123")
|
|
314
|
+
|
|
315
|
+
# Remove several at once, choosing what happens to their assignments
|
|
316
|
+
from MyPrepCourse import RemovalStrategy
|
|
317
|
+
|
|
318
|
+
client.remove_students_from_class(
|
|
319
|
+
[student_a, student_b],
|
|
320
|
+
class_id="class-123",
|
|
321
|
+
strategy=RemovalStrategy.KEEP_STARTED,
|
|
322
|
+
)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| Strategy | Effect on the class's assignments |
|
|
326
|
+
|----------|-----------------------------------|
|
|
327
|
+
| `KEEP_NONE` (default) | Removed from the student entirely, completed ones included |
|
|
328
|
+
| `KEEP_ALL` | All left available to the student |
|
|
329
|
+
| `KEEP_STARTED` | Only those already started or completed are kept |
|
|
330
|
+
|
|
331
|
+
A classroom that has not been scheduled yet returns `None` for `start_date`
|
|
332
|
+
and `end_date`, and empty lists for `students`, `instructors` and `sessions`.
|
|
333
|
+
|
|
334
|
+
The students embedded in a classroom come from the classroom payload, which
|
|
335
|
+
carries fewer fields than the student endpoint does. Attributes it omits --
|
|
336
|
+
`timezone`, `locations`, `instructors`, `stats` -- take their empty defaults;
|
|
337
|
+
call `get_student_by_id` for a fully populated profile.
|
|
338
|
+
|
|
339
|
+
### Locations
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
locations = client.get_locations()
|
|
343
|
+
client.set_default_location(locations[0].id)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Calls that need a location, such as `create_student`, use `client.default_location`
|
|
347
|
+
when they are not given one explicitly. It can also be set at construction or
|
|
348
|
+
through the environment, which avoids hardcoding an ID that differs per account:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
client = MyPrepCourse(email=..., password=..., origin=..., default_location="loc-123")
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
export MYPREPCOURSE_DEFAULT_LOCATION="loc-123"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The ID is not validated at construction -- an unknown one surfaces when a call
|
|
359
|
+
uses it. `client.default_location` is a plain attribute, so assigning to it works
|
|
360
|
+
the same as calling `set_default_location()`.
|
|
361
|
+
|
|
362
|
+
## Domain Models
|
|
363
|
+
|
|
364
|
+
All API responses are parsed into typed, immutable dataclass objects. Key types:
|
|
365
|
+
|
|
366
|
+
| Type | Description |
|
|
367
|
+
|------|-------------|
|
|
368
|
+
| `Student` | Full student profile with courses, locations, instructors |
|
|
369
|
+
| `StudentCourse` | Course enrollment with subjects and stats |
|
|
370
|
+
| `StudentTest` | A test attempt with metadata and section breakdown |
|
|
371
|
+
| `DSATTestScore` | dSAT score with subjects and categories |
|
|
372
|
+
| `ACTTestScore` | ACT score with subjects and sub-sections |
|
|
373
|
+
| `OldSATTestScore` | Old SAT score with cross-test scores |
|
|
374
|
+
| `EACTTestScore` | eACT score with subjects and categories |
|
|
375
|
+
| `Classroom` | A class with its location, students and instructors |
|
|
376
|
+
| `Location` | Testing location |
|
|
377
|
+
| `Instructor` | Instructor profile |
|
|
378
|
+
|
|
379
|
+
All domain types are importable from the top-level package:
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from MyPrepCourse import Student, DSATTestScore, ACTTestScore, Location
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Calls that act on a student take a reference rather than a bare string, so an
|
|
386
|
+
email cannot be mistaken for an ID. `Email`, `StudentId`, `StudentSummary` and
|
|
387
|
+
the `StudentRef` union are exported from the top-level package too:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
from MyPrepCourse import Email, StudentId
|
|
391
|
+
|
|
392
|
+
client.get_student_tests_by_course(Email("jane@example.com"))
|
|
393
|
+
client.remove_students_from_class([StudentId("12345")], class_id)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
A `Student` or `StudentSummary` the client already gave you is a reference as
|
|
397
|
+
well, so anything it returns can be passed straight back in:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
for student in client.get_class(class_id).students:
|
|
401
|
+
client.get_all_student_tests(student)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`resolve_student_id()` turns any of them into a `StudentId`, looking the
|
|
405
|
+
student up by email when that is all you have.
|
|
406
|
+
|
|
407
|
+
## Errors
|
|
408
|
+
|
|
409
|
+
Every error the SDK raises derives from `MyPrepCourseError`, so one `except`
|
|
410
|
+
clause covers the package:
|
|
411
|
+
|
|
412
|
+
```
|
|
413
|
+
MyPrepCourseError
|
|
414
|
+
├── AuthenticationError # login failed, or the token was rejected
|
|
415
|
+
└── ObjectFetchError # a fetch did not produce the expected object
|
|
416
|
+
├── NoSuchObjectError # nothing matched
|
|
417
|
+
└── TooManyObjectsFoundError # several matched where one was expected
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
They are importable from the top-level package alongside the domain types:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from MyPrepCourse import MyPrepCourseError, NoSuchObjectError
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
student = client.get_student(Email("nobody@example.com"))
|
|
427
|
+
except NoSuchObjectError:
|
|
428
|
+
...
|
|
429
|
+
except MyPrepCourseError as exc:
|
|
430
|
+
...
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Configuration mistakes -- missing credentials, an unknown course name, a call
|
|
434
|
+
that needs a location when none is set -- raise the built-in `ValueError`
|
|
435
|
+
rather than a package exception, since they are caller errors rather than API
|
|
436
|
+
failures.
|
|
437
|
+
|
|
438
|
+
## Configuration
|
|
439
|
+
|
|
440
|
+
| Parameter | Environment variable | Default | Description |
|
|
441
|
+
|-----------|----------------------|---------|-------------|
|
|
442
|
+
| `origin` | `MYPREPCOURSE_ORIGIN` | *(required)* | Your tenant URL (e.g., `https://yourorg.myprepcourse.com`) |
|
|
443
|
+
| `email` | `MYPREPCOURSE_EMAIL` | `None` | Admin email for authentication |
|
|
444
|
+
| `password` | `MYPREPCOURSE_PASSWORD` | `None` | Admin password for authentication |
|
|
445
|
+
| `base_url` | `MYPREPCOURSE_BASE_URL` | `https://api.myprepcourse.com` | API base URL |
|
|
446
|
+
| `default_course` | `MYPREPCOURSE_DEFAULT_COURSE` | `dSAT`, then the first course you have | Course selected at startup |
|
|
447
|
+
| `default_location` | `MYPREPCOURSE_DEFAULT_LOCATION` | `None` | Location used by calls that need one and were not given it |
|
|
448
|
+
| `session` | -- | `None` | Pre-configured `MPCSession` instance |
|
|
449
|
+
| `timeout` | -- | `30` | Request timeout in seconds |
|
|
450
|
+
|
|
451
|
+
### Environment Variables
|
|
452
|
+
|
|
453
|
+
Any of the variables above may be set in the environment instead of being passed to
|
|
454
|
+
`MyPrepCourse(...)` or `MPCSession(...)`. Values are resolved in this order:
|
|
455
|
+
|
|
456
|
+
1. An explicit keyword argument
|
|
457
|
+
2. The matching `MYPREPCOURSE_*` environment variable
|
|
458
|
+
3. The built-in default (`base_url` and `default_course` only -- the rest have none)
|
|
459
|
+
|
|
460
|
+
Missing credentials raise a `ValueError` naming what is absent, so a misconfigured
|
|
461
|
+
environment fails at construction rather than on the first request:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
>>> MyPrepCourse()
|
|
465
|
+
ValueError: Email and password are required when no session is provided.
|
|
466
|
+
>>> MPCSession()
|
|
467
|
+
ValueError: Origin URL is required (e.g., 'https://yourorg.myprepcourse.com').
|
|
468
|
+
Pass origin= or set MYPREPCOURSE_ORIGIN.
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Using a `.env` File
|
|
472
|
+
|
|
473
|
+
The SDK reads the process environment and does not parse `.env` files itself, which keeps
|
|
474
|
+
its dependencies to `requests` and `python-dateutil`. To use a `.env` file, load it in your
|
|
475
|
+
application before constructing the client:
|
|
476
|
+
|
|
477
|
+
```bash
|
|
478
|
+
pip install python-dotenv
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
# .env
|
|
483
|
+
MYPREPCOURSE_EMAIL=admin@example.com
|
|
484
|
+
MYPREPCOURSE_PASSWORD=your-password
|
|
485
|
+
MYPREPCOURSE_ORIGIN=https://yourorg.myprepcourse.com
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
from dotenv import load_dotenv
|
|
490
|
+
|
|
491
|
+
load_dotenv() # loads .env into the environment
|
|
492
|
+
|
|
493
|
+
from MyPrepCourse import MyPrepCourse
|
|
494
|
+
|
|
495
|
+
client = MyPrepCourse()
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Never commit a `.env` file holding real credentials. This repository's `.gitignore`
|
|
499
|
+
already excludes `.env` and `.env.*`.
|
|
500
|
+
|
|
501
|
+
## Development
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
# Clone and install in editable mode with dev dependencies
|
|
505
|
+
git clone https://github.com/north-star-tutors/MyPrepCourse.git
|
|
506
|
+
cd MyPrepCourse
|
|
507
|
+
pip install -e ".[dev]"
|
|
508
|
+
|
|
509
|
+
# Run tests
|
|
510
|
+
python -m pytest tests/ -v
|
|
511
|
+
|
|
512
|
+
# Run tests with coverage
|
|
513
|
+
python -m pytest tests/ -v --cov=src/MyPrepCourse --cov-report=term-missing
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## License
|
|
517
|
+
|
|
518
|
+
MIT
|