csiapps 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.
- csiapps-0.1.0/.claude/launch.json +17 -0
- csiapps-0.1.0/.github/workflows/ci.yml +25 -0
- csiapps-0.1.0/.github/workflows/docs.yml +19 -0
- csiapps-0.1.0/.gitignore +26 -0
- csiapps-0.1.0/LICENSE +21 -0
- csiapps-0.1.0/PKG-INFO +86 -0
- csiapps-0.1.0/PORTING_PLAN.md +184 -0
- csiapps-0.1.0/README.md +48 -0
- csiapps-0.1.0/docs/api.md +37 -0
- csiapps-0.1.0/docs/index.md +78 -0
- csiapps-0.1.0/docs/parity.md +85 -0
- csiapps-0.1.0/docs/rest-api.md +362 -0
- csiapps-0.1.0/docs/sandbox.md +735 -0
- csiapps-0.1.0/docs/shiny-apps.md +299 -0
- csiapps-0.1.0/examples/app.py +39 -0
- csiapps-0.1.0/examples/warehouse_ingest.py +37 -0
- csiapps-0.1.0/mkdocs.yml +58 -0
- csiapps-0.1.0/pyproject.toml +50 -0
- csiapps-0.1.0/src/csiapps/__init__.py +54 -0
- csiapps-0.1.0/src/csiapps/app.py +375 -0
- csiapps-0.1.0/src/csiapps/auth.py +194 -0
- csiapps-0.1.0/src/csiapps/client.py +521 -0
- csiapps-0.1.0/src/csiapps/config.py +164 -0
- csiapps-0.1.0/src/csiapps/sandbox.py +623 -0
- csiapps-0.1.0/tests/conftest.py +76 -0
- csiapps-0.1.0/tests/test_app.py +67 -0
- csiapps-0.1.0/tests/test_auth.py +67 -0
- csiapps-0.1.0/tests/test_client.py +197 -0
- csiapps-0.1.0/tests/test_config.py +60 -0
- csiapps-0.1.0/tests/test_sandbox.py +323 -0
- csiapps-0.1.0/tests/test_sandbox_registry.py +204 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.1",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"name": "example-app",
|
|
6
|
+
"runtimeExecutable": "uv",
|
|
7
|
+
"runtimeArgs": ["run", "shiny", "run", "--port", "8123", "examples/app.py"],
|
|
8
|
+
"port": 8123
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "docs",
|
|
12
|
+
"runtimeExecutable": "uv",
|
|
13
|
+
"runtimeArgs": ["run", "--group", "docs", "mkdocs", "serve", "-a", "localhost:8000"],
|
|
14
|
+
"port": 8000
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v3
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
run: uv python install ${{ matrix.python-version }}
|
|
20
|
+
- name: Install deps
|
|
21
|
+
run: uv sync --python ${{ matrix.python-version }}
|
|
22
|
+
- name: Lint
|
|
23
|
+
run: uv run ruff check .
|
|
24
|
+
- name: Test
|
|
25
|
+
run: uv run pytest
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
# Needed for mkdocs gh-deploy to push the built site to the gh-pages branch.
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v3
|
|
18
|
+
- name: Build and deploy docs
|
|
19
|
+
run: uv run --group docs mkdocs gh-deploy --force
|
csiapps-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
.env
|
|
13
|
+
|
|
14
|
+
# Docs / build output
|
|
15
|
+
site/
|
|
16
|
+
|
|
17
|
+
# Tooling caches
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
|
|
22
|
+
# uv
|
|
23
|
+
uv.lock
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
csiapps-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 csiapps authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
csiapps-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csiapps
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Helper functions and utilities for CSI data warehouse ingestion and Shiny web applications (Python port of the csiapps R package).
|
|
5
|
+
Project-URL: Homepage, https://github.com/CSIOntario/csiapps-py
|
|
6
|
+
Project-URL: Issues, https://github.com/CSIOntario/csiapps-py/issues
|
|
7
|
+
Author: Kyu Min Shim
|
|
8
|
+
Author-email: David Awosoga <dawosoga@csiontario.ca>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 csiapps authors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: csi,csiontario,oauth2,shiny,warehouse
|
|
32
|
+
Requires-Python: >=3.10
|
|
33
|
+
Requires-Dist: faker>=25
|
|
34
|
+
Requires-Dist: httpx>=0.27
|
|
35
|
+
Requires-Dist: jsonschema>=4.21
|
|
36
|
+
Requires-Dist: shiny>=1.0
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# csiapps (Python)
|
|
40
|
+
|
|
41
|
+
Python port of the CSIO [`csiapps`](https://github.com/CSIOntario/csiapps) R
|
|
42
|
+
package. Helper functions and utilities for CSI data warehouse ingestion and
|
|
43
|
+
[Shiny for Python](https://shiny.posit.co/py/) web applications.
|
|
44
|
+
|
|
45
|
+
Full feature parity with the R package: the API client (`make_request`,
|
|
46
|
+
`fetch_*`), the local sandbox (schema → ingest → retrieve, plus a dummy
|
|
47
|
+
registration registry), and the Shiny app wrappers (`ui_wrapper`,
|
|
48
|
+
`server_wrapper`). **Sandbox mode is on by default** so nothing hits production
|
|
49
|
+
by accident.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install csiapps
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import csiapps
|
|
61
|
+
|
|
62
|
+
csiapps.register_sandbox_schema("demo", {
|
|
63
|
+
"type": "object", "required": ["id"],
|
|
64
|
+
"properties": {"id": {"type": "string"}},
|
|
65
|
+
})
|
|
66
|
+
csiapps.make_request("api/warehouse/ingestion/primary/", method="POST",
|
|
67
|
+
body={"source": "demo", "records": [{"id": "a1"}], "subject_field": "id"})
|
|
68
|
+
page = csiapps.make_request("api/warehouse/data-records", query={"source_uuid": "demo"})
|
|
69
|
+
print(page["count"]) # 1
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See the [cross-language documentation](https://csiontario.github.io/csiapps-py/)
|
|
73
|
+
(R and Python side by side, plus a [parity checklist](https://csiontario.github.io/csiapps-py/parity/))
|
|
74
|
+
and the runnable [`examples/`](examples/) (`warehouse_ingest.py`, `app.py`).
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
Uses [uv](https://docs.astral.sh/uv/).
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv sync # install deps + dev tools
|
|
82
|
+
uv run pytest # run tests
|
|
83
|
+
uv run ruff check . # lint
|
|
84
|
+
uv run --group docs mkdocs serve # preview docs
|
|
85
|
+
uv build # build sdist + wheel
|
|
86
|
+
```
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# csiapps: R → Python porting plan
|
|
2
|
+
|
|
3
|
+
Port of the CSIO [`csiapps`](https://github.com/CSIOntario/csiapps) R package to
|
|
4
|
+
Python, with full feature parity. Target web framework for the app-wrapper
|
|
5
|
+
layer: **[Shiny for Python](https://shiny.posit.co/py/)** (chosen because it is a
|
|
6
|
+
near-1:1 match for the R Shiny wrapper).
|
|
7
|
+
|
|
8
|
+
## The package is three layers
|
|
9
|
+
|
|
10
|
+
| Layer | R source | Nature | Port difficulty |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| **API client** | `R/utils.R` | HTTP + OAuth2 PKCE + JSON, pure logic | Mechanical |
|
|
13
|
+
| **Sandbox** | `R/sandbox.R` | In-memory warehouse emulation, JSON-schema validation, dummy data registry | Mechanical (two dep swaps) |
|
|
14
|
+
| **App wrapper** | `R/shiny.R` | Shiny UI/server wrapper, navbar/footer chrome, in-app OAuth redirect | Mechanical *because* target is Shiny for Python |
|
|
15
|
+
|
|
16
|
+
## Repository decision
|
|
17
|
+
|
|
18
|
+
Separate repo (`csiapps-py`, publishing package `csiapps` to PyPI) rather than a
|
|
19
|
+
monorepo or mixing into the R repo. R packaging artifacts (`DESCRIPTION`,
|
|
20
|
+
`NAMESPACE`, `man/`, roxygen, pkgdown) don't coexist cleanly with
|
|
21
|
+
`pyproject.toml`/pytest/ruff. The two implementations stay in sync via the
|
|
22
|
+
**shared REST API contract + JSON schemas**, not shared code.
|
|
23
|
+
|
|
24
|
+
## Dependency mapping
|
|
25
|
+
|
|
26
|
+
Most R deps collapse to Python stdlib.
|
|
27
|
+
|
|
28
|
+
| R dependency | Used for | Python replacement |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `httr2` | HTTP requests, retries, pagination | `httpx` |
|
|
31
|
+
| `httr2` OAuth2 PKCE helpers | auth flow | **stdlib** `secrets` + `hashlib.sha256` + `base64.urlsafe_b64encode` |
|
|
32
|
+
| `jsonlite` | JSON parse/serialize | **stdlib** `json` |
|
|
33
|
+
| `openssl` | base64, `rand_bytes` | **stdlib** `base64`, `secrets` |
|
|
34
|
+
| `jsonvalidate` (Ajv) | sandbox schema validation | `jsonschema` |
|
|
35
|
+
| `babynames` | random dummy athlete names | `faker` |
|
|
36
|
+
| `shiny` / `shinyjs` | app wrapper | `shiny` (Shiny for Python); `shinyjs` → custom-message JS handler |
|
|
37
|
+
|
|
38
|
+
Runtime deps: `httpx`, `jsonschema`, `faker`, `shiny`. Everything else is stdlib.
|
|
39
|
+
|
|
40
|
+
## Function map (mirrors the R `NAMESPACE`)
|
|
41
|
+
|
|
42
|
+
All exported functions carry over; names stay `snake_case`.
|
|
43
|
+
|
|
44
|
+
**config.py** — `set_institute`, `is_sandbox_mode`. R uses a `package_state`
|
|
45
|
+
environment + `options(csiapps.sandbox=)` + `CSIAPPS_ENV`. Python: module-level
|
|
46
|
+
state object + `CSIAPPS_ENV` env var. Do **not** build a Config class — mirror
|
|
47
|
+
the R global.
|
|
48
|
+
|
|
49
|
+
**auth.py** — `check_secrets`, PKCE helpers (`pkce_state_encode/decode`,
|
|
50
|
+
base64url), `exchange_code_for_token`. All stdlib, ~30 lines of PKCE.
|
|
51
|
+
|
|
52
|
+
**client.py** — `make_request` (with `paginate`, `sandbox` dispatch),
|
|
53
|
+
`fetch_org_options`, `fetch_profiles` (auto-paginate), `fetch_profile`.
|
|
54
|
+
Two-tier token resolution `.current_token()`: per-session token first, then
|
|
55
|
+
`CSIAPPS_ACCESS_TOKEN` env var.
|
|
56
|
+
|
|
57
|
+
**sandbox.py** — `register_sandbox_schema`, `clear_sandbox`, `browse_sandbox`,
|
|
58
|
+
`create_sport_org`, `create_profile`, plus the internal request router
|
|
59
|
+
(`data-sources/{uuid}`, `ingestion/primary`, `data-records`) and
|
|
60
|
+
`sandbox_ingest`. Keep **read-time** subject resolution so athletes registered
|
|
61
|
+
after ingestion backfill.
|
|
62
|
+
|
|
63
|
+
**app.py** — `ui_wrapper`, `server_wrapper`, navbar/footer/CSS chrome, sandbox
|
|
64
|
+
banner, `/me` load, logout re-seed. (`global_wrapper` was ported then dropped in
|
|
65
|
+
the ponytail audit — a no-op in Python; module scope covers the R use case.)
|
|
66
|
+
|
|
67
|
+
### Shiny R → Shiny for Python
|
|
68
|
+
|
|
69
|
+
| R (Shiny) | Python (Shiny for Python) |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `reactiveVal(NULL)` | `reactive.value(None)` |
|
|
72
|
+
| `observe({...})` | `@reactive.effect` |
|
|
73
|
+
| `observeEvent(x(), {...})` | `@reactive.effect` + `@reactive.event(x)` |
|
|
74
|
+
| `renderUI` / `uiOutput` | `@render.ui` / `ui.output_ui` |
|
|
75
|
+
| `tags$div`, `HTML`, `tagList`, `fluidPage` | `ui.tags.div`, `ui.HTML`, `ui.TagList`, `ui.page_fluid` |
|
|
76
|
+
| `session$sendCustomMessage(...)` | `session.send_custom_message` |
|
|
77
|
+
| `getDefaultReactiveDomain()` | `shiny.session.get_current_session()` |
|
|
78
|
+
| `parseQueryString(session$clientData$url_search)` | `session.clientdata.url_search()` |
|
|
79
|
+
|
|
80
|
+
Two gotchas:
|
|
81
|
+
|
|
82
|
+
* **`shinyjs` has no Python port.** Only used for
|
|
83
|
+
`runjs("window.location.href = ...")`. Replace with the same custom-message
|
|
84
|
+
handler the redirect already uses — no dependency; `useShinyjs()` drops out.
|
|
85
|
+
* **Per-session token** (`session$userData$csiapps_token`): implemented as a
|
|
86
|
+
`WeakKeyDictionary` keyed on the Shiny session (`client.set_session_token`),
|
|
87
|
+
read back by `client.current_token()` via `shiny.session.get_current_session()`,
|
|
88
|
+
falling back to `CSIAPPS_ACCESS_TOKEN`. (A `contextvar`, tried in phase 3, does
|
|
89
|
+
**not** work: Shiny reactive contexts don't propagate it across effects.)
|
|
90
|
+
|
|
91
|
+
## Verification strategy
|
|
92
|
+
|
|
93
|
+
The R package's `testthat` suite is the spec. Port each test to `pytest`
|
|
94
|
+
alongside the module it covers:
|
|
95
|
+
|
|
96
|
+
| R test | Python |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `tests/testthat/test-sandbox.R` | `tests/test_sandbox.py` |
|
|
99
|
+
| `tests/testthat/test-sandbox-wrapper.R` | `tests/test_sandbox_wrapper.py` |
|
|
100
|
+
| `tests/testthat/test-sandbox-registry.R` | `tests/test_sandbox_registry.py` |
|
|
101
|
+
| `tests/testthat/test-sandbox-mode.R` | `tests/test_sandbox_mode.py` |
|
|
102
|
+
|
|
103
|
+
## Phases
|
|
104
|
+
|
|
105
|
+
- [x] **Phase 1 — Scaffold.** New repo, `pyproject.toml` (uv/hatchling), CI
|
|
106
|
+
(pytest + ruff), package skeleton, this plan.
|
|
107
|
+
- [x] **Phase 2 — config + auth.** `config.py` (set_institute, is_sandbox_mode,
|
|
108
|
+
set_sandbox_mode, URL helpers) + `auth.py` (PKCE stdlib, token exchange,
|
|
109
|
+
check_secrets). Ported the `is_sandbox_mode` portion of `test-sandbox-mode.R`
|
|
110
|
+
plus PKCE round-trip / token-exchange / check_secrets tests (16 passing).
|
|
111
|
+
- [x] **Phase 3 — client.** `client.py`: `make_request` (retry + pagination),
|
|
112
|
+
`fetch_org_options`, `fetch_profiles` (auto-paginate), `fetch_profile`, and
|
|
113
|
+
per-session `current_token()` (contextvar → `CSIAPPS_ACCESS_TOKEN`). Sandbox
|
|
114
|
+
branches delegate to `csiapps.sandbox` internals (lazy import) landing in
|
|
115
|
+
phase 4. HTTP paths tested with `respx` (27 passing).
|
|
116
|
+
|
|
117
|
+
> **Phase 4 must provide these in `sandbox.py`:** `_make_sandbox_request(...)`,
|
|
118
|
+
> `_sandbox_org_options()`, `_sandbox_profiles(sport_org_id)`,
|
|
119
|
+
> `_sandbox_profile(profile_id)` — the delegation targets `client.py` calls.
|
|
120
|
+
> `flatten_record` (R internal, unexported, uncalled within the package) was
|
|
121
|
+
> **skipped**; add it only if a consuming app actually needs it.
|
|
122
|
+
- [x] **Phase 4 — sandbox.** `sandbox.py`: request router, ingest + Draft-7
|
|
123
|
+
`jsonschema` validation, dummy sport-org/athlete registry (`faker` names),
|
|
124
|
+
read-time subject resolution, and the four `_sandbox_*` delegation functions
|
|
125
|
+
`client.py` calls. Ported `test-sandbox.R`, `test-sandbox-registry.R`, the
|
|
126
|
+
`make_request` routing + `fetch_*` sandbox cases (69 passing total). Validator
|
|
127
|
+
wording adapted Ajv→jsonschema ("too short" vs "fewer than 10 characters").
|
|
128
|
+
`flatten_record` and its test remain skipped (see phase 3 note).
|
|
129
|
+
- [x] **Phase 5 — app wrapper.** `app.py` (Shiny for Python): `ui_wrapper`
|
|
130
|
+
(navbar/footer/chrome, sandbox banner, auth-status), `server_wrapper` (OAuth2
|
|
131
|
+
PKCE login + simulated sandbox login, per-session token, `/me` load, logout),
|
|
132
|
+
`global_wrapper`. `shinyjs` dropped (replaced by a `csip_reset`
|
|
133
|
+
custom-message handler); message-sending effects are `async`. Ported the
|
|
134
|
+
`ui_wrapper`/`server_wrapper`/`global_wrapper` cases from
|
|
135
|
+
`test-sandbox-wrapper.R` (77 passing); the deep `testServer` reactive cases
|
|
136
|
+
have no simple Shiny-for-Python equivalent, so sandbox-seed + auth-status
|
|
137
|
+
logic is covered via extracted pure helpers, and a real `App` is smoke-built.
|
|
138
|
+
Phase 3's token mechanism was corrected here (contextvar → session
|
|
139
|
+
`WeakKeyDictionary`).
|
|
140
|
+
- [x] **Phase 6 — docs + packaging.** mkdocs-material + mkdocstrings site
|
|
141
|
+
(`mkdocs.yml`, `docs/`), runnable `examples/` (`warehouse_ingest.py`,
|
|
142
|
+
`app.py`), README quickstart. `uv build` produces sdist + wheel; wheel
|
|
143
|
+
verified to install and import in a clean venv.
|
|
144
|
+
**PyPI publish is intentionally NOT done here** — it is an outward-facing
|
|
145
|
+
release and requires credentials the assistant cannot handle. Publish with
|
|
146
|
+
`uv publish` (or `twine upload dist/*`) when ready.
|
|
147
|
+
|
|
148
|
+
Phases 1–4 are framework-independent and safe to build in any order after 1.
|
|
149
|
+
|
|
150
|
+
## Dashboard dogfooding findings (fixed)
|
|
151
|
+
|
|
152
|
+
Building a real Shiny for Python dashboard against the package surfaced three
|
|
153
|
+
issues from common workflows, all fixed:
|
|
154
|
+
|
|
155
|
+
1. **`fetch_org_options` shape** — returned R's `[{"label","value"}]`, which
|
|
156
|
+
crashed `ui.input_select` (`TypeError: unhashable type: 'dict'`). Now returns
|
|
157
|
+
a `{value: label}` dict that feeds `input_select(choices=...)` directly.
|
|
158
|
+
Deliberate divergence from R (whose shape suited `selectInput`).
|
|
159
|
+
2. **No path from nested results to a table** — `fetch_profiles` returns deeply
|
|
160
|
+
nested dicts; `render.DataGrid` rejected them ("Unsupported dataframe type").
|
|
161
|
+
Added `flatten_profile` producing scalar rows; docs show wrapping in a
|
|
162
|
+
pandas/polars frame (kept out of package deps).
|
|
163
|
+
3. **Fixed-bottom footer overlapped content** — the R wrapper's
|
|
164
|
+
`padding-bottom: 80px` was dropped in the port; restored in `ui_wrapper`.
|
|
165
|
+
|
|
166
|
+
## Ponytail audit cleanup
|
|
167
|
+
|
|
168
|
+
Repo-wide over-engineering pass, all applied: dropped `global_wrapper` (no-op in
|
|
169
|
+
Python) and `flatten_record` (no caller; only `flatten_profile` is used); deleted
|
|
170
|
+
`config.clear_token` (dead) and `tests/test_smoke.py` (redundant vs 78 real
|
|
171
|
+
tests); hoisted the duplicated `_message` helper into `config`; inlined the
|
|
172
|
+
`_rmtree` wrapper to `shutil.rmtree`. ~55 lines removed, no dep changes.
|
|
173
|
+
|
|
174
|
+
## Parity caveats carried over from R
|
|
175
|
+
|
|
176
|
+
* **Validation parity is approximate.** Sandbox ingestion validates against the
|
|
177
|
+
JSON Schema with `jsonschema` (Python) where R used Ajv via `jsonvalidate`.
|
|
178
|
+
Validators differ on edge cases (e.g. `format` keyword enforcement), and the
|
|
179
|
+
real server enforces things the schema can't (subject resolution against
|
|
180
|
+
registered profiles, duplicate/dataset handling, token permissions). A green
|
|
181
|
+
sandbox run is **necessary but not sufficient** for production acceptance.
|
|
182
|
+
* **Subject linkage** is emulated only for athletes registered with
|
|
183
|
+
`create_profile()`, resolved at read time; unresolved → `subject = None`
|
|
184
|
+
(production would reject).
|
csiapps-0.1.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# csiapps (Python)
|
|
2
|
+
|
|
3
|
+
Python port of the CSIO [`csiapps`](https://github.com/CSIOntario/csiapps) R
|
|
4
|
+
package. Helper functions and utilities for CSI data warehouse ingestion and
|
|
5
|
+
[Shiny for Python](https://shiny.posit.co/py/) web applications.
|
|
6
|
+
|
|
7
|
+
Full feature parity with the R package: the API client (`make_request`,
|
|
8
|
+
`fetch_*`), the local sandbox (schema → ingest → retrieve, plus a dummy
|
|
9
|
+
registration registry), and the Shiny app wrappers (`ui_wrapper`,
|
|
10
|
+
`server_wrapper`). **Sandbox mode is on by default** so nothing hits production
|
|
11
|
+
by accident.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install csiapps
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import csiapps
|
|
23
|
+
|
|
24
|
+
csiapps.register_sandbox_schema("demo", {
|
|
25
|
+
"type": "object", "required": ["id"],
|
|
26
|
+
"properties": {"id": {"type": "string"}},
|
|
27
|
+
})
|
|
28
|
+
csiapps.make_request("api/warehouse/ingestion/primary/", method="POST",
|
|
29
|
+
body={"source": "demo", "records": [{"id": "a1"}], "subject_field": "id"})
|
|
30
|
+
page = csiapps.make_request("api/warehouse/data-records", query={"source_uuid": "demo"})
|
|
31
|
+
print(page["count"]) # 1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See the [cross-language documentation](https://csiontario.github.io/csiapps-py/)
|
|
35
|
+
(R and Python side by side, plus a [parity checklist](https://csiontario.github.io/csiapps-py/parity/))
|
|
36
|
+
and the runnable [`examples/`](examples/) (`warehouse_ingest.py`, `app.py`).
|
|
37
|
+
|
|
38
|
+
## Development
|
|
39
|
+
|
|
40
|
+
Uses [uv](https://docs.astral.sh/uv/).
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync # install deps + dev tools
|
|
44
|
+
uv run pytest # run tests
|
|
45
|
+
uv run ruff check . # lint
|
|
46
|
+
uv run --group docs mkdocs serve # preview docs
|
|
47
|
+
uv build # build sdist + wheel
|
|
48
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python API reference
|
|
2
|
+
|
|
3
|
+
Everything exported from the top-level `csiapps` Python package. For the R
|
|
4
|
+
package, see the [R reference](https://csiontario.github.io/csiapps/reference/)
|
|
5
|
+
(pkgdown); for how the two map onto each other, see the
|
|
6
|
+
[parity checklist](parity.md).
|
|
7
|
+
|
|
8
|
+
## Configuration
|
|
9
|
+
|
|
10
|
+
::: csiapps.config.set_institute
|
|
11
|
+
::: csiapps.config.is_sandbox_mode
|
|
12
|
+
::: csiapps.config.set_sandbox_mode
|
|
13
|
+
|
|
14
|
+
## Authentication
|
|
15
|
+
|
|
16
|
+
::: csiapps.auth.check_secrets
|
|
17
|
+
|
|
18
|
+
## Client
|
|
19
|
+
|
|
20
|
+
::: csiapps.client.make_request
|
|
21
|
+
::: csiapps.client.fetch_org_options
|
|
22
|
+
::: csiapps.client.fetch_profiles
|
|
23
|
+
::: csiapps.client.fetch_profile
|
|
24
|
+
::: csiapps.client.flatten_profile
|
|
25
|
+
|
|
26
|
+
## Sandbox
|
|
27
|
+
|
|
28
|
+
::: csiapps.sandbox.register_sandbox_schema
|
|
29
|
+
::: csiapps.sandbox.create_sport_org
|
|
30
|
+
::: csiapps.sandbox.create_profile
|
|
31
|
+
::: csiapps.sandbox.clear_sandbox
|
|
32
|
+
::: csiapps.sandbox.browse_sandbox
|
|
33
|
+
|
|
34
|
+
## App wrappers
|
|
35
|
+
|
|
36
|
+
::: csiapps.app.ui_wrapper
|
|
37
|
+
::: csiapps.app.server_wrapper
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# csiapps
|
|
2
|
+
|
|
3
|
+
Helper functions and utilities for CSI data warehouse ingestion and
|
|
4
|
+
[Shiny](https://shiny.posit.co/) web applications. `csiapps` ships as **two
|
|
5
|
+
packages with full feature parity** — one for R, one for Python — so a team can
|
|
6
|
+
work in either language against the same CSIAPPS warehouse and registration API.
|
|
7
|
+
|
|
8
|
+
This site documents **both**: every tutorial shows the R and the Python code
|
|
9
|
+
side by side. Pick your language with the tabs and it stays selected across the
|
|
10
|
+
whole page.
|
|
11
|
+
|
|
12
|
+
The library has three layers:
|
|
13
|
+
|
|
14
|
+
- **API client** — `make_request`, `fetch_org_options`, `fetch_profiles`,
|
|
15
|
+
`fetch_profile`, with OAuth2 PKCE auth.
|
|
16
|
+
- **Sandbox** — a local, in-memory emulation of the warehouse and registration
|
|
17
|
+
API so you can develop the full schema → ingest → retrieve workflow with no
|
|
18
|
+
network and no credentials. **Sandbox mode is on by default.**
|
|
19
|
+
- **App wrapper** — `ui_wrapper` / `server_wrapper` add a consistent CSI
|
|
20
|
+
navbar/footer and handle authentication for a Shiny app.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
=== "R"
|
|
25
|
+
|
|
26
|
+
```r
|
|
27
|
+
# install.packages("remotes")
|
|
28
|
+
remotes::install_github("CSIOntario/csiapps")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
=== "Python"
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install csiapps
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Sandbox mode is the default
|
|
38
|
+
|
|
39
|
+
Every call routes to the local sandbox until you explicitly turn it off, so you
|
|
40
|
+
never hit the production warehouse by accident:
|
|
41
|
+
|
|
42
|
+
=== "R"
|
|
43
|
+
|
|
44
|
+
```r
|
|
45
|
+
library(csiapps)
|
|
46
|
+
|
|
47
|
+
is_sandbox_mode() # TRUE
|
|
48
|
+
|
|
49
|
+
# Turn it off for deployment:
|
|
50
|
+
options(csiapps.sandbox = FALSE) # or set CSIAPPS_ENV=production
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
=== "Python"
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import csiapps
|
|
57
|
+
|
|
58
|
+
csiapps.is_sandbox_mode() # True
|
|
59
|
+
|
|
60
|
+
# Turn it off for deployment:
|
|
61
|
+
csiapps.set_sandbox_mode(False) # or set CSIAPPS_ENV=production
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Where to go next
|
|
65
|
+
|
|
66
|
+
- **[Shiny apps](shiny-apps.md)** — wrap an app's UI and server with CSIAPPS
|
|
67
|
+
authentication and chrome.
|
|
68
|
+
- **[REST API](rest-api.md)** — `make_request` and the registration helpers.
|
|
69
|
+
- **[Sandbox mode](sandbox.md)** — the full local warehouse/registration
|
|
70
|
+
workflow.
|
|
71
|
+
- **[Parity checklist](parity.md)** — how each R function maps to its Python
|
|
72
|
+
equivalent.
|
|
73
|
+
|
|
74
|
+
## Function reference
|
|
75
|
+
|
|
76
|
+
- **Python:** [Python reference](api.md) (on this site).
|
|
77
|
+
- **R:** [R reference](https://csiontario.github.io/csiapps/reference/)
|
|
78
|
+
(pkgdown).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Parity checklist
|
|
2
|
+
|
|
3
|
+
This page is a translation guide for the operational parity between the R and
|
|
4
|
+
Python versions of `csiapps`. Each row is a capability; the R and Python columns
|
|
5
|
+
give the function(s) that provide it. An empty cell marks a capability that
|
|
6
|
+
exists in only one language (see the notes below for why).
|
|
7
|
+
|
|
8
|
+
The two packages have **full feature parity** — every capability is available in
|
|
9
|
+
both — with two exceptions that are intentional and explained under
|
|
10
|
+
[Intentional divergences](#intentional-divergences).
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
| Capability | R | Python |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Set the target institute | `set_institute()` | `set_institute()` |
|
|
17
|
+
| Check whether sandbox mode is on | `is_sandbox_mode()` | `is_sandbox_mode()` |
|
|
18
|
+
| Force sandbox mode on/off | `options(csiapps.sandbox = )` | `set_sandbox_mode()` |
|
|
19
|
+
| Check auth environment / secrets | `check_secrets()` | `check_secrets()` |
|
|
20
|
+
|
|
21
|
+
## REST API client
|
|
22
|
+
|
|
23
|
+
| Capability | R | Python |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| Authenticated request to any endpoint | `make_request()` | `make_request()` |
|
|
26
|
+
| Fetch organisation options | `fetch_org_options()` | `fetch_org_options()` |
|
|
27
|
+
| Fetch profiles (auto-paginating) | `fetch_profiles()` | `fetch_profiles()` |
|
|
28
|
+
| Fetch a single profile by id | `fetch_profile()` | `fetch_profile()` |
|
|
29
|
+
| Flatten a profile into a table row | | `flatten_profile()` |
|
|
30
|
+
|
|
31
|
+
## Sandbox
|
|
32
|
+
|
|
33
|
+
| Capability | R | Python |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Register a JSON schema for a source | `register_sandbox_schema()` | `register_sandbox_schema()` |
|
|
36
|
+
| Create a dummy sport organization | `create_sport_org()` | `create_sport_org()` |
|
|
37
|
+
| Create dummy athlete profiles | `create_profile()` | `create_profile()` |
|
|
38
|
+
| Clear sandbox state | `clear_sandbox()` | `clear_sandbox()` |
|
|
39
|
+
| Open the sandbox payload folder | `browse_sandbox()` | `browse_sandbox()` |
|
|
40
|
+
|
|
41
|
+
## Shiny app wrappers
|
|
42
|
+
|
|
43
|
+
| Capability | R | Python |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Wrap app UI (navbar/footer/auth status) | `ui_wrapper()` | `ui_wrapper()` |
|
|
46
|
+
| Wrap app server (OAuth2 / sandbox login) | `server_wrapper()` | `server_wrapper()` |
|
|
47
|
+
| Make globals visible to the server | `global_wrapper()` | |
|
|
48
|
+
|
|
49
|
+
## Intentional divergences
|
|
50
|
+
|
|
51
|
+
These are the only differences in the public surface, and each exists for a
|
|
52
|
+
concrete reason:
|
|
53
|
+
|
|
54
|
+
- **`global_wrapper()` is R-only.** It evaluates a code block in R's global
|
|
55
|
+
environment so top-level objects are reachable from the `server` function — an
|
|
56
|
+
R Shiny scoping concern. In Shiny for Python a module-level assignment is
|
|
57
|
+
already captured by the server closure, so no equivalent is needed.
|
|
58
|
+
- **`flatten_profile()` is Python-only.** Shiny for Python's `render.data_frame`
|
|
59
|
+
rejects the nested dicts `fetch_profiles()` returns, so this helper flattens
|
|
60
|
+
one profile into scalar fields for a table. R users flatten inline with
|
|
61
|
+
`data.frame()` / `do.call(rbind, ...)`, so no dedicated helper ships in R.
|
|
62
|
+
|
|
63
|
+
Beyond the public surface, a few behaviours differ by language idiom or as a
|
|
64
|
+
deliberate hardening — same capability, adapted shape:
|
|
65
|
+
|
|
66
|
+
- **Sandbox toggle mechanism.** R uses the `csiapps.sandbox` **option**
|
|
67
|
+
(`options(csiapps.sandbox = FALSE)`); Python has no options system, so it uses
|
|
68
|
+
the `set_sandbox_mode()` function. Both also respect `CSIAPPS_ENV=production`.
|
|
69
|
+
- **`fetch_org_options()` return shape.** R returns `label`/`value` pairs for
|
|
70
|
+
`selectInput()`; Python returns a `{value: label}` dict for
|
|
71
|
+
`ui.input_select(choices = )`. Same data, framework-native shape.
|
|
72
|
+
- **`fetch_profile()` argument order.** R is `fetch_profile(token, profile_id)`;
|
|
73
|
+
Python puts the required argument first: `fetch_profile(profile_id, token)`.
|
|
74
|
+
Equivalent when called by keyword.
|
|
75
|
+
- **Schema validator engine.** Sandbox ingestion validates with Ajv (via
|
|
76
|
+
`jsonvalidate`) in R and `jsonschema` (Draft 7) in Python. Error wording and
|
|
77
|
+
some `format`-keyword edge cases differ.
|
|
78
|
+
- **`fetch_profiles()` pagination guard.** The Python port adds a `max_pages`
|
|
79
|
+
bound with cycle detection and a truncation warning; the R version paginates
|
|
80
|
+
until the API stops returning a `next` link. Python is a hardened superset.
|
|
81
|
+
|
|
82
|
+
## Function reference
|
|
83
|
+
|
|
84
|
+
- **R:** [R reference](https://csiontario.github.io/csiapps/reference/) (pkgdown)
|
|
85
|
+
- **Python:** [Python reference](api.md) (on this site)
|