quai 0.3.0__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.
quai-0.3.0/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .egg-info/
2
+ dist/
3
+ build/
4
+ __pycache__/
quai-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: quai
3
+ Version: 0.3.0
4
+ Summary: Interactive Quarb — a session REPL where every line becomes a reusable query (&1, &2, …)
5
+ Project-URL: Homepage, https://quarb.org/
6
+ Project-URL: Playground, https://demo.quarb.org/quai/
7
+ Project-URL: Cookbook, https://quarb.org/cookbooks/quai.html
8
+ Project-URL: Repository, https://gitlab.com/quarb/engine
9
+ Author: Bojan Đuričković
10
+ License: MIT OR Apache-2.0
11
+ Keywords: interactive,json,quarb,query,repl
12
+ Classifier: Environment :: Console
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: quarb>=0.3
19
+ Description-Content-Type: text/markdown
20
+
21
+ # quai
22
+
23
+ Interactive [Quarb][quarb] — a session REPL where every line becomes a
24
+ reusable query.
25
+
26
+ ```bash
27
+ pip install quai
28
+ quai data.json
29
+ ```
30
+
31
+ `quai` opens a session over one or more sources and holds it. Each
32
+ line you run is labelled `&1`, `&2`, … — and every label is a
33
+ reusable *macro*: not the printed value, but the *query* that produced
34
+ it, ready to continue through the pipe. It is the notebook loop, cell
35
+ by cell, except a cell is a path into your data.
36
+
37
+ ```
38
+ &1 /teams/*/members/*
39
+ /teams/0/members/0 ⋮ (every member, across all teams, one path)
40
+ &2 &1 | [/langs/*:: = 'Go'] | /name::
41
+ ada
42
+ eu
43
+ &3 &1 @| count
44
+ 6
45
+ ```
46
+
47
+ - `&N` re-runs line N; continue with a pipe (`&N | /key::`,
48
+ `&N | [pred]`, `&N @| count`).
49
+ - `&N#` replays line N's output frozen, as it was when it ran.
50
+ - `def &x: … ;` adds a named fragment; `:history`, `:reset`, `:help`,
51
+ `:quit` are the commands.
52
+
53
+ The engine rides in on the [`quarb`][quarb] dependency, so `quai`
54
+ reaches every local source `quarb` does — JSON, YAML, TOML, CSV, XML,
55
+ HTML, Markdown, SQLite, kaiv, the filesystem, git, archives, XLSX, and
56
+ source code — and several at once mount under one root for
57
+ cross-source `<=>` joins.
58
+
59
+ Try it in the browser at [demo.quarb.org/quai][playground] (no
60
+ install), and read the [graph cookbook][cookbook] for the recipes.
61
+
62
+ [quarb]: https://quarb.org/
63
+ [playground]: https://demo.quarb.org/quai/
64
+ [cookbook]: https://quarb.org/cookbooks/quai.html
65
+
66
+ ## License
67
+
68
+ Licensed under either of Apache License, Version 2.0 or the MIT license
69
+ at your option.
quai-0.3.0/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # quai
2
+
3
+ Interactive [Quarb][quarb] — a session REPL where every line becomes a
4
+ reusable query.
5
+
6
+ ```bash
7
+ pip install quai
8
+ quai data.json
9
+ ```
10
+
11
+ `quai` opens a session over one or more sources and holds it. Each
12
+ line you run is labelled `&1`, `&2`, … — and every label is a
13
+ reusable *macro*: not the printed value, but the *query* that produced
14
+ it, ready to continue through the pipe. It is the notebook loop, cell
15
+ by cell, except a cell is a path into your data.
16
+
17
+ ```
18
+ &1 /teams/*/members/*
19
+ /teams/0/members/0 ⋮ (every member, across all teams, one path)
20
+ &2 &1 | [/langs/*:: = 'Go'] | /name::
21
+ ada
22
+ eu
23
+ &3 &1 @| count
24
+ 6
25
+ ```
26
+
27
+ - `&N` re-runs line N; continue with a pipe (`&N | /key::`,
28
+ `&N | [pred]`, `&N @| count`).
29
+ - `&N#` replays line N's output frozen, as it was when it ran.
30
+ - `def &x: … ;` adds a named fragment; `:history`, `:reset`, `:help`,
31
+ `:quit` are the commands.
32
+
33
+ The engine rides in on the [`quarb`][quarb] dependency, so `quai`
34
+ reaches every local source `quarb` does — JSON, YAML, TOML, CSV, XML,
35
+ HTML, Markdown, SQLite, kaiv, the filesystem, git, archives, XLSX, and
36
+ source code — and several at once mount under one root for
37
+ cross-source `<=>` joins.
38
+
39
+ Try it in the browser at [demo.quarb.org/quai][playground] (no
40
+ install), and read the [graph cookbook][cookbook] for the recipes.
41
+
42
+ [quarb]: https://quarb.org/
43
+ [playground]: https://demo.quarb.org/quai/
44
+ [cookbook]: https://quarb.org/cookbooks/quai.html
45
+
46
+ ## License
47
+
48
+ Licensed under either of Apache License, Version 2.0 or the MIT license
49
+ at your option.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "quai"
7
+ version = "0.3.0"
8
+ description = "Interactive Quarb — a session REPL where every line becomes a reusable query (&1, &2, …)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT OR Apache-2.0" }
12
+ authors = [{ name = "Bojan Đuričković" }]
13
+ keywords = ["query", "repl", "interactive", "quarb", "json"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Topic :: Utilities",
19
+ "Environment :: Console",
20
+ ]
21
+ dependencies = ["quarb>=0.3"]
22
+
23
+ [project.scripts]
24
+ quai = "quai.cli:main"
25
+
26
+ [project.urls]
27
+ Homepage = "https://quarb.org/"
28
+ Playground = "https://demo.quarb.org/quai/"
29
+ Cookbook = "https://quarb.org/cookbooks/quai.html"
30
+ Repository = "https://gitlab.com/quarb/engine"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["quai"]
@@ -0,0 +1,14 @@
1
+ """Interactive Quarb — the ``quai`` session REPL.
2
+
3
+ A thin, pure-Python front end over the :mod:`quarb` engine: it opens a
4
+ source, holds a session, and labels each accepted line ``&1``, ``&2``,
5
+ … — every label a reusable macro (the *query*, not the printed value),
6
+ ready to continue through the pipe. ``pip install quai`` gives you the
7
+ ``quai`` command; the engine itself rides in on the ``quarb``
8
+ dependency.
9
+ """
10
+
11
+ from .session import Session
12
+
13
+ __version__ = "0.3.0"
14
+ __all__ = ["Session"]
quai-0.3.0/quai/cli.py ADDED
@@ -0,0 +1,190 @@
1
+ """The ``quai`` command: an interactive Quarb REPL.
2
+
3
+ Built on :func:`input`, with the :mod:`readline` module imported so
4
+ backspace, arrow keys, and input-line history work out of the box. The
5
+ color codes in the prompt are wrapped in readline's zero-width markers
6
+ (``\\001``/``\\002``) so cursor math near the prompt stays correct.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+
16
+ try:
17
+ import readline # noqa: F401 — enables line editing for input()
18
+ except ImportError: # pragma: no cover — e.g. Windows without pyreadline
19
+ pass
20
+
21
+ import quarb
22
+
23
+ from .session import Session
24
+
25
+ _CYAN = "\001\x1b[36m\002"
26
+ _RESET = "\001\x1b[0m\002"
27
+
28
+
29
+ def main() -> None:
30
+ ap = argparse.ArgumentParser(
31
+ prog="quai",
32
+ description="Interactive Quarb — each line becomes a reusable "
33
+ "query (&1, &2, ...).",
34
+ )
35
+ ap.add_argument(
36
+ "paths",
37
+ nargs="*",
38
+ help="source(s): a document (.json/.yaml/.toml/.csv/.xml/.html/"
39
+ ".md), a SQLite/kaiv/xlsx file, a directory, or git:PATH. "
40
+ "Several mount as one root, so a single query — including a "
41
+ "<=> join — spans them all.",
42
+ )
43
+ args = ap.parse_args()
44
+ if not args.paths:
45
+ ap.error("quai needs at least one source (a document, a directory, or git:PATH)")
46
+
47
+ try:
48
+ doc = (
49
+ quarb.open(args.paths[0])
50
+ if len(args.paths) == 1
51
+ else quarb.mount(args.paths)
52
+ )
53
+ except Exception as e: # noqa: BLE001 — surface the engine's message
54
+ sys.exit(f"quai: {e}")
55
+
56
+ session = Session(doc)
57
+ color = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
58
+ print(
59
+ f"quai - interactive Quarb over {', '.join(args.paths)}. "
60
+ ":help for commands, :quit (or Ctrl-D) to leave."
61
+ )
62
+ _repl(session, color)
63
+
64
+
65
+ def _repl(session: Session, color: bool) -> None:
66
+ while True:
67
+ prompt = (
68
+ f"{_CYAN}&{session.line_no}{_RESET} " if color else f"&{session.line_no} "
69
+ )
70
+ try:
71
+ line = input(prompt)
72
+ except EOFError:
73
+ print()
74
+ break
75
+ except KeyboardInterrupt:
76
+ print()
77
+ continue
78
+ line = line.strip()
79
+ if not line:
80
+ continue
81
+
82
+ # A ':' command (a query cannot start with a lone ':').
83
+ if line.startswith(":") and not line.startswith("::"):
84
+ if _command(session, line):
85
+ break
86
+ continue
87
+
88
+ # A definition extends the macro table but is not itself run.
89
+ if (
90
+ line.startswith("def ")
91
+ or line == "def"
92
+ or line.startswith("macro ")
93
+ or line == "macro"
94
+ ):
95
+ try:
96
+ session.add_def(line)
97
+ except Exception as e: # noqa: BLE001
98
+ print(f"error: {e}", file=sys.stderr)
99
+ continue
100
+
101
+ # Capture references (&N#, &N!) are resolved here, not by the
102
+ # engine (its lexer has no '#', and '!' marks data-aware macros).
103
+ try:
104
+ kind, arg = _prepare(line)
105
+ except ValueError as e:
106
+ print(f"error: {e}", file=sys.stderr)
107
+ continue
108
+
109
+ if kind == "frozen":
110
+ snap = session.frozen(arg)
111
+ if snap is None:
112
+ print(
113
+ f"error: &{arg}# has no captured result (line {arg} hasn't run)",
114
+ file=sys.stderr,
115
+ )
116
+ else:
117
+ for v in snap:
118
+ print(_render(v))
119
+ session.record_frozen(snap)
120
+ continue
121
+
122
+ try:
123
+ values = session.eval(arg)
124
+ except Exception as e: # noqa: BLE001
125
+ print(f"error: {e}", file=sys.stderr)
126
+ continue
127
+ for v in values:
128
+ print(_render(v))
129
+ n = session.line_no
130
+ if not session.commit(arg, values):
131
+ print(
132
+ f"note: &{n} is not referenceable (its shape can't be a macro body)",
133
+ file=sys.stderr,
134
+ )
135
+
136
+
137
+ def _prepare(line: str):
138
+ n = _numeric_ref(line, "#")
139
+ if n is not None:
140
+ return ("frozen", n)
141
+ n = _numeric_ref(line, "!")
142
+ if n is not None:
143
+ # Live re-run; for a fixed in-process document this equals &N.
144
+ return ("eval", f"&{n}")
145
+ if "#" in line:
146
+ raise ValueError(
147
+ "'#' is the frozen-history suffix, valid only as a standalone '&N#'"
148
+ )
149
+ return ("eval", line)
150
+
151
+
152
+ def _numeric_ref(line: str, suffix: str):
153
+ if line.endswith(suffix) and line.startswith("&"):
154
+ body = line[1:-1]
155
+ if body.isdigit():
156
+ return int(body)
157
+ return None
158
+
159
+
160
+ def _render(v) -> str:
161
+ if isinstance(v, dict):
162
+ return json.dumps(v, ensure_ascii=False)
163
+ return str(v)
164
+
165
+
166
+ def _command(session: Session, line: str) -> bool:
167
+ if line in (":q", ":quit"):
168
+ return True
169
+ if line in (":help", ":?"):
170
+ print(
171
+ " <query> run a query; its result is labelled &N and reusable\n"
172
+ " &N re-run line N (a macro); continue with a pipe: &N | /key::\n"
173
+ " &N# replay line N's frozen output (as it was when it ran)\n"
174
+ " def &x: ... ; add a named fragment to the session\n"
175
+ " :history show the macro table (&1, &2, ...)\n"
176
+ " :reset clear the history and restart numbering\n"
177
+ " :quit leave (also Ctrl-D)"
178
+ )
179
+ elif line == ":history":
180
+ h = session.history().strip()
181
+ print(h if h else "(no history yet)")
182
+ elif line == ":reset":
183
+ session.reset()
184
+ else:
185
+ print(f"unknown command '{line}' (:help lists them)")
186
+ return False
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
@@ -0,0 +1,89 @@
1
+ """The interactive session: a macro table where each accepted line
2
+ becomes ``def &N: <line> ;``, evaluated through the :mod:`quarb`
3
+ engine.
4
+
5
+ History is the language's own reuse mechanism, not a bolted-on cell
6
+ store: line 3 is the fragment ``&3``, continued through the pipe
7
+ (``&3 | /name::``, ``&3 | [pred]``, ``&3 @| count``). A standalone
8
+ ``&N#`` replays a line's captured output (the frozen footprint); in
9
+ this pure-Python front end the source is materialized once, so
10
+ ``&N``, ``&N!`` and ``&N#`` coincide for a fixed document.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import quarb
16
+
17
+
18
+ class Session:
19
+ def __init__(self, doc: "quarb.Document"):
20
+ self.doc = doc
21
+ # The macro table as definition text, prepended to each query
22
+ # so ``&N`` history resolves inline.
23
+ self.defs_text = ""
24
+ # Each line's output, captured at commit — what ``&N#`` replays.
25
+ self.snapshots: dict[int, list] = {}
26
+ # The next line's number — the ``&N`` a fresh line will claim.
27
+ self.line_no = 1
28
+
29
+ def _combined(self, line: str) -> str:
30
+ if not self.defs_text:
31
+ return line
32
+ return f"{self.defs_text}\n{line}"
33
+
34
+ def eval(self, line: str) -> list:
35
+ """Run a line against the source with the current macro table
36
+ prepended. Node results come back as locator strings, value
37
+ results as typed Python values (dicts for records)."""
38
+ return self.doc.values(self._combined(line))
39
+
40
+ def commit(self, line: str, snapshot: list) -> bool:
41
+ """Register an accepted line as ``&N`` and capture its output as
42
+ the frozen footprint for ``&N#``. Returns whether the line's
43
+ shape can be a macro body (so ``&N`` will resolve); either way
44
+ the line number advances so labels track what the user saw."""
45
+ self.snapshots[self.line_no] = list(snapshot)
46
+ # A space before the ``;`` terminator: a line ending in a ``::``
47
+ # projection would otherwise lex ``::;`` as the metadata sigil.
48
+ candidate = f"{self.defs_text}def &{self.line_no}: {line} ;\n"
49
+ referenceable = self._defs_ok(candidate)
50
+ if referenceable:
51
+ self.defs_text = candidate
52
+ self.line_no += 1
53
+ return referenceable
54
+
55
+ def _defs_ok(self, candidate: str) -> bool:
56
+ # Validate by running the candidate table plus a reference to
57
+ # the new line; a parse error means it cannot be a macro body.
58
+ try:
59
+ self.doc.values(f"{candidate}&{self.line_no}")
60
+ return True
61
+ except Exception:
62
+ return False
63
+
64
+ def frozen(self, n: int):
65
+ """The frozen output of line ``n``, if captured."""
66
+ return self.snapshots.get(n)
67
+
68
+ def record_frozen(self, snapshot: list) -> None:
69
+ """Record a frozen-recall line: it takes the next number and
70
+ keeps its own snapshot, but is not a referenceable macro body."""
71
+ self.snapshots[self.line_no] = list(snapshot)
72
+ self.line_no += 1
73
+
74
+ def add_def(self, line: str) -> None:
75
+ """Add a ``def``/``macro`` line to the table (validated first)."""
76
+ candidate = f"{self.defs_text}{line}\n"
77
+ # Parse the whole table by running it with a trivial trailing
78
+ # query (``/`` — the root — always parses); a malformed
79
+ # definition raises here, before it can poison later lines.
80
+ self.doc.values(f"{candidate}/")
81
+ self.defs_text = candidate
82
+
83
+ def history(self) -> str:
84
+ return self.defs_text
85
+
86
+ def reset(self) -> None:
87
+ self.defs_text = ""
88
+ self.snapshots = {}
89
+ self.line_no = 1