exist-shell 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.
- exist_shell/__init__.py +5 -0
- exist_shell/cache.py +186 -0
- exist_shell/client/__init__.py +19 -0
- exist_shell/client/_base.py +65 -0
- exist_shell/client/_collections.py +158 -0
- exist_shell/client/_documents.py +134 -0
- exist_shell/client/_groups.py +116 -0
- exist_shell/client/_permissions.py +172 -0
- exist_shell/client/_queries.py +37 -0
- exist_shell/client/_users.py +158 -0
- exist_shell/commands/__init__.py +1 -0
- exist_shell/commands/cat.py +51 -0
- exist_shell/commands/chmod.py +236 -0
- exist_shell/commands/chown.py +121 -0
- exist_shell/commands/collection.py +189 -0
- exist_shell/commands/cp.py +142 -0
- exist_shell/commands/edit.py +85 -0
- exist_shell/commands/exec.py +65 -0
- exist_shell/commands/group.py +183 -0
- exist_shell/commands/ls.py +66 -0
- exist_shell/commands/mkdir.py +24 -0
- exist_shell/commands/mv.py +124 -0
- exist_shell/commands/put.py +63 -0
- exist_shell/commands/rm.py +23 -0
- exist_shell/commands/server.py +114 -0
- exist_shell/commands/sync.py +767 -0
- exist_shell/commands/user.py +300 -0
- exist_shell/completions.py +221 -0
- exist_shell/config.py +233 -0
- exist_shell/exceptions.py +68 -0
- exist_shell/main.py +64 -0
- exist_shell/models.py +61 -0
- exist_shell/utils.py +179 -0
- exist_shell/xquery.py +267 -0
- exist_shell-0.1.0.dist-info/METADATA +10 -0
- exist_shell-0.1.0.dist-info/RECORD +38 -0
- exist_shell-0.1.0.dist-info/WHEEL +4 -0
- exist_shell-0.1.0.dist-info/entry_points.txt +2 -0
exist_shell/xquery.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""XQuery preprocessing pipeline and local validator support."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ValidatorResult:
|
|
14
|
+
"""Result from a local XQuery validator run.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
ok: True if the query is valid (or no validator was found).
|
|
18
|
+
error: Human-readable error message, or None when ok is True.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
ok: bool
|
|
22
|
+
error: str | None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class XQueryValidator(Protocol):
|
|
26
|
+
"""Interface that every validator wrapper must implement."""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def probe(cls) -> "XQueryValidator | None":
|
|
32
|
+
"""Find the validator binary and return a ready instance, or None if not installed.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
An initialised validator bound to the discovered binary, or None.
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def validate(self, code: str) -> ValidatorResult:
|
|
40
|
+
"""Validate XQuery source code.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
code: XQuery source code to validate.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
ValidatorResult with ok=True on success, or ok=False with an error message.
|
|
47
|
+
"""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BasexValidator:
|
|
52
|
+
"""Validator wrapper for BaseX (https://basex.org).
|
|
53
|
+
|
|
54
|
+
BaseX accepts ``INSPECT XQUERY <file>`` via its command-line client.
|
|
55
|
+
A parse error causes a non-zero exit code and the error text on stderr.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
name = "basex"
|
|
59
|
+
|
|
60
|
+
def __init__(self, binary: str) -> None:
|
|
61
|
+
"""Initialise with a known-good path to the basex binary."""
|
|
62
|
+
self._binary = binary
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def probe(cls) -> "BasexValidator | None":
|
|
66
|
+
"""Find the basex binary on PATH and return a bound instance, or None.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
BasexValidator bound to the discovered binary, or None if not found.
|
|
70
|
+
"""
|
|
71
|
+
binary = shutil.which("basex")
|
|
72
|
+
return cls(binary) if binary else None
|
|
73
|
+
|
|
74
|
+
def validate(self, code: str) -> ValidatorResult:
|
|
75
|
+
"""Validate XQuery by running it through basex in parse-only mode.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
code: XQuery source code to validate.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
ValidatorResult indicating success or the first reported error.
|
|
82
|
+
"""
|
|
83
|
+
with tempfile.NamedTemporaryFile(suffix=".xq", delete=False, mode="w", encoding="utf-8") as f:
|
|
84
|
+
f.write(code)
|
|
85
|
+
tmp = f.name
|
|
86
|
+
try:
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
[self._binary, "-c", f"INSPECT XQUERY {tmp}"],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
)
|
|
92
|
+
finally:
|
|
93
|
+
Path(tmp).unlink(missing_ok=True)
|
|
94
|
+
if result.returncode == 0:
|
|
95
|
+
return ValidatorResult(ok=True, error=None)
|
|
96
|
+
detail = (result.stderr or result.stdout).strip()
|
|
97
|
+
return ValidatorResult(ok=False, error=detail or "validation failed")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SaxonValidator:
|
|
101
|
+
"""Validator wrapper for Saxon (https://www.saxonica.com).
|
|
102
|
+
|
|
103
|
+
Looks for a ``saxon`` wrapper script on PATH (the standard install name on
|
|
104
|
+
most package managers). Saxon is invoked with ``-q:<file>``; a non-zero
|
|
105
|
+
exit code signals a parse or static error.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
name = "saxon"
|
|
109
|
+
|
|
110
|
+
def __init__(self, binary: str) -> None:
|
|
111
|
+
"""Initialise with a known-good path to the saxon binary."""
|
|
112
|
+
self._binary = binary
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def probe(cls) -> "SaxonValidator | None":
|
|
116
|
+
"""Find the saxon binary on PATH and return a bound instance, or None.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
SaxonValidator bound to the discovered binary, or None if not found.
|
|
120
|
+
"""
|
|
121
|
+
binary = shutil.which("saxon")
|
|
122
|
+
return cls(binary) if binary else None
|
|
123
|
+
|
|
124
|
+
def validate(self, code: str) -> ValidatorResult:
|
|
125
|
+
"""Validate XQuery by running it through Saxon.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
code: XQuery source code to validate.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
ValidatorResult indicating success or the first reported error.
|
|
132
|
+
"""
|
|
133
|
+
with tempfile.NamedTemporaryFile(suffix=".xq", delete=False, mode="w", encoding="utf-8") as f:
|
|
134
|
+
f.write(code)
|
|
135
|
+
tmp = f.name
|
|
136
|
+
try:
|
|
137
|
+
result = subprocess.run(
|
|
138
|
+
[self._binary, f"-q:{tmp}"],
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
)
|
|
142
|
+
finally:
|
|
143
|
+
Path(tmp).unlink(missing_ok=True)
|
|
144
|
+
if result.returncode == 0:
|
|
145
|
+
return ValidatorResult(ok=True, error=None)
|
|
146
|
+
detail = (result.stderr or result.stdout).strip()
|
|
147
|
+
return ValidatorResult(ok=False, error=detail or "validation failed")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Registry of known validator classes in preference order.
|
|
151
|
+
_VALIDATORS: list[type[BasexValidator] | type[SaxonValidator]] = [
|
|
152
|
+
BasexValidator,
|
|
153
|
+
SaxonValidator,
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
_VALIDATORS_BY_NAME: dict[str, type[BasexValidator] | type[SaxonValidator]] = {
|
|
157
|
+
cls.name: cls for cls in _VALIDATORS
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def list_validators() -> list[tuple[str, str | None]]:
|
|
162
|
+
"""Return each known validator paired with its installed path, or None.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of ``(name, path)`` tuples; path is None when not installed.
|
|
166
|
+
"""
|
|
167
|
+
return [(cls.name, shutil.which(cls.name)) for cls in _VALIDATORS]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def validate_locally(code: str, *, validator: str | None = None) -> ValidatorResult:
|
|
171
|
+
"""Validate XQuery using the first locally available validator.
|
|
172
|
+
|
|
173
|
+
When ``validator`` is given, that specific validator is required; the call
|
|
174
|
+
fails if it is unknown or not installed. When omitted, the first installed
|
|
175
|
+
validator in the registry is used. If none are installed, returns ok=True
|
|
176
|
+
so the caller can proceed without blocking.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
code: XQuery source code to validate.
|
|
180
|
+
validator: Name of the validator to use, or None for auto-discovery.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
ValidatorResult from the chosen or first available validator.
|
|
184
|
+
"""
|
|
185
|
+
if validator is not None:
|
|
186
|
+
cls = _VALIDATORS_BY_NAME.get(validator)
|
|
187
|
+
if cls is None:
|
|
188
|
+
known = ", ".join(_VALIDATORS_BY_NAME)
|
|
189
|
+
return ValidatorResult(ok=False, error=f"unknown validator '{validator}'; known: {known}")
|
|
190
|
+
v = cls.probe()
|
|
191
|
+
if v is None:
|
|
192
|
+
return ValidatorResult(ok=False, error=f"validator '{validator}' is not installed")
|
|
193
|
+
return v.validate(code)
|
|
194
|
+
for cls in _VALIDATORS:
|
|
195
|
+
v = cls.probe()
|
|
196
|
+
if v is not None:
|
|
197
|
+
return v.validate(code)
|
|
198
|
+
return ValidatorResult(ok=True, error=None)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Preprocessing pipeline
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _ensure_version(code: str) -> str:
|
|
207
|
+
"""Prepend ``xquery version "3.1";`` if no version declaration is present.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
code: XQuery source code.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Code with a version declaration at the top.
|
|
214
|
+
"""
|
|
215
|
+
if "xquery version" in code.lower():
|
|
216
|
+
return code
|
|
217
|
+
return 'xquery version "3.1";\n' + code
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _ensure_functx(code: str) -> str:
|
|
221
|
+
"""Add the functx module import when ``functx:`` is used but not declared.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
code: XQuery source code.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Code with the functx import inserted after the version declaration,
|
|
228
|
+
or unchanged if functx is already imported or not referenced.
|
|
229
|
+
"""
|
|
230
|
+
if "functx:" not in code:
|
|
231
|
+
return code
|
|
232
|
+
if "namespace functx" in code:
|
|
233
|
+
return code
|
|
234
|
+
# No "at" hint: eXist resolves the module from its package registry by URI.
|
|
235
|
+
# A file-path hint would override registry lookup and fail unless the file
|
|
236
|
+
# happens to exist at that path relative to the query context.
|
|
237
|
+
import_line = 'import module namespace functx = "http://www.functx.com";\n'
|
|
238
|
+
lines = code.splitlines(keepends=True)
|
|
239
|
+
for i, line in enumerate(lines):
|
|
240
|
+
if line.lower().startswith("xquery version"):
|
|
241
|
+
lines.insert(i + 1, import_line)
|
|
242
|
+
return "".join(lines)
|
|
243
|
+
return import_line + code
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# Ordered list of transformations applied by preprocess().
|
|
247
|
+
_PIPELINE: list[Callable[[str], str]] = [
|
|
248
|
+
_ensure_version,
|
|
249
|
+
_ensure_functx,
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def preprocess(code: str) -> str:
|
|
254
|
+
"""Apply all preprocessing transformations to XQuery source code.
|
|
255
|
+
|
|
256
|
+
Transformations are applied in pipeline order. Each step is a pure
|
|
257
|
+
``str -> str`` function.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
code: Raw XQuery source code.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Preprocessed XQuery source code.
|
|
264
|
+
"""
|
|
265
|
+
for step in _PIPELINE:
|
|
266
|
+
code = step(code)
|
|
267
|
+
return code
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: exist-shell
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line tool to interact with eXist-db via REST
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: platformdirs>=4.10.0; sys_platform == 'win32'
|
|
8
|
+
Requires-Dist: pydantic>=2.13.4
|
|
9
|
+
Requires-Dist: tomlkit>=0.15.0
|
|
10
|
+
Requires-Dist: typer>=0.26.7
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
exist_shell/__init__.py,sha256=flpc-GRUnIfinblX5UP4xNX8KgBhTfcE2iGysGPSNR8,157
|
|
2
|
+
exist_shell/cache.py,sha256=MDxIG9wEzMek30rI52-WR7DI0NLBpU5e2tzhrEb6Enw,5734
|
|
3
|
+
exist_shell/completions.py,sha256=U8i3-K1-0LOZrQ7PCaGMO6LkGaItWwBmf82aiAmEflQ,7733
|
|
4
|
+
exist_shell/config.py,sha256=_N9X9NOFsP5f-h6q2Mxh7P4XY9iFk3apZFhbNx3LHoI,7580
|
|
5
|
+
exist_shell/exceptions.py,sha256=EpbkKT58LiLOd-meP1e050vnwRDYfwEYgLpVbn5SWSc,2068
|
|
6
|
+
exist_shell/main.py,sha256=g4PjS7HOCgc_17DUbhAIMCuxV5wftm79lNG3klFlhbk,2745
|
|
7
|
+
exist_shell/models.py,sha256=U2R4_m8gZ-A5EIduw1RWiZr1xlK8FwWdsWJnCwKo5dI,1315
|
|
8
|
+
exist_shell/utils.py,sha256=mEex6Ity1MvepAWER3xJ2XLMiieQwMDK-3tg_YaTHTM,5904
|
|
9
|
+
exist_shell/xquery.py,sha256=lTf9dRJ1B9N_6nWiiK2CsGSVaTowysM5xTjnKjh4L5E,8492
|
|
10
|
+
exist_shell/client/__init__.py,sha256=JHAxs_UZBp_JEN_TXwWp4RV-qSL13WK4k4HjYnzRBJM,616
|
|
11
|
+
exist_shell/client/_base.py,sha256=5NfH53K_L2QXMh55DU0znY6ZvYgBCLfayUfrzZ2E9fo,2012
|
|
12
|
+
exist_shell/client/_collections.py,sha256=jQ50CHQTFGN0yTCiQhDuJv6M0-q9LSnDyA1BfRft6EU,6082
|
|
13
|
+
exist_shell/client/_documents.py,sha256=eHJJVvjJsjMD8_wqHfDssuBxKt2Q-hKn6pzE0L-BZFg,5030
|
|
14
|
+
exist_shell/client/_groups.py,sha256=eVdRaz6TdB9E18w0gJauOTcoFliPU9JFBeMN3jBILqQ,4108
|
|
15
|
+
exist_shell/client/_permissions.py,sha256=-Ht2gC8rh5w4MPKTos3xgCJMgi4oFopxgnZsZ5ZECgA,5805
|
|
16
|
+
exist_shell/client/_queries.py,sha256=UG7ljT3iy_n1jyz65_q8JWYvHtVcD6StxxAMOvuZ5p8,1316
|
|
17
|
+
exist_shell/client/_users.py,sha256=ka4DlaSQdmUF1fIm3eHTHjR0S1WaVLWl1XQACc5H4Ss,5825
|
|
18
|
+
exist_shell/commands/__init__.py,sha256=UqPqFwJczCmPqINpy7Ve6f096l4L-s7tnvi_Wht9Q_c,32
|
|
19
|
+
exist_shell/commands/cat.py,sha256=IFpXFyU2vsgvcEUyVzHYCivBnJr06QLyRzW7LAFbcLM,1688
|
|
20
|
+
exist_shell/commands/chmod.py,sha256=nSHd70aDoNTbFrbKY9gV4JTs62H6KpdV7fWrL3-lu4o,8297
|
|
21
|
+
exist_shell/commands/chown.py,sha256=TXcFZ6n5KnZX2o1m3Ep07sTkjEz0MrG90D0fhKOnHag,4614
|
|
22
|
+
exist_shell/commands/collection.py,sha256=psbOkSCR6k1OzS4pLYZTK7UJ2kKW4kmyeZjlIaslBqo,7011
|
|
23
|
+
exist_shell/commands/cp.py,sha256=utSBUHgyI32JjiPgSP67DJdBnX4PkIvxHYIBrhHcwVI,4749
|
|
24
|
+
exist_shell/commands/edit.py,sha256=YoqCwkwaJzGhxv-Hd6_4Jkbn5xiFeGhAdsUvdD5frIY,3011
|
|
25
|
+
exist_shell/commands/exec.py,sha256=KDKYcmZT1Wo3s5-l1Ik_P58qXwhafSHfXNBdjEgtQUE,2549
|
|
26
|
+
exist_shell/commands/group.py,sha256=quHtG-YrA_jFawkb_jS2LrvobX6CNrg3fPvMgYtCEKE,7137
|
|
27
|
+
exist_shell/commands/ls.py,sha256=vbV0v7CY9cubDgLE7V_w71TzTGRrgDIXXQDV_hqbFf0,2593
|
|
28
|
+
exist_shell/commands/mkdir.py,sha256=uEmQwaWdhMi14SFYP8JHFWl5MuiAyhutPGm3CfW9v2E,848
|
|
29
|
+
exist_shell/commands/mv.py,sha256=PtVd0XJE0Rqmae_fEeU9R-sNdsrQqv3Q0NULlScuUcU,4584
|
|
30
|
+
exist_shell/commands/put.py,sha256=xxOG14s4lwsdLYIFwNVG7Dv0QjUJRYKzjrWhZrWT9Wk,2330
|
|
31
|
+
exist_shell/commands/rm.py,sha256=YYbdgEQJL9y9Jx9z-Yo1ZgAMpr2kaNLeH723_Pp2DQ0,854
|
|
32
|
+
exist_shell/commands/server.py,sha256=36cRqH6GIhE2arf-Y794FWoUO61Xh3o9u0KHXnTjiZw,4104
|
|
33
|
+
exist_shell/commands/sync.py,sha256=pYAIEdrg-iJ770pLww57NMTxjqKs_AP9Vez4APe1SDo,27666
|
|
34
|
+
exist_shell/commands/user.py,sha256=ib6fW9s-6bwYRLKHHXHfXfzP8nkiKORVjx3_xwY2S5c,12295
|
|
35
|
+
exist_shell-0.1.0.dist-info/METADATA,sha256=S4EimEwRV-mFlKonz9vr4z0kFklUf0IfuZeP5ktgNF4,321
|
|
36
|
+
exist_shell-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
37
|
+
exist_shell-0.1.0.dist-info/entry_points.txt,sha256=qP6IBfiEW6WXxFDqiTB56DsYAVwIF7nRCluB8iMwta8,46
|
|
38
|
+
exist_shell-0.1.0.dist-info/RECORD,,
|