tha-edfi-runner 0.1.2__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.
- tha_edfi_runner-0.1.2/.github/workflows/ci.yml +36 -0
- tha_edfi_runner-0.1.2/.github/workflows/publish.yml +62 -0
- tha_edfi_runner-0.1.2/.gitignore +32 -0
- tha_edfi_runner-0.1.2/PKG-INFO +278 -0
- tha_edfi_runner-0.1.2/README.md +250 -0
- tha_edfi_runner-0.1.2/pyproject.toml +56 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/__init__.py +12 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/auth.py +80 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/base.py +168 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/endpoints.py +33 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/errors.py +2 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/py.typed +0 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/resources/__init__.py +0 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/resources/student_assessment/__init__.py +0 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/resources/student_assessment/api.py +51 -0
- tha_edfi_runner-0.1.2/src/tha_edfi_runner/resources/student_assessment/runner.py +574 -0
- tha_edfi_runner-0.1.2/tests/conftest.py +27 -0
- tha_edfi_runner-0.1.2/tests/test_auth.py +81 -0
- tha_edfi_runner-0.1.2/tests/test_base.py +289 -0
- tha_edfi_runner-0.1.2/tests/test_student_assessment.py +865 -0
- tha_edfi_runner-0.1.2/uv.lock +649 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v4
|
|
20
|
+
with:
|
|
21
|
+
version: "latest"
|
|
22
|
+
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
run: uv python install ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --extra dev --python ${{ matrix.python-version }}
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run ruff check src/ tests/
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: uv run pytest
|
|
34
|
+
|
|
35
|
+
- name: Type check
|
|
36
|
+
run: uv run mypy src/
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- name: Install uv
|
|
15
|
+
uses: astral-sh/setup-uv@v4
|
|
16
|
+
|
|
17
|
+
- name: Build
|
|
18
|
+
run: uv build
|
|
19
|
+
|
|
20
|
+
- name: Upload dist
|
|
21
|
+
uses: actions/upload-artifact@v4
|
|
22
|
+
with:
|
|
23
|
+
name: dist
|
|
24
|
+
path: dist/
|
|
25
|
+
|
|
26
|
+
publish-testpypi:
|
|
27
|
+
needs: build
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
environment: testpypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write
|
|
32
|
+
steps:
|
|
33
|
+
- name: Download dist
|
|
34
|
+
uses: actions/download-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
|
|
39
|
+
- name: Install uv
|
|
40
|
+
uses: astral-sh/setup-uv@v4
|
|
41
|
+
|
|
42
|
+
- name: Publish to TestPyPI
|
|
43
|
+
run: uv publish --publish-url https://test.pypi.org/legacy/ --trusted-publishing always
|
|
44
|
+
|
|
45
|
+
publish-pypi:
|
|
46
|
+
needs: publish-testpypi
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
environment: pypi
|
|
49
|
+
permissions:
|
|
50
|
+
id-token: write
|
|
51
|
+
steps:
|
|
52
|
+
- name: Download dist
|
|
53
|
+
uses: actions/download-artifact@v4
|
|
54
|
+
with:
|
|
55
|
+
name: dist
|
|
56
|
+
path: dist/
|
|
57
|
+
|
|
58
|
+
- name: Install uv
|
|
59
|
+
uses: astral-sh/setup-uv@v4
|
|
60
|
+
|
|
61
|
+
- name: Publish to PyPI
|
|
62
|
+
run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
env/
|
|
17
|
+
|
|
18
|
+
# Tools
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.tox/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
|
25
|
+
|
|
26
|
+
# uv
|
|
27
|
+
.python-version
|
|
28
|
+
|
|
29
|
+
# IDEs
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tha-edfi-runner
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A Tabular Helper API library that wraps the Ed-Fi ODS REST API with a typed, pipeline-friendly interface.
|
|
5
|
+
Project-URL: Homepage, https://github.com/tha-guy-nate/tha-edfi-runner
|
|
6
|
+
Project-URL: Issues, https://github.com/tha-guy-nate/tha-edfi-runner/issues
|
|
7
|
+
Author: Nate Wright
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: api,ed-fi,edfi,education,helper,ods,tabular
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: tha-req-runner>=0.2.2
|
|
21
|
+
Requires-Dist: tqdm>=4.65
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
25
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# tha-edfi-runner
|
|
30
|
+
|
|
31
|
+
[](https://github.com/tha-guy-nate/tha-edfi-runner/actions/workflows/ci.yml)
|
|
32
|
+
|
|
33
|
+
A Tabular Helper API library that wraps the Ed-Fi ODS REST API with a typed, pipeline-friendly interface.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install tha-edfi-runner
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from tha_edfi_runner import ThaEdfiBase, ThaStudentAssessment
|
|
45
|
+
|
|
46
|
+
# Fetch tokens for each district
|
|
47
|
+
base = ThaEdfiBase()
|
|
48
|
+
token_rows = base.batch_fetch_tokens(
|
|
49
|
+
district_rows,
|
|
50
|
+
oauth_endpoint="oauth/token",
|
|
51
|
+
account_col="District BK",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# POST student assessment payloads
|
|
55
|
+
runner = ThaStudentAssessment(api_version="v3")
|
|
56
|
+
results = runner.batch_post_payload(
|
|
57
|
+
rows,
|
|
58
|
+
payload_col="payload",
|
|
59
|
+
key_col="Student Assessment BK",
|
|
60
|
+
workers=4,
|
|
61
|
+
commit=True,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Auth
|
|
66
|
+
|
|
67
|
+
Three auth modes are supported — pass exactly one set of credentials:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# OAuth2 client credentials (fetches a token automatically)
|
|
71
|
+
ThaEdfiBase(base_url="...", client_id="key", client_secret="secret")
|
|
72
|
+
|
|
73
|
+
# Pre-fetched bearer token
|
|
74
|
+
ThaEdfiBase(base_url="...", bearer_token="eyJ...")
|
|
75
|
+
|
|
76
|
+
# No auth (valid for ThaEdfiBase construction; _session() will raise)
|
|
77
|
+
ThaEdfiBase(base_url="...")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
By default, `token_url` is derived as `{base_url}/oauth/token`. Override with `token_url=`.
|
|
81
|
+
|
|
82
|
+
## URL structure
|
|
83
|
+
|
|
84
|
+
Ed-Fi ODS data endpoints follow the pattern:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
{base_url}/data/{api_version}/ed-fi/{resource}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pass `api_version` once and the library handles the rest:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
runner = ThaStudentAssessment(base_url="https://ods.example.com/api", api_version="v3")
|
|
94
|
+
# data calls hit: https://ods.example.com/api/data/v3/ed-fi/studentAssessments
|
|
95
|
+
# OAuth token: https://ods.example.com/api/oauth/token
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Multi-district deployments
|
|
99
|
+
|
|
100
|
+
When each row carries its own URL and token, pass `base_url` and `api_version` at the runner level as defaults, then override per-row via column names:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
runner = ThaStudentAssessment(api_version="v3") # default version
|
|
104
|
+
results = runner.batch_get_all(
|
|
105
|
+
rows,
|
|
106
|
+
key_col="District BK",
|
|
107
|
+
url_col="targetUrl", # per-row ODS base URL
|
|
108
|
+
token_col="EdFi Token", # per-row bearer token
|
|
109
|
+
api_version_col="apiVersion", # per-row override (takes precedence over "v3")
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Typical workflow
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from tha_edfi_runner import ThaEdfiBase, ThaStudentAssessment
|
|
117
|
+
from tha_map_runner import ThaMap
|
|
118
|
+
|
|
119
|
+
# 1. Fetch tokens for each district
|
|
120
|
+
base = ThaEdfiBase()
|
|
121
|
+
token_rows = base.batch_fetch_tokens(
|
|
122
|
+
district_rows,
|
|
123
|
+
oauth_endpoint="oauth/token",
|
|
124
|
+
account_col="District BK",
|
|
125
|
+
workers=4,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# 2. Enrich district rows with tokens
|
|
129
|
+
mapper = ThaMap()
|
|
130
|
+
enriched = mapper.enrich_rows(
|
|
131
|
+
district_rows,
|
|
132
|
+
source=token_rows,
|
|
133
|
+
mapping={"EdFi Token": "EdFi Token", "token_expires_at": "token_expires_at"},
|
|
134
|
+
row_key="District BK",
|
|
135
|
+
source_key="District BK",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# 3. Fetch all assessments across districts
|
|
139
|
+
runner = ThaStudentAssessment(api_version="v3")
|
|
140
|
+
flat_assessments = runner.batch_get_all(
|
|
141
|
+
enriched,
|
|
142
|
+
key_col="District BK",
|
|
143
|
+
workers=4,
|
|
144
|
+
show_progress=True,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 4. Join back to original rows
|
|
148
|
+
enriched = mapper.expand_rows(
|
|
149
|
+
district_rows,
|
|
150
|
+
source=flat_assessments,
|
|
151
|
+
mapping={"Assessment ID": "id", "Score": "scoreResults.result"},
|
|
152
|
+
row_key="District BK",
|
|
153
|
+
source_key="District BK",
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## API
|
|
158
|
+
|
|
159
|
+
### `ThaEdfiBase`
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
ThaEdfiBase(
|
|
163
|
+
*,
|
|
164
|
+
base_url="",
|
|
165
|
+
api_version="",
|
|
166
|
+
client_id=None,
|
|
167
|
+
client_secret=None,
|
|
168
|
+
token_url=None,
|
|
169
|
+
bearer_token=None,
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### `base.fetch_token()`
|
|
174
|
+
|
|
175
|
+
Fetch a token using this instance's OAuth2 credentials. Returns:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
{"token": "eyJ...", "status": None, "message": None} # success
|
|
179
|
+
{"token": None, "status": "error", "message": "auth error: HTTP 401"} # failure
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
#### `base.batch_fetch_tokens()`
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
base.batch_fetch_tokens(
|
|
186
|
+
rows,
|
|
187
|
+
*,
|
|
188
|
+
oauth_endpoint, # e.g. "oauth/token"
|
|
189
|
+
account_col, # deduplication key
|
|
190
|
+
workers=1,
|
|
191
|
+
show_progress=False,
|
|
192
|
+
progress_desc=None,
|
|
193
|
+
skip_statuses=["error", "warning"],
|
|
194
|
+
status_col="row status",
|
|
195
|
+
url_col="targetUrl",
|
|
196
|
+
key_col="oAuthKey",
|
|
197
|
+
secret_col="oAuthSecret",
|
|
198
|
+
token_col="EdFi Token",
|
|
199
|
+
expires_col="token_expires_at",
|
|
200
|
+
) -> list[dict]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Returns one record per unique `account_col` value with `token_col` and `expires_col` set. Results stored in `base.rows`.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### `ThaStudentAssessment`
|
|
208
|
+
|
|
209
|
+
Inherits `ThaEdfiBase`. Default endpoint: `ed-fi/studentAssessments`.
|
|
210
|
+
|
|
211
|
+
#### Single methods
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
runner.post_payload(payload, *, key, endpoint=..., commit=False) -> dict
|
|
215
|
+
# {"key": ..., "status": None | "error" | "dry_run", "message": ...}
|
|
216
|
+
|
|
217
|
+
runner.get_by_id(resource_id, *, endpoint=...) -> dict
|
|
218
|
+
# {"id": ..., "status": None | "error", "message": ..., "data": dict | None}
|
|
219
|
+
|
|
220
|
+
runner.get_all(*, key, endpoint=..., params=None, limit=500, show_progress=False) -> dict
|
|
221
|
+
# {"key": ..., "status": None | "error", "message": ..., "data": [dict, ...]}
|
|
222
|
+
|
|
223
|
+
runner.delete_by_id(resource_id, *, key, endpoint=..., commit=False) -> dict
|
|
224
|
+
# {"id": ..., "key": ..., "status": "deleted" | "error" | "dry_run", "message": ...}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
All write methods require `commit=True` to execute — otherwise return `status="dry_run"`.
|
|
228
|
+
|
|
229
|
+
#### Batch methods
|
|
230
|
+
|
|
231
|
+
All batch methods share these common parameters:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
rows,
|
|
235
|
+
*,
|
|
236
|
+
endpoint=...,
|
|
237
|
+
workers=1,
|
|
238
|
+
show_progress=False,
|
|
239
|
+
progress_desc=None,
|
|
240
|
+
skip_statuses=["error", "warning"],
|
|
241
|
+
status_col="row status",
|
|
242
|
+
url_col="targetUrl",
|
|
243
|
+
token_col="EdFi Token",
|
|
244
|
+
api_version_col=None, # column holding per-row ODS version (overrides runner api_version)
|
|
245
|
+
auth_key_col=None, # enable reactive re-auth on 401
|
|
246
|
+
auth_secret_col=None,
|
|
247
|
+
oauth_endpoint=None,
|
|
248
|
+
expires_col=None, # enable proactive re-auth before token expiry
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Method-specific required params:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
runner.batch_post_payload(rows, *, payload_col, key_col, ..., commit=False) -> list[dict]
|
|
255
|
+
runner.batch_get_by_id(rows, *, id_col, ...) -> list[dict]
|
|
256
|
+
runner.batch_get_all(rows, *, key_col, ...) -> list[dict] # flat list; inject key_col into each record
|
|
257
|
+
runner.batch_delete_by_id(rows, *, id_col, key_col, ..., commit=False) -> list[dict]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Results stored in `runner.rows`.
|
|
261
|
+
|
|
262
|
+
### Re-auth
|
|
263
|
+
|
|
264
|
+
Provide `auth_key_col`, `auth_secret_col`, and `oauth_endpoint` to enable automatic token refresh:
|
|
265
|
+
|
|
266
|
+
- **Proactive**: if `expires_col` is set and the token has expired, a fresh token is fetched before the call
|
|
267
|
+
- **Reactive**: if a call returns 401, the token is refreshed once and the call retried
|
|
268
|
+
|
|
269
|
+
## Alternatives
|
|
270
|
+
|
|
271
|
+
- [**requests**](https://docs.python-requests.org) — HTTP client underlying this library; use directly for full control
|
|
272
|
+
- [**edfi-client**](https://pypi.org/project/edfi-client/) — Ed-Fi-specific client with broader resource coverage
|
|
273
|
+
|
|
274
|
+
Choose this library when you're already working in the `tha-*` row-dict pipeline and want Ed-Fi batch operations to follow the same conventions (skip_statuses, commit flag, self.rows, per-row credentials).
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# tha-edfi-runner
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tha-guy-nate/tha-edfi-runner/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
A Tabular Helper API library that wraps the Ed-Fi ODS REST API with a typed, pipeline-friendly interface.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install tha-edfi-runner
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from tha_edfi_runner import ThaEdfiBase, ThaStudentAssessment
|
|
17
|
+
|
|
18
|
+
# Fetch tokens for each district
|
|
19
|
+
base = ThaEdfiBase()
|
|
20
|
+
token_rows = base.batch_fetch_tokens(
|
|
21
|
+
district_rows,
|
|
22
|
+
oauth_endpoint="oauth/token",
|
|
23
|
+
account_col="District BK",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# POST student assessment payloads
|
|
27
|
+
runner = ThaStudentAssessment(api_version="v3")
|
|
28
|
+
results = runner.batch_post_payload(
|
|
29
|
+
rows,
|
|
30
|
+
payload_col="payload",
|
|
31
|
+
key_col="Student Assessment BK",
|
|
32
|
+
workers=4,
|
|
33
|
+
commit=True,
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Auth
|
|
38
|
+
|
|
39
|
+
Three auth modes are supported — pass exactly one set of credentials:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
# OAuth2 client credentials (fetches a token automatically)
|
|
43
|
+
ThaEdfiBase(base_url="...", client_id="key", client_secret="secret")
|
|
44
|
+
|
|
45
|
+
# Pre-fetched bearer token
|
|
46
|
+
ThaEdfiBase(base_url="...", bearer_token="eyJ...")
|
|
47
|
+
|
|
48
|
+
# No auth (valid for ThaEdfiBase construction; _session() will raise)
|
|
49
|
+
ThaEdfiBase(base_url="...")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
By default, `token_url` is derived as `{base_url}/oauth/token`. Override with `token_url=`.
|
|
53
|
+
|
|
54
|
+
## URL structure
|
|
55
|
+
|
|
56
|
+
Ed-Fi ODS data endpoints follow the pattern:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
{base_url}/data/{api_version}/ed-fi/{resource}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Pass `api_version` once and the library handles the rest:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
runner = ThaStudentAssessment(base_url="https://ods.example.com/api", api_version="v3")
|
|
66
|
+
# data calls hit: https://ods.example.com/api/data/v3/ed-fi/studentAssessments
|
|
67
|
+
# OAuth token: https://ods.example.com/api/oauth/token
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Multi-district deployments
|
|
71
|
+
|
|
72
|
+
When each row carries its own URL and token, pass `base_url` and `api_version` at the runner level as defaults, then override per-row via column names:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
runner = ThaStudentAssessment(api_version="v3") # default version
|
|
76
|
+
results = runner.batch_get_all(
|
|
77
|
+
rows,
|
|
78
|
+
key_col="District BK",
|
|
79
|
+
url_col="targetUrl", # per-row ODS base URL
|
|
80
|
+
token_col="EdFi Token", # per-row bearer token
|
|
81
|
+
api_version_col="apiVersion", # per-row override (takes precedence over "v3")
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Typical workflow
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from tha_edfi_runner import ThaEdfiBase, ThaStudentAssessment
|
|
89
|
+
from tha_map_runner import ThaMap
|
|
90
|
+
|
|
91
|
+
# 1. Fetch tokens for each district
|
|
92
|
+
base = ThaEdfiBase()
|
|
93
|
+
token_rows = base.batch_fetch_tokens(
|
|
94
|
+
district_rows,
|
|
95
|
+
oauth_endpoint="oauth/token",
|
|
96
|
+
account_col="District BK",
|
|
97
|
+
workers=4,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# 2. Enrich district rows with tokens
|
|
101
|
+
mapper = ThaMap()
|
|
102
|
+
enriched = mapper.enrich_rows(
|
|
103
|
+
district_rows,
|
|
104
|
+
source=token_rows,
|
|
105
|
+
mapping={"EdFi Token": "EdFi Token", "token_expires_at": "token_expires_at"},
|
|
106
|
+
row_key="District BK",
|
|
107
|
+
source_key="District BK",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# 3. Fetch all assessments across districts
|
|
111
|
+
runner = ThaStudentAssessment(api_version="v3")
|
|
112
|
+
flat_assessments = runner.batch_get_all(
|
|
113
|
+
enriched,
|
|
114
|
+
key_col="District BK",
|
|
115
|
+
workers=4,
|
|
116
|
+
show_progress=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# 4. Join back to original rows
|
|
120
|
+
enriched = mapper.expand_rows(
|
|
121
|
+
district_rows,
|
|
122
|
+
source=flat_assessments,
|
|
123
|
+
mapping={"Assessment ID": "id", "Score": "scoreResults.result"},
|
|
124
|
+
row_key="District BK",
|
|
125
|
+
source_key="District BK",
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## API
|
|
130
|
+
|
|
131
|
+
### `ThaEdfiBase`
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
ThaEdfiBase(
|
|
135
|
+
*,
|
|
136
|
+
base_url="",
|
|
137
|
+
api_version="",
|
|
138
|
+
client_id=None,
|
|
139
|
+
client_secret=None,
|
|
140
|
+
token_url=None,
|
|
141
|
+
bearer_token=None,
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `base.fetch_token()`
|
|
146
|
+
|
|
147
|
+
Fetch a token using this instance's OAuth2 credentials. Returns:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
{"token": "eyJ...", "status": None, "message": None} # success
|
|
151
|
+
{"token": None, "status": "error", "message": "auth error: HTTP 401"} # failure
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `base.batch_fetch_tokens()`
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
base.batch_fetch_tokens(
|
|
158
|
+
rows,
|
|
159
|
+
*,
|
|
160
|
+
oauth_endpoint, # e.g. "oauth/token"
|
|
161
|
+
account_col, # deduplication key
|
|
162
|
+
workers=1,
|
|
163
|
+
show_progress=False,
|
|
164
|
+
progress_desc=None,
|
|
165
|
+
skip_statuses=["error", "warning"],
|
|
166
|
+
status_col="row status",
|
|
167
|
+
url_col="targetUrl",
|
|
168
|
+
key_col="oAuthKey",
|
|
169
|
+
secret_col="oAuthSecret",
|
|
170
|
+
token_col="EdFi Token",
|
|
171
|
+
expires_col="token_expires_at",
|
|
172
|
+
) -> list[dict]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Returns one record per unique `account_col` value with `token_col` and `expires_col` set. Results stored in `base.rows`.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `ThaStudentAssessment`
|
|
180
|
+
|
|
181
|
+
Inherits `ThaEdfiBase`. Default endpoint: `ed-fi/studentAssessments`.
|
|
182
|
+
|
|
183
|
+
#### Single methods
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
runner.post_payload(payload, *, key, endpoint=..., commit=False) -> dict
|
|
187
|
+
# {"key": ..., "status": None | "error" | "dry_run", "message": ...}
|
|
188
|
+
|
|
189
|
+
runner.get_by_id(resource_id, *, endpoint=...) -> dict
|
|
190
|
+
# {"id": ..., "status": None | "error", "message": ..., "data": dict | None}
|
|
191
|
+
|
|
192
|
+
runner.get_all(*, key, endpoint=..., params=None, limit=500, show_progress=False) -> dict
|
|
193
|
+
# {"key": ..., "status": None | "error", "message": ..., "data": [dict, ...]}
|
|
194
|
+
|
|
195
|
+
runner.delete_by_id(resource_id, *, key, endpoint=..., commit=False) -> dict
|
|
196
|
+
# {"id": ..., "key": ..., "status": "deleted" | "error" | "dry_run", "message": ...}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
All write methods require `commit=True` to execute — otherwise return `status="dry_run"`.
|
|
200
|
+
|
|
201
|
+
#### Batch methods
|
|
202
|
+
|
|
203
|
+
All batch methods share these common parameters:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
rows,
|
|
207
|
+
*,
|
|
208
|
+
endpoint=...,
|
|
209
|
+
workers=1,
|
|
210
|
+
show_progress=False,
|
|
211
|
+
progress_desc=None,
|
|
212
|
+
skip_statuses=["error", "warning"],
|
|
213
|
+
status_col="row status",
|
|
214
|
+
url_col="targetUrl",
|
|
215
|
+
token_col="EdFi Token",
|
|
216
|
+
api_version_col=None, # column holding per-row ODS version (overrides runner api_version)
|
|
217
|
+
auth_key_col=None, # enable reactive re-auth on 401
|
|
218
|
+
auth_secret_col=None,
|
|
219
|
+
oauth_endpoint=None,
|
|
220
|
+
expires_col=None, # enable proactive re-auth before token expiry
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Method-specific required params:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
runner.batch_post_payload(rows, *, payload_col, key_col, ..., commit=False) -> list[dict]
|
|
227
|
+
runner.batch_get_by_id(rows, *, id_col, ...) -> list[dict]
|
|
228
|
+
runner.batch_get_all(rows, *, key_col, ...) -> list[dict] # flat list; inject key_col into each record
|
|
229
|
+
runner.batch_delete_by_id(rows, *, id_col, key_col, ..., commit=False) -> list[dict]
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Results stored in `runner.rows`.
|
|
233
|
+
|
|
234
|
+
### Re-auth
|
|
235
|
+
|
|
236
|
+
Provide `auth_key_col`, `auth_secret_col`, and `oauth_endpoint` to enable automatic token refresh:
|
|
237
|
+
|
|
238
|
+
- **Proactive**: if `expires_col` is set and the token has expired, a fresh token is fetched before the call
|
|
239
|
+
- **Reactive**: if a call returns 401, the token is refreshed once and the call retried
|
|
240
|
+
|
|
241
|
+
## Alternatives
|
|
242
|
+
|
|
243
|
+
- [**requests**](https://docs.python-requests.org) — HTTP client underlying this library; use directly for full control
|
|
244
|
+
- [**edfi-client**](https://pypi.org/project/edfi-client/) — Ed-Fi-specific client with broader resource coverage
|
|
245
|
+
|
|
246
|
+
Choose this library when you're already working in the `tha-*` row-dict pipeline and want Ed-Fi batch operations to follow the same conventions (skip_statuses, commit flag, self.rows, per-row credentials).
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|