Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
no unreleased changes
[0.0.17] - 2026-07-20
- documentation updates
[0.0.16] - 2026-07-20
Added
Catalog.prefetch()now supports concurrent downloading viamax_workers(default4). Downloads run in parallel across a thread pool with thread-safe progress tracking and worker slot indicators. Passingmax_workers=1forces sequential execution.Catalog.release_recording(drive_path, base_name)andCatalog.release_item(drive_path, name)delete a recording's or item's local files and clear itslocal_path, returning the number of bytes freed. Releasing a recording removes only its own xdat files (never the shareddrive_pathdirectory or sibling entries); releasing a folder item removes its full local subtree. Both are idempotent no-ops when the entry is not downloaded.Catalog.local_footprint()returns the total on-disk size, in bytes, of all downloaded recordings and items. Together withrelease_*, 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.
Fixed
- Local size totals (the
summary()"local" row, and nowlocal_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 Drivesizevalues and the Drive-size total were already correct and are unchanged.
[0.0.15] - 2026-07-18
Added
match_series(), exported from the package root, for filtering an already-fetchedrecordings_df/items_df(or anypd.Series[str]) with the same exact/prefix/contains semanticsCatalog.list_recordings()andCatalog.list_items()use internally.
Changed
Catalog'sdrive_path-taking methods (list_recordings(),list_items(),get_recording(),download_recording(),get_recording_path(),download_item(),get_item_path(),prefetch()) now acceptstrorPathfordrive_path— previouslystr-only.Pathvalues are converted internally, so aPathbuilt via ordinary path arithmetic (e.g.some_path.parent) can be passed directly instead of failing a silentPath != strcomparison.
[0.0.14] - 2026-07-18
Added
base_name/base_name_modefiltering onCatalog.list_recordings()andCatalog.prefetch()— recordings can now be queried and bulk-downloaded by filename stem (exact match, prefix, or substring), not just bydrive_path.MatchModetype (Literal["exact", "prefix", "contains"]), exported from the package root, describing how a filter value is matched.Catalog.get_recording()accepts an optionaldrive_pathargument to disambiguate recordings that share abase_nameat different Drive locations.
Changed
- Breaking:
Catalog.list_recordings(),Catalog.list_items(), andCatalog.prefetch()replace thedrive_path_prefix/drive_path_containsparameters with a singledrive_path_mode: MatchModeparameter (default"exact"), paired withdrive_path. - Breaking: All filter parameters on
list_recordings(),list_items(), andprefetch()are now keyword-only.
[0.0.13] - 2026-07-16
Added
size: int | Nonefield onRecordingEntryandDriveItemEntry, populated from Drive's file size metadata duringscan()(no download required). For recordings, it's the combined size of the constituent xdat files that reported a size.Nonefor 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 (sizedefaults toNone).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). Passrescan=Trueto refresh the catalog first. Passverbose=Truefor 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.
Changed
Config.from_file()auto-discovery of.secrets/config.json/config.jsonnow 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 innotebooks/).
Fixed
- The
~/.config/radiens-drive/config.jsonfallback 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.
[0.0.12] - 2026-07-16
Fixed
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. DirectConfig(...)construction is unaffected and still resolves relative paths against the current working directory.
[0.0.11] - 2026-06-08
Added
RecordingEntryTypedDict — replacesDatasetEntry(fields unchanged).DriveItemEntryTypedDict — replacesAssetEntry;asset_name/asset_typefields replaced byname: strandis_folder: bool.AmbiguousRecordingErrorexception (subclass ofCatalogError) — raised byget_recording()when more than one recording matches a givenbase_name.Catalog.get_recording(base_name)— look up a recording by base name, requiring it to be unique across all drive paths. RaisesEntryNotFoundError(zero matches) orAmbiguousRecordingError(multiple matches).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].xdat_recording_exists_locally()utility — replacesxdat_dataset_exists_locally().
Changed
- Breaking:
DatasetEntryrenamed toRecordingEntry;AssetEntryrenamed toDriveItemEntry. - Breaking:
Catalog.recordings_dfreplacesCatalog.df;Catalog.items_dfreplacesCatalog.assets_df. - Breaking:
Catalog.list_recordings()replacesCatalog.list_datasets();Catalog.list_items()replacesCatalog.list_assets(). Theasset_type: str | Nonefilter onlist_items()is replaced byis_folder: bool | None. - Breaking:
Catalog.download_recording()replacesCatalog.download_dataset();Catalog.get_recording_path()replacesCatalog.get_dataset_path(). - Breaking:
Catalog.download_item()replacesCatalog.download_asset();Catalog.get_item_path()replacesCatalog.get_asset_path(). - Breaking:
ScanResultfields renamed:datasets_new/existing/removed→recordings_new/existing/removed;assets_new/existing/removed→items_new/existing/removed. - Breaking:
PrefetchResultfields renamed:datasets_downloaded/skipped→recordings_downloaded/skipped;assets_downloaded/skipped→items_downloaded/skipped. - Breaking:
prefetch()parameterasset_typereplaced byis_folder: bool | None. - 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. scan_drive()andscan_drive_flat()now catalog all non-xdat files and all non-root folders asDriveItemEntryregardless of depth. The previous depth-based_classify_itemheuristic is removed entirely — consumer code owns the semantic classification of items._load_catalogreads both new ("recordings"/"items") and old ("datasets"/"assets") JSON keys, and handles bothname/is_folderand legacyasset_name/asset_typefield names, so existing catalog files continue to load without error.
Removed
DatasetEntry,AssetEntry— replaced byRecordingEntryandDriveItemEntryrespectively.xdat_dataset_exists_locally()— replaced byxdat_recording_exists_locally().- Depth-based
_classify_item()internal function — catalog no longer applies structural heuristics to classify items.
[0.0.10] - 2026-06-04
Added
xdat_dataset_exists_locally()utility to verify all three canonical xdat files (_data.xdat,.xdat.json,_timestamp.xdat) exist locally.Catalog.scan()now warns when a dataset on Drive is missing one or more required xdat files.Catalog.get_dataset_path()andCatalog.get_asset_path()now automatically trigger ascan()if the requested entry is not found in the local catalog, improving resilience to out-of-date catalogs.
Changed
- Catalog JSON writes are now atomic (using a temporary file and
replace()) to prevent data loss during interruptions. - File downloads are now atomic (using a
.tmpsuffix) to ensure interrupted downloads do not leave partial or corrupted files on disk. 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.- Refactored
Cataloginternal path restoration logic into_restore_dataset_local_paths()and_restore_asset_local_paths().
Fixed
- Improved validation of asset entries during catalog load;
drive_pathmust now be a non-empty string. - Streamlined local path construction for assets in
download_asset().
[0.0.9] - 2026-05-20
Added
upload_timecolumn inCatalog.dfandCatalog.assets_df: a UTC-awaredatetime64column populated from the Drive API'screatedTimefield duringscan(). Rows without a recorded date areNaT. Existing catalogs without this field load cleanly — missing values default toNone/NaT.
Changed
Catalog.prefetch()progress bar now covers only items being downloaded (not skipped ones), so the total count and ETA reflect real work. The bar is suppressed entirely when all matching items are already local.- Skipped items in
prefetch()are logged atDEBUGlevel instead of being silently counted. - Per-item download log messages in
Catalog.download_dataset()andCatalog.download_asset()lowered fromINFOtoDEBUG. Theprefetch()progress bar is the intended progress signal for bulk operations; direct single-item calls remain observable via debug logging.
[0.0.8] - 2026-04-21
Added
Catalog.scan()now returns aScanResultdataclass with counts ofdatasets_new,datasets_existing,datasets_removed,assets_new,assets_existing, andassets_removed— making it easy to inspect what changed after a Drive rescan.ScanResult.__str__renders as a compact table.ScanResultdataclass exported from the top-level package.- Progress bar during Drive file traversal in
scan()(respects thequiet=flag passed toCatalog). - Progress bar in
prefetch()showing overall item count and the name of the entry currently being downloaded (also respectsquiet=). Catalog.prefetch(drive_path, drive_path_prefix, drive_path_contains, datasets=True, assets=True, asset_type=None, force=False)— bulk, idempotent downloader that skips entries already present on disk. Passforce=Trueto re-download every matching entry regardless of local state. Returns aPrefetchResultwith per-category download and skip counts.PrefetchResultdataclass exported from the top-level package.CatalogErrorandEntryNotFoundErrorexceptions exported from the top-level package.EntryNotFoundError(subclass ofCatalogError) is raised when a dataset or asset lookup fails;CatalogErroris raised for malformed catalog JSON.Catalog.list_datasets(drive_path, drive_path_prefix, drive_path_contains)— convenience method returning a filtered dataset DataFrame.Catalog.list_assets(drive_path, drive_path_prefix, drive_path_contains, asset_type)— convenience method returning a filtered asset DataFrame. Supports an additionalasset_type="folder"or"file"filter.quietkeyword argument onCatalog(...)suppresses tqdm progress bars during downloads.DatasetEntryTypedDict exported from the top-level package (previously onlyAssetEntrywas exported).
Changed
Catalog.scan()return type changed fromNonetoScanResult.- Drive scan warnings (e.g. incomplete search results) are now emitted
via
logging.getLoggerrather thanwarnings.warn. - Dataset identity is now
(drive_path, base_name)instead ofbase_namealone, allowing distinct recordings with the same base name in different folders. - Dataset downloads now mirror Drive hierarchy under local storage:
local_data_dir/{drive_path}/.... Catalog.dfandCatalog.assets_dfnow use a path-first schema and no longer include legacydate_folder/experimentcolumns.- Breaking:
Catalog.download()renamed toCatalog.download_dataset()for symmetry withdownload_asset(). - Breaking:
Catalog.get_path()renamed toCatalog.get_dataset_path()for symmetry withget_asset_path(). Catalog.download_dataset()andCatalog.get_dataset_path()now require bothdrive_pathandbase_nameto identify a dataset.- Catalog JSON parse errors now raise
CatalogErrorinstead ofValueError. "Not found" errors on single-item download / get_path methods now raiseEntryNotFoundErrorinstead ofValueError.
Removed
- Breaking:
Catalog(config_path=...)constructor shortcut removed. UseCatalog(Config.from_file("path"))orCatalog()(which callsConfig.from_file()with auto-discovery). - Removed
Catalog.list_experiment_paths(),Catalog.list_by_path(), andCatalog.status()convenience methods. UseCatalog.list_datasets()/Catalog.list_assets()or operate on the raw DataFrames directly.
Fixed
Catalog.scan()now preserveslocal_pathfor datasets by(drive_path, base_name)during refresh, preventing path collisions when multiple folders contain the samebase_name.py.typedmarker (PEP 561) — the package now declares itself as typed, allowing mypy to use its annotations in downstream consumers.Catalog.scan()now reconcileslocal_pathagainst disk rather than trusting the previous catalog blindly. If the catalog is deleted, entries whose data already exists at the canonical local path (local_data_dir/{drive_path}for datasets,local_data_dir/assets/{drive_path}/{asset_name}for assets) are recovered automatically instead of triggering unnecessary re-downloads. Stale catalog paths that no longer exist on disk are also cleared.
[v0.0.7] - 2026-04-08
Changed
Catalog.scan()now defaults to the flat scanner (flat=True), which is faster and works even when the root folder is inaccessible. Passflat=Falseto use the recursive traversal.- Removed automatic fallback logic from
scan(). The caller now chooses the scan strategy via theflatkeyword argument.
[v0.0.6] - 2026-04-07
Added
Catalog.list_experiment_paths()andCatalog.list_by_path()to help discover and filter datasets by their full Drive path.
Fixed
- Depth-based asset cataloging: nested subfolders inside experiments (depth 3+) are now correctly cataloged as folder assets. Previously only direct subfolders (depth 2) were included.
[v0.0.5] - 2026-04-06
Added
- Flat-scan fallback: when the root folder is inaccessible (HTTP 403/404) or
returns an empty listing,
Catalog.scan()automatically falls back to a flatfiles.list()of all files visible to the service account and reconstructs the folder hierarchy fromparentsmetadata. AUserWarningis emitted when the fallback activates. scan_drive_flat()internal function for flat-listing all Drive files with paginatedpageSize=1000requests and parent-chain path reconstruction.Catalogcan now be created without a pre-builtConfigobject:Catalog(config_path="/path/to/config.json")andCatalog()are both supported.
Changed
Config.from_file()discovery now also checks~/.config/radiens-drive/config.jsonand/etc/radiens-drive/config.jsonafter the existing environment variable and local path checks.
[0.0.4] - 2026-04-02
Fixed
[0.0.3] - 2026-04-02
Changed
Config.from_file()now auto-discovers config files when called with no arguments: checksRADIENS_DRIVE_CATALOG_CONFIGenv var, then.secrets/config.json, thenconfig.jsonin the current working directory. RaisesFileNotFoundError(previouslyValueError) when no config is found.
[0.0.2] - 2026-04-02
Added
AssetEntryTypedDict for non-xdat Drive content (folders likelogs/, PowerPoints, writeups, etc.).scan_drive()now returns(datasets, assets)tuple. Assets are auto-discovered during scan: non-xdat files inside date or experiment folders become file assets; subfolders of experiment folders become folder assets and are still recursed for xdat datasets.download_asset()indrive.pyfor downloading file or folder assets; folder assets are downloaded recursively, mirroring the Drive subtree.Catalog.assets_dfproperty — full asset catalog as a pandas DataFrame.Catalog.list_assets()— query assets with optionaldate_folder,experiment, andasset_typefilters.Catalog.download_asset(drive_path, asset_name)— download an asset tolocal_data_dir/assets/{drive_path}/{asset_name}.Catalog.get_asset_path(drive_path, asset_name)— return local path, downloading automatically if needed.- Catalog JSON format changed from a bare list to
{"datasets": [...], "assets": [...]}. Old flat-list catalogs are migrated automatically on the nextscan(). AssetEntryexported from the top-level package.
Changed
Catalog.list()andCatalog.list_assets(): renamed thedateparameter todate_folderto match the catalog column name and make clear that an exact folder name (e.g."2026-02-15_batch") is required, not a date prefix.
[0.0.1] - 2026-04-02
Added
Configdataclass withfrom_file()classmethod; supports~and$ENV_VARexpansion in path fields andRADIENS_DRIVE_CATALOG_CONFIGenv var fallback.build_drive_service()for authenticating with a Google service account.scan_drive()for recursive Drive scanning; returnsDatasetEntryrecords withdate_folder,experiment,drive_path, anddrive_file_ids.download_dataset()for chunked download of xdat filesets to a local directory.Catalogclass withscan(),df,list(),download(),get_path(), andstatus().- MkDocs documentation site with Material theme, auto-generated API reference,
and versioned deployment via
mike. - CI/CD workflows: linting, type checking, tests, docs deployment, and PyPI publishing via GitHub Actions.