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.
Files changed (50) hide show
  1. timeback_oneroster-0.1.0/.gitignore +48 -0
  2. timeback_oneroster-0.1.0/PKG-INFO +275 -0
  3. timeback_oneroster-0.1.0/README.md +258 -0
  4. timeback_oneroster-0.1.0/pyproject.toml +30 -0
  5. timeback_oneroster-0.1.0/pytest.ini +5 -0
  6. timeback_oneroster-0.1.0/src/timeback_oneroster/__init__.py +139 -0
  7. timeback_oneroster-0.1.0/src/timeback_oneroster/client.py +280 -0
  8. timeback_oneroster-0.1.0/src/timeback_oneroster/constants.py +11 -0
  9. timeback_oneroster-0.1.0/src/timeback_oneroster/exceptions.py +30 -0
  10. timeback_oneroster-0.1.0/src/timeback_oneroster/lib/__init__.py +23 -0
  11. timeback_oneroster-0.1.0/src/timeback_oneroster/lib/filter.py +21 -0
  12. timeback_oneroster-0.1.0/src/timeback_oneroster/lib/pagination.py +305 -0
  13. timeback_oneroster-0.1.0/src/timeback_oneroster/lib/transport.py +88 -0
  14. timeback_oneroster-0.1.0/src/timeback_oneroster/py.typed +1 -0
  15. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/__init__.py +87 -0
  16. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/assessment/__init__.py +12 -0
  17. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/assessment/line_items.py +116 -0
  18. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/assessment/results.py +84 -0
  19. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/base.py +477 -0
  20. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/__init__.py +19 -0
  21. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/categories.py +101 -0
  22. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/line_items.py +239 -0
  23. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/results.py +116 -0
  24. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/gradebook/score_scales.py +108 -0
  25. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/resources/__init__.py +12 -0
  26. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/resources/resources.py +134 -0
  27. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/__init__.py +63 -0
  28. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/academic_sessions.py +325 -0
  29. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/classes.py +997 -0
  30. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/courses.py +346 -0
  31. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/demographics.py +94 -0
  32. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/enrollments.py +108 -0
  33. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/orgs.py +95 -0
  34. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/schools.py +855 -0
  35. timeback_oneroster-0.1.0/src/timeback_oneroster/resources/rostering/users.py +593 -0
  36. timeback_oneroster-0.1.0/src/timeback_oneroster/types/__init__.py +108 -0
  37. timeback_oneroster-0.1.0/src/timeback_oneroster/types/assessment.py +75 -0
  38. timeback_oneroster-0.1.0/src/timeback_oneroster/types/base.py +55 -0
  39. timeback_oneroster-0.1.0/src/timeback_oneroster/types/gradebook.py +113 -0
  40. timeback_oneroster-0.1.0/src/timeback_oneroster/types/input.py +457 -0
  41. timeback_oneroster-0.1.0/src/timeback_oneroster/types/resources.py +108 -0
  42. timeback_oneroster-0.1.0/src/timeback_oneroster/types/rostering.py +233 -0
  43. timeback_oneroster-0.1.0/src/timeback_oneroster/utils.py +89 -0
  44. timeback_oneroster-0.1.0/tests/__init__.py +1 -0
  45. timeback_oneroster-0.1.0/tests/test_client.py +129 -0
  46. timeback_oneroster-0.1.0/tests/test_filter.py +211 -0
  47. timeback_oneroster-0.1.0/tests/test_pagination.py +97 -0
  48. timeback_oneroster-0.1.0/tests/test_resources.py +339 -0
  49. timeback_oneroster-0.1.0/tests/test_types.py +124 -0
  50. 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"]
@@ -0,0 +1,5 @@
1
+ [pytest]
2
+ asyncio_mode = auto
3
+ testpaths = tests
4
+ python_files = test_*.py
5
+ python_functions = test_*