{"id":51433165,"url":"https://github.com/tha-guy-nate/tha-edfi-runner","last_synced_at":"2026-07-05T05:03:45.624Z","repository":{"id":362784636,"uuid":"1260773301","full_name":"tha-guy-nate/tha-edfi-runner","owner":"tha-guy-nate","description":"A Tabular Helper API library that wraps the Ed-Fi ODS REST API with a typed, pipeline-friendly interface.","archived":false,"fork":false,"pushed_at":"2026-06-30T02:24:11.000Z","size":155,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-30T04:14:48.942Z","etag":null,"topics":["ed-fi","education","python","rest-api","tabular-helper"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tha-guy-nate.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T21:23:21.000Z","updated_at":"2026-06-30T02:24:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tha-guy-nate/tha-edfi-runner","commit_stats":null,"previous_names":["tha-guy-nate/tha-edfi-runner"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/tha-guy-nate/tha-edfi-runner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tha-guy-nate%2Ftha-edfi-runner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tha-guy-nate%2Ftha-edfi-runner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tha-guy-nate%2Ftha-edfi-runner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tha-guy-nate%2Ftha-edfi-runner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tha-guy-nate","download_url":"https://codeload.github.com/tha-guy-nate/tha-edfi-runner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tha-guy-nate%2Ftha-edfi-runner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35143815,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ed-fi","education","python","rest-api","tabular-helper"],"created_at":"2026-07-05T05:03:44.145Z","updated_at":"2026-07-05T05:03:45.614Z","avatar_url":"https://github.com/tha-guy-nate.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tha-edfi-runner\n\n[![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)\n[![codecov](https://codecov.io/gh/tha-guy-nate/tha-edfi-runner/graph/badge.svg)](https://codecov.io/gh/tha-guy-nate/tha-edfi-runner)\n[![PyPI](https://img.shields.io/pypi/v/tha-edfi-runner)](https://pypi.org/project/tha-edfi-runner/)\n[![Python](https://img.shields.io/pypi/pyversions/tha-edfi-runner)](https://pypi.org/project/tha-edfi-runner/)\n[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)\n[![wheel size](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fpypi.org%2Fpypi%2Ftha-edfi-runner%2Fjson\u0026label=wheel%20size\u0026query=%24.urls%5B0%5D.size\u0026suffix=%20B)](https://pypi.org/project/tha-edfi-runner/#files)\n\nA Tabular Helper API library that wraps the Ed-Fi ODS REST API with a typed, pipeline-friendly interface.\n\n## Install\n\n```bash\npip install tha-edfi-runner\n```\n\n## Quick start\n\n```python\nfrom tha_edfi_runner import ThaEdfiBase, ThaStudentAssessment\n\n# Fetch tokens for each district\nbase = ThaEdfiBase()\ntoken_rows = base.batch_fetch_tokens(\n    district_rows,\n    oauth_endpoint=\"oauth/token\",\n    account_col=\"District BK\",\n)\n\n# POST student assessment payloads\nrunner = ThaStudentAssessment(api_version=\"v3\")\nresults = runner.batch_post_payload(\n    rows,\n    payload_col=\"payload\",\n    key_col=\"Student Assessment BK\",\n    workers=4,\n    commit=True,\n)\n```\n\n## Auth\n\nThree auth modes are supported — pass exactly one set of credentials:\n\n```python\n# OAuth2 client credentials (fetches a token automatically)\nThaEdfiBase(base_url=\"...\", client_id=\"key\", client_secret=\"secret\")\n\n# Pre-fetched bearer token\nThaEdfiBase(base_url=\"...\", bearer_token=\"eyJ...\")\n\n# No auth (valid for ThaEdfiBase construction; _session() will raise)\nThaEdfiBase(base_url=\"...\")\n```\n\nBy default, `token_url` is derived as `{base_url}/oauth/token`. Override with `token_url=`.\n\n## URL structure\n\nEd-Fi ODS data endpoints follow the pattern:\n\n```\n{base_url}/data/{api_version}/ed-fi/{resource}\n```\n\nPass `api_version` once and the library handles the rest:\n\n```python\nrunner = ThaStudentAssessment(base_url=\"https://ods.example.com/api\", api_version=\"v3\")\n# data calls hit: https://ods.example.com/api/data/v3/ed-fi/studentAssessments\n# OAuth token:    https://ods.example.com/api/oauth/token\n```\n\n## Multi-district deployments\n\nWhen 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:\n\n```python\nrunner = ThaStudentAssessment(api_version=\"v3\")   # default version\nresults = runner.batch_get_all(\n    rows,\n    key_col=\"District BK\",\n    url_col=\"targetUrl\",          # per-row ODS base URL\n    token_col=\"EdFi Token\",       # per-row bearer token\n    api_version_col=\"apiVersion\", # per-row override (takes precedence over \"v3\")\n)\n```\n\n## Typical workflow\n\n```python\nfrom tha_edfi_runner import ThaEdfiBase, ThaStudentAssessment\nfrom tha_map_runner import ThaMap\n\n# 1. Fetch tokens for each district\nbase = ThaEdfiBase()\ntoken_rows = base.batch_fetch_tokens(\n    district_rows,\n    oauth_endpoint=\"oauth/token\",\n    account_col=\"District BK\",\n    workers=4,\n)\n\n# 2. Enrich district rows with tokens\nmapper = ThaMap()\nenriched = mapper.enrich_rows(\n    district_rows,\n    source=token_rows,\n    mapping={\"EdFi Token\": \"EdFi Token\", \"token_expires_at\": \"token_expires_at\"},\n    row_key=\"District BK\",\n    source_key=\"District BK\",\n)\n\n# 3. Fetch all assessments across districts\nrunner = ThaStudentAssessment(api_version=\"v3\")\nflat_assessments = runner.batch_get_all(\n    enriched,\n    key_col=\"District BK\",\n    workers=4,\n    show_progress=True,\n)\n\n# 4. Join back to original rows\nenriched = mapper.expand_rows(\n    district_rows,\n    source=flat_assessments,\n    mapping={\"Assessment ID\": \"id\", \"Score\": \"scoreResults.result\"},\n    row_key=\"District BK\",\n    source_key=\"District BK\",\n)\n```\n\n## API\n\n### `ThaEdfiBase`\n\n```python\nThaEdfiBase(\n    *,\n    base_url=\"\",\n    api_version=\"\",\n    client_id=None,\n    client_secret=None,\n    token_url=None,\n    bearer_token=None,\n)\n```\n\n#### `base.fetch_token()`\n\nFetch a token using this instance's OAuth2 credentials. Returns:\n\n```python\n{\"token\": \"eyJ...\", \"status\": None, \"message\": None}           # success\n{\"token\": None, \"status\": \"error\", \"message\": \"auth error: HTTP 401\"}  # failure\n```\n\n#### `base.batch_fetch_tokens()`\n\n```python\nbase.batch_fetch_tokens(\n    rows,\n    *,\n    oauth_endpoint,                    # e.g. \"oauth/token\"\n    account_col,                       # deduplication key\n    workers=1,\n    show_progress=False,\n    progress_desc=None,\n    skip_statuses=[\"error\", \"warning\"],\n    status_col=\"row status\",\n    url_col=\"targetUrl\",\n    key_col=\"oAuthKey\",\n    secret_col=\"oAuthSecret\",\n    token_col=\"EdFi Token\",\n    expires_col=\"token_expires_at\",\n) -\u003e list[dict]\n```\n\nReturns one record per unique `account_col` value with `token_col` and `expires_col` set. Results stored in `base.rows`.\n\n---\n\n### `ThaStudentAssessment`\n\nInherits `ThaEdfiBase`. Default endpoint: `ed-fi/studentAssessments`.\n\n#### Single methods\n\n```python\nrunner.post_payload(payload, *, key, endpoint=..., commit=False) -\u003e dict\n# {\"key\": ..., \"status\": None | \"error\" | \"dry_run\", \"message\": ...}\n\nrunner.get_by_id(resource_id, *, endpoint=...) -\u003e dict\n# {\"id\": ..., \"status\": None | \"error\", \"message\": ..., \"data\": dict | None}\n\nrunner.get_all(*, key, endpoint=..., params=None, limit=500, show_progress=False) -\u003e dict\n# {\"key\": ..., \"status\": None | \"error\", \"message\": ..., \"data\": [dict, ...]}\n\nrunner.delete_by_id(resource_id, *, key, endpoint=..., commit=False) -\u003e dict\n# {\"id\": ..., \"key\": ..., \"status\": \"deleted\" | \"error\" | \"dry_run\", \"message\": ...}\n```\n\nAll write methods require `commit=True` to execute — otherwise return `status=\"dry_run\"`.\n\n#### Batch methods\n\nAll batch methods share these common parameters:\n\n```python\nrows,\n*,\nendpoint=...,\nworkers=1,\nshow_progress=False,\nprogress_desc=None,\nskip_statuses=[\"error\", \"warning\"],\nstatus_col=\"row status\",\nurl_col=\"targetUrl\",\ntoken_col=\"EdFi Token\",\napi_version_col=None,           # column holding per-row ODS version (overrides runner api_version)\nauth_key_col=None,              # enable reactive re-auth on 401\nauth_secret_col=None,\noauth_endpoint=None,\nexpires_col=None,               # enable proactive re-auth before token expiry\n```\n\nMethod-specific required params:\n\n```python\nrunner.batch_post_payload(rows, *, payload_col, key_col, ..., commit=False) -\u003e list[dict]\nrunner.batch_get_by_id(rows, *, id_col, ...) -\u003e list[dict]\nrunner.batch_get_all(rows, *, key_col, ...) -\u003e list[dict]   # flat list; inject key_col into each record\nrunner.batch_delete_by_id(rows, *, id_col, key_col, ..., commit=False) -\u003e list[dict]\n```\n\nResults stored in `runner.rows`.\n\n### Re-auth\n\nProvide `auth_key_col`, `auth_secret_col`, and `oauth_endpoint` to enable automatic token refresh:\n\n- **Proactive**: if `expires_col` is set and the token has expired, a fresh token is fetched before the call\n- **Reactive**: if a call returns 401, the token is refreshed once and the call retried\n\n## Alternatives\n\n- [**requests**](https://docs.python-requests.org) — HTTP client underlying this library; use directly for full control\n- [**edfi-client**](https://pypi.org/project/edfi-client/) — Ed-Fi-specific client with broader resource coverage\n\nChoose 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).\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftha-guy-nate%2Ftha-edfi-runner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftha-guy-nate%2Ftha-edfi-runner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftha-guy-nate%2Ftha-edfi-runner/lists"}