ethoscopy 2.2.0__tar.gz → 2.3.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.
Files changed (82) hide show
  1. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.github/workflows/release.yml +0 -12
  2. ethoscopy-2.3.0/.gitignore +24 -0
  3. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/CLAUDE.md +41 -27
  4. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.dummy.example +4 -0
  5. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.github.example +4 -0
  6. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.google.example +4 -0
  7. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.keycloak.example +4 -0
  8. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/Dockerfile +10 -2
  9. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/README_AUTH.md +28 -0
  10. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/config/jupyterhub_config.py +39 -5
  11. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/docker-compose.yml +11 -1
  12. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/PKG-INFO +3 -2
  13. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/pyproject.toml +5 -1
  14. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/__init__.py +6 -0
  15. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/load.py +766 -111
  16. ethoscopy-2.3.0/tasks/todo.md +269 -0
  17. ethoscopy-2.3.0/tests/test_load_diagnostics.py +200 -0
  18. ethoscopy-2.3.0/tests/test_load_light.py +420 -0
  19. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load_optimizations.py +8 -2
  20. ethoscopy-2.3.0/tests/test_load_wal.py +493 -0
  21. ethoscopy-2.2.0/.gitignore +0 -11
  22. ethoscopy-2.2.0/Docker/.env.keycloak +0 -39
  23. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.codecov.yml +0 -0
  24. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.github/workflows/ci.yml +0 -0
  25. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/.pre-commit-config.yaml +0 -0
  26. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/.env.gitlab.example +0 -0
  27. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/README.md +0 -0
  28. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/config/users.py +0 -0
  29. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/install_r_packages.r +0 -0
  30. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/Docker/jupyterhub_data/jupyterhub_config.py +0 -0
  31. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/LICENSE +0 -0
  32. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/README.md +0 -0
  33. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/TESTING.md +0 -0
  34. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/_config.yml +0 -0
  35. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/pytest.ini +0 -0
  36. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/run_tests.py +0 -0
  37. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/README.md +0 -0
  38. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/convert_databases.sh +0 -0
  39. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/convert_wal_to_delete.py +0 -0
  40. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/scripts/publish_tutorials.py +0 -0
  41. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/setup.py +0 -0
  42. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/analyse.py +0 -0
  43. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy.py +0 -0
  44. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_HMM_class.py +0 -0
  45. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_class.py +0 -0
  46. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_core.py +0 -0
  47. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_draw.py +0 -0
  48. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_periodogram_class.py +0 -0
  49. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_plotly.py +0 -0
  50. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/behavpy_seaborn.py +0 -0
  51. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/metadata_db.py +0 -0
  52. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/__init__.py +0 -0
  53. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/circadian_bars.py +0 -0
  54. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/general_functions.py +0 -0
  55. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/get_HMM.py +0 -0
  56. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/get_tutorials.py +0 -0
  57. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/hmm_functions.py +0 -0
  58. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/periodogram_functions.py +0 -0
  59. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/src/ethoscopy/misc/validate_datetime.py +0 -0
  60. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/__init__.py +0 -0
  61. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/conftest.py +0 -0
  62. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/data/README.md +0 -0
  63. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/data/test_ethoscope.db +0 -0
  64. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_analyse.py +0 -0
  65. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_baseline_enhancements.py +0 -0
  66. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_behavpy.py +0 -0
  67. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_behavpy_core_simple.py +0 -0
  68. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_compatibility_classes.py +0 -0
  69. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_general_functions.py +0 -0
  70. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_get_tutorials.py +0 -0
  71. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load.py +0 -0
  72. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load_comprehensive.py +0 -0
  73. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tests/test_load_metadata_fixes.py +0 -0
  74. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/1_Overview_tutorial.ipynb +0 -0
  75. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/2_HMM_tutorial.ipynb +0 -0
  76. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/3_Circadian_tutorial.ipynb +0 -0
  77. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/4_Navigating_db_tutorial.ipynb +0 -0
  78. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/5_Ethoscopy_catch22_tutorial.ipynb +0 -0
  79. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/6_Ethoscopy_to_hctsa_tutorial.ipynb +0 -0
  80. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/ethoscope_db.csv +0 -0
  81. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/tutorial_notebook/jones_et_al_metadata.csv +0 -0
  82. {ethoscopy-2.2.0 → ethoscopy-2.3.0}/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 }}
