Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
3f4748c
Add CTable .b2z support: metadata model, fetch pipeline via to_cframe…
FrancescAlted Jun 30, 2026
32947e8
Fix CTable whole-fetch bug, add Array/Table client hierarchy, and .b2…
FrancescAlted Jul 1, 2026
6a45518
Add web preview and CLI support for CTable (.b2z) datasets
FrancescAlted Jul 1, 2026
93d9e81
Harden CTable (.b2z) row slicing, previews, and CLI edge cases
FrancescAlted Jul 1, 2026
ebd52a8
Much cleaner error message for cat2-client on invalid commands
FrancescAlted Jul 1, 2026
86e850a
Much cleaner error message for cat2-client on invalid commands
FrancescAlted Jul 1, 2026
2adefd8
Potential fix for pull request finding
FrancescAlted Jul 1, 2026
6ce34ca
Fix hardcoded suffix set in get_abspath() to use srv_utils.BLOSC2_NAT…
Copilot Jul 1, 2026
e3fc9b3
Potential fix for pull request finding
FrancescAlted Jul 1, 2026
e410ea3
Add test for the table[-1] fix
FrancescAlted Jul 1, 2026
8ee2af2
Support browsing TreeStore .b2z containers via virtual paths
FrancescAlted Jul 1, 2026
4a7e954
Browse hierarchical .b2z (TreeStore) containers and directories
FrancescAlted Jul 1, 2026
af2cce6
Add plan for virtual descent for h5 too
FrancescAlted Jul 1, 2026
04ba213
Add mountable virtual roots for .b2z TreeStore containers in web UI
FrancescAlted Jul 2, 2026
aee01e4
Browse .h5/.hdf5 files as mountable virtual roots
FrancescAlted Jul 2, 2026
4070d91
Report each dataset's own size in the web listing, not the container's
FrancescAlted Jul 2, 2026
2be8746
[WebUI] CTable sorting now works using blosc2's sort_by()
FrancescAlted Jul 3, 2026
004e2b9
[WebUI] Table sorting works with ascending/descending directions now
FrancescAlted Jul 3, 2026
3b9e14a
feat: filter/sort for .b2z TreeStore container members + fix 0d scala…
FrancescAlted Jul 3, 2026
2261a80
feat: sort for .h5 container members + avoid full numpy materialization
FrancescAlted Jul 3, 2026
d93a19f
Different fixes for .b2z and .h5 navigation:
FrancescAlted Jul 3, 2026
454b032
[WebUI] Mouse/touch scroll is supported in Datasets and Display panels
FrancescAlted Jul 3, 2026
6225364
Add click-to-select + arrow-key navigation for datasets and data rows
FrancescAlted Jul 3, 2026
51773ec
Initial plans for remote peer mounts
FrancescAlted Jul 4, 2026
3ec12dd
Initial implementation of plan
FrancescAlted Jul 4, 2026
dbb67dd
Fix peer-mounts review findings: correctness, error taxonomy, event-l…
FrancescAlted Jul 4, 2026
a1c1987
hdf5 required for the new server
FrancescAlted Jul 5, 2026
eab1d62
Add Docker/Apple-container tooling to manually exercise peer mounts
FrancescAlted Jul 5, 2026
19742ec
Make peer roots fully browsable from the web UI
FrancescAlted Jul 6, 2026
161cb1b
Add missing files
FrancescAlted Jul 6, 2026
64c5fea
Add compiled static .js and .css files
FrancescAlted Jul 6, 2026
e4d9ac2
Decouple peer-mount capability into a c2cache provider plugin
FrancescAlted Jul 6, 2026
9f5aabb
test_peers: add concurrency regression test for peer cache eviction
FrancescAlted Jul 6, 2026
2f8eacb
Replace peercache's global io_lock with per-cache locks
FrancescAlted Jul 8, 2026
ecd6091
Updated instruction for the ongoing split
FrancescAlted Jul 8, 2026
81849e0
blosc2 requirement updated to 4.8.0
FrancescAlted Jul 9, 2026
2a31271
c2cache: adopt blosc2's real async aget_chunk, drop the to_thread shim
FrancescAlted Jul 9, 2026
6a4ffc4
New plan for allowing CTable mounted from remote peers
FrancescAlted Jul 10, 2026
ac417b7
Updated plan
FrancescAlted Jul 10, 2026
768dac3
New simplified version for supporting .b2z files living in remote peers
FrancescAlted Jul 10, 2026
bad3e68
Implements plans/caterva3-remote-peer-simplified.md:
FrancescAlted Jul 11, 2026
2711c73
Require blosc2 >= 4.8.1 for zero-copy from_cframe safety
FrancescAlted Jul 11, 2026
fb38325
Add unit test: null sentinels survive the structured CTable cache
FrancescAlted Jul 11, 2026
f3ae633
Add a todo list
FrancescAlted Jul 11, 2026
fac0787
Retry peer chunk fetch once before marking the peer offline
FrancescAlted Jul 11, 2026
8c1694d
Enforce per-peer cache_quota with a scoped eviction pass
FrancescAlted Jul 11, 2026
99508ae
Relay whole-file api/download for peer paths
FrancescAlted Jul 11, 2026
0d782ee
Pin DictStore .b2z container browsing with a test
FrancescAlted Jul 11, 2026
58b2199
New plans for cache server and dynamic mounts
FrancescAlted Jul 11, 2026
5675997
Some refinements on plans
FrancescAlted Jul 12, 2026
c98265f
Cache whole-file .b2 entries for peer plain-file downloads
FrancescAlted Jul 14, 2026
76b2854
Say which responses serve byte ranges, and refuse the rest
FrancescAlted Aug 17, 2026
735e90a
Serve chunks of a container leaf, and refuse the ones that are not st…
FrancescAlted Aug 18, 2026
3576fd4
Serve a container leaf from its window in the file
FrancescAlted Aug 18, 2026
42a958d
Merge pull request #289 from ironArray/range-honesty
FrancescAlted Aug 20, 2026
542b7a5
Accept one chunk at a time into a slot that holds none
FrancescAlted Aug 20, 2026
2585cfa
Publish an array once every one of its chunks has landed
FrancescAlted Aug 20, 2026
7f5510c
Move a published array into place instead of streaming into it
FrancescAlted Aug 20, 2026
33d9f2a
Name a filled array by a nonce, and say when it is complete
FrancescAlted Aug 21, 2026
64f1bdf
Reach the fill from the client, and say in the docs that it is there
FrancescAlted Aug 21, 2026
d760c6f
Say on api/info whether a dataset can be read in ranges
FrancescAlted Aug 22, 2026
2aa1928
Gather scattered coordinates on the server, for clients that ask
FrancescAlted Aug 22, 2026
a45fe2a
Take a fetch's parameters in a body, for keys an URL cannot hold
FrancescAlted Aug 22, 2026
e230409
Let this client ask for coordinates too, not only boxes
FrancescAlted Aug 22, 2026
17f5527
Browse a DictStore .b2z, and stop caching nulls the cache cannot carry
FrancescAlted Aug 22, 2026
47132ad
Carry a column's validity across the cache, so masked nulls stay cach…
FrancescAlted Aug 22, 2026
91948b3
Check what a small table's ratio is, not that it flatters the table
FrancescAlted Aug 22, 2026
415b349
Keep a write inside the root that let it in
FrancescAlted Aug 23, 2026
36277e7
Refuse a fetch parameter this server cannot narrow with
FrancescAlted Aug 23, 2026
3b727c3
Say of a dataset only what fetching it will really do
FrancescAlted Aug 23, 2026
6ed6665
Check the whole of a chunk's geometry, and stage each publish apart
FrancescAlted Aug 23, 2026
b861f80
Spell a key the way the server will read it back
FrancescAlted Aug 23, 2026
3d86a68
Rebuild a peer cache whose format this code has outgrown
FrancescAlted Aug 23, 2026
b984475
Say in the notes what these endpoints actually do now
FrancescAlted Aug 23, 2026
9012f0a
Run the tests on every branch, not only on main
FrancescAlted Aug 23, 2026
1bc0fa2
Ask for fsspec, which the fill path has needed all along
FrancescAlted Aug 23, 2026
47a1cbe
Rebuild a container leaf where blosc2 cannot say where it lies
FrancescAlted Aug 23, 2026
c22f20b
Stand down the tests that need a blosc2 nobody can install yet
FrancescAlted Aug 23, 2026
bf4263f
Let the server import on a Python that reads its annotations eagerly
FrancescAlted Aug 23, 2026
989d5b4
Publish @personal under the user it belongs to
FrancescAlted Aug 28, 2026
244b13c
Read a peer CTable's info as a CTable's
FrancescAlted Aug 28, 2026
29ecf93
Name a relayed download's file once
FrancescAlted Aug 28, 2026
d0fdeda
Keep one bad peer from raising out of the registry
FrancescAlted Aug 28, 2026
13d1c31
Normalize the path in get_chunk
FrancescAlted Aug 28, 2026
0ee135b
List a container path that names one leaf as that leaf
FrancescAlted Aug 28, 2026
d88e2be
Break the connection on a leaf window that ends early
FrancescAlted Aug 28, 2026
1ba07d4
Bound the POST api/fetch body at the request, not at the parse
FrancescAlted Aug 28, 2026
438fbbd
Gather eviction candidates only when a quota is exceeded
FrancescAlted Aug 28, 2026
1482598
Hold the per-cache locks weakly
FrancescAlted Aug 28, 2026
42dc2f7
Share one filtered-array cache entry between callers
FrancescAlted Aug 28, 2026
07b217c
Ship c2cache in the container image
FrancescAlted Aug 28, 2026
d37c48b
Fix the review findings on the c2cache branch
FrancescAlted Aug 31, 2026
b06dd4f
Pin ruff at 0.16.5, and select rules instead of extending them
FrancescAlted Aug 31, 2026
bbbb5d4
Bundle C2Cache inside Caterva2
FrancescAlted Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*
!caterva2/
!c2cache/
!root-example/
!pyproject.toml
!README.md
!docker/
12 changes: 10 additions & 2 deletions .github/workflows/python-app-arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@

