ontodag 0.1.0__py3-none-any.whl
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/__init__.py +17 -0
- ontodag/__main__.py +590 -0
- ontodag/dag.py +741 -0
- ontodag/owl.py +216 -0
- ontodag/swarm_adapter.py +103 -0
- ontodag-0.1.0.dist-info/METADATA +77 -0
- ontodag-0.1.0.dist-info/RECORD +10 -0
- ontodag-0.1.0.dist-info/WHEEL +5 -0
- ontodag-0.1.0.dist-info/entry_points.txt +2 -0
- ontodag-0.1.0.dist-info/top_level.txt +1 -0
ontodag/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from ontodag.dag import DAG, OntoDAG, Item, OntoDAGVisualizer
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def __getattr__(name):
|
|
5
|
+
# OWL support needs owlready2; import it only when actually used so the
|
|
6
|
+
# core data structure works without it.
|
|
7
|
+
if name == "OWLOntology":
|
|
8
|
+
from ontodag.owl import OWLOntology
|
|
9
|
+
|
|
10
|
+
return OWLOntology
|
|
11
|
+
# The Swarm adapter is optional persistence; keep plain `import ontodag`
|
|
12
|
+
# free of it (tests/test_boundaries.py, B1).
|
|
13
|
+
if name == "SwarmOntoDAG":
|
|
14
|
+
from ontodag.swarm_adapter import SwarmOntoDAG
|
|
15
|
+
|
|
16
|
+
return SwarmOntoDAG
|
|
17
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
ontodag/__main__.py
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""odag — a Unix-style command line for OntoDAG.
|
|
2
|
+
|
|
3
|
+
(The command is `odag`, not `od`: `od` is the standard octal-dump utility.)
|
|
4
|
+
|
|
5
|
+
Design goals (see the module docstring history in CLAUDE.md):
|
|
6
|
+
* silent on success, errors on stderr with a non-zero exit code;
|
|
7
|
+
* a persistent default store in ~/.ontodag so `odag put cat` / `odag get cat`
|
|
8
|
+
work with no file argument;
|
|
9
|
+
* stdin / stdout / pipes: `odag` with no command reads commands from a pipe,
|
|
10
|
+
or drops into an interactive prompt on a tty;
|
|
11
|
+
* `-o FILE` redirects query output; `-f STORE` picks the store for one run;
|
|
12
|
+
* `set store PATH` changes the persistent default.
|
|
13
|
+
|
|
14
|
+
The core (`ontodag.dag`) has no heavy dependencies, so the common native-store
|
|
15
|
+
path imports nothing else; OWL/Manchester support and the visualizer are
|
|
16
|
+
imported lazily only when a command actually needs them.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
import shlex
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from ontodag.dag import OntoDAG, Item
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
28
|
+
try:
|
|
29
|
+
__version__ = version("ontodag")
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
except Exception: # pragma: no cover - importlib.metadata always present on 3.8+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# --------------------------------------------------------------------------- #
|
|
37
|
+
# Home directory, config and store resolution
|
|
38
|
+
# --------------------------------------------------------------------------- #
|
|
39
|
+
|
|
40
|
+
def _home_dir():
|
|
41
|
+
return os.environ.get("ONTODAG_HOME") or os.path.join(
|
|
42
|
+
os.path.expanduser("~"), ".ontodag"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _config_path():
|
|
47
|
+
return os.path.join(_home_dir(), "config")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_config():
|
|
51
|
+
cfg = {}
|
|
52
|
+
path = _config_path()
|
|
53
|
+
if not os.path.exists(path):
|
|
54
|
+
return cfg
|
|
55
|
+
with open(path) as fh:
|
|
56
|
+
for line in fh:
|
|
57
|
+
line = line.strip()
|
|
58
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
59
|
+
continue
|
|
60
|
+
key, _, value = line.partition("=")
|
|
61
|
+
cfg[key.strip()] = value.strip()
|
|
62
|
+
return cfg
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _write_config(cfg):
|
|
66
|
+
os.makedirs(_home_dir(), exist_ok=True)
|
|
67
|
+
with open(_config_path(), "w") as fh:
|
|
68
|
+
for key in sorted(cfg):
|
|
69
|
+
fh.write(f"{key} = {cfg[key]}\n")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _abspath(path):
|
|
73
|
+
return os.path.abspath(os.path.expanduser(path))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_swarm(spec):
|
|
77
|
+
return spec.startswith("swarm:")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _normalize_spec(spec):
|
|
81
|
+
"""A store spec is either a `swarm:NAME` URI or a filesystem path.
|
|
82
|
+
|
|
83
|
+
Swarm specs are kept verbatim; file paths are made absolute so a spec
|
|
84
|
+
saved to config resolves the same from any working directory."""
|
|
85
|
+
return spec if _is_swarm(spec) else _abspath(spec)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _resolve_store(override):
|
|
89
|
+
"""Store precedence: -f flag > $ONTODAG_STORE > config > default."""
|
|
90
|
+
if override:
|
|
91
|
+
return _normalize_spec(override)
|
|
92
|
+
env = os.environ.get("ONTODAG_STORE")
|
|
93
|
+
if env:
|
|
94
|
+
return _normalize_spec(env)
|
|
95
|
+
cfg = _read_config()
|
|
96
|
+
if cfg.get("store"):
|
|
97
|
+
return _normalize_spec(cfg["store"])
|
|
98
|
+
return os.path.join(_home_dir(), "store.od")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --------------------------------------------------------------------------- #
|
|
102
|
+
# Serialization: native line format by default, OWL/Manchester by extension
|
|
103
|
+
# --------------------------------------------------------------------------- #
|
|
104
|
+
|
|
105
|
+
def _detect_format(path):
|
|
106
|
+
ext = os.path.splitext(path)[1].lower()
|
|
107
|
+
if ext == ".omn":
|
|
108
|
+
return "manchester"
|
|
109
|
+
if ext == ".owl":
|
|
110
|
+
return "owl"
|
|
111
|
+
return "native"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _load_native(path):
|
|
115
|
+
"""Read the native store: one line per node, `name parent1 parent2 ...`.
|
|
116
|
+
|
|
117
|
+
A missing file is an empty DAG (the default store need not exist yet).
|
|
118
|
+
The format is canonical (nodes and parents sorted on save) and the graph
|
|
119
|
+
is rebuilt via add_edge, so even a hand-edited, non-reduced file loads as
|
|
120
|
+
its unique transitive reduction.
|
|
121
|
+
"""
|
|
122
|
+
dag = OntoDAG()
|
|
123
|
+
if not os.path.exists(path):
|
|
124
|
+
return dag
|
|
125
|
+
edges = []
|
|
126
|
+
with open(path) as fh:
|
|
127
|
+
for line in fh:
|
|
128
|
+
line = line.strip()
|
|
129
|
+
if not line or line.startswith("#"):
|
|
130
|
+
continue
|
|
131
|
+
tokens = shlex.split(line)
|
|
132
|
+
name = tokens[0]
|
|
133
|
+
if name not in dag.nodes:
|
|
134
|
+
dag.add_node(Item(name))
|
|
135
|
+
for parent in tokens[1:]:
|
|
136
|
+
if parent not in dag.nodes:
|
|
137
|
+
dag.add_node(Item(parent))
|
|
138
|
+
edges.append((parent, name))
|
|
139
|
+
for parent, child in edges:
|
|
140
|
+
dag.add_edge(dag.nodes[parent], dag.nodes[child])
|
|
141
|
+
return dag
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _save_native(dag, path):
|
|
145
|
+
lines = ["# ontodag store v1"]
|
|
146
|
+
for name in sorted(dag.nodes):
|
|
147
|
+
if name == dag.root.name:
|
|
148
|
+
continue
|
|
149
|
+
node = dag.nodes[name]
|
|
150
|
+
parents = sorted(
|
|
151
|
+
p.name for p in node.parents if dag.nodes.get(p.name) is p
|
|
152
|
+
)
|
|
153
|
+
lines.append(" ".join(shlex.quote(t) for t in [name] + parents))
|
|
154
|
+
with open(path, "w") as fh:
|
|
155
|
+
fh.write("\n".join(lines) + "\n")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load(path):
|
|
159
|
+
fmt = _detect_format(path)
|
|
160
|
+
if fmt == "native":
|
|
161
|
+
return _load_native(path)
|
|
162
|
+
from ontodag.owl import OWLOntology
|
|
163
|
+
if fmt == "manchester":
|
|
164
|
+
return OWLOntology.import_dag_manchester(file_name=path)
|
|
165
|
+
return OWLOntology(f"file://{_abspath(path)}").import_dag(file_name=path)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _save(dag, path):
|
|
169
|
+
parent = os.path.dirname(path)
|
|
170
|
+
if parent:
|
|
171
|
+
os.makedirs(parent, exist_ok=True)
|
|
172
|
+
fmt = _detect_format(path)
|
|
173
|
+
if fmt == "native":
|
|
174
|
+
_save_native(dag, path)
|
|
175
|
+
return
|
|
176
|
+
from ontodag.owl import OWLOntology
|
|
177
|
+
if fmt == "manchester":
|
|
178
|
+
OWLOntology.export_dag_manchester(dag, path)
|
|
179
|
+
else:
|
|
180
|
+
OWLOntology.export_dag(dag, path)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# --------------------------------------------------------------------------- #
|
|
184
|
+
# Storage backends
|
|
185
|
+
#
|
|
186
|
+
# A backend hides *where* the store lives behind load()/save(dag)/describe().
|
|
187
|
+
# The default is a local file (native/OWL/Manchester by extension). A
|
|
188
|
+
# `swarm:NAME` spec persists through the SwarmOntoDAG adapter over a
|
|
189
|
+
# RecordStore: content blobs on a Bee node, the mutable "latest root" in a
|
|
190
|
+
# local FilePointer (no signing key needed). recordstore and the adapter are
|
|
191
|
+
# imported lazily here, so `import ontodag` and the native path stay
|
|
192
|
+
# dependency-free (tests/test_boundaries.py B1).
|
|
193
|
+
# --------------------------------------------------------------------------- #
|
|
194
|
+
|
|
195
|
+
class FileBackend:
|
|
196
|
+
def __init__(self, path):
|
|
197
|
+
self.path = path
|
|
198
|
+
|
|
199
|
+
def load(self):
|
|
200
|
+
return _load(self.path)
|
|
201
|
+
|
|
202
|
+
def save(self, dag):
|
|
203
|
+
_save(dag, self.path)
|
|
204
|
+
|
|
205
|
+
def describe(self):
|
|
206
|
+
return self.path
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class SwarmBackend:
|
|
210
|
+
def __init__(self, name, store_factory=None):
|
|
211
|
+
if not name:
|
|
212
|
+
raise ValueError("swarm store needs a name, e.g. swarm:mydag")
|
|
213
|
+
if os.sep in name or (os.altsep and os.altsep in name) or name == "..":
|
|
214
|
+
raise ValueError(f"invalid swarm store name: {name!r}")
|
|
215
|
+
self.name = name
|
|
216
|
+
# Injection seam: tests pass a factory returning a RecordStore over an
|
|
217
|
+
# in-memory bytes store, exercising the whole wiring without a node.
|
|
218
|
+
self._store_factory = store_factory
|
|
219
|
+
|
|
220
|
+
def pointer_path(self):
|
|
221
|
+
return os.path.join(_home_dir(), self.name + ".root")
|
|
222
|
+
|
|
223
|
+
def _record_store(self):
|
|
224
|
+
if self._store_factory is not None:
|
|
225
|
+
return self._store_factory()
|
|
226
|
+
cfg = _read_config()
|
|
227
|
+
api = os.environ.get("BEE_API") or cfg.get("bee_api") or "http://localhost:1633"
|
|
228
|
+
batch = os.environ.get("BEE_BATCH") or cfg.get("bee_batch") or ""
|
|
229
|
+
try:
|
|
230
|
+
# BeeBytesStore imports `requests` in its constructor, so a missing
|
|
231
|
+
# optional dependency surfaces here rather than at module import.
|
|
232
|
+
from recordstore import RecordStore, BeeBytesStore, FilePointer
|
|
233
|
+
os.makedirs(_home_dir(), exist_ok=True)
|
|
234
|
+
return RecordStore(BeeBytesStore(api, batch),
|
|
235
|
+
pointer=FilePointer(self.pointer_path()))
|
|
236
|
+
except ImportError as exc:
|
|
237
|
+
missing = exc.name or "requests"
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"the swarm backend needs an optional dependency that is not "
|
|
240
|
+
f"installed ({missing!r}); install the swarm extra with: "
|
|
241
|
+
f"pip install -e \".[swarm]\" (or: pip install requests)"
|
|
242
|
+
) from exc
|
|
243
|
+
|
|
244
|
+
def load(self):
|
|
245
|
+
from ontodag.swarm_adapter import SwarmOntoDAG
|
|
246
|
+
return SwarmOntoDAG(self._record_store())
|
|
247
|
+
|
|
248
|
+
def save(self, dag):
|
|
249
|
+
dag.commit()
|
|
250
|
+
|
|
251
|
+
def describe(self):
|
|
252
|
+
return f"swarm:{self.name}"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _make_backend(spec):
|
|
256
|
+
if _is_swarm(spec):
|
|
257
|
+
return SwarmBackend(spec[len("swarm:"):])
|
|
258
|
+
return FileBackend(spec)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# --------------------------------------------------------------------------- #
|
|
262
|
+
# The in-memory session (the loaded store)
|
|
263
|
+
# --------------------------------------------------------------------------- #
|
|
264
|
+
|
|
265
|
+
class Session:
|
|
266
|
+
def __init__(self, spec):
|
|
267
|
+
self.switch(spec)
|
|
268
|
+
|
|
269
|
+
def switch(self, spec):
|
|
270
|
+
self.spec = spec
|
|
271
|
+
self.backend = _make_backend(spec)
|
|
272
|
+
self.dag = self.backend.load()
|
|
273
|
+
|
|
274
|
+
def save(self):
|
|
275
|
+
self.backend.save(self.dag)
|
|
276
|
+
|
|
277
|
+
def describe(self):
|
|
278
|
+
return self.backend.describe()
|
|
279
|
+
|
|
280
|
+
def import_from(self, incoming):
|
|
281
|
+
"""Replace the store's contents with `incoming`, in place.
|
|
282
|
+
|
|
283
|
+
Mutating the live DAG (rather than rebinding self.dag) keeps a
|
|
284
|
+
SwarmOntoDAG's identity, so its commit() still diffs against what it
|
|
285
|
+
hydrated. Works for either backend via the public API alone: clearing
|
|
286
|
+
to the root then merging reproduces `incoming` exactly (remove
|
|
287
|
+
reconnects children upward, never deletes siblings)."""
|
|
288
|
+
for name in list(self.dag.nodes):
|
|
289
|
+
if name != self.dag.root.name and name in self.dag.nodes:
|
|
290
|
+
self.dag.remove(name)
|
|
291
|
+
self.dag.merge(incoming)
|
|
292
|
+
self.save()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# --------------------------------------------------------------------------- #
|
|
296
|
+
# Command handlers — (args, session, out); silent on success
|
|
297
|
+
# --------------------------------------------------------------------------- #
|
|
298
|
+
|
|
299
|
+
def _print_dag(dag, out):
|
|
300
|
+
for node in dag.topological_sort():
|
|
301
|
+
children = sorted(n.name for n in node.neighbors)
|
|
302
|
+
if node.name == dag.root.name:
|
|
303
|
+
print(f"{node.name} [root] -> {' '.join(children)}".rstrip(), file=out)
|
|
304
|
+
else:
|
|
305
|
+
parents = sorted(
|
|
306
|
+
p.name for p in node.parents if dag.nodes.get(p.name) is p
|
|
307
|
+
)
|
|
308
|
+
print(f"{node.name} ({' '.join(parents)}) -> {' '.join(children)}".rstrip(),
|
|
309
|
+
file=out)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def cmd_put(args, session, out):
|
|
313
|
+
session.dag.put(args.item, args.parents, optimized=args.optimized)
|
|
314
|
+
session.save()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cmd_get(args, session, out):
|
|
318
|
+
for name in sorted(item.name for item in session.dag.get(args.categories)):
|
|
319
|
+
print(name, file=out)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def cmd_remove(args, session, out):
|
|
323
|
+
session.dag.remove(args.item)
|
|
324
|
+
session.save()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def cmd_show(args, session, out):
|
|
328
|
+
_print_dag(session.dag, out)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_list(args, session, out):
|
|
332
|
+
for name in sorted(n for n in session.dag.nodes if n != session.dag.root.name):
|
|
333
|
+
print(name, file=out)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def cmd_merge(args, session, out):
|
|
337
|
+
session.dag.merge(_load(args.file))
|
|
338
|
+
session.save()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def cmd_import(args, session, out):
|
|
342
|
+
session.import_from(_load(args.file))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def cmd_export(args, session, out):
|
|
346
|
+
_save(session.dag, args.file)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def cmd_visualize(args, session, out):
|
|
350
|
+
from ontodag.dag import OntoDAGVisualizer
|
|
351
|
+
base = args.out or os.path.splitext(session.path)[0]
|
|
352
|
+
OntoDAGVisualizer(format=args.format).visualize(session.dag, filename=base)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# Settings `set` can show and change. `store` is the active store spec;
|
|
356
|
+
# bee_api/bee_batch configure the Swarm backend's Bee node.
|
|
357
|
+
_SETTINGS = ("store", "bee_api", "bee_batch")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _effective_setting(session, key):
|
|
361
|
+
"""The value currently in effect, honoring env/config precedence."""
|
|
362
|
+
if key == "store":
|
|
363
|
+
return session.describe()
|
|
364
|
+
cfg = _read_config()
|
|
365
|
+
if key == "bee_api":
|
|
366
|
+
return os.environ.get("BEE_API") or cfg.get("bee_api") or "http://localhost:1633"
|
|
367
|
+
if key == "bee_batch":
|
|
368
|
+
return os.environ.get("BEE_BATCH") or cfg.get("bee_batch") or ""
|
|
369
|
+
return cfg.get(key, "")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def cmd_set(args, session, out):
|
|
373
|
+
# No key: show every setting. Key but no value: show that one. Both:
|
|
374
|
+
# change it. Displaying on a missing value never errors.
|
|
375
|
+
if not args.key:
|
|
376
|
+
for key in _SETTINGS:
|
|
377
|
+
print(f"{key} = {_effective_setting(session, key)}", file=out)
|
|
378
|
+
return
|
|
379
|
+
if args.key not in _SETTINGS:
|
|
380
|
+
raise ValueError(f"unknown setting: {args.key} "
|
|
381
|
+
f"(known: {', '.join(_SETTINGS)})")
|
|
382
|
+
if args.value is None:
|
|
383
|
+
print(f"{args.key} = {_effective_setting(session, args.key)}", file=out)
|
|
384
|
+
return
|
|
385
|
+
if args.key == "store":
|
|
386
|
+
spec = _normalize_spec(args.value)
|
|
387
|
+
cfg = _read_config()
|
|
388
|
+
cfg["store"] = spec
|
|
389
|
+
_write_config(cfg)
|
|
390
|
+
session.switch(spec)
|
|
391
|
+
else:
|
|
392
|
+
cfg = _read_config()
|
|
393
|
+
cfg[args.key] = args.value
|
|
394
|
+
_write_config(cfg)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
HELP_TEXT = """\
|
|
398
|
+
Usage: odag [-f STORE] <command> [args]
|
|
399
|
+
|
|
400
|
+
Commands:
|
|
401
|
+
put SUB [PARENT...] add SUB under the PARENT categories (or the root)
|
|
402
|
+
get CAT [CAT...] print items below all of the CATs, one per line
|
|
403
|
+
remove ITEM remove ITEM from the store
|
|
404
|
+
show print the DAG structure
|
|
405
|
+
list print every item name
|
|
406
|
+
merge FILE merge FILE into the store
|
|
407
|
+
import FILE replace the store with the contents of FILE
|
|
408
|
+
export FILE write the store to FILE
|
|
409
|
+
visualize [--out B] render the DAG to an image
|
|
410
|
+
set [KEY [VALUE]] show settings, or set one (store, bee_api, bee_batch)
|
|
411
|
+
help show this help
|
|
412
|
+
|
|
413
|
+
With no command odag reads commands from a pipe, or opens an interactive
|
|
414
|
+
prompt on a terminal. Files ending in .owl/.omn use OWL/Manchester syntax;
|
|
415
|
+
any other path is the native line format.
|
|
416
|
+
|
|
417
|
+
A store may also be `swarm:NAME`, persisted on Ethereum Swarm (content on a
|
|
418
|
+
Bee node, latest root in ~/.ontodag/NAME.root). `set store swarm:NAME` makes
|
|
419
|
+
it the default, so every later command uses Swarm. Needs the swarm extra
|
|
420
|
+
(`pip install -e ".[swarm]"`). Configure the node with $BEE_API / $BEE_BATCH
|
|
421
|
+
or `bee_api` / `bee_batch` in ~/.ontodag/config.
|
|
422
|
+
|
|
423
|
+
The store can also be browsed as a filesystem (paths as category queries,
|
|
424
|
+
FUSE-mountable) with `odag-fs`, which shares these settings — see
|
|
425
|
+
https://github.com/petfold/ontodag-fs.
|
|
426
|
+
|
|
427
|
+
Options:
|
|
428
|
+
-f, --store PATH use PATH (or swarm:NAME) as the store for this run
|
|
429
|
+
-o, --output FILE write output to FILE instead of stdout (get/show/list)
|
|
430
|
+
"""
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def cmd_help(args, session, out):
|
|
434
|
+
print(HELP_TEXT, file=out, end="")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# --------------------------------------------------------------------------- #
|
|
438
|
+
# Parser
|
|
439
|
+
# --------------------------------------------------------------------------- #
|
|
440
|
+
|
|
441
|
+
def build_parser():
|
|
442
|
+
parser = argparse.ArgumentParser(prog="odag", add_help=False,
|
|
443
|
+
description="Manipulate an OntoDAG store.")
|
|
444
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
445
|
+
sub.required = True
|
|
446
|
+
|
|
447
|
+
p = sub.add_parser("put", add_help=True, help="add an item")
|
|
448
|
+
p.add_argument("item")
|
|
449
|
+
p.add_argument("parents", nargs="*")
|
|
450
|
+
p.add_argument("--optimized", action="store_true",
|
|
451
|
+
help="infer most-specific parents")
|
|
452
|
+
p.set_defaults(func=cmd_put)
|
|
453
|
+
|
|
454
|
+
p = sub.add_parser("get", add_help=True, help="query common subcategories")
|
|
455
|
+
p.add_argument("categories", nargs="+")
|
|
456
|
+
p.add_argument("-o", "--output")
|
|
457
|
+
p.set_defaults(func=cmd_get, stream_output=True)
|
|
458
|
+
|
|
459
|
+
p = sub.add_parser("remove", add_help=True, help="remove an item")
|
|
460
|
+
p.add_argument("item")
|
|
461
|
+
p.set_defaults(func=cmd_remove)
|
|
462
|
+
|
|
463
|
+
p = sub.add_parser("show", add_help=True, help="print the DAG structure")
|
|
464
|
+
p.add_argument("-o", "--output")
|
|
465
|
+
p.set_defaults(func=cmd_show, stream_output=True)
|
|
466
|
+
|
|
467
|
+
p = sub.add_parser("list", add_help=True, help="print all item names")
|
|
468
|
+
p.add_argument("-o", "--output")
|
|
469
|
+
p.set_defaults(func=cmd_list, stream_output=True)
|
|
470
|
+
|
|
471
|
+
p = sub.add_parser("merge", add_help=True, help="merge a file into the store")
|
|
472
|
+
p.add_argument("file")
|
|
473
|
+
p.set_defaults(func=cmd_merge)
|
|
474
|
+
|
|
475
|
+
p = sub.add_parser("import", add_help=True, help="replace the store with a file")
|
|
476
|
+
p.add_argument("file")
|
|
477
|
+
p.set_defaults(func=cmd_import)
|
|
478
|
+
|
|
479
|
+
p = sub.add_parser("export", add_help=True, help="write the store to a file")
|
|
480
|
+
p.add_argument("file")
|
|
481
|
+
p.set_defaults(func=cmd_export)
|
|
482
|
+
|
|
483
|
+
p = sub.add_parser("visualize", add_help=True, help="render an image")
|
|
484
|
+
p.add_argument("--out", help="output filename without extension")
|
|
485
|
+
p.add_argument("--format", default="png", choices=["png", "svg", "pdf"])
|
|
486
|
+
p.set_defaults(func=cmd_visualize)
|
|
487
|
+
|
|
488
|
+
p = sub.add_parser("set", add_help=True,
|
|
489
|
+
help="show settings, or change one")
|
|
490
|
+
p.add_argument("key", nargs="?")
|
|
491
|
+
p.add_argument("value", nargs="?")
|
|
492
|
+
p.set_defaults(func=cmd_set)
|
|
493
|
+
|
|
494
|
+
p = sub.add_parser("help", add_help=True, help="show help")
|
|
495
|
+
p.set_defaults(func=cmd_help)
|
|
496
|
+
|
|
497
|
+
return parser
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
PARSER = build_parser()
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def dispatch(argv, session):
|
|
504
|
+
"""Parse one command line and run it. Returns a process-style exit code."""
|
|
505
|
+
try:
|
|
506
|
+
args = PARSER.parse_args(argv)
|
|
507
|
+
except SystemExit as exc: # argparse handled --help or a usage error
|
|
508
|
+
return exc.code or 0
|
|
509
|
+
|
|
510
|
+
out = sys.stdout
|
|
511
|
+
handle = None
|
|
512
|
+
outpath = getattr(args, "output", None)
|
|
513
|
+
try:
|
|
514
|
+
if outpath and getattr(args, "stream_output", False):
|
|
515
|
+
handle = open(outpath, "w")
|
|
516
|
+
out = handle
|
|
517
|
+
args.func(args, session, out)
|
|
518
|
+
return 0
|
|
519
|
+
except (ValueError, OSError) as exc:
|
|
520
|
+
print(f"odag: {exc}", file=sys.stderr)
|
|
521
|
+
return 1
|
|
522
|
+
finally:
|
|
523
|
+
if handle is not None:
|
|
524
|
+
handle.close()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
# --------------------------------------------------------------------------- #
|
|
528
|
+
# Interactive and batch (stdin) modes
|
|
529
|
+
# --------------------------------------------------------------------------- #
|
|
530
|
+
|
|
531
|
+
def _run_stream(session, stream, interactive):
|
|
532
|
+
if interactive:
|
|
533
|
+
print(f"Ontodag {__version__} - type help for help")
|
|
534
|
+
while True:
|
|
535
|
+
if interactive:
|
|
536
|
+
try:
|
|
537
|
+
line = input("> ")
|
|
538
|
+
except EOFError:
|
|
539
|
+
print()
|
|
540
|
+
break
|
|
541
|
+
else:
|
|
542
|
+
line = stream.readline()
|
|
543
|
+
if not line:
|
|
544
|
+
break
|
|
545
|
+
line = line.strip()
|
|
546
|
+
if not line or line.startswith("#"):
|
|
547
|
+
continue
|
|
548
|
+
try:
|
|
549
|
+
tokens = shlex.split(line)
|
|
550
|
+
except ValueError as exc:
|
|
551
|
+
print(f"odag: {exc}", file=sys.stderr)
|
|
552
|
+
continue
|
|
553
|
+
if tokens[0] in ("quit", "exit"):
|
|
554
|
+
break
|
|
555
|
+
dispatch(tokens, session)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# --------------------------------------------------------------------------- #
|
|
559
|
+
# Entry point
|
|
560
|
+
# --------------------------------------------------------------------------- #
|
|
561
|
+
|
|
562
|
+
def main(argv=None):
|
|
563
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
564
|
+
|
|
565
|
+
store_override = None
|
|
566
|
+
while argv and argv[0] in ("-f", "--store", "--file"):
|
|
567
|
+
if len(argv) < 2:
|
|
568
|
+
print("odag: option requires a path", file=sys.stderr)
|
|
569
|
+
sys.exit(2)
|
|
570
|
+
store_override = argv[1]
|
|
571
|
+
argv = argv[2:]
|
|
572
|
+
|
|
573
|
+
if argv and argv[0] in ("-V", "--version"):
|
|
574
|
+
print(__version__)
|
|
575
|
+
sys.exit(0)
|
|
576
|
+
if argv and argv[0] in ("-h", "--help"):
|
|
577
|
+
sys.stdout.write(HELP_TEXT)
|
|
578
|
+
sys.exit(0)
|
|
579
|
+
|
|
580
|
+
session = Session(_resolve_store(store_override))
|
|
581
|
+
|
|
582
|
+
if not argv:
|
|
583
|
+
_run_stream(session, sys.stdin, interactive=sys.stdin.isatty())
|
|
584
|
+
sys.exit(0)
|
|
585
|
+
|
|
586
|
+
sys.exit(dispatch(argv, session))
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
if __name__ == "__main__":
|
|
590
|
+
main()
|