Skip to content

feat(data): integrate vosfs data commands - #217

Open
shinybrar wants to merge 58 commits into
mainfrom
feat/vosfs-support
Open

feat(data): integrate vosfs data commands#217
shinybrar wants to merge 58 commits into
mainfrom
feat/vosfs-support

Conversation

@shinybrar

@shinybrar shinybrar commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Implements epic #150: named VOSpace services and the upstream canfar data integration.

Closes #150.

Delivered

Packaging and policy

  • Normal dependencies (no data extra):
    • vosfs @ git+https://github.com/shinybrar/vosfs@v0.8.0
    • fsspec-cli @ git+https://github.com/shinybrar/vosfs@fsspec-cli-v0.7.0#subdirectory=src/fsspec-cli
  • Standard installation: pip install canfar.
  • Recursive copy enabled; recursive removal disabled.
  • Cross-source mv remains unsupported; documented workflow is cp, verify, then separate rm.
  • No CANFAR VOSpace backend, transfer engine, FUSE layer, public storage API, JSON/YAML wrapper, progress/prompt layer, or active alias.

Review

  • Per-ticket fixed-base Standards and Spec reviews completed.
  • GPT-5.6 Sol High end-of-wave reviews completed after every integration wave.
  • Final GPT-5.6 Sol xHigh suite completed across thermonuclear maintainability, architecture, over-engineering, deslop, and simplification lenses.
  • Final xHigh fixes removed hidden configuration state, made registry pairing environment-safe, moved discovery orchestration into a private focused module, made capability enrichment concurrent, and materialized immutable runtime credential snapshots before fan-out.
  • Final architecture review: clean.
  • Final thermonuclear targeted confirmation: clean.

Validation at final epic head

  • uv run pre-commit run --all-files: passed.
  • uv run --no-sync ruff check . --no-cache: passed.
  • uv run --no-sync ruff format --check .: passed.
  • uv run ty check canfar: passed; the explicitly authorized pre-existing unused-ignore warning class is ignored, with no other ty rule weakened.
  • uv run --no-sync pytest tests -m "not slow" --no-cov -q -o cache_dir=/tmp/canfar-pytest-cache: 811 passed.
  • uv run --group docs mkdocs build: passed; the git-committers plugin logged its known GitHub API rate-limit warning without failing the build.
  • uv build: sdist and wheel passed.
  • Optional authenticated smoke: cleanly skipped before live I/O because no configured canfar service / valid saved Authentication Record and certificate were available.

Upstream audit


Update (2026-07-25)

Work added to this branch after the original description was written.

Upstream releases bumped

  • vosfs v0.6.0 -> v0.7.0, fsspec-cli v0.5.0 -> v0.6.0.
  • fsspec-cli v0.6.0 hands parsing to Typer (breaking upstream). The consumed contract is unchanged: App(sources, capabilities=...), .typer_app, AsyncFilesystemSource and the capability TypedDicts are identical; only the unused CommandExtension protocol was replaced. VOSpaceFileSystem keeps its constructor and aclose().
  • Two upstream behaviours moved, so their tests were updated: disabled recursion.remove now omits -R/-r from rm entirely rather than rejecting them at runtime, and -h became a human-readable size flag for ls that requires a long listing.

arc and vault both configured

The CADC Server shipped no default storage, and discovery named a discovered Service after its Server Name, so the ARC Service was addressed as canfar: and Vault was never configured.

  • Both CADC VOSpace Services are now defaults, taken from the IVOA registry: arc (ws-uv.canfar.net/arc) and vault (cadc-west-01.canfar.net/vault). canfar data ls arc:/ and vault:/ work out of the box.
  • Configurations saved by an older client heal on load: an entry keyed by Server Name is restored to its registry leaf and missing defaults are merged in. Scoped to the default Servers, so federated Servers keep their Server Name keys and deliberate identifiers are untouched.
  • Discovery merges by IVOA URI, so rediscovery refreshes an existing entry instead of duplicating it.

Verified live against both services with a CADC certificate.

One vocabulary: Storage Identifier

The same concept was called three things — data (CLI), "Storage Name" (config and errors), identifier (operand grammar). Now settled: Storage Identifier, with canfar/storages.py renamed to canfar/storage.py and the operand placeholder written storage-identifier:/absolute/path.

