ethoscopy 2.1.0__tar.gz → 2.2.1__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.
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/.github/workflows/release.yml +0 -12
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/CLAUDE.md +41 -27
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/.env.dummy.example +4 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/.env.github.example +4 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/.env.google.example +4 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/.env.keycloak.example +4 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/Dockerfile +19 -2
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/README_AUTH.md +28 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/config/jupyterhub_config.py +39 -5
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/docker-compose.yml +11 -1
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/PKG-INFO +3 -2
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/pyproject.toml +2 -1
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_core.py +12 -9
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_plotly.py +9 -2
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_seaborn.py +2 -3
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/load.py +276 -183
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_load_optimizations.py +8 -2
- ethoscopy-2.2.1/tests/test_load_wal.py +493 -0
- ethoscopy-2.1.0/.claude/settings.local.json +0 -48
- ethoscopy-2.1.0/.coverage +0 -0
- ethoscopy-2.1.0/Docker/jupyterhub_data/jupyterhub.sqlite +0 -0
- ethoscopy-2.1.0/Docker/jupyterhub_data/jupyterhub_cookie_secret +0 -1
- ethoscopy-2.1.0/tasks/todo.md +0 -130
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/.codecov.yml +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/.github/workflows/ci.yml +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/.gitignore +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/.pre-commit-config.yaml +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/.env.gitlab.example +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/.env.keycloak +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/README.md +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/config/users.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/install_r_packages.r +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/Docker/jupyterhub_data/jupyterhub_config.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/LICENSE +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/README.md +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/TESTING.md +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/_config.yml +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/pytest.ini +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/run_tests.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/scripts/README.md +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/scripts/convert_databases.sh +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/scripts/convert_wal_to_delete.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/scripts/publish_tutorials.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/setup.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/__init__.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/analyse.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_HMM_class.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_class.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_draw.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/behavpy_periodogram_class.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/metadata_db.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/__init__.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/circadian_bars.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/general_functions.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/get_HMM.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/get_tutorials.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/hmm_functions.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/periodogram_functions.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/src/ethoscopy/misc/validate_datetime.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/__init__.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/conftest.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/data/README.md +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/data/test_ethoscope.db +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_analyse.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_baseline_enhancements.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_behavpy.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_behavpy_core_simple.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_compatibility_classes.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_general_functions.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_get_tutorials.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_load.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_load_comprehensive.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tests/test_load_metadata_fixes.py +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/1_Overview_tutorial.ipynb +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/2_HMM_tutorial.ipynb +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/3_Circadian_tutorial.ipynb +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/4_Navigating_db_tutorial.ipynb +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/5_Ethoscopy_catch22_tutorial.ipynb +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/6_Ethoscopy_to_hctsa_tutorial.ipynb +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/ethoscope_db.csv +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/jones_et_al_metadata.csv +0 -0
- {ethoscopy-2.1.0 → ethoscopy-2.2.1}/tutorial_notebook/notebook_paper.ipynb +0 -0
|
@@ -3,9 +3,6 @@ name: Release to PyPI
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
-
push:
|
|
7
|
-
tags:
|
|
8
|
-
- 'v*.*.*'
|
|
9
6
|
|
|
10
7
|
jobs:
|
|
11
8
|
test:
|
|
@@ -58,16 +55,7 @@ jobs:
|
|
|
58
55
|
run: |
|
|
59
56
|
twine check dist/*
|
|
60
57
|
|
|
61
|
-
- name: Publish to Test PyPI
|
|
62
|
-
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
|
63
|
-
env:
|
|
64
|
-
TWINE_USERNAME: __token__
|
|
65
|
-
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
|
66
|
-
run: |
|
|
67
|
-
twine upload --repository testpypi dist/*
|
|
68
|
-
|
|
69
58
|
- name: Publish to PyPI
|
|
70
|
-
if: github.event_name == 'release'
|
|
71
59
|
env:
|
|
72
60
|
TWINE_USERNAME: __token__
|
|
73
61
|
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -36,48 +36,62 @@ pip install -e ".[dev]"
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
### Testing
|
|
39
|
-
- **
|
|
40
|
-
-
|
|
41
|
-
|
|
39
|
+
- **Pytest suite in `tests/`**: `python -m pytest tests/ -q` (~230 tests, seconds to run)
|
|
40
|
+
- **Caveat**: `pytest.ini` declares its section as `[tool:pytest]`, which is the
|
|
41
|
+
*setup.cfg* spelling — pytest silently ignores the file, so `addopts` (coverage,
|
|
42
|
+
`--strict-markers`) and the `markers` registrations do **not** apply. Renaming the
|
|
43
|
+
section to `[pytest]` would switch on `--cov-fail-under=70` and `--strict-markers`
|
|
44
|
+
for the whole suite; check it passes before doing so.
|
|
45
|
+
- Tutorial notebooks in `tutorial_notebook/` complement the suite for end-to-end checks
|
|
42
46
|
|
|
43
47
|
### Docker Environment
|
|
44
48
|
- **Docker available** for JupyterHub deployment
|
|
45
49
|
- Build: `JUPYTER_HUB_TAG=5.3.0 ETHOSCOPE_LAB_TAG=1.0 docker compose build`
|
|
46
50
|
- Run: `docker compose up -d` (from Docker/ directory)
|
|
47
51
|
|
|
48
|
-
###
|
|
52
|
+
### Opening databases: WAL mode and read-only mounts
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
Ethoscopes record in **WAL** (Write-Ahead Logging) mode and the results tree is
|
|
55
|
+
normally mounted read-only (`:ro` in Docker). That combination has broken loading
|
|
56
|
+
repeatedly — as `database disk image is malformed`, and as
|
|
57
|
+
`unable to open database file` on `SELECT * FROM ROI_MAP`.
|
|
51
58
|
|
|
52
|
-
**
|
|
59
|
+
**How `_connect_db()` handles it** (load.py)
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
python3 scripts/convert_wal_to_delete.py /mnt/ethoscope_data/results --verbose
|
|
61
|
+
ethoscopy never writes to these files, so the connection is *always* read-only —
|
|
62
|
+
this also stops a load from checkpointing or truncating raw data as a side effect.
|
|
63
|
+
It walks an ordered ladder of open modes, `_READ_STRATEGIES`, and **probes each
|
|
64
|
+
one with a real statement** before returning it:
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
| rung | reads | notes |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `mode=ro` | every recoverable state, incl. WAL with `-shm`, corrupt `-shm`, hot rollback journal | the only mode that sees data still in an uncheckpointed `-wal` |
|
|
69
|
+
| `immutable=1` | WAL database whose sidecars are absent on a read-only mount | ignores the `-wal`; warns via `_warn_if_wal_ignored()` if it would hide committed rows |
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
- The `_connect_db()` function in `load.py` now detects WAL mode automatically
|
|
66
|
-
- Uses `mode=ro&nolock=1` URI parameters for WAL databases on read-only mounts
|
|
67
|
-
- Includes retry logic in `read_single_roi_optimized()` for resilience
|
|
71
|
+
Two traps worth not rediscovering:
|
|
68
72
|
|
|
69
|
-
**
|
|
70
|
-
-
|
|
71
|
-
|
|
73
|
+
- **Never use `mode=ro&nolock=1`.** SQLite rejects it for *every* WAL database.
|
|
74
|
+
- **`sqlite3.connect()` never touches the file**, so a bad URI opens "successfully"
|
|
75
|
+
and only fails on the first query — past any `try/except` around the connect
|
|
76
|
+
call. That is why every rung is probed, and it is how the `nolock=1` bug survived
|
|
77
|
+
a fallback that looked like it covered the case.
|
|
78
|
+
- `immutable=1` will also "open" a **missing** path, *creating* an empty database
|
|
79
|
+
and reporting `no such table: ROI_MAP`. `_connect_db()` checks the file exists first.
|
|
72
80
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
`read_single_roi_optimized()` retries a failed read with `_connect_db(path,
|
|
82
|
+
degraded=True)`, which skips to the last rung — a retry with the default ladder
|
|
83
|
+
would just pick the same failing mode again.
|
|
84
|
+
|
|
85
|
+
**Optional**: convert databases to DELETE mode to avoid the question entirely
|
|
86
|
+
```bash
|
|
87
|
+
python3 scripts/convert_wal_to_delete.py /mnt/ethoscope_data/results --verbose
|
|
88
|
+
./scripts/convert_databases.sh /mnt/ethoscope_data/results # bash wrapper
|
|
79
89
|
```
|
|
80
90
|
|
|
91
|
+
**Tests**: `tests/test_load_wal.py` builds real SQLite files in each on-disk state
|
|
92
|
+
(read-only and writable directories) rather than mocking sqlite3 — the failures
|
|
93
|
+
being guarded against come from SQLite itself and only appear when a statement runs.
|
|
94
|
+
|
|
81
95
|
**See Also**: `Docker/README.md` for detailed database preparation instructions
|
|
82
96
|
|
|
83
97
|
## Architecture and Code Structure
|
|
@@ -16,6 +16,10 @@ ALLOWED_USERS=amadabhushi,ggilestro,mjoyce,lguo,labguest1,labguest2,labguest3,la
|
|
|
16
16
|
# Comma-separated list of admin users
|
|
17
17
|
ADMIN_USERS=ggilestro
|
|
18
18
|
|
|
19
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
20
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
21
|
+
SUPERUSERS=ggilestro
|
|
22
|
+
|
|
19
23
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
20
24
|
UID=1000
|
|
21
25
|
GID=1000
|
|
@@ -30,6 +30,10 @@ ALLOWED_USERS=username1,username2,username3
|
|
|
30
30
|
# Comma-separated list of admin users (GitHub usernames)
|
|
31
31
|
ADMIN_USERS=your-github-username
|
|
32
32
|
|
|
33
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
34
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
35
|
+
SUPERUSERS=your-github-username
|
|
36
|
+
|
|
33
37
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
34
38
|
UID=1000
|
|
35
39
|
GID=1000
|
|
@@ -32,6 +32,10 @@ ALLOWED_USERS=user1,user2,user3
|
|
|
32
32
|
# Comma-separated list of admin users
|
|
33
33
|
ADMIN_USERS=your-email-username
|
|
34
34
|
|
|
35
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
36
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
37
|
+
SUPERUSERS=your-email-username
|
|
38
|
+
|
|
35
39
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
36
40
|
UID=1000
|
|
37
41
|
GID=1000
|
|
@@ -44,6 +44,10 @@ ALLOWED_USERS=amadabhushi,ggilestro,mjoyce,lguo,labguest1,labguest2,labguest3,la
|
|
|
44
44
|
# Comma-separated list of admin users
|
|
45
45
|
ADMIN_USERS=ggilestro
|
|
46
46
|
|
|
47
|
+
# Superusers see /home as their JupyterLab root and can browse/open/run
|
|
48
|
+
# notebooks across every user. Defaults to ADMIN_USERS if unset.
|
|
49
|
+
SUPERUSERS=ggilestro
|
|
50
|
+
|
|
47
51
|
# Docker user/group (optional, defaults to 1000:1000)
|
|
48
52
|
UID=1000
|
|
49
53
|
GID=1000
|
|
@@ -66,7 +66,7 @@ RUN ln -s /bin/tar /bin/gtar && \
|
|
|
66
66
|
# JUPYTER INSTALLATION AND CONFIGURATION
|
|
67
67
|
# ============================================================================
|
|
68
68
|
|
|
69
|
-
# Install JupyterLab (
|
|
69
|
+
# Install JupyterLab (latest 4.x; pip resolves the newest compatible release)
|
|
70
70
|
RUN pip3 install jupyterlab --ignore-installed
|
|
71
71
|
|
|
72
72
|
# To upgrade to version 4.0.2 (commented out)
|
|
@@ -75,6 +75,14 @@ RUN pip3 install jupyterlab --ignore-installed
|
|
|
75
75
|
# Install Jupyter extensions (compatible with 3.x only)
|
|
76
76
|
RUN pip3 install jupyterlab-spreadsheet-editor
|
|
77
77
|
|
|
78
|
+
# AI assistant: Copilot-style inline completions + chat panel.
|
|
79
|
+
# Reason: lightweight (declares only jupyter-server as its Jupyter dep), so it
|
|
80
|
+
# does NOT pull in jupyter-collaboration / server-documents like jupyter-ai does
|
|
81
|
+
# — that RTC stack is what caused "File ID could not be retrieved" on file open.
|
|
82
|
+
# Pinned for reproducible builds. Supports OpenRouter via its OpenAI-compatible
|
|
83
|
+
# provider (base URL https://openrouter.ai/api/v1).
|
|
84
|
+
RUN pip3 install --no-cache-dir "notebook-intelligence==5.1.0"
|
|
85
|
+
|
|
78
86
|
# ============================================================================
|
|
79
87
|
# PYTHON PACKAGES AND ETHOSCOPE SETUP
|
|
80
88
|
# ============================================================================
|
|
@@ -87,9 +95,18 @@ RUN pip3 install \
|
|
|
87
95
|
seaborn \
|
|
88
96
|
bokeh \
|
|
89
97
|
lifelines \
|
|
98
|
+
ipywidgets \
|
|
99
|
+
ipympl \
|
|
90
100
|
opencv-python \
|
|
91
101
|
mysql-connector \
|
|
92
|
-
|
|
102
|
+
scikit-learn \
|
|
103
|
+
statsmodels \
|
|
104
|
+
pingouin \
|
|
105
|
+
openpyxl \
|
|
106
|
+
pyarrow \
|
|
107
|
+
tqdm \
|
|
108
|
+
jupyterlab-git \
|
|
109
|
+
ethoscopy==2.2.0
|
|
93
110
|
# pycatch22
|
|
94
111
|
|
|
95
112
|
# Pre-populate tutorial datasets inside the installed package so non-root
|
|
@@ -15,6 +15,7 @@ This JupyterHub deployment supports multiple authentication methods, configurabl
|
|
|
15
15
|
- [Adding New Users (OAuth)](#adding-new-users-oauth)
|
|
16
16
|
- [Adding New Users (Dummy Auth)](#adding-new-users-dummy-auth)
|
|
17
17
|
- [Making Users Admins](#making-users-admins)
|
|
18
|
+
- [Granting Cross-User File Access (Superusers)](#granting-cross-user-file-access-superusers)
|
|
18
19
|
- [Switching Authentication Methods](#switching-authentication-methods)
|
|
19
20
|
- [Dockerfile Optimization](#dockerfile-optimization)
|
|
20
21
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -388,6 +389,33 @@ docker compose restart
|
|
|
388
389
|
- Navigate to: `https://jupyter.lab.gilest.ro/hub/admin`
|
|
389
390
|
- You should see the admin panel with all users listed
|
|
390
391
|
|
|
392
|
+
### Granting Cross-User File Access (Superusers)
|
|
393
|
+
|
|
394
|
+
By default JupyterLab roots each user's file browser at their own home (`/home/<username>`). A **superuser** instead gets `/home` as their root, so they can browse, open, and run notebooks across every user's directory directly from their own JupyterLab session.
|
|
395
|
+
|
|
396
|
+
This works because all single-user servers in this deployment run under the same container UID (see `ConfigUserSpawner` in `config/jupyterhub_config.py`), so filesystem-level access to all homes already exists — `SUPERUSERS` simply surfaces it in the UI.
|
|
397
|
+
|
|
398
|
+
**Configuration**:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
# .env
|
|
402
|
+
SUPERUSERS=ggilestro,jdoe
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
If `SUPERUSERS` is unset, it defaults to `ADMIN_USERS`.
|
|
406
|
+
|
|
407
|
+
**Apply changes**:
|
|
408
|
+
```bash
|
|
409
|
+
docker compose restart
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Verify**:
|
|
413
|
+
- Log in as a superuser
|
|
414
|
+
- The JupyterLab file browser should list every user's home folder
|
|
415
|
+
- Opening another user's `.ipynb` runs it in place; edits and kernel output write back to that user's home
|
|
416
|
+
|
|
417
|
+
**Security note**: Regular users are not isolated from each other at the filesystem level in this setup — anyone with a JupyterLab terminal can already `cd` into another user's home. `SUPERUSERS` only changes what is shown by default in the file browser. True per-user isolation would require a different spawner (e.g. `DockerSpawner` or `KubeSpawner` with distinct UIDs).
|
|
418
|
+
|
|
391
419
|
---
|
|
392
420
|
|
|
393
421
|
## Dockerfile Optimization
|
|
@@ -120,6 +120,17 @@ else:
|
|
|
120
120
|
# Use imported list from users.py
|
|
121
121
|
c.Authenticator.admin_users = ADMIN_USERS
|
|
122
122
|
|
|
123
|
+
# Superusers: users whose JupyterLab file browser is rooted at /home instead
|
|
124
|
+
# of their own home, so they can browse/open/run notebooks across every user.
|
|
125
|
+
# Reason: all single-user servers run as the same container UID (see
|
|
126
|
+
# ConfigUserSpawner below), so filesystem access already exists — this just
|
|
127
|
+
# exposes it in the UI. Defaults to admins; override with SUPERUSERS env var.
|
|
128
|
+
superusers_env = os.getenv('SUPERUSERS')
|
|
129
|
+
if superusers_env is not None:
|
|
130
|
+
SUPERUSERS = {u.strip() for u in superusers_env.split(',') if u.strip()}
|
|
131
|
+
else:
|
|
132
|
+
SUPERUSERS = set(c.Authenticator.admin_users)
|
|
133
|
+
|
|
123
134
|
# ============================================================================
|
|
124
135
|
# CUSTOM SPAWNER (Preserved - critical for current setup)
|
|
125
136
|
# ============================================================================
|
|
@@ -129,25 +140,48 @@ class ConfigUserSpawner(LocalProcessSpawner):
|
|
|
129
140
|
def make_preexec_fn(self, name):
|
|
130
141
|
"""Don't try to switch users - run everything as current user"""
|
|
131
142
|
return None
|
|
132
|
-
|
|
143
|
+
|
|
133
144
|
def user_env(self, env):
|
|
134
145
|
"""Set user environment without system user lookup"""
|
|
135
146
|
env = env.copy()
|
|
136
147
|
home_dir = f'/home/{self.user.name}'
|
|
137
|
-
|
|
148
|
+
|
|
138
149
|
# Ensure home directory exists with proper permissions
|
|
139
150
|
os.makedirs(home_dir, mode=0o755, exist_ok=True)
|
|
140
|
-
|
|
151
|
+
|
|
141
152
|
# Set environment variables
|
|
142
153
|
env['USER'] = self.user.name
|
|
143
154
|
env['HOME'] = home_dir
|
|
144
155
|
env['SHELL'] = '/bin/bash'
|
|
145
156
|
env['LOGNAME'] = self.user.name
|
|
146
|
-
|
|
157
|
+
|
|
158
|
+
# Notebook Intelligence (AI assistant): forward central LLM config from
|
|
159
|
+
# the container environment so students never enter API keys themselves
|
|
160
|
+
# (avoids keys leaking into dotfiles, cf. the pxin OpenAI-key incident).
|
|
161
|
+
# LiteLLM reads OPENROUTER_API_KEY directly and routes the
|
|
162
|
+
# `openrouter/...` model id; values come from .env via docker-compose.
|
|
163
|
+
for _key in (
|
|
164
|
+
'OPENROUTER_API_KEY',
|
|
165
|
+
'NBI_CHAT_MODEL_PROVIDER', 'NBI_CHAT_MODEL_ID',
|
|
166
|
+
'NBI_INLINE_COMPLETION_MODEL_PROVIDER', 'NBI_INLINE_COMPLETION_MODEL_ID',
|
|
167
|
+
):
|
|
168
|
+
_val = os.getenv(_key)
|
|
169
|
+
if _val:
|
|
170
|
+
env[_key] = _val
|
|
171
|
+
|
|
147
172
|
return env
|
|
148
173
|
|
|
149
174
|
c.JupyterHub.spawner_class = ConfigUserSpawner
|
|
150
|
-
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _set_notebook_dir(spawner):
|
|
178
|
+
if spawner.user.name in SUPERUSERS:
|
|
179
|
+
spawner.notebook_dir = '/home'
|
|
180
|
+
else:
|
|
181
|
+
spawner.notebook_dir = f'/home/{spawner.user.name}'
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
c.Spawner.pre_spawn_hook = _set_notebook_dir
|
|
151
185
|
|
|
152
186
|
# Timeouts
|
|
153
187
|
c.Spawner.http_timeout = 60
|
|
@@ -6,13 +6,14 @@ services:
|
|
|
6
6
|
dockerfile: Dockerfile
|
|
7
7
|
args:
|
|
8
8
|
JUPYTER_HUB_TAG: ${JUPYTER_HUB_TAG:-5.4.0}
|
|
9
|
-
image: ggilestro/ethoscope-lab:${ETHOSCOPE_LAB_TAG:-1.
|
|
9
|
+
image: ggilestro/ethoscope-lab:${ETHOSCOPE_LAB_TAG:-1.3}
|
|
10
10
|
container_name: ethoscope-lab
|
|
11
11
|
ports:
|
|
12
12
|
- 8082:8000
|
|
13
13
|
volumes:
|
|
14
14
|
- /mnt/ethoscope_data/results:/mnt/ethoscope_results:ro
|
|
15
15
|
- /mnt/ethoscope_data/ethoscope_metadata:/opt/ethoscope_metadata
|
|
16
|
+
- /mnt/data/projects:/mnt/data/projects:ro
|
|
16
17
|
- /mnt/homes:/home
|
|
17
18
|
- /mnt/cache:/home/cache
|
|
18
19
|
- ./jupyterhub_data:/srv/jupyterhub/
|
|
@@ -34,6 +35,7 @@ services:
|
|
|
34
35
|
- DUMMY_PASSWORD=${DUMMY_PASSWORD:-ethoscope}
|
|
35
36
|
- ALLOWED_USERS=${ALLOWED_USERS}
|
|
36
37
|
- ADMIN_USERS=${ADMIN_USERS}
|
|
38
|
+
- SUPERUSERS=${SUPERUSERS}
|
|
37
39
|
# OAuth settings (only needed when using OAuth authenticators)
|
|
38
40
|
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
|
|
39
41
|
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
|
|
@@ -47,6 +49,14 @@ services:
|
|
|
47
49
|
- GITHUB_ORG=${GITHUB_ORG}
|
|
48
50
|
- GOOGLE_HOSTED_DOMAIN=${GOOGLE_HOSTED_DOMAIN}
|
|
49
51
|
- GITLAB_URL=${GITLAB_URL}
|
|
52
|
+
# Notebook Intelligence (AI assistant) — central OpenRouter config.
|
|
53
|
+
# Put OPENROUTER_API_KEY in .env (gitignored). Models are overridable;
|
|
54
|
+
# verify/adjust slugs at https://openrouter.ai/models
|
|
55
|
+
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
|
56
|
+
- NBI_CHAT_MODEL_PROVIDER=${NBI_CHAT_MODEL_PROVIDER:-litellm-compatible}
|
|
57
|
+
- NBI_CHAT_MODEL_ID=${NBI_CHAT_MODEL_ID:-openrouter/anthropic/claude-sonnet-4.5}
|
|
58
|
+
- NBI_INLINE_COMPLETION_MODEL_PROVIDER=${NBI_INLINE_COMPLETION_MODEL_PROVIDER:-litellm-compatible}
|
|
59
|
+
- NBI_INLINE_COMPLETION_MODEL_ID=${NBI_INLINE_COMPLETION_MODEL_ID:-openrouter/openai/gpt-4o-mini}
|
|
50
60
|
|
|
51
61
|
user: "${UID:-1000}:${GID:-1000}" # Adjust to your own user. Make sure this matches the permissions of your /mnt/{homes,cache} folder
|
|
52
62
|
restart: always
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ethoscopy
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: "A python based toolkit to download and anlyse data from the Ethoscope hardware system."
|
|
5
5
|
Author-email: Lblackhurst29 <lblackhurst29@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -23,6 +23,7 @@ Requires-Dist: plotly<6.0.0,>=5.22.0
|
|
|
23
23
|
Requires-Dist: pywavelets<2.0.0,>=1.6.0
|
|
24
24
|
Requires-Dist: seaborn<0.14.0,>=0.13.2
|
|
25
25
|
Requires-Dist: tabulate<0.10.0,>=0.9.0
|
|
26
|
+
Requires-Dist: tqdm<5.0,>=4.66
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: black<25.0.0,>=24.0.0; extra == 'dev'
|
|
28
29
|
Requires-Dist: ipykernel<7.0.0,>=6.29.5; extra == 'dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ethoscopy"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.2.1"
|
|
4
4
|
description = "\"A python based toolkit to download and anlyse data from the Ethoscope hardware system.\""
|
|
5
5
|
authors = [{name = "Lblackhurst29",email = "lblackhurst29@gmail.com"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -15,6 +15,7 @@ dependencies = [
|
|
|
15
15
|
"pywavelets>=1.6.0,<2.0.0",
|
|
16
16
|
"astropy>=7.0,<8.0",
|
|
17
17
|
"nbformat>=5.10.4,<6.0.0",
|
|
18
|
+
"tqdm>=4.66,<5.0",
|
|
18
19
|
]
|
|
19
20
|
requires-python = ">=3.12,<4.0"
|
|
20
21
|
classifiers = [
|
|
@@ -9,6 +9,7 @@ from hmmlearn import hmm
|
|
|
9
9
|
from scipy.signal import find_peaks
|
|
10
10
|
from scipy.stats import zscore
|
|
11
11
|
from tabulate import tabulate
|
|
12
|
+
from tqdm.auto import tqdm
|
|
12
13
|
|
|
13
14
|
from ethoscopy.analyse import max_velocity_detector
|
|
14
15
|
from ethoscopy.misc.general_functions import concat, rle
|
|
@@ -2330,9 +2331,7 @@ class behavpy_core(pd.DataFrame):
|
|
|
2330
2331
|
seq_test = np.concatenate(test, 0)
|
|
2331
2332
|
seq_test = seq_test.reshape(-1, 1)
|
|
2332
2333
|
|
|
2333
|
-
for i in range(iterations):
|
|
2334
|
-
print(f"Iteration {i+1} of {iterations}")
|
|
2335
|
-
|
|
2334
|
+
for i in tqdm(range(iterations), desc="HMM training", unit="iter"):
|
|
2336
2335
|
init_params = ""
|
|
2337
2336
|
# h = hmm.MultinomialHMM(n_components = n_states, n_iter = hmm_iterations, tol = tol, params = 'ste', verbose = verbose)
|
|
2338
2337
|
h = hmm.CategoricalHMM(
|
|
@@ -2397,11 +2396,13 @@ class behavpy_core(pd.DataFrame):
|
|
|
2397
2396
|
h.fit(seq_train, len_seq_train)
|
|
2398
2397
|
|
|
2399
2398
|
# Boolean output of if the number of runs convererged on set of appropriate probabilites for s, t, an e
|
|
2400
|
-
|
|
2399
|
+
tqdm.write(
|
|
2401
2400
|
"True Convergence:"
|
|
2402
2401
|
+ str(h.monitor_.history[-1] - h.monitor_.history[-2] < h.monitor_.tol)
|
|
2403
2402
|
)
|
|
2404
|
-
|
|
2403
|
+
tqdm.write(
|
|
2404
|
+
"Final log liklihood score:" + str(h.score(seq_train, len_seq_train))
|
|
2405
|
+
)
|
|
2405
2406
|
|
|
2406
2407
|
# On first iteration save and print the first trained matrix
|
|
2407
2408
|
if i == 0:
|
|
@@ -2410,9 +2411,11 @@ class behavpy_core(pd.DataFrame):
|
|
|
2410
2411
|
if h.score(seq_test, len_seq_test) > h_old.score(
|
|
2411
2412
|
seq_test, len_seq_test
|
|
2412
2413
|
):
|
|
2413
|
-
|
|
2414
|
+
tqdm.write("New Matrix:")
|
|
2414
2415
|
df_t = pd.DataFrame(h.transmat_, index=states, columns=states)
|
|
2415
|
-
|
|
2416
|
+
tqdm.write(
|
|
2417
|
+
tabulate(df_t, headers="keys", tablefmt="github") + "\n"
|
|
2418
|
+
)
|
|
2416
2419
|
|
|
2417
2420
|
with open(file_name, "wb") as file:
|
|
2418
2421
|
pickle.dump(h, file)
|
|
@@ -2426,9 +2429,9 @@ class behavpy_core(pd.DataFrame):
|
|
|
2426
2429
|
if h.score(seq_test, len_seq_test) > h_old.score(
|
|
2427
2430
|
seq_test, len_seq_test
|
|
2428
2431
|
):
|
|
2429
|
-
|
|
2432
|
+
tqdm.write("New Matrix:")
|
|
2430
2433
|
df_t = pd.DataFrame(h.transmat_, index=states, columns=states)
|
|
2431
|
-
|
|
2434
|
+
tqdm.write(tabulate(df_t, headers="keys", tablefmt="github") + "\n")
|
|
2432
2435
|
with open(file_name, "wb") as file:
|
|
2433
2436
|
pickle.dump(h, file)
|
|
2434
2437
|
|
|
@@ -6,6 +6,7 @@ import pandas as pd
|
|
|
6
6
|
import plotly.graph_objs as go
|
|
7
7
|
from plotly.subplots import make_subplots
|
|
8
8
|
from scipy.stats import zscore
|
|
9
|
+
from tqdm.auto import tqdm
|
|
9
10
|
|
|
10
11
|
from ethoscopy.behavpy_draw import behavpy_draw
|
|
11
12
|
from ethoscopy.behavpy_seaborn import behavpy_seaborn
|
|
@@ -3559,10 +3560,16 @@ class behavpy_plotly(behavpy_draw):
|
|
|
3559
3560
|
min_t = []
|
|
3560
3561
|
max_t = []
|
|
3561
3562
|
|
|
3562
|
-
for c, (df, b) in enumerate(
|
|
3563
|
+
for c, (df, b) in enumerate(
|
|
3564
|
+
tqdm(
|
|
3565
|
+
list(zip(decoded, b_list)),
|
|
3566
|
+
desc="Plotting",
|
|
3567
|
+
unit="fly",
|
|
3568
|
+
total=len(decoded),
|
|
3569
|
+
)
|
|
3570
|
+
):
|
|
3563
3571
|
df["colour"] = df["previous_state"].map(colours_index)
|
|
3564
3572
|
id = df.first_valid_index()
|
|
3565
|
-
print(f"Plotting: {id}")
|
|
3566
3573
|
|
|
3567
3574
|
if stim_df is not None:
|
|
3568
3575
|
df2 = stim_df.xmv("id", id)
|
|
@@ -6,6 +6,7 @@ import matplotlib.pyplot as plt
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import seaborn as sns
|
|
9
|
+
from tqdm.auto import tqdm
|
|
9
10
|
|
|
10
11
|
from ethoscopy.behavpy_draw import behavpy_draw
|
|
11
12
|
from ethoscopy.misc.circadian_bars import circadian_bars
|
|
@@ -3000,9 +3001,7 @@ class behavpy_seaborn(behavpy_draw):
|
|
|
3000
3001
|
min_t = []
|
|
3001
3002
|
max_t = []
|
|
3002
3003
|
|
|
3003
|
-
for ax, data in enumerate(decoded):
|
|
3004
|
-
print(f"Plotting: {data.index[0]}")
|
|
3005
|
-
|
|
3004
|
+
for ax, data in enumerate(tqdm(decoded, desc="Plotting", unit="fly")):
|
|
3006
3005
|
data["bin"] = data["bin"] / (60 * 60)
|
|
3007
3006
|
st = data["state"].to_numpy()
|
|
3008
3007
|
time = data["bin"].to_numpy()
|