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.
- radiens_data_catalog-0.1.0/.gitignore +25 -0
- radiens_data_catalog-0.1.0/CHANGELOG.md +361 -0
- radiens_data_catalog-0.1.0/LICENSE +21 -0
- radiens_data_catalog-0.1.0/PKG-INFO +156 -0
- radiens_data_catalog-0.1.0/README.md +144 -0
- radiens_data_catalog-0.1.0/pyproject.toml +102 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/__init__.py +27 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/auth.py +30 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/catalog.py +1703 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/config.py +169 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/drive.py +654 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/exceptions.py +17 -0
- radiens_data_catalog-0.1.0/src/radiens_data_catalog/py.typed +0 -0
- radiens_data_catalog-0.1.0/tests/__init__.py +1 -0
- radiens_data_catalog-0.1.0/tests/unit/__init__.py +0 -0
- radiens_data_catalog-0.1.0/tests/unit/conftest.py +103 -0
- radiens_data_catalog-0.1.0/tests/unit/test_basic.py +5 -0
- radiens_data_catalog-0.1.0/tests/unit/test_catalog.py +2917 -0
- radiens_data_catalog-0.1.0/tests/unit/test_config.py +235 -0
- radiens_data_catalog-0.1.0/tests/unit/test_drive.py +1585 -0
|
@@ -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).
|