Deliberately unchanged: the storage key in the configuration file (format invariant), the canfar data command group, and Server Name.

Caching

  • VOSpace directory listings are cached per command. Each command builds and closes its own filesystem, so a cached listing cannot outlive the command that produced it.
  • Documented how to cache file contents from Python against /scratch on a Session.
  • Correction: an earlier revision of these docs recommended MMapCache for byte-range caching. That was wrong and is fixed. vosfs sends no Range header, so a partial read downloads the whole object — a block cache issues one full download per block. The limit is in the client and is not uniform: vault (minoc) answers a ranged GET with 206 and Accept-Ranges: bytes, while arc (Cavern) does not. Filed upstream as Honour HTTP Range where the backend supports it (vault/minoc does; Cavern does not) shinybrar/vosfs#334.

Housekeeping

  • mypy replaced by ty, scoped to canfar/ only ([tool.ty], astral-sh/ty-pre-commit). CI type-checks via the pre-commit action, so no separate CI step changed.
  • Naming and model pass: EnrichmentWorkers -> Enrichment, _upstream_group -> group, discovery helpers shortened, remaining dataclasses converted to pydantic.
  • Design research for a future public storage Python API recorded in docs/agents/research/2026-07-25-storage-python-api.md. No public Python API ships in this PR; it is scoped to a follow-up targeting 1.5.0.

Gate

ruff clean, ty check canfar clean, pytest -m "not slow" 819 passed at 89.00% coverage, mkdocs build clean, all pre-commit hooks pass.


Amended 2026-07-25

Landed after the epic was written, at the maintainer's request. The acceptance
criteria on #150, #151, #152, and #155 were amended to match.

  • Dependencies are now vosfs@v0.8.0 and fsspec-cli-v0.7.0; v0.8.0 adds
    server-side HTTP Range, honoured by vault and not by Cavern behind arc.
  • Two CADC VOSpace Services ship as defaults, arc and vault, superseding
    Discover primary VOSpace services from registry records #152's "do not automatically add CADC Vault" and its Server-Name keying.
  • A public Python API ships in canfar.storage
    (from canfar.storage import arc, vault, local, plus identifiers(),
    filesystem(), sources()), superseding the "no public storage API"
    non-goal. Documented in docs/client/data.md.
  • Storage Name became Storage Identifier throughout, in CONTEXT.md and code.
  • A two-axis review (standards + spec) ran against origin/main; the
    standards findings are addressed in 54a5fc65.

Upstream pin provenance is recorded in docs/agents/upstream-provenance.md,
replacing the audited-commit note #155 asked for in the user docs.

@codecov-commenter

codecov-commenter commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.09959% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.48%. Comparing base (5411f95) to head (54a5fc6).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
canfar/utils/registry.py 83.33% 12 Missing and 3 partials ⚠️
canfar/server.py 88.42% 7 Missing and 4 partials ⚠️
canfar/client.py 75.00% 4 Missing and 4 partials ⚠️
canfar/models/config.py 78.94% 4 Missing and 4 partials ⚠️
canfar/utils/vosi.py 46.66% 3 Missing and 5 partials ⚠️
canfar/auth/oidc.py 76.92% 5 Missing and 1 partial ⚠️
canfar/utils/discover.py 68.75% 2 Missing and 3 partials ⚠️
canfar/cli/data.py 91.66% 1 Missing and 1 partial ⚠️
canfar/cli/login_auth.py 83.33% 1 Missing and 1 partial ⚠️
canfar/models/http.py 95.12% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #217      +/-   ##
==========================================
- Coverage   87.69%   87.48%   -0.22%     
==========================================
  Files          59       62       +3     
  Lines        3698     4083     +385     
  Branches      440      502      +62     