name: Python application (ubuntu, arm64)

# Every commit on every branch, not just `main`: work lives on long-running
# feature branches here, and a branch nobody runs the tests on is a branch whose
# tests are found broken at merge time. A pull request from a fork triggers
# `pull_request` (a fork's pushes are not pushes to this repo), so both are kept.
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

# One run per branch: pushing again supersedes the run already going, which
# matters most on the paid arm64 runner. A run for `main` is never cancelled.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
build:
runs-on: ubicloud-standard-2-arm
Expand Down
12 changes: 10 additions & 2 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@

name: Python Intel (ubuntu, amd64)

# Every commit on every branch, not just `main`: work lives on long-running
# feature branches here, and a branch nobody runs the tests on is a branch whose
# tests are found broken at merge time. A pull request from a fork triggers
# `pull_request` (a fork's pushes are not pushes to this repo), so both are kept.
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

# One run per branch: pushing again supersedes the run already going, which
# matters most on the paid arm64 runner. A run for `main` is never cancelled.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
build:

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ __pycache__
# jupyterlite
/.jupyterlite.doit.db
/caterva2/services/static/jupyterlite/
/jupyterlite-exts/.dev-jupyter-path/

# container image exports (docker/caterva2-server/, see plans/remote-peer-containers.md)
/caterva2-peer.tar
9 changes: 7 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.1
rev: v0.16.5
hooks:
- id: ruff
- id: ruff-check
args: ["--fix", "--show-fixes"]
- id: ruff-format
# Python, notebooks and stubs only. ruff-format grew Markdown support
# in 0.16 and the hook now claims `.md` by default, where blacken-docs
# below already owns those code blocks -- at black's width rather than
# this line-length, so the two rewrite each other on every run.
types_or: [python, pyi, jupyter]
exclude: ^bench/

