timeback-oneroster 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.
- timeback_oneroster-0.1.0/.gitignore +48 -0
- timeback_oneroster-0.1.0/PKG-INFO +275 -0
- timeback_oneroster-0.1.0/README.md +258 -0
- timeback_oneroster-0.1.0/pyproject.toml +30 -0
- timeback_oneroster-0.1.0/pytest.ini +5 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/__init__.py +139 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/client.py +280 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/constants.py +11 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/exceptions.py +30 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/lib/__init__.py +23 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/lib/filter.py +21 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/lib/pagination.py +305 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/lib/transport.py +88 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/py.typed +1 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/__init__.py +87 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/assessment/__init__.py +12 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/assessment/line_items.py +116 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/assessment/results.py +84 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/base.py +477 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/__init__.py +19 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/categories.py +101 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/line_items.py +239 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/results.py +116 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/score_scales.py +108 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/resources/__init__.py +12 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/resources/resources.py +134 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/__init__.py +63 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/academic_sessions.py +325 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/classes.py +997 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/courses.py +346 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/demographics.py +94 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/enrollments.py +108 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/orgs.py +95 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/schools.py +855 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/users.py +593 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/__init__.py +108 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/assessment.py +75 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/base.py +55 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/gradebook.py +113 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/input.py +457 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/resources.py +108 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/types/rostering.py +233 -0
- timeback_oneroster-0.1.0/src/timeback_oneroster/utils.py +89 -0
- timeback_oneroster-0.1.0/tests/__init__.py +1 -0
- timeback_oneroster-0.1.0/tests/test_client.py +129 -0
- timeback_oneroster-0.1.0/tests/test_filter.py +211 -0
- timeback_oneroster-0.1.0/tests/test_pagination.py +97 -0
- timeback_oneroster-0.1.0/tests/test_resources.py +339 -0
- timeback_oneroster-0.1.0/tests/test_types.py +124 -0
- timeback_oneroster-0.1.0/tests/test_utils.py +73 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
|
|
22
|
+
# Virtual environments
|
|
23
|
+
.venv/
|
|
24
|
+
venv/
|
|
25
|
+
ENV/
|
|
26
|
+
|
|
27
|
+
# uv
|
|
28
|
+
uv.lock
|
|
29
|
+
|
|
30
|
+
# ruff
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
|
|
33
|
+
# Testing
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.coverage
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
|
|
40
|
+
# IDEs
|
|
41
|
+
.idea/
|
|
42
|
+
.vscode/
|
|
43
|
+
*.swp
|
|
44
|
+
*.swo
|
|
45
|
+
|
|
46
|
+
# OS
|
|
47
|
+
.DS_Store
|
|
48
|
+
Thumbs.db
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: timeback-oneroster
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Timeback OneRoster v1.2 client for rostering and gradebook APIs
|
|
5
|
+
Author-email: Timeback <dev@timeback.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: timeback-common>=0.1.0
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# timeback-oneroster
|
|
19
|
+
|
|
20
|
+
Python client for the OneRoster v1.2 API.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install timeback-oneroster
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from timeback_oneroster import OneRosterClient
|
|
32
|
+
|
|
33
|
+
async def main():
|
|
34
|
+
client = OneRosterClient(
|
|
35
|
+
env="staging", # or "production"
|
|
36
|
+
client_id="your-client-id",
|
|
37
|
+
client_secret="your-client-secret",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# List all schools
|
|
41
|
+
schools = await client.schools.list()
|
|
42
|
+
for school in schools:
|
|
43
|
+
print(school.name)
|
|
44
|
+
|
|
45
|
+
# Get a specific user
|
|
46
|
+
user = await client.users.get("user-sourced-id")
|
|
47
|
+
print(f"{user.given_name} {user.family_name}")
|
|
48
|
+
|
|
49
|
+
await client.close()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Client Structure
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
client = OneRosterClient(options)
|
|
56
|
+
|
|
57
|
+
# Rostering
|
|
58
|
+
client.users # All users
|
|
59
|
+
client.students # Students (filtered users)
|
|
60
|
+
client.teachers # Teachers (filtered users)
|
|
61
|
+
client.classes # Classes
|
|
62
|
+
client.schools # Schools
|
|
63
|
+
# client.courses # Coming soon
|
|
64
|
+
# client.enrollments # Coming soon
|
|
65
|
+
# client.terms # Coming soon
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Resource Operations
|
|
69
|
+
|
|
70
|
+
Each resource supports:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# List all items
|
|
74
|
+
users = await client.users.list()
|
|
75
|
+
|
|
76
|
+
# List with type-safe filtering (recommended)
|
|
77
|
+
active_teachers = await client.users.list(
|
|
78
|
+
where={"status": "active", "role": "teacher"}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# With operators
|
|
82
|
+
teachers_or_aides = await client.users.list(
|
|
83
|
+
where={"role": {"in_": ["teacher", "aide"]}}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Not equal
|
|
87
|
+
non_deleted = await client.users.list(
|
|
88
|
+
where={"status": {"ne": "deleted"}}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Sorting
|
|
92
|
+
sorted_users = await client.users.list(
|
|
93
|
+
where={"status": "active"},
|
|
94
|
+
sort="familyName",
|
|
95
|
+
order_by="asc",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Legacy filter string (still supported)
|
|
99
|
+
active_users = await client.users.list(filter="status='active'")
|
|
100
|
+
|
|
101
|
+
# Get by sourcedId
|
|
102
|
+
user = await client.users.get("user-id")
|
|
103
|
+
|
|
104
|
+
# Create (where supported)
|
|
105
|
+
await client.classes.create({
|
|
106
|
+
"title": "Math 101",
|
|
107
|
+
"course": {"sourcedId": "course-id"},
|
|
108
|
+
"school": {"sourcedId": "school-id"},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
# Update (where supported)
|
|
112
|
+
await client.classes.update("class-id", {"title": "Math 102"})
|
|
113
|
+
|
|
114
|
+
# Delete (where supported)
|
|
115
|
+
await client.classes.delete("class-id")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Nested Resources
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# Schools
|
|
122
|
+
classes = await client.schools("school-id").classes()
|
|
123
|
+
students = await client.schools("school-id").students()
|
|
124
|
+
teachers = await client.schools("school-id").teachers()
|
|
125
|
+
courses = await client.schools("school-id").courses()
|
|
126
|
+
|
|
127
|
+
# Classes
|
|
128
|
+
students = await client.classes("class-id").students()
|
|
129
|
+
teachers = await client.classes("class-id").teachers()
|
|
130
|
+
enrollments = await client.classes("class-id").enrollments()
|
|
131
|
+
|
|
132
|
+
# Enroll a student
|
|
133
|
+
await client.classes("class-id").enroll("student-id", role="student")
|
|
134
|
+
|
|
135
|
+
# Users
|
|
136
|
+
classes = await client.users("user-id").classes()
|
|
137
|
+
demographics = await client.users("user-id").demographics()
|
|
138
|
+
|
|
139
|
+
# Students / Teachers
|
|
140
|
+
classes = await client.students("student-id").classes()
|
|
141
|
+
classes = await client.teachers("teacher-id").classes()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Filtering
|
|
145
|
+
|
|
146
|
+
The client supports type-safe filtering with the `where` parameter:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# Simple equality
|
|
150
|
+
users = await client.users.list(where={"status": "active"})
|
|
151
|
+
|
|
152
|
+
# Multiple conditions (AND)
|
|
153
|
+
users = await client.users.list(
|
|
154
|
+
where={"status": "active", "role": "teacher"}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Operators
|
|
158
|
+
users = await client.users.list(where={"score": {"gte": 90}}) # >=
|
|
159
|
+
users = await client.users.list(where={"score": {"gt": 90}}) # >
|
|
160
|
+
users = await client.users.list(where={"score": {"lte": 90}}) # <=
|
|
161
|
+
users = await client.users.list(where={"score": {"lt": 90}}) # <
|
|
162
|
+
users = await client.users.list(where={"status": {"ne": "deleted"}}) # !=
|
|
163
|
+
users = await client.users.list(where={"email": {"contains": "@school.edu"}}) # substring
|
|
164
|
+
|
|
165
|
+
# Match any of multiple values (OR)
|
|
166
|
+
users = await client.users.list(
|
|
167
|
+
where={"role": {"in_": ["teacher", "aide"]}}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Exclude multiple values
|
|
171
|
+
users = await client.users.list(
|
|
172
|
+
where={"status": {"not_in": ["deleted", "inactive"]}}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Explicit OR across fields
|
|
176
|
+
users = await client.users.list(
|
|
177
|
+
where={"OR": [{"role": "teacher"}, {"status": "active"}]}
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Pagination
|
|
182
|
+
|
|
183
|
+
For large datasets, use streaming:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Collect all users
|
|
187
|
+
all_users = await client.users.stream().to_list()
|
|
188
|
+
|
|
189
|
+
# With limits
|
|
190
|
+
first_100 = await client.users.stream(max_items=100).to_list()
|
|
191
|
+
|
|
192
|
+
# With filtering
|
|
193
|
+
active_users = await client.users.stream(
|
|
194
|
+
where={"status": "active"}
|
|
195
|
+
).to_list()
|
|
196
|
+
|
|
197
|
+
# Get first item only
|
|
198
|
+
first_user = await client.users.stream().first()
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Configuration
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
OneRosterClient(
|
|
205
|
+
# Environment-based (recommended)
|
|
206
|
+
env="production", # or "staging"
|
|
207
|
+
client_id="...",
|
|
208
|
+
client_secret="...",
|
|
209
|
+
|
|
210
|
+
# Or explicit URLs
|
|
211
|
+
base_url="https://api.example.com",
|
|
212
|
+
auth_url="https://auth.example.com/oauth2/token",
|
|
213
|
+
client_id="...",
|
|
214
|
+
client_secret="...",
|
|
215
|
+
|
|
216
|
+
# Optional
|
|
217
|
+
timeout=30.0, # Request timeout in seconds
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Environment Variables
|
|
222
|
+
|
|
223
|
+
If credentials are not provided explicitly, the client reads from:
|
|
224
|
+
|
|
225
|
+
- `ONEROSTER_CLIENT_ID`
|
|
226
|
+
- `ONEROSTER_CLIENT_SECRET`
|
|
227
|
+
- `ONEROSTER_BASE_URL` (optional)
|
|
228
|
+
- `ONEROSTER_TOKEN_URL` (optional)
|
|
229
|
+
|
|
230
|
+
## Error Handling
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from timeback_oneroster import OneRosterError, NotFoundError, AuthenticationError
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
user = await client.users.get("invalid-id")
|
|
237
|
+
except NotFoundError as e:
|
|
238
|
+
print(f"User not found: {e.sourced_id}")
|
|
239
|
+
except AuthenticationError:
|
|
240
|
+
print("Invalid credentials")
|
|
241
|
+
except OneRosterError as e:
|
|
242
|
+
print(f"API error: {e}")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Async Context Manager
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
async with OneRosterClient(client_id="...", client_secret="...") as client:
|
|
249
|
+
schools = await client.schools.list()
|
|
250
|
+
# Client is automatically closed
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## FastAPI Integration
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from fastapi import FastAPI, Depends
|
|
257
|
+
from timeback_oneroster import OneRosterClient
|
|
258
|
+
|
|
259
|
+
app = FastAPI()
|
|
260
|
+
|
|
261
|
+
async def get_oneroster():
|
|
262
|
+
client = OneRosterClient(
|
|
263
|
+
env="production",
|
|
264
|
+
client_id="...",
|
|
265
|
+
client_secret="...",
|
|
266
|
+
)
|
|
267
|
+
try:
|
|
268
|
+
yield client
|
|
269
|
+
finally:
|
|
270
|
+
await client.close()
|
|
271
|
+
|
|
272
|
+
@app.get("/schools")
|
|
273
|
+
async def list_schools(client: OneRosterClient = Depends(get_oneroster)):
|
|
274
|
+
return await client.schools.list()
|
|
275
|
+
```
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# timeback-oneroster
|
|
2
|
+
|
|
3
|
+
Python client for the OneRoster v1.2 API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install timeback-oneroster
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from timeback_oneroster import OneRosterClient
|
|
15
|
+
|
|
16
|
+
async def main():
|
|
17
|
+
client = OneRosterClient(
|
|
18
|
+
env="staging", # or "production"
|
|
19
|
+
client_id="your-client-id",
|
|
20
|
+
client_secret="your-client-secret",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# List all schools
|
|
24
|
+
schools = await client.schools.list()
|
|
25
|
+
for school in schools:
|
|
26
|
+
print(school.name)
|
|
27
|
+
|
|
28
|
+
# Get a specific user
|
|
29
|
+
user = await client.users.get("user-sourced-id")
|
|
30
|
+
print(f"{user.given_name} {user.family_name}")
|
|
31
|
+
|
|
32
|
+
await client.close()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Client Structure
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
client = OneRosterClient(options)
|
|
39
|
+
|
|
40
|
+
# Rostering
|
|
41
|
+
client.users # All users
|
|
42
|
+
client.students # Students (filtered users)
|
|
43
|
+
client.teachers # Teachers (filtered users)
|
|
44
|
+
client.classes # Classes
|
|
45
|
+
client.schools # Schools
|
|
46
|
+
# client.courses # Coming soon
|
|
47
|
+
# client.enrollments # Coming soon
|
|
48
|
+
# client.terms # Coming soon
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Resource Operations
|
|
52
|
+
|
|
53
|
+
Each resource supports:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# List all items
|
|
57
|
+
users = await client.users.list()
|
|
58
|
+
|
|
59
|
+
# List with type-safe filtering (recommended)
|
|
60
|
+
active_teachers = await client.users.list(
|
|
61
|
+
where={"status": "active", "role": "teacher"}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# With operators
|
|
65
|
+
teachers_or_aides = await client.users.list(
|
|
66
|
+
where={"role": {"in_": ["teacher", "aide"]}}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Not equal
|
|
70
|
+
non_deleted = await client.users.list(
|
|
71
|
+
where={"status": {"ne": "deleted"}}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Sorting
|
|
75
|
+
sorted_users = await client.users.list(
|
|
76
|
+
where={"status": "active"},
|
|
77
|
+
sort="familyName",
|
|
78
|
+
order_by="asc",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Legacy filter string (still supported)
|
|
82
|
+
active_users = await client.users.list(filter="status='active'")
|
|
83
|
+
|
|
84
|
+
# Get by sourcedId
|
|
85
|
+
user = await client.users.get("user-id")
|
|
86
|
+
|
|
87
|
+
# Create (where supported)
|
|
88
|
+
await client.classes.create({
|
|
89
|
+
"title": "Math 101",
|
|
90
|
+
"course": {"sourcedId": "course-id"},
|
|
91
|
+
"school": {"sourcedId": "school-id"},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# Update (where supported)
|
|
95
|
+
await client.classes.update("class-id", {"title": "Math 102"})
|
|
96
|
+
|
|
97
|
+
# Delete (where supported)
|
|
98
|
+
await client.classes.delete("class-id")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Nested Resources
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
# Schools
|
|
105
|
+
classes = await client.schools("school-id").classes()
|
|
106
|
+
students = await client.schools("school-id").students()
|
|
107
|
+
teachers = await client.schools("school-id").teachers()
|
|
108
|
+
courses = await client.schools("school-id").courses()
|
|
109
|
+
|
|
110
|
+
# Classes
|
|
111
|
+
students = await client.classes("class-id").students()
|
|
112
|
+
teachers = await client.classes("class-id").teachers()
|
|
113
|
+
enrollments = await client.classes("class-id").enrollments()
|
|
114
|
+
|
|
115
|
+
# Enroll a student
|
|
116
|
+
await client.classes("class-id").enroll("student-id", role="student")
|
|
117
|
+
|
|
118
|
+
# Users
|
|
119
|
+
classes = await client.users("user-id").classes()
|
|
120
|
+
demographics = await client.users("user-id").demographics()
|
|
121
|
+
|
|
122
|
+
# Students / Teachers
|
|
123
|
+
classes = await client.students("student-id").classes()
|
|
124
|
+
classes = await client.teachers("teacher-id").classes()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Filtering
|
|
128
|
+
|
|
129
|
+
The client supports type-safe filtering with the `where` parameter:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# Simple equality
|
|
133
|
+
users = await client.users.list(where={"status": "active"})
|
|
134
|
+
|
|
135
|
+
# Multiple conditions (AND)
|
|
136
|
+
users = await client.users.list(
|
|
137
|
+
where={"status": "active", "role": "teacher"}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Operators
|
|
141
|
+
users = await client.users.list(where={"score": {"gte": 90}}) # >=
|
|
142
|
+
users = await client.users.list(where={"score": {"gt": 90}}) # >
|
|
143
|
+
users = await client.users.list(where={"score": {"lte": 90}}) # <=
|
|
144
|
+
users = await client.users.list(where={"score": {"lt": 90}}) # <
|
|
145
|
+
users = await client.users.list(where={"status": {"ne": "deleted"}}) # !=
|
|
146
|
+
users = await client.users.list(where={"email": {"contains": "@school.edu"}}) # substring
|
|
147
|
+
|
|
148
|
+
# Match any of multiple values (OR)
|
|
149
|
+
users = await client.users.list(
|
|
150
|
+
where={"role": {"in_": ["teacher", "aide"]}}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Exclude multiple values
|
|
154
|
+
users = await client.users.list(
|
|
155
|
+
where={"status": {"not_in": ["deleted", "inactive"]}}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Explicit OR across fields
|
|
159
|
+
users = await client.users.list(
|
|
160
|
+
where={"OR": [{"role": "teacher"}, {"status": "active"}]}
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Pagination
|
|
165
|
+
|
|
166
|
+
For large datasets, use streaming:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# Collect all users
|
|
170
|
+
all_users = await client.users.stream().to_list()
|
|
171
|
+
|
|
172
|
+
# With limits
|
|
173
|
+
first_100 = await client.users.stream(max_items=100).to_list()
|
|
174
|
+
|
|
175
|
+
# With filtering
|
|
176
|
+
active_users = await client.users.stream(
|
|
177
|
+
where={"status": "active"}
|
|
178
|
+
).to_list()
|
|
179
|
+
|
|
180
|
+
# Get first item only
|
|
181
|
+
first_user = await client.users.stream().first()
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Configuration
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
OneRosterClient(
|
|
188
|
+
# Environment-based (recommended)
|
|
189
|
+
env="production", # or "staging"
|
|
190
|
+
client_id="...",
|
|
191
|
+
client_secret="...",
|
|
192
|
+
|
|
193
|
+
# Or explicit URLs
|
|
194
|
+
base_url="https://api.example.com",
|
|
195
|
+
auth_url="https://auth.example.com/oauth2/token",
|
|
196
|
+
client_id="...",
|
|
197
|
+
client_secret="...",
|
|
198
|
+
|
|
199
|
+
# Optional
|
|
200
|
+
timeout=30.0, # Request timeout in seconds
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Environment Variables
|
|
205
|
+
|
|
206
|
+
If credentials are not provided explicitly, the client reads from:
|
|
207
|
+
|
|
208
|
+
- `ONEROSTER_CLIENT_ID`
|
|
209
|
+
- `ONEROSTER_CLIENT_SECRET`
|
|
210
|
+
- `ONEROSTER_BASE_URL` (optional)
|
|
211
|
+
- `ONEROSTER_TOKEN_URL` (optional)
|
|
212
|
+
|
|
213
|
+
## Error Handling
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from timeback_oneroster import OneRosterError, NotFoundError, AuthenticationError
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
user = await client.users.get("invalid-id")
|
|
220
|
+
except NotFoundError as e:
|
|
221
|
+
print(f"User not found: {e.sourced_id}")
|
|
222
|
+
except AuthenticationError:
|
|
223
|
+
print("Invalid credentials")
|
|
224
|
+
except OneRosterError as e:
|
|
225
|
+
print(f"API error: {e}")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Async Context Manager
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
async with OneRosterClient(client_id="...", client_secret="...") as client:
|
|
232
|
+
schools = await client.schools.list()
|
|
233
|
+
# Client is automatically closed
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## FastAPI Integration
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from fastapi import FastAPI, Depends
|
|
240
|
+
from timeback_oneroster import OneRosterClient
|
|
241
|
+
|
|
242
|
+
app = FastAPI()
|
|
243
|
+
|
|
244
|
+
async def get_oneroster():
|
|
245
|
+
client = OneRosterClient(
|
|
246
|
+
env="production",
|
|
247
|
+
client_id="...",
|
|
248
|
+
client_secret="...",
|
|
249
|
+
)
|
|
250
|
+
try:
|
|
251
|
+
yield client
|
|
252
|
+
finally:
|
|
253
|
+
await client.close()
|
|
254
|
+
|
|
255
|
+
@app.get("/schools")
|
|
256
|
+
async def list_schools(client: OneRosterClient = Depends(get_oneroster)):
|
|
257
|
+
return await client.schools.list()
|
|
258
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "timeback-oneroster"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Timeback OneRoster v1.2 client for rostering and gradebook APIs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "Timeback", email = "dev@timeback.dev" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Typing :: Typed",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"timeback-common>=0.1.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.uv.sources]
|
|
23
|
+
timeback-common = { workspace = true }
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/timeback_oneroster"]
|