@@ -0,0 +1,24 @@
1
+ *.txt
2
+ *.pyc
3
+ *venv
4
+ dist/
5
+ *vscode
6
+ __pycache__/
7
+ ethoscopy.egg-info/
8
+ .gitignore
9
+ *.pkl
10
+ .python-version
11
+ tmp
12
+
13
+ # JupyterHub runtime state, generated when the Docker stack first runs.
14
+ # jupyterhub_cookie_secret signs session cookies and jupyterhub.sqlite holds
15
+ # user records and API tokens; neither belongs in the repo. Committing the
16
+ # secret would be the same failure as the tracked .env.keycloak removed in
17
+ # 6cbcbbe. jupyterhub_config.py stays tracked - it is configuration, not state.
18
+ Docker/jupyterhub_data/jupyterhub_cookie_secret
19
+ Docker/jupyterhub_data/jupyterhub.sqlite
20
+
21
+ # Coverage output
22
+ .coverage
23
+ htmlcov/
24
+ coverage.xml
@@ -36,48 +36,62 @@ pip install -e ".[dev]"
36
36
  ```
37
37
 
38
38
  ### Testing
39
- - **No formal test suite found** - project relies on Jupyter notebooks for validation
40
- - Use the tutorial notebooks in `tutorial_notebook/` for testing functionality
41
- - Run notebooks: `jupyter notebook tutorial_notebook/`
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
- ### Troubleshooting: Database "Malformed" Errors
52
+ ### Opening databases: WAL mode and read-only mounts
49
53
 
50
- **Problem**: Intermittent "database disk image is malformed" errors when loading ethoscope data in Docker
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
- **Root Cause**: SQLite databases in WAL (Write-Ahead Logging) mode on read-only Docker mounts
59
+ **How `_connect_db()` handles it** (load.py)
53
60
 
54
- **Solution Options**:
55
- 1. **Convert databases to DELETE mode** (recommended for immediate fix)
56
- ```bash
57
- # Using the conversion script (from project root)
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
- # Or using bash wrapper
61
- ./scripts/convert_databases.sh /mnt/ethoscope_data/results
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
- 2. **Improved connection handling** (v2.0.4+)
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
- **Key Implementation Details** (load.py):
70
- - Lines 18-70: `_connect_db()` - Smart connection with WAL detection and appropriate parameters
71
- - Lines 943-979: Retry logic in `read_single_roi_optimized()` - Handles transient errors with fresh connections
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
- **Testing After Changes**:
74
- ```python
75
- import ethoscopy as etho
76
- metadata = etho.link_meta_index('metadata.csv', '/mnt/ethoscope_results')
77
- data = etho.load_ethoscope(metadata, reference_hour=9.0, FUN=etho.sleep_annotation)
78
- # Should load all ROIs without "malformed" errors
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 (version 3.6.4)
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
  # ============================================================================
@@ -98,7 +106,7 @@ RUN pip3 install \
98
106
  pyarrow \
99
107
  tqdm \
100
108
  jupyterlab-git \
101
- ethoscopy==2.1.0
109
+ ethoscopy==2.2.1
102
110
  # pycatch22
103
111
 
104
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
- c.Spawner.notebook_dir = '/home/{username}'
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.2}
9
+ image: ggilestro/ethoscope-lab:${ETHOSCOPE_LAB_TAG:-1.4}
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: ethoscopy
3
- Version: 2.2.0
3
+ Version: 2.3.0
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
@@ -19,6 +19,7 @@ Requires-Dist: hmmlearn<0.4.0,>=0.3.2
19
19
  Requires-Dist: nbformat<6.0.0,>=5.10.4
20
20
  Requires-Dist: numpy<3.0.0,>=2.0.0
21
21
  Requires-Dist: pandas<3.0.0,>=2.2.2
22
+ Requires-Dist: pillow<13.0,>=10.0
22
23
  Requires-Dist: plotly<6.0.0,>=5.22.0
23
24
  Requires-Dist: pywavelets<2.0.0,>=1.6.0
24
25
  Requires-Dist: seaborn<0.14.0,>=0.13.2
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ethoscopy"
3
- version = "2.2.0"
3
+ version = "2.3.0"
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"
@@ -16,6 +16,10 @@ dependencies = [
16
16
  "astropy>=7.0,<8.0",
17
17
  "nbformat>=5.10.4,<6.0.0",
18
18
  "tqdm>=4.66,<5.0",
19
+ # estimate_light_cycle() decodes the stored IMG_SNAPSHOTS JPEGs. Pillow
20
+ # already arrived transitively through seaborn -> matplotlib; this makes the
21
+ # direct dependency explicit.
22
+ "pillow>=10.0,<13.0",
19
23
  ]
20
24
  requires-python = ">=3.12,<4.0"
21
25
  classifiers = [
@@ -15,8 +15,11 @@ from ethoscopy.analyse import (
15
15
  from ethoscopy.behavpy import behavpy
16
16
  from ethoscopy.load import (
17
17
  download_from_remote_dir,
18
+ estimate_light_cycle,
18
19
  link_meta_index,
19
20
  load_ethoscope,
21
+ load_ethoscope_diagnostics,
22
+ load_ethoscope_light_schedule,
20
23
  load_ethoscope_metadata,
21
24
  )
22
25
  from ethoscopy.misc.general_functions import concat
@@ -28,9 +31,12 @@ __all__ = [
28
31
  "behavpy",
29
32
  "download_from_remote_dir",
30
33
  "download_tutorial_data",
34
+ "estimate_light_cycle",
31
35
  "get_tutorial",
32
36
  "link_meta_index",
33
37
  "load_ethoscope",
38
+ "load_ethoscope_diagnostics",
39
+ "load_ethoscope_light_schedule",
34
40
  "load_ethoscope_metadata",
35
41
  "max_velocity_detector",
36
42
  "sleep_annotation",