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.
@@ -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
+ [![CI](https://github.com/tha-guy-nate/tha-edfi-runner/actions/workflows/ci.yml/badge.svg)](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
+ [![CI](https://github.com/tha-guy-nate/tha-edfi-runner/actions/workflows/ci.yml/badge.svg)](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