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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ exsh = exist_shell.main:app