ontodag-fs 0.0.1__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.
- ontodag_fs-0.0.1/PKG-INFO +12 -0
- ontodag_fs-0.0.1/README.md +125 -0
- ontodag_fs-0.0.1/pyproject.toml +37 -0
- ontodag_fs-0.0.1/setup.cfg +4 -0
- ontodag_fs-0.0.1/src/ontodag_fs/__init__.py +15 -0
- ontodag_fs-0.0.1/src/ontodag_fs/__main__.py +368 -0
- ontodag_fs-0.0.1/src/ontodag_fs/fs.py +528 -0
- ontodag_fs-0.0.1/src/ontodag_fs/index.py +75 -0
- ontodag_fs-0.0.1/src/ontodag_fs/memory.py +140 -0
- ontodag_fs-0.0.1/src/ontodag_fs/ontodag_index.py +167 -0
- ontodag_fs-0.0.1/src/ontodag_fs.egg-info/PKG-INFO +12 -0
- ontodag_fs-0.0.1/src/ontodag_fs.egg-info/SOURCES.txt +21 -0
- ontodag_fs-0.0.1/src/ontodag_fs.egg-info/dependency_links.txt +1 -0
- ontodag_fs-0.0.1/src/ontodag_fs.egg-info/entry_points.txt +5 -0
- ontodag_fs-0.0.1/src/ontodag_fs.egg-info/requires.txt +8 -0
- ontodag_fs-0.0.1/src/ontodag_fs.egg-info/top_level.txt +1 -0
- ontodag_fs-0.0.1/tests/test_cli.py +74 -0
- ontodag_fs-0.0.1/tests/test_info.py +78 -0
- ontodag_fs-0.0.1/tests/test_invariants.py +120 -0
- ontodag_fs-0.0.1/tests/test_listing.py +121 -0
- ontodag_fs-0.0.1/tests/test_persistence.py +47 -0
- ontodag_fs-0.0.1/tests/test_readonly.py +45 -0
- ontodag_fs-0.0.1/tests/test_reserved.py +59 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ontodag-fs
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Stateless fsspec adapter presenting an OntoDAG concept lattice as a browsable filesystem, with bytes on Ethereum Swarm via swarmfs.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: fsspec
|
|
7
|
+
Requires-Dist: ontodag
|
|
8
|
+
Requires-Dist: swarmfs
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
12
|
+
Requires-Dist: hypothesis; extra == "dev"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# ontodag-fs
|
|
2
|
+
|
|
3
|
+
**Browse your knowledge, not your folders.** ontodag-fs presents an
|
|
4
|
+
[OntoDAG](https://github.com/petfold/ontodag) category lattice as a real,
|
|
5
|
+
mountable filesystem, with file content stored on
|
|
6
|
+
[Ethereum Swarm](https://www.ethswarm.org/) via
|
|
7
|
+
[swarmfs](https://github.com/petfold/swarmfs).
|
|
8
|
+
|
|
9
|
+
It is a modern descendant of Gifford's *Semantic File System* (SOSP '91):
|
|
10
|
+
directory names are interpreted as **queries**, not locations — but with a
|
|
11
|
+
concept lattice instead of flat attributes, and content-addressed storage
|
|
12
|
+
instead of a local disk.
|
|
13
|
+
|
|
14
|
+
```console
|
|
15
|
+
$ odag-fs tree /
|
|
16
|
+
/
|
|
17
|
+
├── animal/
|
|
18
|
+
│ ├── dog/
|
|
19
|
+
│ │ └── rex.txt
|
|
20
|
+
│ ├── pet/
|
|
21
|
+
│ │ └── rex.txt
|
|
22
|
+
│ └── spider/
|
|
23
|
+
│ ├── document/
|
|
24
|
+
│ │ └── web-study.md
|
|
25
|
+
│ └── charlotte.txt
|
|
26
|
+
└── document/
|
|
27
|
+
├── spider/
|
|
28
|
+
│ └── web-study.md
|
|
29
|
+
├── DESIGN_DECISIONS.md
|
|
30
|
+
└── SPEC.md
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The same file appears under every path it *belongs* to — `web-study.md` is
|
|
34
|
+
both a spider thing and a document, so it lives at `/animal/spider/document/`,
|
|
35
|
+
`/document/spider/`, and every reordering of those. No copies, no symlinks:
|
|
36
|
+
one object, several true names.
|
|
37
|
+
|
|
38
|
+
## The ideas in five lines
|
|
39
|
+
|
|
40
|
+
- **Paths are queries.** `/pet/dog` means "everything that is a pet AND a
|
|
41
|
+
dog". Order never matters: `/dog/pet` is the same place.
|
|
42
|
+
- **Directories are concepts.** Subdirectories are the categories that
|
|
43
|
+
meaningfully refine what you're looking at.
|
|
44
|
+
- **Files are classified objects.** A file's identity is its Swarm content
|
|
45
|
+
address; its name is just a display label.
|
|
46
|
+
- **The ontology does the work.** File something under `dog` and it is
|
|
47
|
+
automatically under `mammal`, `animal`, `pet` — subsumption is free.
|
|
48
|
+
- **References cannot dangle.** Content addressing means a classification
|
|
49
|
+
can never point at a file that "moved" — there is nowhere to move to.
|
|
50
|
+
(Tag filesystems on top of paths fight this forever; here it is
|
|
51
|
+
structurally impossible.)
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```console
|
|
56
|
+
$ pip install swarmfs # on PyPI (as are recordstore and swarmlite)
|
|
57
|
+
$ pip install \
|
|
58
|
+
"ontodag[swarm] @ git+https://github.com/petfold/ontodag.git" \
|
|
59
|
+
"ontodag-fs @ git+https://github.com/petfold/ontodag-fs.git"
|
|
60
|
+
$ odag-fs set store swarm:my-store # once — the same setting odag uses
|
|
61
|
+
$ odag-fs tree /
|
|
62
|
+
$ odag-fs cat /pet/dog/rex.txt
|
|
63
|
+
$ odag-fs # interactive: cd/ls/cat with a > prompt
|
|
64
|
+
$ pip install fusepy && odag-fs mount ~/mnt
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
New here? Read the **[User Guide](docs/USER_GUIDE.md)** — a tutorial that
|
|
68
|
+
takes you from an empty machine to a mounted, browsable ontology, with
|
|
69
|
+
worked examples of every capability.
|
|
70
|
+
|
|
71
|
+
## Status
|
|
72
|
+
|
|
73
|
+
**v0 — read-only view.** Browsing (`ls`, `tree`, `cat`, `info`, FUSE mount)
|
|
74
|
+
is complete and tested. Filing through the filesystem (`cp` into a concept
|
|
75
|
+
directory, `rm` as reclassification, `mv` between concepts) is **v0.1**, in
|
|
76
|
+
progress; today filing is done with a short Python helper (see the User
|
|
77
|
+
Guide). The lattice itself (creating categories) is edited through OntoDAG's
|
|
78
|
+
own API, never through the mount. See [ROADMAP.md](ROADMAP.md).
|
|
79
|
+
|
|
80
|
+
## Architecture
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
FUSE mount (fsspec.fuse — a deployment mode, not architecture)
|
|
84
|
+
│
|
|
85
|
+
OntoDAGFileSystem (this repo: fsspec AbstractFileSystem, stateless glue)
|
|
86
|
+
│ │
|
|
87
|
+
OntoDAG swarmfs
|
|
88
|
+
(classifier/index) (bytestore: fsspec backend for Swarm)
|
|
89
|
+
│ │
|
|
90
|
+
recordstore Bee node
|
|
91
|
+
(persistence of the DAG)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This repo owns **no state**: classifications live in OntoDAG, the DAG
|
|
95
|
+
persists through recordstore, bytes live on Swarm. `OntoDAGFileSystem` is a
|
|
96
|
+
pure [fsspec](https://filesystem-spec.readthedocs.io/) backend, so besides
|
|
97
|
+
the CLI and FUSE you also get the whole fsspec ecosystem (pandas, pyarrow,
|
|
98
|
+
DuckDB, …) for free.
|
|
99
|
+
|
|
100
|
+
| Repo | Role |
|
|
101
|
+
|---|---|
|
|
102
|
+
| [ontodag](https://github.com/petfold/ontodag) | the category DAG — index and classifier |
|
|
103
|
+
| [swarmfs](https://github.com/petfold/swarmfs) | fsspec backend for Swarm — the bytestore |
|
|
104
|
+
| [recordstore](https://github.com/petfold/recordstore) | versioned key→record store over Swarm — DAG persistence |
|
|
105
|
+
| [mdl-fca](https://github.com/petfold/mdl-fca) | MDL/FCA learning over the same DAG (upstream, not a dependency) |
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- [User Guide](docs/USER_GUIDE.md) — tutorial, setup, worked examples
|
|
110
|
+
- [SPEC.md](SPEC.md) — the precise v0/v0.1 contract, method by method
|
|
111
|
+
- [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) — prior art and every decision, with reasoning
|
|
112
|
+
- [ROADMAP.md](ROADMAP.md) — what's in scope now vs later
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```console
|
|
117
|
+
$ git clone https://github.com/petfold/ontodag-fs && cd ontodag-fs
|
|
118
|
+
$ python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
119
|
+
$ .venv/bin/pytest
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The test suite runs entirely offline — no Bee node, no FUSE — and every
|
|
123
|
+
test runs against both the in-memory reference index and the real OntoDAG
|
|
124
|
+
adapter, so the two cannot drift apart. Path-resolution invariants are
|
|
125
|
+
property-based (hypothesis); see SPEC §6 for the invariant list.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ontodag-fs"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Stateless fsspec adapter presenting an OntoDAG concept lattice as a browsable filesystem, with bytes on Ethereum Swarm via swarmfs."
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fsspec",
|
|
12
|
+
"ontodag",
|
|
13
|
+
"swarmfs",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest",
|
|
19
|
+
"pytest-asyncio",
|
|
20
|
+
"hypothesis",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.entry-points."fsspec.specs"]
|
|
24
|
+
ontodag = "ontodag_fs.fs:OntoDAGFileSystem"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
odag-fs = "ontodag_fs.__main__:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
markers = [
|
|
35
|
+
"fuse: FUSE integration tests (require libfuse; skipped by default)",
|
|
36
|
+
]
|
|
37
|
+
addopts = "-m 'not fuse'"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""ontodag-fs: an OntoDAG concept lattice as an fsspec filesystem over Swarm."""
|
|
2
|
+
|
|
3
|
+
from .fs import OntoDAGFileSystem
|
|
4
|
+
from .index import ConceptIndex, ObjectInfo, UnknownAttributeError
|
|
5
|
+
from .memory import InMemoryIndex
|
|
6
|
+
from .ontodag_index import OntoDAGIndex
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ConceptIndex",
|
|
10
|
+
"InMemoryIndex",
|
|
11
|
+
"ObjectInfo",
|
|
12
|
+
"OntoDAGFileSystem",
|
|
13
|
+
"OntoDAGIndex",
|
|
14
|
+
"UnknownAttributeError",
|
|
15
|
+
]
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Thin browse CLI for the v0 manual milestone (the full CLI is ROADMAP v1).
|
|
2
|
+
|
|
3
|
+
Loads a DAG through odag's store machinery — the same store specs odag
|
|
4
|
+
uses: a path to a native/OWL file, or ``swarm:NAME`` — and serves it
|
|
5
|
+
read-only with OntoDAGFileSystem over swarmfs.
|
|
6
|
+
|
|
7
|
+
odag-fs [-s STORE] [--bee-api URL] [COMMAND [args]]
|
|
8
|
+
|
|
9
|
+
Commands: ls, tree, cat, info, cd, pwd, mount, set, help. With no command,
|
|
10
|
+
odag-fs follows odag's convention: it reads commands from a pipe, or opens
|
|
11
|
+
an interactive ``>`` prompt on a terminal — with a current directory, so
|
|
12
|
+
paths may be relative (``cd pet`` then ``ls``). The store is hydrated once
|
|
13
|
+
per session, so repeated commands are fast.
|
|
14
|
+
|
|
15
|
+
``set`` reads and writes the same ``~/.ontodag/config`` as odag (keys:
|
|
16
|
+
store, bee_api, bee_batch), so ``odag set store swarm:NAME`` and
|
|
17
|
+
``odag-fs set store swarm:NAME`` are interchangeable and no ``-s`` is
|
|
18
|
+
needed once a default store is set. With nothing configured the default
|
|
19
|
+
is the local ``~/.ontodag`` store. ``mount`` uses fsspec's generic FUSE
|
|
20
|
+
wrapper (needs fusepy; deployment mode, not architecture).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import os
|
|
27
|
+
import posixpath
|
|
28
|
+
import shlex
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
__version__ = version("ontodag-fs")
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
__version__ = "dev"
|
|
38
|
+
except Exception: # pragma: no cover
|
|
39
|
+
__version__ = "dev"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
HELP_TEXT = """\
|
|
43
|
+
Usage: odag-fs [-s STORE] [--bee-api URL] [<command> [args]]
|
|
44
|
+
|
|
45
|
+
Commands:
|
|
46
|
+
ls [-l] [PATH] list a concept directory
|
|
47
|
+
tree [PATH] [--depth N] recursive listing of the lattice
|
|
48
|
+
cat PATH print an object's bytes
|
|
49
|
+
info PATH show details for a path
|
|
50
|
+
cd [PATH] change the current directory (interactive mode)
|
|
51
|
+
pwd print the current directory
|
|
52
|
+
mount MOUNTPOINT FUSE-mount the view (needs fusepy)
|
|
53
|
+
set [KEY [VALUE]] show settings, or set one (store, bee_api, bee_batch)
|
|
54
|
+
help show this help
|
|
55
|
+
|
|
56
|
+
With no command odag-fs reads commands from a pipe, or opens an interactive
|
|
57
|
+
`>` prompt on a terminal — the same convention as odag. In that mode paths
|
|
58
|
+
may be relative to the current directory (`cd pet` then `ls`), and the
|
|
59
|
+
store is loaded once for the whole session.
|
|
60
|
+
|
|
61
|
+
Settings are shared with odag (~/.ontodag/config): `set store swarm:NAME`
|
|
62
|
+
in either tool makes -s unnecessary everywhere. With nothing configured
|
|
63
|
+
the default is the local ~/.ontodag store.
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
-s, --store STORE one-off store override: a file path or swarm:NAME
|
|
67
|
+
--bee-api URL Bee API URL (default: $BEE_API, configured bee_api,
|
|
68
|
+
or localhost)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ----------------------------------------------------------------- session
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Session:
|
|
76
|
+
"""One loaded store + a current directory, shared across commands."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, store_spec: str | None, bee_api: str | None):
|
|
79
|
+
self.store_spec = store_spec
|
|
80
|
+
self.bee_api = bee_api
|
|
81
|
+
self.cwd = "/"
|
|
82
|
+
self._fs = None
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def fs(self):
|
|
86
|
+
if self._fs is None:
|
|
87
|
+
self._fs = _build_fs(self.store_spec, self.bee_api)
|
|
88
|
+
return self._fs
|
|
89
|
+
|
|
90
|
+
def switch(self) -> None:
|
|
91
|
+
"""Drop the loaded store (after `set store`); reload lazily."""
|
|
92
|
+
self._fs = None
|
|
93
|
+
self.store_spec = None # the new config default takes over
|
|
94
|
+
self.cwd = "/"
|
|
95
|
+
|
|
96
|
+
def resolve(self, path: str) -> str:
|
|
97
|
+
"""Make a path absolute against the current directory (supports
|
|
98
|
+
`.` and `..`, which operate on the typed path, not the lattice)."""
|
|
99
|
+
if not path.startswith("/"):
|
|
100
|
+
path = posixpath.join(self.cwd, path)
|
|
101
|
+
norm = posixpath.normpath(path)
|
|
102
|
+
return "/" if norm in (".", "//") else norm
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_fs(store_spec: str | None, bee_api: str | None):
|
|
106
|
+
# odag's CLI module is the authority on store specs/config; reusing its
|
|
107
|
+
# (private) helpers is accepted milestone tooling — the real CLI (v1)
|
|
108
|
+
# gets a public seam.
|
|
109
|
+
from ontodag.__main__ import _make_backend, _read_config, _resolve_store
|
|
110
|
+
|
|
111
|
+
from swarmfs import SwarmFileSystem
|
|
112
|
+
|
|
113
|
+
from . import OntoDAGFileSystem, OntoDAGIndex
|
|
114
|
+
|
|
115
|
+
dag = _make_backend(_resolve_store(store_spec)).load()
|
|
116
|
+
api = bee_api or os.environ.get("BEE_API") or _read_config().get("bee_api")
|
|
117
|
+
swarm = SwarmFileSystem(api_url=api) if api else SwarmFileSystem()
|
|
118
|
+
return OntoDAGFileSystem(index=OntoDAGIndex(dag), swarm=swarm)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------- commands
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _basename(entry: dict) -> str:
|
|
125
|
+
return entry["name"].rstrip("/").rsplit("/", 1)[-1]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cmd_ls(session: Session, args) -> None:
|
|
129
|
+
for e in session.fs.ls(session.resolve(args.path), detail=True):
|
|
130
|
+
base = _basename(e)
|
|
131
|
+
if e["type"] == "directory":
|
|
132
|
+
print(base + "/")
|
|
133
|
+
elif args.long:
|
|
134
|
+
print(f"{base} [{e.get('swarm_ref', '')[:8]}] {sorted(e.get('intent', []))}")
|
|
135
|
+
else:
|
|
136
|
+
print(base)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_tree(session: Session, args) -> None:
|
|
140
|
+
path = session.resolve(args.path)
|
|
141
|
+
print(path)
|
|
142
|
+
_tree(session.fs, path, args.depth)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _tree(fs, path, depth, prefix="") -> None:
|
|
146
|
+
entries = fs.ls(path, detail=True)
|
|
147
|
+
entries.sort(key=lambda e: (e["type"] != "directory", e["name"]))
|
|
148
|
+
for i, e in enumerate(entries):
|
|
149
|
+
last = i == len(entries) - 1
|
|
150
|
+
connector = "└── " if last else "├── "
|
|
151
|
+
base = _basename(e)
|
|
152
|
+
if e["type"] == "directory":
|
|
153
|
+
print(prefix + connector + base + "/")
|
|
154
|
+
# .swarm is unenumerable; .all repeats what the tree already
|
|
155
|
+
# shows — descend the lattice and .unfiled only
|
|
156
|
+
if depth > 1 and base not in (".all", ".swarm"):
|
|
157
|
+
_tree(fs, e["name"], depth - 1, prefix + (" " if last else "│ "))
|
|
158
|
+
else:
|
|
159
|
+
ref = e.get("swarm_ref", "")
|
|
160
|
+
print(prefix + connector + base + (f" [{ref[:8]}]" if ref else ""))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def cmd_cat(session: Session, args) -> None:
|
|
164
|
+
sys.stdout.buffer.write(session.fs.cat_file(session.resolve(args.path)))
|
|
165
|
+
sys.stdout.buffer.flush()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def cmd_info(session: Session, args) -> None:
|
|
169
|
+
for key, value in session.fs.info(session.resolve(args.path)).items():
|
|
170
|
+
print(f"{key}: {value}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cmd_cd(session: Session, args) -> None:
|
|
174
|
+
path = session.resolve(args.path)
|
|
175
|
+
if not session.fs.isdir(path):
|
|
176
|
+
raise FileNotFoundError(f"not a directory: {path}")
|
|
177
|
+
session.cwd = path
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def cmd_pwd(session: Session, args) -> None:
|
|
181
|
+
print(session.cwd)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_mount(session: Session, args) -> None:
|
|
185
|
+
try:
|
|
186
|
+
from fsspec.fuse import run
|
|
187
|
+
except ImportError:
|
|
188
|
+
raise ValueError("odag-fs mount needs fusepy: pip install fusepy") from None
|
|
189
|
+
print(f"mounting ontodag view at {args.mountpoint} — Ctrl-C or "
|
|
190
|
+
f"`fusermount -u {args.mountpoint}` to unmount")
|
|
191
|
+
run(session.fs, "/", args.mountpoint)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cmd_set(session: Session, args) -> None:
|
|
195
|
+
"""Show or change settings — same keys and config file as odag's `set`
|
|
196
|
+
(~/.ontodag/config), so either tool's `set store` configures both."""
|
|
197
|
+
from ontodag.__main__ import (
|
|
198
|
+
_SETTINGS,
|
|
199
|
+
_normalize_spec,
|
|
200
|
+
_read_config,
|
|
201
|
+
_resolve_store,
|
|
202
|
+
_write_config,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def effective(key: str) -> str:
|
|
206
|
+
cfg = _read_config()
|
|
207
|
+
if key == "store":
|
|
208
|
+
return session.store_spec or _resolve_store(None)
|
|
209
|
+
if key == "bee_api":
|
|
210
|
+
return os.environ.get("BEE_API") or cfg.get("bee_api") or "http://localhost:1633"
|
|
211
|
+
if key == "bee_batch":
|
|
212
|
+
return os.environ.get("BEE_BATCH") or cfg.get("bee_batch") or ""
|
|
213
|
+
return cfg.get(key, "")
|
|
214
|
+
|
|
215
|
+
if not args.key:
|
|
216
|
+
for key in _SETTINGS:
|
|
217
|
+
print(f"{key} = {effective(key)}")
|
|
218
|
+
return
|
|
219
|
+
if args.key not in _SETTINGS:
|
|
220
|
+
raise ValueError(f"unknown setting: {args.key} "
|
|
221
|
+
f"(known: {', '.join(_SETTINGS)})")
|
|
222
|
+
if args.value is None:
|
|
223
|
+
print(f"{args.key} = {effective(args.key)}")
|
|
224
|
+
return
|
|
225
|
+
cfg = _read_config()
|
|
226
|
+
cfg[args.key] = _normalize_spec(args.value) if args.key == "store" else args.value
|
|
227
|
+
_write_config(cfg)
|
|
228
|
+
if args.key == "store":
|
|
229
|
+
session.switch()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def cmd_help(session: Session, args) -> None:
|
|
233
|
+
sys.stdout.write(HELP_TEXT)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------------ parser
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _build_command_parser(with_globals: bool) -> argparse.ArgumentParser:
|
|
240
|
+
parser = argparse.ArgumentParser(prog="odag-fs", add_help=with_globals)
|
|
241
|
+
if with_globals:
|
|
242
|
+
parser.add_argument("-s", "--store", default=None,
|
|
243
|
+
help="one-off store override: a file path or "
|
|
244
|
+
"swarm:NAME (default: the configured store; "
|
|
245
|
+
"see `odag-fs set`)")
|
|
246
|
+
parser.add_argument("--bee-api", default=None,
|
|
247
|
+
help="Bee API URL for the bytestore (default: "
|
|
248
|
+
"$BEE_API, configured bee_api, or localhost)")
|
|
249
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
250
|
+
|
|
251
|
+
p = sub.add_parser("ls", help="list a concept directory")
|
|
252
|
+
p.add_argument("path", nargs="?", default=".")
|
|
253
|
+
p.add_argument("-l", "--long", action="store_true")
|
|
254
|
+
p.set_defaults(func=cmd_ls)
|
|
255
|
+
|
|
256
|
+
p = sub.add_parser("tree", help="recursive listing of the lattice")
|
|
257
|
+
p.add_argument("path", nargs="?", default=".")
|
|
258
|
+
p.add_argument("--depth", type=int, default=4)
|
|
259
|
+
p.set_defaults(func=cmd_tree)
|
|
260
|
+
|
|
261
|
+
p = sub.add_parser("cat", help="print an object's bytes")
|
|
262
|
+
p.add_argument("path")
|
|
263
|
+
p.set_defaults(func=cmd_cat)
|
|
264
|
+
|
|
265
|
+
p = sub.add_parser("info", help="show info for a path")
|
|
266
|
+
p.add_argument("path")
|
|
267
|
+
p.set_defaults(func=cmd_info)
|
|
268
|
+
|
|
269
|
+
p = sub.add_parser("cd", help="change the current directory")
|
|
270
|
+
p.add_argument("path", nargs="?", default="/")
|
|
271
|
+
p.set_defaults(func=cmd_cd)
|
|
272
|
+
|
|
273
|
+
p = sub.add_parser("pwd", help="print the current directory")
|
|
274
|
+
p.set_defaults(func=cmd_pwd)
|
|
275
|
+
|
|
276
|
+
p = sub.add_parser("mount", help="FUSE-mount the view (needs fusepy)")
|
|
277
|
+
p.add_argument("mountpoint")
|
|
278
|
+
p.set_defaults(func=cmd_mount)
|
|
279
|
+
|
|
280
|
+
p = sub.add_parser("set", help="show or change settings (shared with odag)")
|
|
281
|
+
p.add_argument("key", nargs="?")
|
|
282
|
+
p.add_argument("value", nargs="?")
|
|
283
|
+
p.set_defaults(func=cmd_set)
|
|
284
|
+
|
|
285
|
+
p = sub.add_parser("help", help="show help")
|
|
286
|
+
p.set_defaults(func=cmd_help)
|
|
287
|
+
|
|
288
|
+
return parser
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
_LINE_PARSER = _build_command_parser(with_globals=False)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def dispatch(tokens: list[str], session: Session) -> int:
|
|
295
|
+
"""Parse one command line and run it. Returns a process-style exit code."""
|
|
296
|
+
try:
|
|
297
|
+
args = _LINE_PARSER.parse_args(tokens)
|
|
298
|
+
except SystemExit as exc: # argparse handled --help or a usage error
|
|
299
|
+
return exc.code or 0
|
|
300
|
+
if args.command is None:
|
|
301
|
+
return 0
|
|
302
|
+
try:
|
|
303
|
+
args.func(session, args)
|
|
304
|
+
return 0
|
|
305
|
+
except (ValueError, OSError) as exc:
|
|
306
|
+
print(f"odag-fs: {exc}", file=sys.stderr)
|
|
307
|
+
return 1
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ----------------------------------------------- interactive / batch modes
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def run_stream(session: Session, stream, interactive: bool) -> int:
|
|
314
|
+
"""Read commands line by line — `>` prompt on a tty, silently from a
|
|
315
|
+
pipe. Errors are reported and the loop continues (odag convention);
|
|
316
|
+
the exit code says whether every line succeeded."""
|
|
317
|
+
if interactive:
|
|
318
|
+
print(f"odag-fs {__version__} - type help for help")
|
|
319
|
+
failed = False
|
|
320
|
+
while True:
|
|
321
|
+
if interactive:
|
|
322
|
+
try:
|
|
323
|
+
line = input(f"{session.cwd}> " if session.cwd != "/" else "> ")
|
|
324
|
+
except EOFError:
|
|
325
|
+
print()
|
|
326
|
+
break
|
|
327
|
+
else:
|
|
328
|
+
line = stream.readline()
|
|
329
|
+
if not line:
|
|
330
|
+
break
|
|
331
|
+
line = line.strip()
|
|
332
|
+
if not line or line.startswith("#"):
|
|
333
|
+
continue
|
|
334
|
+
try:
|
|
335
|
+
tokens = shlex.split(line)
|
|
336
|
+
except ValueError as exc:
|
|
337
|
+
print(f"odag-fs: {exc}", file=sys.stderr)
|
|
338
|
+
failed = True
|
|
339
|
+
continue
|
|
340
|
+
if tokens[0] in ("quit", "exit"):
|
|
341
|
+
break
|
|
342
|
+
if dispatch(tokens, session) != 0:
|
|
343
|
+
failed = True
|
|
344
|
+
return 1 if failed and not interactive else 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------- entry point
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main(argv=None) -> None:
|
|
351
|
+
parser = _build_command_parser(with_globals=True)
|
|
352
|
+
args = parser.parse_args(argv)
|
|
353
|
+
session = Session(getattr(args, "store", None), getattr(args, "bee_api", None))
|
|
354
|
+
|
|
355
|
+
if args.command is None:
|
|
356
|
+
sys.exit(run_stream(session, sys.stdin, interactive=sys.stdin.isatty()))
|
|
357
|
+
|
|
358
|
+
code = 0
|
|
359
|
+
try:
|
|
360
|
+
args.func(session, args)
|
|
361
|
+
except (ValueError, OSError) as exc:
|
|
362
|
+
print(f"odag-fs: {exc}", file=sys.stderr)
|
|
363
|
+
code = 1
|
|
364
|
+
sys.exit(code)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if __name__ == "__main__":
|
|
368
|
+
main()
|