jsonpit 4.5.4__tar.gz → 4.5.8__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.
- {jsonpit-4.5.4 → jsonpit-4.5.8}/API.md +1 -1
- {jsonpit-4.5.4 → jsonpit-4.5.8}/PKG-INFO +4 -4
- {jsonpit-4.5.4 → jsonpit-4.5.8}/README.md +3 -3
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/__init__.py +1 -1
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/cli.py +206 -106
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/fs.py +44 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/pyproject.toml +1 -1
- {jsonpit-4.5.4 → jsonpit-4.5.8}/.gitignore +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/audit.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/canonical.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/changes.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/config.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/exceptions.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/flags.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/history.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/icons.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/item.py +0 -0
- {jsonpit-4.5.4 → jsonpit-4.5.8}/jsonpit/store.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# jsonpit API Reference (v4.5.
|
|
1
|
+
# jsonpit API Reference (v4.5.5)
|
|
2
2
|
|
|
3
3
|
This document provides a foldable, searchable reference for the public `jsonpit` Python API. Designed for 100% C# JsonPit parity, zero third-party runtime dependencies, and Smalltalk-grade object-oriented architecture.
|
|
4
4
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: jsonpit
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.8
|
|
4
4
|
Summary: Daemon-free distributed storage over Cloud Drives for developers and AI agents — multi-process, immutable history, zero dependencies.
|
|
5
5
|
Author: Dr. Rainer Burkhardt
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -35,9 +35,9 @@ The canonical **reference (lead) implementation** is C# `JsonPit` and the `pits`
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
-
## Cross-Tool & Cross-Language Version Alignment (`v4.5.
|
|
38
|
+
## Cross-Tool & Cross-Language Version Alignment (`v4.5.5`)
|
|
39
39
|
|
|
40
|
-
`jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.
|
|
40
|
+
`jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.5`** in direct lockstep with C# `JsonPit` in [RAIkeep](https://github.com/Burkhardt/RAIkeep).
|
|
41
41
|
|
|
42
42
|
### Why Harmonized Versioning is Essential:
|
|
43
43
|
- **Instant Compatibility Clarity:** Operators, DevOps pipelines, and autonomous AI agents immediately know whether `jpit` and `pits` share the exact same distributed protocol revision. If both tools report `4.4.x`, they guarantee identical master leasing rules, receipt mechanisms, 7-digit UTC timestamp parsing, and CR041 clean change file conventions.
|
|
@@ -107,7 +107,7 @@ pipx install jsonpit
|
|
|
107
107
|
# (or via companion alias)
|
|
108
108
|
pipx install jpit
|
|
109
109
|
|
|
110
|
-
# Upgrade to the latest lockstep release (v4.5.
|
|
110
|
+
# Upgrade to the latest lockstep release (v4.5.5)
|
|
111
111
|
pipx upgrade jsonpit
|
|
112
112
|
# (or)
|
|
113
113
|
pipx upgrade jpit
|
|
@@ -13,9 +13,9 @@ The canonical **reference (lead) implementation** is C# `JsonPit` and the `pits`
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
## Cross-Tool & Cross-Language Version Alignment (`v4.5.
|
|
16
|
+
## Cross-Tool & Cross-Language Version Alignment (`v4.5.5`)
|
|
17
17
|
|
|
18
|
-
`jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.
|
|
18
|
+
`jsonpit` and the `jpit` CLI are version-synchronized to **`v4.5.5`** in direct lockstep with C# `JsonPit` in [RAIkeep](https://github.com/Burkhardt/RAIkeep).
|
|
19
19
|
|
|
20
20
|
### Why Harmonized Versioning is Essential:
|
|
21
21
|
- **Instant Compatibility Clarity:** Operators, DevOps pipelines, and autonomous AI agents immediately know whether `jpit` and `pits` share the exact same distributed protocol revision. If both tools report `4.4.x`, they guarantee identical master leasing rules, receipt mechanisms, 7-digit UTC timestamp parsing, and CR041 clean change file conventions.
|
|
@@ -85,7 +85,7 @@ pipx install jsonpit
|
|
|
85
85
|
# (or via companion alias)
|
|
86
86
|
pipx install jpit
|
|
87
87
|
|
|
88
|
-
# Upgrade to the latest lockstep release (v4.5.
|
|
88
|
+
# Upgrade to the latest lockstep release (v4.5.5)
|
|
89
89
|
pipx upgrade jsonpit
|
|
90
90
|
# (or)
|
|
91
91
|
pipx upgrade jpit
|
|
@@ -27,7 +27,7 @@ from .exceptions import (
|
|
|
27
27
|
ProtectedAttributeError,
|
|
28
28
|
StrictPatchValidationError,
|
|
29
29
|
)
|
|
30
|
-
from .fs import resolve_pit_target
|
|
30
|
+
from .fs import resolve_command_pit_root, resolve_pit_target
|
|
31
31
|
from .icons import Icons
|
|
32
32
|
from .store import (
|
|
33
33
|
Pit,
|
|
@@ -59,7 +59,7 @@ def run_jq_filter(json_text: str, jq_filter: str) -> None:
|
|
|
59
59
|
|
|
60
60
|
def discover_pits(
|
|
61
61
|
target: str,
|
|
62
|
-
cloud: str | None =
|
|
62
|
+
cloud: str | None = None,
|
|
63
63
|
root: str | None = None,
|
|
64
64
|
) -> list[tuple[Path, str]]:
|
|
65
65
|
"""
|
|
@@ -133,38 +133,9 @@ def discover_pits(
|
|
|
133
133
|
return []
|
|
134
134
|
|
|
135
135
|
|
|
136
|
-
def
|
|
137
|
-
"""Discovers all pit names under a resolved tenant root directory."""
|
|
138
|
-
if not root:
|
|
139
|
-
return []
|
|
140
|
-
raw_str = str(root).strip()
|
|
141
|
-
p = Path(os.path.expanduser(raw_str))
|
|
142
|
-
if p.is_dir():
|
|
143
|
-
candidate: Path | None = p
|
|
144
|
-
else:
|
|
145
|
-
cfg = OsConfig.load()
|
|
146
|
-
if not cfg.is_config_loaded:
|
|
147
|
-
raise PitNotFoundError(missing_configuration_diagnostic())
|
|
148
|
-
cloud_root = cfg.get_cloud_root(cloud or "OneDrive")
|
|
149
|
-
candidate = cloud_root / root if cloud_root else None
|
|
150
|
-
|
|
151
|
-
if not candidate or not candidate.is_dir():
|
|
152
|
-
return []
|
|
153
|
-
|
|
154
|
-
found: list[str] = []
|
|
155
|
-
for child in sorted(candidate.iterdir()):
|
|
156
|
-
if child.is_dir():
|
|
157
|
-
pit_file = child / f"{child.name}.pit"
|
|
158
|
-
if pit_file.is_file():
|
|
159
|
-
found.append(child.name)
|
|
160
|
-
elif child.suffix.lower() == ".pit":
|
|
161
|
-
found.append(child.stem)
|
|
162
|
-
return sorted(list(dict.fromkeys(found)))
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def extract_cloud_and_root(argv: list[str]) -> tuple[str, str | None]:
|
|
136
|
+
def extract_cloud_and_root(argv: list[str]) -> tuple[str | None, str | None]:
|
|
166
137
|
"""Extracts -c/--cloud and -r/--root options from CLI token list."""
|
|
167
|
-
cloud =
|
|
138
|
+
cloud: str | None = None
|
|
168
139
|
root = None
|
|
169
140
|
i = 0
|
|
170
141
|
while i < len(argv):
|
|
@@ -186,6 +157,19 @@ def extract_cloud_and_root(argv: list[str]) -> tuple[str, str | None]:
|
|
|
186
157
|
return cloud, root
|
|
187
158
|
|
|
188
159
|
|
|
160
|
+
def expand_list_flag_bundles(argv: list[str]) -> list[str]:
|
|
161
|
+
"""Expands the supported POSIX list bundles without changing other commands."""
|
|
162
|
+
if not any(argument in ("list", "ls") for argument in argv):
|
|
163
|
+
return argv
|
|
164
|
+
expanded: list[str] = []
|
|
165
|
+
for argument in argv:
|
|
166
|
+
if argument in ("-la", "-al"):
|
|
167
|
+
expanded.extend(["-l", "-a"])
|
|
168
|
+
else:
|
|
169
|
+
expanded.append(argument)
|
|
170
|
+
return expanded
|
|
171
|
+
|
|
172
|
+
|
|
189
173
|
def cmd_grep(args: argparse.Namespace) -> int:
|
|
190
174
|
"""
|
|
191
175
|
Pit-Grep: Living-state semantic search across single or multiple pits.
|
|
@@ -342,34 +326,144 @@ def cmd_history(args: argparse.Namespace) -> int:
|
|
|
342
326
|
return 0
|
|
343
327
|
|
|
344
328
|
|
|
345
|
-
def
|
|
346
|
-
"""
|
|
347
|
-
|
|
329
|
+
def _is_explicit_local_directory(value: str) -> bool:
|
|
330
|
+
"""Returns whether value identifies a local filesystem path rather than a tenant name."""
|
|
331
|
+
return (
|
|
332
|
+
value.startswith(("/", "~", ".", "\\"))
|
|
333
|
+
or "/" in value
|
|
334
|
+
or "\\" in value
|
|
335
|
+
or Path(os.path.expanduser(value)).is_dir()
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _discover_pit_names(directory: Path) -> list[str]:
|
|
340
|
+
if not directory.is_dir():
|
|
341
|
+
return []
|
|
342
|
+
return sorted({
|
|
343
|
+
child.name
|
|
344
|
+
for child in directory.iterdir()
|
|
345
|
+
if child.is_dir() and (child / f"{child.name}.pit").is_file()
|
|
346
|
+
} | {
|
|
347
|
+
file.stem for file in directory.glob("*.pit") if file.is_file()
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _format_pit_listing(directory: Path, name: str, long_listing: bool) -> str:
|
|
352
|
+
if not long_listing:
|
|
353
|
+
return f" {name}"
|
|
354
|
+
nested = directory / name / f"{name}.pit"
|
|
355
|
+
flat = directory / f"{name}.pit"
|
|
356
|
+
metadata = (nested if nested.is_file() else flat).stat()
|
|
357
|
+
size = f"{metadata.st_size / 1024:.1f} KB"
|
|
358
|
+
modified = datetime.datetime.fromtimestamp(metadata.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
359
|
+
return f" {name:<12} {size:>9} {modified}"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _format_discovery_directory(directory: Path) -> str:
|
|
363
|
+
"""Matches pits' RaiPath display form, including its terminal separator."""
|
|
364
|
+
text = str(directory)
|
|
365
|
+
return text if text.endswith(os.sep) else text + os.sep
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _render_discovery(
|
|
369
|
+
provider: str | None,
|
|
370
|
+
directory: Path,
|
|
371
|
+
pits: list[str],
|
|
372
|
+
all_clouds: bool,
|
|
373
|
+
long_listing: bool = False,
|
|
374
|
+
) -> str:
|
|
375
|
+
display_directory = _format_discovery_directory(directory)
|
|
376
|
+
if all_clouds:
|
|
377
|
+
if not pits:
|
|
378
|
+
return f"[{provider}] ({display_directory}): No pits found."
|
|
379
|
+
heading = f"[{provider}] ({display_directory}): {len(pits)} pit(s) found"
|
|
380
|
+
else:
|
|
381
|
+
heading = f"Found {len(pits)} pit(s) in cloud '{provider}' ({display_directory}):"
|
|
382
|
+
return "\n".join([heading, *[_format_pit_listing(directory, name, long_listing) for name in pits]])
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _discover_tenant_roots(root: str, cloud: str | None, all_clouds: bool) -> list[tuple[str | None, Path, list[str]]]:
|
|
386
|
+
"""Discovers a local root or configured cloud tenant roots without guessing a provider."""
|
|
387
|
+
if cloud:
|
|
388
|
+
candidate = resolve_command_pit_root(root=root, cloud=cloud, target_name=root, operation="list")
|
|
389
|
+
return [(cloud, candidate, _discover_pit_names(candidate))]
|
|
390
|
+
if _is_explicit_local_directory(root):
|
|
391
|
+
candidate = Path(os.path.abspath(os.path.expanduser(root)))
|
|
392
|
+
return [(None, candidate, _discover_pit_names(candidate))]
|
|
393
|
+
|
|
394
|
+
cfg = OsConfig.load()
|
|
395
|
+
if not cfg.is_config_loaded:
|
|
396
|
+
raise PitNotFoundError(missing_configuration_diagnostic())
|
|
397
|
+
providers = [provider for provider in cfg.default_cloud_order if cfg.get_cloud_root(provider) is not None]
|
|
398
|
+
if not providers:
|
|
399
|
+
raise PitNotFoundError("No configured DefaultCloudOrder providers are available for tenant discovery.")
|
|
400
|
+
discoveries = [
|
|
401
|
+
(provider, cfg.get_cloud_root(provider) / root, _discover_pit_names(cfg.get_cloud_root(provider) / root))
|
|
402
|
+
for provider in providers
|
|
403
|
+
]
|
|
404
|
+
if all_clouds:
|
|
405
|
+
return discoveries
|
|
406
|
+
return [next((discovery for discovery in discoveries if discovery[2]), discoveries[0])]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _require_explicit_mutation_location(args: argparse.Namespace) -> None:
|
|
410
|
+
"""Prevents mutations from silently selecting a provider from DefaultCloudOrder."""
|
|
411
|
+
if getattr(args, "cloud", None):
|
|
412
|
+
return
|
|
348
413
|
root = getattr(args, "root", None)
|
|
349
|
-
|
|
414
|
+
if root and _is_explicit_local_directory(root):
|
|
415
|
+
return
|
|
416
|
+
pit = getattr(args, "pit", None)
|
|
417
|
+
if pit and _is_explicit_local_directory(str(pit)):
|
|
418
|
+
return
|
|
419
|
+
label = root or pit or "the requested Pit"
|
|
420
|
+
raise JsonPitError(
|
|
421
|
+
f"Mutation refuses to guess a cloud for '{label}'. Supply -c <provider> or an explicit local directory path."
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def cmd_list_discovery(args: argparse.Namespace) -> int:
|
|
426
|
+
"""Lists tenant pits with mandatory cloud and directory provenance."""
|
|
427
|
+
root = getattr(args, "root", None)
|
|
428
|
+
cloud = getattr(args, "cloud", None)
|
|
429
|
+
all_clouds = bool(getattr(args, "all", False))
|
|
430
|
+
long_listing = bool(getattr(args, "long", False))
|
|
350
431
|
if not root:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
432
|
+
raise JsonPitError("list requires -r or --root <tenant-or-path>.")
|
|
433
|
+
if all_clouds and cloud:
|
|
434
|
+
raise JsonPitError("--all scans DefaultCloudOrder and cannot be combined with -c or --cloud.")
|
|
354
435
|
|
|
355
|
-
|
|
436
|
+
discoveries = _discover_tenant_roots(root, cloud, all_clouds)
|
|
356
437
|
if getattr(args, "json", False):
|
|
357
|
-
|
|
438
|
+
payload = [
|
|
439
|
+
{"cloud": provider, "path": str(directory), "pits": pits}
|
|
440
|
+
for provider, directory, pits in discoveries
|
|
441
|
+
]
|
|
442
|
+
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
358
443
|
return 0
|
|
359
444
|
|
|
360
|
-
if
|
|
361
|
-
|
|
362
|
-
|
|
445
|
+
if all_clouds:
|
|
446
|
+
for provider, directory, pits in discoveries:
|
|
447
|
+
sys.stdout.write(_render_discovery(provider, directory, pits, all_clouds=True, long_listing=long_listing) + "\n")
|
|
448
|
+
return 0
|
|
363
449
|
|
|
364
|
-
|
|
365
|
-
|
|
450
|
+
provider, directory, pits = discoveries[0]
|
|
451
|
+
if provider is None:
|
|
452
|
+
sys.stdout.write(f"Found {len(pits)} pit(s) in local directory '{_format_discovery_directory(directory)}':\n")
|
|
453
|
+
for name in pits:
|
|
454
|
+
sys.stdout.write(_format_pit_listing(directory, name, long_listing) + "\n")
|
|
455
|
+
return 0
|
|
456
|
+
if not pits and cloud is None:
|
|
457
|
+
sys.stdout.write(f"No pits found under tenant root '{root}' across configured clouds.\n")
|
|
458
|
+
return 0
|
|
459
|
+
sys.stdout.write(_render_discovery(provider, directory, pits, all_clouds=False, long_listing=long_listing) + "\n")
|
|
366
460
|
return 0
|
|
367
461
|
|
|
368
462
|
|
|
369
463
|
def cmd_list(args: argparse.Namespace) -> int:
|
|
370
464
|
"""Lists active entities in a Pit, or available pits if <pit> is omitted."""
|
|
371
465
|
if not getattr(args, "pit", None):
|
|
372
|
-
return
|
|
466
|
+
return cmd_list_discovery(args)
|
|
373
467
|
|
|
374
468
|
with Pit.open(
|
|
375
469
|
args.pit,
|
|
@@ -836,21 +930,11 @@ def cmd_maintain(args: argparse.Namespace) -> int:
|
|
|
836
930
|
targets: list[tuple[Path, Path, str]] = []
|
|
837
931
|
|
|
838
932
|
if wwwa:
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if base_dir is None:
|
|
845
|
-
cfg = OsConfig.load()
|
|
846
|
-
if not cfg.is_config_loaded:
|
|
847
|
-
sys.stderr.write(f"[jpit] Error: {missing_configuration_diagnostic()}\n")
|
|
848
|
-
return 1
|
|
849
|
-
cloud_root = cfg.get_cloud_root(cloud)
|
|
850
|
-
if not cloud_root or not cloud_root.is_dir():
|
|
851
|
-
sys.stderr.write(f"[jpit] Error: Cloud provider '{cloud}' root directory not found.\n")
|
|
852
|
-
return 1
|
|
853
|
-
base_dir = cloud_root / root.strip("/\\") if root else cloud_root
|
|
933
|
+
try:
|
|
934
|
+
base_dir = resolve_command_pit_root(root=root, cloud=cloud, target_name="WWWA", operation="maintenance")
|
|
935
|
+
except Exception as ex:
|
|
936
|
+
sys.stderr.write(f"[jpit] Error: {ex}\n")
|
|
937
|
+
return 1
|
|
854
938
|
|
|
855
939
|
if not base_dir.is_dir():
|
|
856
940
|
sys.stderr.write(f"[jpit] Error: The maintenance root does not exist: {base_dir}\n")
|
|
@@ -952,21 +1036,11 @@ def cmd_audit(args: argparse.Namespace) -> int:
|
|
|
952
1036
|
directories: list[Path] = []
|
|
953
1037
|
|
|
954
1038
|
if wwwa:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if base_dir is None:
|
|
961
|
-
cfg = OsConfig.load()
|
|
962
|
-
if not cfg.is_config_loaded:
|
|
963
|
-
sys.stderr.write(f"[jpit] Error: {missing_configuration_diagnostic()}\n")
|
|
964
|
-
return 1
|
|
965
|
-
cloud_root = cfg.get_cloud_root(cloud)
|
|
966
|
-
if not cloud_root or not cloud_root.is_dir():
|
|
967
|
-
sys.stderr.write(f"[jpit] Error: Cloud provider '{cloud}' root directory not found.\n")
|
|
968
|
-
return 1
|
|
969
|
-
base_dir = cloud_root / root.strip("/\\") if root else cloud_root
|
|
1039
|
+
try:
|
|
1040
|
+
base_dir = resolve_command_pit_root(root=root, cloud=cloud, target_name="WWWA", operation="audit")
|
|
1041
|
+
except Exception as ex:
|
|
1042
|
+
sys.stderr.write(f"[jpit] Error: {ex}\n")
|
|
1043
|
+
return 1
|
|
970
1044
|
|
|
971
1045
|
for name in WWWA_PITS:
|
|
972
1046
|
directories.append(base_dir / name)
|
|
@@ -1033,8 +1107,7 @@ def get_cloud_options_description(use_color: bool = True) -> str:
|
|
|
1033
1107
|
glyph = Icons.cloud_provider_icon(name, idx + 1)
|
|
1034
1108
|
badge_color = OPTION_BADGE_COLORS[idx % len(OPTION_BADGE_COLORS)]
|
|
1035
1109
|
badge = color(glyph, badge_color, use_color)
|
|
1036
|
-
|
|
1037
|
-
formatted.append(f"{badge} {name}{default_str}")
|
|
1110
|
+
formatted.append(f"{badge} {name}")
|
|
1038
1111
|
|
|
1039
1112
|
return ", ".join(formatted)
|
|
1040
1113
|
|
|
@@ -1055,7 +1128,8 @@ def print_top_help(
|
|
|
1055
1128
|
nologo: bool = False,
|
|
1056
1129
|
use_color: bool = True,
|
|
1057
1130
|
root: str | None = None,
|
|
1058
|
-
cloud: str | None =
|
|
1131
|
+
cloud: str | None = None,
|
|
1132
|
+
wwwa: bool = False,
|
|
1059
1133
|
) -> None:
|
|
1060
1134
|
"""Prints the branded jpit help screen with Nerd Font glyphs and multi-token ANSI accents."""
|
|
1061
1135
|
if not nologo:
|
|
@@ -1067,14 +1141,14 @@ def print_top_help(
|
|
|
1067
1141
|
i_folder = color(Icons.FOLDER, C_YELLOW, use_color)
|
|
1068
1142
|
i_banner = color(Icons.BANNER, C_CYAN, use_color)
|
|
1069
1143
|
|
|
1070
|
-
cmd_list = "grep, get, history, list, put, set, del, del-prop, rename, export, status,
|
|
1144
|
+
cmd_list = "grep, get, history, list (ls), put, set, del, del-prop, rename, export, status, maintain, audit"
|
|
1071
1145
|
sys.stdout.write(f"{c_cmd}\t{i_info}\t{cmd_list}\n")
|
|
1072
1146
|
|
|
1073
1147
|
commands_spec = [
|
|
1074
1148
|
("grep", "<pattern> [<target>] [-i] [-e] [-p <prop>] [--at <ts>] [--json] [--jq <expr>]"),
|
|
1075
1149
|
("get", "<PitName> <ItemId> [--at <ts>] [--jq <expr>] [--with-deleted]"),
|
|
1076
1150
|
("history", "<PitName> <ItemId> [--jq <expr>]"),
|
|
1077
|
-
("list", "[<PitName>] [
|
|
1151
|
+
("list", "[<PitName>] [-r <tenant|path>] [-c <cloud>] [-a|--all] [-l|--long] [--json]"),
|
|
1078
1152
|
("put", "<PitName> [<source>] [-s <source>] (alias: seed)"),
|
|
1079
1153
|
("set", "<PitName> <ItemId> <payload>"),
|
|
1080
1154
|
("del", "<PitName> <ItemId> [--by <author>]"),
|
|
@@ -1082,7 +1156,6 @@ def print_top_help(
|
|
|
1082
1156
|
("rename", "<PitName> <OldId> <NewId> [--by <author>]"),
|
|
1083
1157
|
("export", "<PitName> [--out <file>] [--at <ts>] [--jq <expr>]"),
|
|
1084
1158
|
("status", "<PitName>"),
|
|
1085
|
-
("pits", "[-r <root>] [-c <cloud>] [--json] (discover available pits)"),
|
|
1086
1159
|
("maintain", "(<PitName> | --wwwa) [--apply] [--prune-process-flags --older-than <dur>] [--json]"),
|
|
1087
1160
|
("audit", "(<PitName> | --wwwa) [--machine <all|local|name>] [--level <severity>] [--json]"),
|
|
1088
1161
|
]
|
|
@@ -1114,7 +1187,7 @@ def print_top_help(
|
|
|
1114
1187
|
pits_title = color(f"{Icons.INFO} PitNames", C_CYAN, use_color)
|
|
1115
1188
|
if root:
|
|
1116
1189
|
try:
|
|
1117
|
-
discovered =
|
|
1190
|
+
discovered = _discover_tenant_roots(root, cloud, all_clouds=False)[0][2]
|
|
1118
1191
|
except Exception:
|
|
1119
1192
|
discovered = []
|
|
1120
1193
|
if discovered:
|
|
@@ -1132,6 +1205,40 @@ def print_top_help(
|
|
|
1132
1205
|
hint = "specify -r <root> (e.g. -r AIA) to discover pits"
|
|
1133
1206
|
sys.stdout.write(f"{pits_title}\t{i_folder}\t{hint}\n")
|
|
1134
1207
|
|
|
1208
|
+
if wwwa or _has_wwwa_pit(root, cloud):
|
|
1209
|
+
for name in WWWA_PITS:
|
|
1210
|
+
separator = "\t\t" if name == "Place" else "\t"
|
|
1211
|
+
status, path = _wwwa_pit_status(name, root, cloud)
|
|
1212
|
+
sys.stdout.write(f"{Icons.INFO} {name}{separator}{status}\t{path}\n")
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _wwwa_roots(root: str | None, cloud: str | None) -> list[Path]:
|
|
1216
|
+
"""Returns resolved help roots without making a cloud selection implicit."""
|
|
1217
|
+
if not root:
|
|
1218
|
+
return []
|
|
1219
|
+
try:
|
|
1220
|
+
return [directory for _, directory, _ in _discover_tenant_roots(root, cloud, all_clouds=False)]
|
|
1221
|
+
except Exception:
|
|
1222
|
+
return []
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def _has_wwwa_pit(root: str | None, cloud: str | None) -> bool:
|
|
1226
|
+
return any(
|
|
1227
|
+
(directory / name / f"{name}.pit").is_file()
|
|
1228
|
+
for directory in _wwwa_roots(root, cloud)
|
|
1229
|
+
for name in WWWA_PITS
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def _wwwa_pit_status(name: str, root: str | None, cloud: str | None) -> tuple[str, str]:
|
|
1234
|
+
"""Matches pits' WWWA help line: status glyph plus its resolved Pit file path."""
|
|
1235
|
+
roots = _wwwa_roots(root, cloud)
|
|
1236
|
+
if not roots:
|
|
1237
|
+
return Icons.NOT_AVAILABLE, f"{name}.pit"
|
|
1238
|
+
|
|
1239
|
+
pit_file = roots[0] / name / f"{name}.pit"
|
|
1240
|
+
return (Icons.SUCCESS if pit_file.is_file() else Icons.NOT_AVAILABLE), str(pit_file)
|
|
1241
|
+
|
|
1135
1242
|
|
|
1136
1243
|
def print_command_help(cmd: str, nologo: bool = False, use_color: bool = True) -> None:
|
|
1137
1244
|
"""Prints formatted help for an individual subcommand."""
|
|
@@ -1199,14 +1306,6 @@ def print_command_help(cmd: str, nologo: bool = False, use_color: bool = True) -
|
|
|
1199
1306
|
("--jq <expr>", "Convenience pipe through jq filter"),
|
|
1200
1307
|
],
|
|
1201
1308
|
},
|
|
1202
|
-
"pits": {
|
|
1203
|
-
"usage": "jpit pits [-r <root>] [-c <cloud>] [--json]",
|
|
1204
|
-
"desc": "Discover available pits under a tenant root (-r is required).",
|
|
1205
|
-
"args": [],
|
|
1206
|
-
"opts": [
|
|
1207
|
-
("--json", "Output JSON array of pit names"),
|
|
1208
|
-
],
|
|
1209
|
-
},
|
|
1210
1309
|
"put": {
|
|
1211
1310
|
"usage": "jpit put <PitName> [<source>] [-s <source>] [--require-existing] [--patch] [options]",
|
|
1212
1311
|
"desc": "Ingest JSON5 entities from file or stdin pipe (alias: seed).",
|
|
@@ -1492,14 +1591,12 @@ def build_parser(prog: str | None = None) -> JsonPitArgumentParser:
|
|
|
1492
1591
|
p_hist.add_argument("--jq", help="Convenience pipe through jq filter")
|
|
1493
1592
|
|
|
1494
1593
|
# list
|
|
1495
|
-
p_list = subparsers.add_parser("list", parents=[common_parser], help="List active entities
|
|
1594
|
+
p_list = subparsers.add_parser("list", aliases=["ls"], parents=[common_parser], help="List active entities or discover tenant pits")
|
|
1496
1595
|
p_list.add_argument("pit", nargs="?", default=None, help="Pit name (optional: if omitted, lists available pits in -r root)")
|
|
1497
1596
|
p_list.add_argument("--json", action="store_true", help="Output JSON array")
|
|
1498
1597
|
p_list.add_argument("--jq", help="Convenience pipe through jq filter")
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
p_pits = subparsers.add_parser("pits", parents=[common_parser], help="Discover available pits under a tenant root (-r is required)")
|
|
1502
|
-
p_pits.add_argument("--json", action="store_true", help="Output JSON array of pit names")
|
|
1598
|
+
p_list.add_argument("-a", "--all", action="store_true", help="Discover the tenant in every DefaultCloudOrder provider")
|
|
1599
|
+
p_list.add_argument("-l", "--long", action="store_true", help="Include each pit file's size and last modification time")
|
|
1503
1600
|
|
|
1504
1601
|
# put / seed
|
|
1505
1602
|
p_put = subparsers.add_parser(
|
|
@@ -1603,20 +1700,22 @@ def build_parser(prog: str | None = None) -> JsonPitArgumentParser:
|
|
|
1603
1700
|
def main(argv: list[str] | None = None) -> int:
|
|
1604
1701
|
if argv is None:
|
|
1605
1702
|
argv = sys.argv[1:]
|
|
1703
|
+
argv = expand_list_flag_bundles(argv)
|
|
1606
1704
|
|
|
1607
1705
|
use_color = should_color()
|
|
1608
1706
|
nologo = "-n" in argv or "--nologo" in argv
|
|
1609
1707
|
cloud, root = extract_cloud_and_root(argv)
|
|
1708
|
+
wwwa = "--wwwa" in argv
|
|
1610
1709
|
|
|
1611
1710
|
known_subcommands = {
|
|
1612
|
-
"grep", "get", "history", "list", "put", "seed", "set", "del",
|
|
1711
|
+
"grep", "get", "history", "list", "ls", "put", "seed", "set", "del",
|
|
1613
1712
|
"delete", "delete-item", "del-prop", "delete-property",
|
|
1614
|
-
"rename", "export", "status", "
|
|
1713
|
+
"rename", "export", "status", "maintain", "audit",
|
|
1615
1714
|
}
|
|
1616
1715
|
|
|
1617
1716
|
# Quick check for top-level help or no arguments
|
|
1618
1717
|
if len(argv) == 0 or (len(argv) == 1 and argv[0] in ("-h", "--help")):
|
|
1619
|
-
print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud)
|
|
1718
|
+
print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud, wwwa=wwwa)
|
|
1620
1719
|
return 0
|
|
1621
1720
|
|
|
1622
1721
|
# Check for command-specific or option-specific help: e.g. jpit --retain-window -h or jpit grep -h
|
|
@@ -1629,14 +1728,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1629
1728
|
if target_cmd:
|
|
1630
1729
|
print_command_help(target_cmd, nologo=nologo, use_color=use_color)
|
|
1631
1730
|
return 0
|
|
1632
|
-
print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud)
|
|
1731
|
+
print_top_help(nologo=nologo, use_color=use_color, root=root, cloud=cloud, wwwa=wwwa)
|
|
1633
1732
|
return 0
|
|
1634
1733
|
|
|
1635
1734
|
parser = build_parser()
|
|
1636
1735
|
args = parser.parse_args(argv)
|
|
1637
1736
|
|
|
1638
1737
|
if not hasattr(args, "cloud"):
|
|
1639
|
-
args.cloud =
|
|
1738
|
+
args.cloud = None
|
|
1640
1739
|
if not hasattr(args, "root"):
|
|
1641
1740
|
args.root = None
|
|
1642
1741
|
|
|
@@ -1645,7 +1744,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1645
1744
|
"get": cmd_get,
|
|
1646
1745
|
"history": cmd_history,
|
|
1647
1746
|
"list": cmd_list,
|
|
1648
|
-
"
|
|
1747
|
+
"ls": cmd_list,
|
|
1649
1748
|
"put": cmd_put,
|
|
1650
1749
|
"seed": cmd_put,
|
|
1651
1750
|
"set": cmd_set,
|
|
@@ -1667,6 +1766,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1667
1766
|
return 1
|
|
1668
1767
|
|
|
1669
1768
|
try:
|
|
1769
|
+
mutating_commands = {"put", "seed", "set", "del", "delete", "delete-item", "del-prop", "delete-property", "rename"}
|
|
1770
|
+
if args.command in mutating_commands or (args.command == "maintain" and args.apply):
|
|
1771
|
+
_require_explicit_mutation_location(args)
|
|
1670
1772
|
return cmd_func(args)
|
|
1671
1773
|
except JsonPitError as ex:
|
|
1672
1774
|
err_icon = color(Icons.ERROR, C_BOLD + C_RED, use_color)
|
|
@@ -1682,5 +1784,3 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1682
1784
|
|
|
1683
1785
|
if __name__ == "__main__":
|
|
1684
1786
|
sys.exit(main())
|
|
1685
|
-
|
|
1686
|
-
|
|
@@ -73,6 +73,50 @@ def resolve_pit_target(
|
|
|
73
73
|
return target_dir, pit_name
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def resolve_command_pit_root(
|
|
77
|
+
root: str | None = None,
|
|
78
|
+
cloud: str | None = None,
|
|
79
|
+
target_name: str = "WWWA",
|
|
80
|
+
operation: str = "maintenance",
|
|
81
|
+
config: OsConfig | None = None,
|
|
82
|
+
) -> Path:
|
|
83
|
+
"""
|
|
84
|
+
Resolves a tenant pit root directory adhering to 100% C# pits CLI parity:
|
|
85
|
+
- If an explicit filesystem path is provided (e.g. starting with /, ~, .), resolves directly.
|
|
86
|
+
- If cloud is provided: resolves cloud root via OsConfig and appends relative root.
|
|
87
|
+
Crucially, does NOT treat relative root strings as local filesystem directories.
|
|
88
|
+
- If cloud is not provided and root is provided: resolves root directly.
|
|
89
|
+
- If neither is provided: raises PitNotFoundError.
|
|
90
|
+
"""
|
|
91
|
+
raw_root = str(root).strip() if root else ""
|
|
92
|
+
is_explicit = (
|
|
93
|
+
isinstance(root, Path)
|
|
94
|
+
or raw_root.startswith(("/", "~", ".", "\\"))
|
|
95
|
+
or ("/" in raw_root and not raw_root.startswith("-"))
|
|
96
|
+
or ("\\" in raw_root)
|
|
97
|
+
)
|
|
98
|
+
if is_explicit:
|
|
99
|
+
return Path(os.path.abspath(os.path.expanduser(raw_root)))
|
|
100
|
+
|
|
101
|
+
cfg = config or OsConfig.load()
|
|
102
|
+
if cloud:
|
|
103
|
+
if not cfg.is_config_loaded:
|
|
104
|
+
raise PitNotFoundError(missing_configuration_diagnostic())
|
|
105
|
+
cloud_root = cfg.get_cloud_root(cloud)
|
|
106
|
+
if not cloud_root or not cloud_root.is_dir():
|
|
107
|
+
raise PitNotFoundError(
|
|
108
|
+
f"The requested cloud provider '{cloud}' is missing or empty in {DEFAULT_CONFIG_FILE_LOCATION}."
|
|
109
|
+
)
|
|
110
|
+
return cloud_root / raw_root.strip("/\\") if raw_root else cloud_root
|
|
111
|
+
|
|
112
|
+
if raw_root:
|
|
113
|
+
return Path(os.path.abspath(os.path.expanduser(raw_root)))
|
|
114
|
+
|
|
115
|
+
raise PitNotFoundError(
|
|
116
|
+
f"Cannot resolve {operation} target '{target_name}' without -r or --pitroot, or a configured -c or --cloud provider."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
76
120
|
def ensure_directory(path: Path | str) -> Path:
|
|
77
121
|
"""Ensures the directory exists, creating intermediate parents if needed."""
|
|
78
122
|
p = Path(path)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jsonpit"
|
|
7
|
-
version = "4.5.
|
|
7
|
+
version = "4.5.8"
|
|
8
8
|
description = "Daemon-free distributed storage over Cloud Drives for developers and AI agents — multi-process, immutable history, zero dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|