- repo: https://github.com/adamchainz/blacken-docs
Expand Down
25 changes: 18 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ VENV = ./venv
# make lite-build BIN=$(dirname $(which jupyter))
BIN ?= $(VENV)/bin

# Repo-local Jupyter search path for the caterva2-save labextension during
# development (see lite-ext/lite-build below). Keeping it outside the active
# environment's own share/jupyter/labextensions/ means it never collides with
# what `pip install` puts there, so no manual cleanup is ever needed between
# the two.
DEV_JUPYTER_PATH = $(CURDIR)/jupyterlite-exts/.dev-jupyter-path

install:
python3 -m venv venv
${BIN}/pip install -U pip
Expand All @@ -23,22 +30,26 @@ assets:
# Build the caterva2-save JupyterLite extension (needs Node + jlpm/JupyterLab).
# Commit the resulting jupyterlite-exts/caterva2-save/labextension/ so the
# caterva2 wheel ships it and downstream installs need no Node toolchain.
# `develop --overwrite` registers it for an *editable* caterva2 install (whose
# wheel shared-data is not staged into share/jupyter/labextensions/).
# Registered for local `jupyter lite build` runs via DEV_JUPYTER_PATH (see
# lite-build) instead of the environment's own share/jupyter/labextensions/:
# an *editable* caterva2 install doesn't stage shared-data there at all, and a
# real wheel install does — either way, writing there ourselves risks
# colliding with what `pip install` manages.
lite-ext:
cd jupyterlite-exts/caterva2-save && jlpm install && jlpm build:prod
# Register for an *editable* caterva2 install (whose wheel shared-data is not
# staged into share/jupyter/labextensions/); a real wheel install does this.
mkdir -p ${BIN}/../share/jupyter/labextensions
ln -sfn $(CURDIR)/jupyterlite-exts/caterva2-save/labextension ${BIN}/../share/jupyter/labextensions/caterva2-save
mkdir -p $(DEV_JUPYTER_PATH)/labextensions
ln -sfn $(CURDIR)/jupyterlite-exts/caterva2-save/labextension $(DEV_JUPYTER_PATH)/labextensions/caterva2-save
git add jupyterlite-exts/caterva2-save/labextension