==========================================
+ Hits         3243     3572     +329     
- Misses        332      365      +33     
- Partials      123      146      +23     
Flag Coverage Δ
3.10 87.46% <86.09%> (-0.21%) ⬇️
3.11 87.46% <86.09%> (-0.21%) ⬇️
3.12 87.46% <86.09%> (-0.21%) ⬇️
3.13 87.46% <86.09%> (-0.21%) ⬇️
3.14 87.48% <86.09%> (-0.22%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@shinybrar shinybrar added the ready-for-human Requires human implementation label Jul 22, 2026
@shinybrar

Copy link
Copy Markdown
Contributor Author

Implementation and automated review are complete. All required CI checks pass, the end-of-wave review is clean, and the xHigh five-lane mega review is clean after fix 5f75b131. Landing is blocked only by main branch protection requiring one approving review after the last push. Auto-merge is disabled for this repository, and the author cannot self-approve. No admin bypass was used.

@shinybrar
shinybrar marked this pull request as draft July 22, 2026 22:10
@shinybrar shinybrar removed the ready-for-human Requires human implementation label Jul 22, 2026
@shinybrar shinybrar changed the title feat(config): configure named VOSpace services feat(data): integrate vosfs data commands Jul 22, 2026
Comment thread tests/test_storage.py Fixed
Comment thread tests/test_storage.py Fixed
@shinybrar
shinybrar marked this pull request as ready for review July 23, 2026 02:13
Rename the discovery seam for clarity and drop the last dataclasses in
favour of pydantic models, per repo convention.

- _discovery: load_registry_evidence -> discover, discover_storage_resource
  -> discover_storage (server_uri/server_url/server_name -> uri/url/name),
  prepare_enrichment_workers -> enrich, select_storage_resource ->
  select_storage; inline the single-use _registry_namespace helper.
- RegistryEvidence and EnrichmentWorkers are frozen pydantic models.
- Rename preferred_storage_leaf -> leaf across IdpInfo, IVOARegistrySearch,
  and RegistryEvidence.
- auth.oidc: _refresh_parameters -> _refresh, _persist_refreshed_credential
  -> _persist.
- Move _storage.py to storages.py, relocate the local filesystem adapter out
  of the CLI, and expose sources() so cli/data.py only mounts the Typer app.
- Google-style docstrings on all touched functions.

No behaviour change.
The CADC Science Platform Server shipped no default storage, and discovery
named a discovered Service after its Server Name, so the arc Service was
addressed as `canfar:` and vault was never configured at all.

- Add both CADC VOSpace Services to the default Server, taken from the IVOA
  registry: arc (ws-uv.canfar.net/arc) and vault
  (cadc-west-01.canfar.net/vault). Both authenticate with the same cadc
  credentials, so `canfar data ls arc:/` and `vault:/` work out of the box.
- Heal Servers saved by an older client on load: an entry keyed by Server
  Name is restored to its registry leaf and missing default Services are
  merged in. Scoped to the default Servers, so federated Servers keep their
  Server Name keys and deliberate Storage Names are left untouched.
- Merge discovered Services by IVOA URI so rediscovery refreshes an existing
  entry in place instead of duplicating it under the Server Name.

Verified against the live services with a CADC certificate.
fsspec-cli v0.6.0 hands parsing to Typer, a breaking upstream change. The
consumed contract is unchanged: App(sources, capabilities=...), .typer_app,
AsyncFilesystemSource, and the capability TypedDicts are all identical, and
only the unused CommandExtension protocol was replaced. VOSpaceFileSystem
keeps its token/certfile/asynchronous/storage_options constructor and
aclose() through its v0.7.0 internal decomposition.

Two upstream behaviours moved, so their tests were updated to match:

- Disabled recursion.remove now omits the flag from `rm` entirely rather
  than registering and rejecting it, so `rm -R` reports `No such option: -R`.
  Verified the capability is still wired: `cp` registers -R/-r, `rm` does not.
- `-h` is now a human-readable size flag for `ls` that requires a long
  listing, instead of an unsupported option.

No production code changes.
Every claim below was reproduced against the CLI before documenting it.

- Use the real Storage Names arc and vault instead of the stale `canfar:`,
  including in quick-start and cli-help.
- Add a real cross-source example copying a 166K public test cutout from
  vault into an arc home directory.
- Drop the dependency-pin section and the audited-commit block; point at the
  current tagged releases instead.
- Simplify the source-mapping paragraph.
- Correct `ls -h`: it is a supported flag that requires a long listing, not
  an unsupported option. `ll` is retained, as it is an upstream command.
- Correct the recursive-removal diagnostic to `No such option: -R`.
Comment thread tests/test_storage.py Fixed
Comment thread tests/test_storage.py Fixed
`rm -R` is rejected by Typer, which renders its usage error through rich. The
panel wraps to the terminal width and carries ANSI colour, so asserting the
substring `No such option: -R` passed locally on a wide console and failed on
every CI Python.

Assert the width-independent behaviour instead (exit status 2, empty stdout)
and pin the real invariant structurally: the recursion capability withholds
-R/-r from `rm` while still registering them on `cp`.
Build every VOSpace filesystem with fsspec's listing cache enabled, so a
command that walks a tree stops re-requesting the same directories. A repeated
listing drops from a round trip to a dictionary lookup.

Each command builds and closes its own filesystem, so a cached listing cannot
outlive the command that produced it and no cross-command staleness is
possible.
Explain the automatic listing cache and show how to cache file contents from
Python, pointing the cache at /scratch on a CANFAR session since it is fast
local disk that is cleared with the session.

VOSpace serves ranged reads, so document MMapCache for caching a large cube
one block at a time instead of downloading it whole; reading a FITS header
transfers a single block. Both snippets were run against the live service
before being documented.
Rename EnrichmentWorkers to Enrichment and cli.data._upstream_group to
group.

The group rename also renames the local variable it was assigned to inside
_DataGroup._delegate, which would otherwise shadow the module-level function
and raise UnboundLocalError. Making group public drops the private-access
suppressions from its tests.

No behaviour change.
The operand grammar used `Storage-Name` as its placeholder, which reads as the
glossary noun rather than the slot it occupies, and does not cover the reserved
`local` prefix. Call the placeholder `identifier` and state that it is either a
Storage Name or `local`.

Keeps the `Storage Name` glossary term from CONTEXT.md intact, since runtime
errors use it verbatim.
The caching section claimed VOSpace serves ranged reads and recommended
MMapCache for fetching a cube one block at a time. That is wrong and the advice
was actively harmful.

vosfs `_cat_file(path, start, end)` reads the whole object and slices it in
memory (filesystem.py), because OpenCADC Cavern does not implement HTTP byte
ranges (staging.py); the package sends no Range header anywhere. A block cache
therefore issues one full download per block, so reading three headers out of
one cube downloads that cube three times.

Replace the recommendation with its opposite: cache whole files, use exactly
one cache layer, and avoid block caches. Also record that chained cache layers
do not compose - the inner layer is never filled and never serves.

Adds the supporting research note under docs/agents/research/.
The previous correction blamed the missing byte ranges on OpenCADC Cavern and
generalised that to every VOSpace Service. Verified live against both services,
that is wrong: vault is backed by minoc and answers a ranged GET with 206,
Accept-Ranges: bytes and Content-Range: bytes 0-2879/3542400, while arc returns
the whole body and advertises no range support.

The limitation that matters is in the client: vosfs sends no Range header, so a
partial read downloads the whole object on either service, and block caches
stay a pessimization today. State it that way, and record that teaching vosfs
to send Range would unlock real byte-range reads on vault but not on arc.

Corrects the same conflation in the research note.
The same concept was called three things: `data` in the CLI, "Storage Name" in
configuration and errors, and `identifier` in the operand grammar. Settle on one
vocabulary.

- CONTEXT.md renames the term to Storage Identifier and records that the
  reserved `local` identifier addresses the machine the command runs on.
- Rename canfar/storages.py to canfar/storage.py, matching the singular module
  convention and the noun the domain now uses.
- Reword user-visible errors, docstrings, and docs to Storage Identifier, and
  make the operand placeholder `storage-identifier:/absolute/path` so it names
  the slot rather than borrowing the glossary noun.

Deliberately unchanged: the `storage` key in the configuration file, which is
already the settled word and whose format is an invariant; the `canfar data`
command group, which names the action rather than the thing; and Server Name,
which is a separate term.

This is naming only. Verified the saved configuration still loads and both
Storage Identifiers still resolve.
Comment thread tests/test_storage.py
Comment thread tests/test_storage.py
Upstream shinybrar/vosfs#335 teaches vosfs to send an HTTP Range header and
use the response when the byte endpoint answers 206, closing the gap this
project reported in shinybrar/vosfs#334.

Verified against the live services:

- vault (minoc) now serves real partial reads. A cat_file(path, 0, 2880) on a
  3542400 byte cube sends `Range: bytes=0-2879` and receives `HTTP 206` with
  `Content-Range: bytes 0-2879/3542400`, transferring 2880 bytes.
- MMapCache over a cat_file fetcher is genuinely partial: one ranged request,
  one block of four materialised, no whole-object fallback. On 0.7.0 the same
  operation downloaded the whole object per block.
- arc (Cavern) serves no ranges and still falls back to a whole-object read
  that is sliced; slices remain correct, so nothing regresses there.
- blockcache still refuses, because Range is honoured for byte reads and not
  through the file-object path: StagedReadFile has no blocksize attribute.

Docs replace the "do not cache byte ranges" guidance with per-backend
guidance, and the research note carries an update banner recording which of
its 0.7.0 conclusions are superseded.
Document how to reach a VOSpace Service from Python: resolve the endpoint and
credential from configuration, then use the vosfs fsspec filesystem. Covers
filesystem operations, materialising a local path, the three cache tiers
(whole file, byte range, RAM), the per-backend range table, integration with
astropy, numpy, pandas, and dask, and the async form.

The page states plainly that no canfar.storage module exists yet and points at
the CLI guide for the equivalent surface, so it documents what ships rather
than a planned API.

Every example was executed against the live services before being written:
astropy reports NAXIS=4 and shape (1, 96, 26, 16) for the test cutout, the
APASS table converts to a 2373-row DataFrame, and dask's lazy mean matches
numpy's to the last digit.
Add the public Python surface to canfar.storage so a Storage Identifier is
importable by name:

    from canfar.storage import arc, vault, local

A module-level __getattr__ resolves any configured Storage Identifier to a
ready, authenticated synchronous filesystem, and __dir__ offers them for tab
completion. Alongside it: identifiers() lists what is addressable,
filesystem() builds one explicitly with an optional runtime credential, and
fetch() copies one object to local disk and returns its Path. The existing
sources() seam used by the data command is unchanged.

Credentials are materialized through fsspec's background event loop, so the
synchronous builder also works from inside a running loop.

__getattr__ checks membership before building rather than catching KeyError
from the build, so a failure to authenticate surfaces as itself instead of as
a misleading "no attribute" error.

Docs use the new imports, and the CLI guide no longer claims the release ships
no public storage API.
Standards axis findings from the two-axis review, plus the Storage Identifier
vocabulary sweep.

Documented-standard violations:
- Glossary: no `storage_name` identifier remains. `_resolve_storage(identifier)`,
  `server_by_identifier`, `_validate_storage_identifiers`, and the "Storage
  Service" docstring now use CONTEXT.md terms.
- Home: `canfar/_discovery.py` moves to `canfar/utils/registry.py`, the
  location AGENTS.md and the architecture Module Map document, and no longer
  sits beside the near-homonymous `canfar/utils/discover.py`.
- Models over dicts: `HTTPClient.build()` replaces the three hand-built
  `client_kwargs` dicts, still omitting unset credentials so settings sources
  keep filling them.

Smells:
- `RuntimeCredential` replaces `tuple[str | None, str | None]`, so the four
  consumers no longer re-switch on which half is None.
- `_vospace` and `filesystem` share `_resolve` and `_build`; the synchronous
  path reuses the asynchronous resolution through fsspec's loop.
- `Configuration.storage_identifiers()` owns the server/storage walk that
  `sources()` and `identifiers()` both duplicated.
- `Workers` replaces the misleading `Enrichment`; `evidence`/`workers` no
  longer collide with the public `server.discover`/`enrich`.
- `LOCAL` and `RESERVED_IDENTIFIERS` live once in `models/http.py`; the
  validator now rejects every identifier that would shadow the module surface.
- `__all__` drives `__dir__`, so completion cannot drift.

Also drops `fetch()` from the public surface in favour of fsspec's `get_file`,
and gives `filesystem("local")` the `skip_instance_cache=True` every other
construction site uses.

Adds docs/agents/upstream-provenance.md recording that both distributions are
pinned to tagged Git refs on a personal fork, with the pin history.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate upstream vosfs and fsspec-cli as canfar data

2 participants