radiens-data-catalog 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.
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ .coverage
6
+ htmlcov/
7
+ coverage.xml
8
+ .vscode/
9
+ .idea/
10
+ *.swp
11
+ *.swo
12
+ .env
13
+ .venv
14
+ venv/
15
+ env/
16
+ .DS_Store
17
+ *.log
18
+ .mypy_cache
19
+ .pytest_cache
20
+ .ruff_cache
21
+ site/
22
+ .secrets/
23
+ catalog.json
24
+
25
+ .claude/settings.local.json
@@ -0,0 +1,361 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ _no unreleased changes_
11
+
12
+ ## [0.1.0] - 2026-10-12
13
+
14
+ ### Changed
15
+
16
+ - **Breaking:** the package is now `radiens-data-catalog`, imported as `radiens_data_catalog`. Install `radiens-data-catalog` in place of `radiens-drive-catalog` and update imports. Releases up to 0.0.19 remain on PyPI as `radiens-drive-catalog`.
17
+ - **Breaking:** the config environment variable is now `RADIENS_DATA_CATALOG_CONFIG`. `RADIENS_DRIVE_CATALOG_CONFIG` is no longer read.
18
+ - The documentation moved to <https://neuronexus.github.io/radiens-data-catalog/latest/>.
19
+
20
+ ## [0.0.19] - 2026-10-12
21
+
22
+ ### Added
23
+
24
+ - A `"regex"` match mode (`MatchMode`) for catalog filters: the pattern is searched anywhere in the field (`re.search` semantics; anchor with `^`/`$` for a full match). It works wherever the other modes do: `list_recordings()`, `list_items()`, `prefetch()` and `match_series()`.
25
+ - `Catalog.get_meta(drive_path, base_name)` returns one recording's parsed `.xdat.json` metadata, and `Catalog.load_meta()` returns every matching recording's, keyed by `(drive_path, base_name)`, without downloading recording data. A downloaded recording's own file is read in place; any other is fetched alone (~100 KB) into `{catalog dir}/meta_cache/`, keyed by Drive file ID so a re-upload is never served stale, with missing files fetched concurrently. A file that fails to fetch or parse is left out with one summary warning, and a file Drive refuses permanently (403/404) is cached as empty so it isn't refetched on every call.
26
+ - `recordings_df` gains `last_used` (when `get_recording_path()` last returned the recording on this machine, falling back to the data file's download time) and `downloaded` (whether every xdat file is actually on disk, since `local_path` is only a hint reconciled by `scan()`). Uses are stamped in `usage.json` beside the catalog, not in `catalog.json`, so a lost update under concurrent writers costs only a timestamp, never download state.
27
+ - `Catalog.prefetch(keys=...)` restricts the download to an exact set of `(drive_path, base_name)` recordings, on top of the other filters, for callers that select recordings by something the filters can't express (e.g. metadata).
28
+ - `Catalog.orphaned_downloads(drive_path=None, min_age_s=600)` lists the temp files that hard-killed recording downloads left behind, keyed by `(drive_path, base_name)`. Temp files written in the last `min_age_s` seconds are left out, since a download may still be writing them.
29
+
30
+ ### Fixed
31
+
32
+ - Drive shortcuts are no longer catalogued or downloaded. A shortcut has no content of its own, so it became a recording with no size that Drive refused to download ("Only files with binary content can be downloaded"), and downloading a folder containing one failed. The files they point at are usually catalogued from their real location anyway.
33
+ - A download interrupted with Ctrl-C (`KeyboardInterrupt`) left its `<file>.<uuid>.tmp` temp file behind; the cleanup now runs for any exception.
34
+ - `Catalog.release_recording()` now also deletes the recording's orphaned download temp files, even when it was never fully downloaded. Before, nothing removed them.
35
+
36
+ ## [0.0.18] - 2026-08-13
37
+
38
+ ### Fixed
39
+
40
+ - The docs site's Quick Start (`docs/index.md`) and Usage Guide (`docs/usage.md`), and the repo's `example.py`, documented an API generation removed in 0.0.8–0.0.14 (`list_datasets()`, `download_dataset()`, `get_dataset_path()`, `list_assets()`, `download_asset()`, `get_asset_path()`, `catalog.df`/`assets_df`, `asset_name`/`asset_type`, `drive_path_prefix`/`drive_path_contains`, and the old `local_data_dir/assets/{drive_path}/{name}` item path). Updated to the current `Catalog` API (`list_recordings()`/`list_items()` with `drive_path_mode`/`base_name_mode`, `recordings_df`/`items_df`, `download_recording()`/`download_item()`, `get_recording_path()`/`get_item_path()`), and added coverage of `prefetch()`, `release_recording()`/`release_item()`, `local_footprint()`, `file_tree()`, and `summary()`, which previously weren't documented anywhere on the site.
41
+
42
+ ## [0.0.17] - 2026-07-20
43
+
44
+ - documentation updates
45
+
46
+ ## [0.0.16] - 2026-07-20
47
+
48
+ ### Added
49
+
50
+ - `Catalog.prefetch()` now supports concurrent downloading via `max_workers` (default `4`). Downloads run in parallel across a thread pool with thread-safe progress tracking and worker slot indicators. Passing `max_workers=1` forces sequential execution.
51
+ - `Catalog.release_recording(drive_path, base_name)` and `Catalog.release_item(drive_path, name)` delete a recording's or item's local files and clear its `local_path`, returning the number of bytes freed. Releasing a recording removes only its own xdat files (never the shared `drive_path` directory or sibling entries); releasing a folder item removes its full local subtree. Both are idempotent no-ops when the entry is not downloaded.
52
+ - `Catalog.local_footprint()` returns the total on-disk size, in bytes, of all downloaded recordings and items. Together with `release_*`, this lets a caller cap local storage by releasing data until the footprint drops below a chosen limit — enabling batch processing of a corpus larger than local disk.
53
+
54
+ ### Fixed
55
+
56
+ - Local size totals (the `summary()` "local" row, and now `local_footprint()`) no longer over-count downloaded folder items. Because the scanner catalogs every nested file and subfolder as its own item, downloading a folder marked its children local too, and each nested entry's subtree was summed again on top of the enclosing folder's — inflating the reported local size (e.g. 800 B on disk reported as 1900 B). Nested entries are now counted once, as part of their enclosing downloaded folder. Per-entry Drive `size` values and the Drive-size total were already correct and are unchanged.
57
+
58
+ ## [0.0.15] - 2026-07-18
59
+
60
+ ### Added
61
+
62
+ - `match_series()`, exported from the package root, for filtering an already-fetched `recordings_df` / `items_df` (or any `pd.Series[str]`) with the same exact/prefix/contains semantics `Catalog.list_recordings()` and `Catalog.list_items()` use internally.
63
+
64
+ ### Changed
65
+
66
+ - `Catalog`'s `drive_path`-taking methods (`list_recordings()`, `list_items()`, `get_recording()`, `download_recording()`, `get_recording_path()`, `download_item()`, `get_item_path()`, `prefetch()`) now accept `str` or `Path` for `drive_path` — previously `str`-only. `Path` values are converted internally, so a `Path` built via ordinary path arithmetic (e.g. `some_path.parent`) can be passed directly instead of failing a silent `Path != str` comparison.
67
+
68
+ ## [0.0.14] - 2026-07-18
69
+
70
+ ### Added
71
+
72
+ - `base_name` / `base_name_mode` filtering on `Catalog.list_recordings()` and `Catalog.prefetch()` — recordings can now be queried and bulk-downloaded by filename stem (exact match, prefix, or substring), not just by `drive_path`.
73
+ - `MatchMode` type (`Literal["exact", "prefix", "contains"]`), exported from the package root, describing how a filter value is matched.
74
+ - `Catalog.get_recording()` accepts an optional `drive_path` argument to disambiguate recordings that share a `base_name` at different Drive locations.
75
+
76
+ ### Changed
77
+
78
+ - **Breaking:** `Catalog.list_recordings()`, `Catalog.list_items()`, and `Catalog.prefetch()` replace the `drive_path_prefix` / `drive_path_contains` parameters with a single `drive_path_mode: MatchMode` parameter (default `"exact"`), paired with `drive_path`.
79
+ - **Breaking:** All filter parameters on `list_recordings()`, `list_items()`, and `prefetch()` are now keyword-only.
80
+
81
+ ## [0.0.13] - 2026-07-16
82
+
83
+ ### Added
84
+
85
+ - `size: int | None` field on `RecordingEntry` and `DriveItemEntry`, populated from Drive's file size metadata during `scan()` (no download required). For recordings, it's the combined size of the constituent xdat files that reported a size. `None` for folders and for files Drive doesn't report a size for (e.g. Google Workspace-native Docs/Sheets/Slides). Older catalog files without this field continue to load fine (`size` defaults to `None`).
86
+ - `Catalog.summary(rescan=False, verbose=False)` — a human-readable report of the whole catalog. The default output is headline-only: recording/item counts and local-download progress (as a table), the upload-date range, and Drive/local storage size (broken out separately for recordings and items, with any unsized entries called out rather than silently dropped from the total, plus average recording size and an incomplete-recordings count broken down by missing file type). Pass `rescan=True` to refresh the catalog first. Pass `verbose=True` for everything that scales with catalog size: an item-type breakdown, the largest entries by size, the itemized incomplete-recordings list, a per-top-level-folder breakdown, and a full per-entry listing with size and local status.
87
+
88
+ ### Changed
89
+
90
+ - `Config.from_file()` auto-discovery of `.secrets/config.json` / `config.json` now searches upward from the current working directory through every parent directory (closest match wins), instead of checking only the current working directory. This mirrors how git locates `.git`, so discovery works the same whether you run from the repo root or a nested subdirectory (e.g. a notebook in `notebooks/`).
91
+
92
+ ### Fixed
93
+
94
+ - The `~/.config/radiens-drive/config.json` fallback location now actually works. Previously it was silently unreachable: the discovery code joined it onto the current working directory without expanding `~` first, so it could never match a real path.
95
+
96
+ ## [0.0.12] - 2026-07-16
97
+
98
+ ### Fixed
99
+
100
+ - `Config.from_file()` now resolves relative paths (`credentials_path`, `local_data_dir`, `catalog_path`) against the config file's own directory instead of the process's current working directory. Previously, loading a config with relative paths from a different working directory (e.g. a Jupyter notebook in a subdirectory) would resolve those paths incorrectly, forcing absolute paths as a workaround. Direct `Config(...)` construction is unaffected and still resolves relative paths against the current working directory.
101
+
102
+ ## [0.0.11] - 2026-06-08
103
+
104
+ ### Added
105
+
106
+ - `RecordingEntry` TypedDict — replaces `DatasetEntry` (fields unchanged).
107
+ - `DriveItemEntry` TypedDict — replaces `AssetEntry`; `asset_name`/`asset_type` fields replaced by `name: str` and `is_folder: bool`.
108
+ - `AmbiguousRecordingError` exception (subclass of `CatalogError`) — raised by `get_recording()` when more than one recording matches a given `base_name`.
109
+ - `Catalog.get_recording(base_name)` — look up a recording by base name, requiring it to be unique across all drive paths. Raises `EntryNotFoundError` (zero matches) or `AmbiguousRecordingError` (multiple matches).
110
+ - `Catalog.file_tree()` — returns a multi-line string rendering of the Drive folder hierarchy, annotating each entry as `[recording]`, `[folder]`, or `[file]` and `[local]` or `[not local]`.
111
+ - `xdat_recording_exists_locally()` utility — replaces `xdat_dataset_exists_locally()`.
112
+
113
+ ### Changed
114
+
115
+ - **Breaking:** `DatasetEntry` renamed to `RecordingEntry`; `AssetEntry` renamed to `DriveItemEntry`.
116
+ - **Breaking:** `Catalog.recordings_df` replaces `Catalog.df`; `Catalog.items_df` replaces `Catalog.assets_df`.
117
+ - **Breaking:** `Catalog.list_recordings()` replaces `Catalog.list_datasets()`; `Catalog.list_items()` replaces `Catalog.list_assets()`. The `asset_type: str | None` filter on `list_items()` is replaced by `is_folder: bool | None`.
118
+ - **Breaking:** `Catalog.download_recording()` replaces `Catalog.download_dataset()`; `Catalog.get_recording_path()` replaces `Catalog.get_dataset_path()`.
119
+ - **Breaking:** `Catalog.download_item()` replaces `Catalog.download_asset()`; `Catalog.get_item_path()` replaces `Catalog.get_asset_path()`.
120
+ - **Breaking:** `ScanResult` fields renamed: `datasets_new/existing/removed` → `recordings_new/existing/removed`; `assets_new/existing/removed` → `items_new/existing/removed`.
121
+ - **Breaking:** `PrefetchResult` fields renamed: `datasets_downloaded/skipped` → `recordings_downloaded/skipped`; `assets_downloaded/skipped` → `items_downloaded/skipped`.
122
+ - **Breaking:** `prefetch()` parameter `asset_type` replaced by `is_folder: bool | None`.
123
+ - **Breaking:** Item local paths simplified from `{local_data_dir}/assets/{drive_path}/{name}` to `{local_data_dir}/{drive_path}/{name}`, matching the same Drive-mirroring convention used for recordings.
124
+ - `scan_drive()` and `scan_drive_flat()` now catalog **all** non-xdat files and **all** non-root folders as `DriveItemEntry` regardless of depth. The previous depth-based `_classify_item` heuristic is removed entirely — consumer code owns the semantic classification of items.
125
+ - `_load_catalog` reads both new (`"recordings"`/`"items"`) and old (`"datasets"`/`"assets"`) JSON keys, and handles both `name`/`is_folder` and legacy `asset_name`/`asset_type` field names, so existing catalog files continue to load without error.
126
+
127
+ ### Removed
128
+
129
+ - `DatasetEntry`, `AssetEntry` — replaced by `RecordingEntry` and `DriveItemEntry` respectively.
130
+ - `xdat_dataset_exists_locally()` — replaced by `xdat_recording_exists_locally()`.
131
+ - Depth-based `_classify_item()` internal function — catalog no longer applies structural heuristics to classify items.
132
+
133
+ ## [0.0.10] - 2026-06-04
134
+
135
+ ### Added
136
+
137
+ - `xdat_dataset_exists_locally()` utility to verify all three canonical xdat files (`_data.xdat`, `.xdat.json`, `_timestamp.xdat`) exist locally.
138
+ - `Catalog.scan()` now warns when a dataset on Drive is missing one or more required xdat files.
139
+ - `Catalog.get_dataset_path()` and `Catalog.get_asset_path()` now automatically trigger a `scan()` if the requested entry is not found in the local catalog, improving resilience to out-of-date catalogs.
140
+
141
+ ### Changed
142
+
143
+ - Catalog JSON writes are now atomic (using a temporary file and `replace()`) to prevent data loss during interruptions.
144
+ - File downloads are now atomic (using a `.tmp` suffix) to ensure interrupted downloads do not leave partial or corrupted files on disk.
145
+ - `Catalog.scan()` now reconciles local datasets by verifying the presence of all three xdat files, preventing a dataset from being marked "local" if it is incomplete.
146
+ - Refactored `Catalog` internal path restoration logic into `_restore_dataset_local_paths()` and `_restore_asset_local_paths()`.
147
+
148
+ ### Fixed
149
+
150
+ - Improved validation of asset entries during catalog load; `drive_path` must now be a non-empty string.
151
+ - Streamlined local path construction for assets in `download_asset()`.
152
+
153
+ ## [0.0.9] - 2026-05-20
154
+
155
+ ### Added
156
+
157
+ - `upload_time` column in `Catalog.df` and `Catalog.assets_df`: a UTC-aware
158
+ `datetime64` column populated from the Drive API's `createdTime` field during
159
+ `scan()`. Rows without a recorded date are `NaT`. Existing catalogs without
160
+ this field load cleanly — missing values default to `None` / `NaT`.
161
+
162
+ ### Changed
163
+
164
+ - `Catalog.prefetch()` progress bar now covers only items being downloaded
165
+ (not skipped ones), so the total count and ETA reflect real work. The bar is
166
+ suppressed entirely when all matching items are already local.
167
+ - Skipped items in `prefetch()` are logged at `DEBUG` level instead of being
168
+ silently counted.
169
+ - Per-item download log messages in `Catalog.download_dataset()` and
170
+ `Catalog.download_asset()` lowered from `INFO` to `DEBUG`. The `prefetch()`
171
+ progress bar is the intended progress signal for bulk operations; direct
172
+ single-item calls remain observable via debug logging.
173
+
174
+ ## [0.0.8] - 2026-04-21
175
+
176
+ ### Added
177
+
178
+ - `Catalog.scan()` now returns a `ScanResult` dataclass with counts of
179
+ `datasets_new`, `datasets_existing`, `datasets_removed`, `assets_new`,
180
+ `assets_existing`, and `assets_removed` — making it easy to inspect
181
+ what changed after a Drive rescan. `ScanResult.__str__` renders as a
182
+ compact table.
183
+ - `ScanResult` dataclass exported from the top-level package.
184
+ - Progress bar during Drive file traversal in `scan()` (respects the
185
+ `quiet=` flag passed to `Catalog`).
186
+ - Progress bar in `prefetch()` showing overall item count and the name
187
+ of the entry currently being downloaded (also respects `quiet=`).
188
+ - `Catalog.prefetch(drive_path, drive_path_prefix, drive_path_contains,
189
+ datasets=True, assets=True, asset_type=None, force=False)` — bulk,
190
+ idempotent downloader that skips entries already present on disk.
191
+ Pass `force=True` to re-download every matching entry regardless of
192
+ local state. Returns a `PrefetchResult` with per-category download
193
+ and skip counts.
194
+ - `PrefetchResult` dataclass exported from the top-level package.
195
+ - `CatalogError` and `EntryNotFoundError` exceptions exported from the
196
+ top-level package. `EntryNotFoundError` (subclass of `CatalogError`)
197
+ is raised when a dataset or asset lookup fails; `CatalogError` is
198
+ raised for malformed catalog JSON.
199
+ - `Catalog.list_datasets(drive_path, drive_path_prefix,
200
+ drive_path_contains)` — convenience method returning a filtered
201
+ dataset DataFrame.
202
+ - `Catalog.list_assets(drive_path, drive_path_prefix,
203
+ drive_path_contains, asset_type)` — convenience method returning a
204
+ filtered asset DataFrame. Supports an additional `asset_type="folder"`
205
+ or `"file"` filter.
206
+ - `quiet` keyword argument on `Catalog(...)` suppresses tqdm progress
207
+ bars during downloads.
208
+ - `DatasetEntry` TypedDict exported from the top-level package
209
+ (previously only `AssetEntry` was exported).
210
+
211
+ ### Changed
212
+
213
+ - `Catalog.scan()` return type changed from `None` to `ScanResult`.
214
+ - Drive scan warnings (e.g. incomplete search results) are now emitted
215
+ via `logging.getLogger` rather than `warnings.warn`.
216
+ - Dataset identity is now `(drive_path, base_name)` instead of
217
+ `base_name` alone, allowing distinct recordings with the same base
218
+ name in different folders.
219
+ - Dataset downloads now mirror Drive hierarchy under local storage:
220
+ `local_data_dir/{drive_path}/...`.
221
+ - `Catalog.df` and `Catalog.assets_df` now use a path-first schema and
222
+ no longer include legacy `date_folder` / `experiment` columns.
223
+ - **Breaking:** `Catalog.download()` renamed to
224
+ `Catalog.download_dataset()` for symmetry with `download_asset()`.
225
+ - **Breaking:** `Catalog.get_path()` renamed to
226
+ `Catalog.get_dataset_path()` for symmetry with `get_asset_path()`.
227
+ - `Catalog.download_dataset()` and `Catalog.get_dataset_path()` now
228
+ require both `drive_path` and `base_name` to identify a dataset.
229
+ - Catalog JSON parse errors now raise `CatalogError` instead of
230
+ `ValueError`. "Not found" errors on single-item download / get_path
231
+ methods now raise `EntryNotFoundError` instead of `ValueError`.
232
+
233
+ ### Removed
234
+
235
+ - **Breaking:** `Catalog(config_path=...)` constructor shortcut removed.
236
+ Use `Catalog(Config.from_file("path"))` or `Catalog()` (which calls
237
+ `Config.from_file()` with auto-discovery).
238
+ - Removed `Catalog.list_experiment_paths()`, `Catalog.list_by_path()`,
239
+ and `Catalog.status()` convenience methods. Use
240
+ `Catalog.list_datasets()` / `Catalog.list_assets()` or operate on the
241
+ raw DataFrames directly.
242
+
243
+ ### Fixed
244
+
245
+ - `Catalog.scan()` now preserves `local_path` for datasets by
246
+ `(drive_path, base_name)` during refresh, preventing path collisions
247
+ when multiple folders contain the same `base_name`.
248
+ - `py.typed` marker (PEP 561) — the package now declares itself as typed,
249
+ allowing mypy to use its annotations in downstream consumers.
250
+ - `Catalog.scan()` now reconciles `local_path` against disk rather than
251
+ trusting the previous catalog blindly. If the catalog is deleted,
252
+ entries whose data already exists at the canonical local path
253
+ (`local_data_dir/{drive_path}` for datasets,
254
+ `local_data_dir/assets/{drive_path}/{asset_name}` for assets) are
255
+ recovered automatically instead of triggering unnecessary re-downloads.
256
+ Stale catalog paths that no longer exist on disk are also cleared.
257
+
258
+ ## [v0.0.7] - 2026-04-08
259
+
260
+ ### Changed
261
+
262
+ - `Catalog.scan()` now defaults to the flat scanner (`flat=True`), which is
263
+ faster and works even when the root folder is inaccessible. Pass
264
+ `flat=False` to use the recursive traversal.
265
+ - Removed automatic fallback logic from `scan()`. The caller now chooses the
266
+ scan strategy via the `flat` keyword argument.
267
+
268
+ ## [v0.0.6] - 2026-04-07
269
+
270
+ ### Added
271
+
272
+ - `Catalog.list_experiment_paths()` and `Catalog.list_by_path()` to help
273
+ discover and filter datasets by their full Drive path.
274
+
275
+ ### Fixed
276
+
277
+ - Depth-based asset cataloging: nested subfolders inside experiments (depth 3+)
278
+ are now correctly cataloged as folder assets. Previously only direct
279
+ subfolders (depth 2) were included.
280
+
281
+ ## [v0.0.5] - 2026-04-06
282
+
283
+ ### Added
284
+
285
+ - Flat-scan fallback: when the root folder is inaccessible (HTTP 403/404) or
286
+ returns an empty listing, `Catalog.scan()` automatically falls back to a
287
+ flat `files.list()` of all files visible to the service account and
288
+ reconstructs the folder hierarchy from `parents` metadata. A
289
+ `UserWarning` is emitted when the fallback activates.
290
+ - `scan_drive_flat()` internal function for flat-listing all Drive files with
291
+ paginated `pageSize=1000` requests and parent-chain path reconstruction.
292
+ - `Catalog` can now be created without a pre-built `Config` object:
293
+ `Catalog(config_path="/path/to/config.json")` and `Catalog()` are both
294
+ supported.
295
+
296
+ ### Changed
297
+
298
+ - `Config.from_file()` discovery now also checks
299
+ `~/.config/radiens-drive/config.json` and `/etc/radiens-drive/config.json`
300
+ after the existing environment variable and local path checks.
301
+
302
+ ## [0.0.4] - 2026-04-02
303
+
304
+ ### Fixed
305
+
306
+ ## [0.0.3] - 2026-04-02
307
+
308
+ ### Changed
309
+
310
+ - `Config.from_file()` now auto-discovers config files when called with no
311
+ arguments: checks `RADIENS_DRIVE_CATALOG_CONFIG` env var, then
312
+ `.secrets/config.json`, then `config.json` in the current working directory.
313
+ Raises `FileNotFoundError` (previously `ValueError`) when no config is found.
314
+
315
+ ## [0.0.2] - 2026-04-02
316
+
317
+ ### Added
318
+
319
+ - `AssetEntry` TypedDict for non-xdat Drive content (folders like `logs/`,
320
+ PowerPoints, writeups, etc.).
321
+ - `scan_drive()` now returns `(datasets, assets)` tuple. Assets are
322
+ auto-discovered during scan: non-xdat files inside date or experiment
323
+ folders become file assets; subfolders of experiment folders become folder
324
+ assets and are still recursed for xdat datasets.
325
+ - `download_asset()` in `drive.py` for downloading file or folder assets;
326
+ folder assets are downloaded recursively, mirroring the Drive subtree.
327
+ - `Catalog.assets_df` property — full asset catalog as a pandas DataFrame.
328
+ - `Catalog.list_assets()` — query assets with optional `date_folder`,
329
+ `experiment`, and `asset_type` filters.
330
+ - `Catalog.download_asset(drive_path, asset_name)` — download an asset to
331
+ `local_data_dir/assets/{drive_path}/{asset_name}`.
332
+ - `Catalog.get_asset_path(drive_path, asset_name)` — return local path,
333
+ downloading automatically if needed.
334
+ - Catalog JSON format changed from a bare list to
335
+ `{"datasets": [...], "assets": [...]}`. Old flat-list catalogs are
336
+ migrated automatically on the next `scan()`.
337
+ - `AssetEntry` exported from the top-level package.
338
+
339
+ ### Changed
340
+
341
+ - `Catalog.list()` and `Catalog.list_assets()`: renamed the `date` parameter
342
+ to `date_folder` to match the catalog column name and make clear that an
343
+ exact folder name (e.g. `"2026-02-15_batch"`) is required, not a date
344
+ prefix.
345
+
346
+ ## [0.0.1] - 2026-04-02
347
+
348
+ ### Added
349
+
350
+ - `Config` dataclass with `from_file()` classmethod; supports `~` and `$ENV_VAR`
351
+ expansion in path fields and `RADIENS_DRIVE_CATALOG_CONFIG` env var fallback.
352
+ - `build_drive_service()` for authenticating with a Google service account.
353
+ - `scan_drive()` for recursive Drive scanning; returns `DatasetEntry` records
354
+ with `date_folder`, `experiment`, `drive_path`, and `drive_file_ids`.
355
+ - `download_dataset()` for chunked download of xdat filesets to a local directory.
356
+ - `Catalog` class with `scan()`, `df`, `list()`, `download()`, `get_path()`,
357
+ and `status()`.
358
+ - MkDocs documentation site with Material theme, auto-generated API reference,
359
+ and versioned deployment via `mike`.
360
+ - CI/CD workflows: linting, type checking, tests, docs deployment, and PyPI
361
+ publishing via GitHub Actions.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NeuroNexus
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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: radiens-data-catalog
3
+ Version: 0.1.0
4
+ License-File: LICENSE
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: google-api-python-client>=2.188.0
7
+ Requires-Dist: google-auth-oauthlib>=1.2.4
8
+ Requires-Dist: google-auth>=2.47.0
9
+ Requires-Dist: pandas>=2.3.3
10
+ Requires-Dist: tqdm>=4.67.3
11
+ Description-Content-Type: text/markdown
12
+
13
+ # radiens-data-catalog
14
+
15
+ A Python package for programmatically managing large neural datasets stored on Google Drive. It handles Drive scanning, local cataloging, and selective dataset download. Analysis is done locally — this package is purely about data management.
16
+
17
+ Documentation: <https://neuronexus.github.io/radiens-data-catalog/latest/>
18
+
19
+ ## Overview
20
+
21
+ Neural data is stored as xdat filesets (NeuroNexus format) on a shared Google Drive. Each dataset consists of 3 files sharing a common `base_name`:
22
+
23
+ ```
24
+ {base_name}_data.xdat
25
+ {base_name}.xdat.json
26
+ {base_name}_timestamp.xdat
27
+ ```
28
+
29
+ `radiens-data-catalog` scans the Drive hierarchy, builds a local catalog indexed by `base_name`, and lets you query and download datasets selectively. Non-xdat content found alongside recordings — logs directories, PowerPoints, writeups — is also discovered and tracked as **Drive items**.
30
+
31
+ ## Usage
32
+
33
+ ### Recordings
34
+
35
+ ```python
36
+ from radiens_data_catalog import Catalog, Config
37
+
38
+ config = Config.from_file("config.json")
39
+ catalog = Catalog(config)
40
+
41
+ # Scan Drive and build the catalog (discovers recordings and Drive items)
42
+ result = catalog.scan() # returns ScanResult with new/existing/removed counts
43
+ result = catalog.scan(flat=False) # recursive traversal instead of flat file scan
44
+
45
+ # Query recordings using pandas directly
46
+ catalog.recordings_df
47
+ catalog.list_recordings() # everything
48
+ catalog.list_recordings(drive_path="2026-02-15_batch/reaching") # exact folder
49
+ catalog.list_recordings(drive_path="2026-02-15_batch", drive_path_mode="prefix") # full date subtree
50
+ catalog.list_recordings(drive_path="reaching", drive_path_mode="contains") # any depth
51
+ catalog.list_recordings(base_name="rat01_session3") # exact base_name
52
+ catalog.list_recordings(base_name="rat01", base_name_mode="prefix") # base_name prefix
53
+ catalog.list_recordings(base_name="session3", base_name_mode="contains") # base_name substring
54
+
55
+ # Filters are ANDed together, e.g. all "rat01" recordings under "2026-02-15_batch"
56
+ catalog.list_recordings(drive_path="2026-02-15_batch", drive_path_mode="prefix", base_name="rat01", base_name_mode="prefix")
57
+
58
+ # Look up a recording by base_name (raises AmbiguousRecordingError if not globally
59
+ # unique — pass drive_path to disambiguate)
60
+ entry = catalog.get_recording("rat01_session3")
61
+ entry = catalog.get_recording("rat01_session3", drive_path="2026-02-15_batch/reaching")
62
+
63
+ # Download a recording (3 xdat files)
64
+ catalog.download_recording("2026-02-15_batch/reaching", "rat01_session3")
65
+
66
+ # Get the local path, downloading automatically if needed
67
+ path = catalog.get_recording_path("2026-02-15_batch/reaching", "rat01_session3")
68
+ ```
69
+
70
+ ### Drive items (non-xdat content)
71
+
72
+ Non-xdat files and folders (e.g. `logs/`, PowerPoints, writeups) are automatically cataloged as Drive items during `scan()`.
73
+
74
+ ```python
75
+ # Query items using pandas directly
76
+ catalog.items_df
77
+ catalog.items_df[catalog.items_df["drive_path"].str.startswith("2026-02-15_batch")]
78
+ catalog.items_df[catalog.items_df["is_folder"] == True]
79
+
80
+ # Download an item (drive_path is the slash-joined path to the item's parent folder)
81
+ catalog.download_item("2026-02-15_batch/reaching", "logs")
82
+
83
+ # Get the local path, downloading automatically if needed
84
+ path = catalog.get_item_path("2026-02-15_batch/reaching", "logs")
85
+
86
+ # Render an annotated tree of the full Drive hierarchy
87
+ print(catalog.file_tree())
88
+
89
+ # Print a headline summary report: counts, download progress, date
90
+ # range, and Drive/local storage totals
91
+ print(catalog.summary())
92
+
93
+ # verbose=True adds an item-type breakdown, largest entries, itemized
94
+ # incomplete recordings, a per-folder breakdown, and a full per-entry listing
95
+ print(catalog.summary(verbose=True))
96
+ ```
97
+
98
+ Items land under `local_data_dir/{drive_path}/{name}`, using the same Drive-mirroring convention as recordings.
99
+
100
+ ## Configuration
101
+
102
+ Create a `config.json` (outside your repo — do not commit it):
103
+
104
+ ```json
105
+ {
106
+ "credentials_path": "/path/to/service_account.json",
107
+ "root_folder_id": "your-drive-folder-id",
108
+ "local_data_dir": "/path/to/local/data",
109
+ "catalog_path": "/path/to/local/data/catalog.json"
110
+ }
111
+ ```
112
+
113
+ `Config.from_file()` locates the config file using this resolution order:
114
+
115
+ 1. Explicit `path` argument.
116
+ 2. `RADIENS_DATA_CATALOG_CONFIG` environment variable.
117
+ 3. `.secrets/config.json`, then `config.json`, searched starting in the current working directory and then each parent directory up to the filesystem root (closest directory wins) — the same convention git uses to locate `.git`, so this works the same whether you run from the repo root or a nested subdirectory (e.g. a notebook in `notebooks/`).
118
+ 4. `~/.config/radiens-drive/config.json`.
119
+ 5. `/etc/radiens-drive/config.json`.
120
+
121
+ ```python
122
+ # Automatic discovery (env var or well-known paths)
123
+ config = Config.from_file()
124
+
125
+ # Explicit path
126
+ config = Config.from_file("/path/to/config.json")
127
+ ```
128
+
129
+ The `root_folder_id` is the alphanumeric string in the Drive URL when you're inside the root data folder.
130
+
131
+ ## Authentication
132
+
133
+ This package uses a Google service account for shared access among collaborators. To set it up:
134
+
135
+ 1. Create a project in [Google Cloud Console](https://console.cloud.google.com)
136
+ 2. Enable the Google Drive API
137
+ 3. Create a service account and download its JSON credentials file
138
+ 4. Share your root Drive data folder with the service account's email address (Viewer access is sufficient)
139
+ 5. Point `credentials_path` in your config at the downloaded JSON file
140
+
141
+ Distribute the credentials file to collaborators securely — treat it like a password.
142
+
143
+ ## Installation
144
+
145
+ ```bash
146
+ pip install radiens-data-catalog
147
+ ```
148
+
149
+ or, with [uv](https://github.com/astral-sh/uv):
150
+
151
+ ```bash
152
+ uv add radiens-data-catalog
153
+ ```
154
+
155
+ Requires Python 3.12+ and a Google service account with read access to the root
156
+ Drive folder (see [Authentication](#authentication) above).