lite-build:
rm -rf .jupyterlite.doit.db caterva2/services/static/jupyterlite
# --lite-dir points at the in-package jupyter-lite.json (contentsAllJsonFile),
# so the build picks it up regardless of the working directory (the deploy
# builds from elsewhere). See caterva2/services/lite-config/.
${BIN}/jupyter lite build --lite-dir caterva2/services/lite-config --output-dir caterva2/services/static/jupyterlite
# JUPYTER_PATH adds the dev labextension (see lite-ext) on top of whatever
# is really installed; harmless/no-op if lite-ext was never run.
JUPYTER_PATH=$(DEV_JUPYTER_PATH):$$JUPYTER_PATH \
${BIN}/jupyter lite build --lite-dir caterva2/services/lite-config --output-dir caterva2/services/static/jupyterlite

# Installs our jupyterlite fork from a local copy, for development purposes
# Before doing this you must run "make build" in our jupyterlite fork
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ Append `[extra1,extra2,...]` to any install command:

**Note:** Test runs create a `_caterva2_tests` directory with state files for inspection.

### Optional peer caching

C2Cache is bundled as an internal Caterva2 provider; it requires no separate
package or installation extra. It remains inactive unless the server
configuration contains at least one `[[server.peer]]` entry. Each configured
peer exposes that server's locally owned `@public` root under the configured
local name and caches requested data on demand. See
`caterva2-server.sample.toml` for the configuration fields and cache quotas.

## Quick start

See [Caterva2 documentation](https://ironarray.io/caterva2-doc/index.html) for detailed tutorials.
Expand Down
94 changes: 94 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,100 @@

#XXX version-specific blurb XXX#

* C2Cache is bundled and registered as the internal `caterva2.c2cache`
provider. It is part of the Caterva2 wheel and release lifecycle, needs no
separate package or installed entry-point metadata, and is inert unless at
least one `[[server.peer]]` entry activates it.

* Arrays can be filled a chunk at a time, by several writers at once. An array
is laid out empty first (`Client.lay_out()`, a couple of hundred bytes
whatever its shape) and each writer posts the chunks it owns with
`Client.fill_chunk()`, over the new `POST api/chunk/{path}`. Every chunk of a
laid-out array is a slot nothing has been written to; a writer claims one by
writing it, and a second write to the same slot is refused with a 409 -- so
two writers that both believe they own a chunk are resolved by the array
rather than by anything either of them holds. `Client.written_chunks()` says
how far a fill has got, read from the frame's own offsets, so there is no
bookkeeping to fall out of step with the array. A chunk is checked against the
whole of the array's geometry before it is stored -- its chunksize, its
blocksize and its typesize -- since one compressed against another typesize
decompresses to values in the wrong places and says nothing about it.

* A filled array is published as one finished frame. Set `publish_root` in the
server configuration -- an fsspec URL of a directory -- and an array is copied
there as soon as its last chunk lands, moved into place so that what appears
at the destination is a whole frame or nothing. `POST api/publish/{path}`
(`Client.publish()`) is the primitive underneath, for finishing a publish that
was interrupted. The destination is the server's own configuration and never
something a caller names.

* `api/fetch` takes an `indices` parameter: a fancy key as JSON, one entry per
dimension -- a list of integers for a dimension indexed by coordinates, an
integer for one indexed by a scalar, a string for one indexed by a slice, null
for one taken whole. The server gathers the points and sends those, reading
only the chunks they land in. The alternative is a client fetching the blocks
the points live in and picking them out, and a block is nearly all waste for a
single coordinate: nine scattered points of a 900^3 array cost 271 bytes in one
request this way against 237 KB in nineteen. Not combinable with `slice_`,
`filter` or `field`, and refused -- rather than quietly dropped -- for
anything that cannot gather points: a container, or a dataset mounted from a
peer, which a provider fetches boxes of. A key may name at most a million
coordinates, since it travels in a body now and is no longer bounded by what a
URL holds. blosc2 4.11 sends it for `C2Array[[...]]`; earlier clients never do.

* `Client.get_slice()` and `ds[key]` take a fancy key -- a list or array of
coordinates, or a boolean mask -- and send it as `indices` for the server to
gather, changing verb to POST where it is too long for a URL. What comes back
is the points, not the chunks holding them. `slice_to_string()` now refuses an
index it cannot express instead of dropping it: a dropped index left an empty
slice string, which asks for the whole dataset and hands back all of it,
neither what was asked for nor smaller. An `Ellipsis` is expanded against the
dataset's shape rather than refused, since it names the dimensions the key
does not, and a bound of 0 is written out: `ds[0:0]` selects nothing, where it
used to be spelled `:` and select everything.

* `POST api/fetch` takes the same parameters in a body that the GET takes in a
query, and is the same code behind them. It exists for the one thing a query
string cannot do: carry a key of more coordinates than a URL has room for,
which is a few thousand and no encoding moves by much. 200,000 coordinates go
through it in one request. Nothing moves off GET -- blosc2 changes verb only
where a query could not have been made, so every deployment goes on serving
what it already served, and byte ranges stay with the GET they belong to.

* `api/fetch` is explicit about byte ranges, which is what lets a client read
single *blocks* of a dataset instead of whole chunks. A stored dataset is
served from its file and honours a `Range` as it always did, and now says so
with `Accept-Ranges: bytes`; anything built per request -- a slice, a field, a
lazy expression, a download -- refuses with a 416 and `Accept-Ranges: none`,
where before it answered 200 with the whole body, which is the download such a
client was trying to avoid in the first place. A leaf inside a container (a
`.b2z` member, or an HDF5 leaf that is a whole frame) is served by seeking to
its window in the file, several spans in one `multipart/byteranges` answer if
that is what was asked for, so it is readable block-wise like any dataset of
its own. `caterva2/tests/test_ranges.py` pins all of it down: a refactor
turning that one `FileResponse` into a `StreamingResponse` would otherwise
silently cost every such client its block granularity.

* `api/info` reports `accept_ranges` for a dataset: `"bytes"` for one served
from a file, `"none"` for one mounted from a peer and re-serialized here, and
absent where it is not settled by the description alone (a container leaf,
whose answer depends on its own window, or a `.b2nd` that proxies an HDF5
dataset, which reads as a stored array here but is rebuilt when fetched).
A field an older peer does not send is simply absent, not an error. It says of `api/fetch` what that
endpoint would answer, a request earlier: a client reading blocks over byte
ranges asks `api/info` anyway, and can now tell a dataset it may range-read
from one it may not without spending a request to find out. A field rather
than an `Accept-Ranges` header, which would be claiming that `api/info` itself
served ranges. Clients that do not know the field are unaffected.

* `api/info` and the ranged file responses carry an `ETag` of our own, built
from the frame's generation counter as well as its size and mtime. Starlette's
default is a digest of the mtime and the size, which a chunk written as a run
of zeros can leave untouched -- such a write stores no payload, so the frame
can come out of it exactly as long as it went in. A container leaf carries the
container's, on `api/info` and on its ranged reads alike: a leaf is a window of
that file, so what says the file did not change says the window did not move.

## Changes from 2025.11.17.1 to 2025.12.3

* Upload in-memory objects
Expand Down
18 changes: 18 additions & 0 deletions caterva2-server.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
# - maxusers: if defined, it will limit the number of users (default: 0, no limit)
# - login: if true, users will need to authenticate (default: true)
# - register: if true, users will be able to register (default: false)
# - publish_root: if defined, where an array filled a chunk at a time is copied
# once every one of its chunks has landed. An fsspec URL of a directory
# ("s3://bucket/prefix", "file:///srv/published"); a dataset names the key it
# wants underneath it and nothing above it. Credentials for it are the
# server's own. Unset (the default) means such arrays are simply not published.
# - peer_cache_quota: maximum total size of the optional C2Cache peer cache
# (default: "1G"). C2Cache ships inside Caterva2 and remains inactive unless
# one or more [[server.peer]] entries are configured.
#
[server]
# listen = "_caterva2/state/uvicorn.socket" # server supports listening from UNIX sockets
Expand All @@ -19,3 +27,13 @@ urlbase = "http://localhost:8000"
quota = "10G"
maxusers = 5
register = true # allow users to register
# publish_root = "s3://a-bucket/published"
# peer_cache_quota = "1G"

# Mount a remote Caterva2 server's locally owned @public root as @labb. This
# activates the bundled C2Cache provider. Repeat the table to mount more peers.
# An optional per-peer cache_quota further bounds this peer within the pool.
# [[server.peer]]
# name = "labb"
# urlbase = "http://server-b:8000"
# cache_quota = "500M"
5 changes: 4 additions & 1 deletion caterva2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@

"""Caterva2 - On demand access to remote Blosc2 data repositories"""

from .client import BasicAuth, Client, Dataset, File, Root
from .client import Array, BasicAuth, Client, Dataset, File, Group, Root, Table

__version__ = "2025.12.4.dev0"
"""The version in use of the Caterva2 package."""

__all__ = [
"Array",
"BasicAuth",
"Client",
"Dataset",
"File",
"Group",
"Root",
"Table",
]
"""List of symbols exported by the Caterva2 package."""
Loading
Loading