heritage-cli 1.0.1__tar.gz → 1.0.2__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.
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/PKG-INFO +1 -1
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/README.md +4 -1
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/pyproject.toml +6 -1
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/PKG-INFO +1 -1
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/SOURCES.txt +1 -0
- heritage_cli-1.0.2/tests/test_integration.py +309 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/setup.cfg +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/__init__.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/__init__.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/dibble.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/hoard.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/libby.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/stratigraph.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/trowel.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/main.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/orchestrator.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/dependency_links.txt +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/entry_points.txt +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/requires.txt +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/top_level.txt +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/tests/test_orchestrator.py +0 -0
- {heritage_cli-1.0.1 → heritage_cli-1.0.2}/tests/test_smoke.py +0 -0
|
@@ -130,7 +130,10 @@ MIT
|
|
|
130
130
|
# Clone and install in dev mode
|
|
131
131
|
pip install -e ".[dev]"
|
|
132
132
|
|
|
133
|
-
# Run tests
|
|
133
|
+
# Run unit tests (no sibling packages required)
|
|
134
|
+
python -m pytest tests/ -v -m "not integration"
|
|
135
|
+
|
|
136
|
+
# Run all tests including integration (requires hoard installed)
|
|
134
137
|
python -m pytest tests/ -v
|
|
135
138
|
|
|
136
139
|
# Type check
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "heritage-cli"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
description = "Unified CLI for the heritage science open-source ecosystem — orchestrates HOARD, StratiGraph, Trowel, Libby, Dibble, and other tools"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -42,3 +42,8 @@ python_version = "3.11"
|
|
|
42
42
|
warn_return_any = true
|
|
43
43
|
warn_unused_configs = true
|
|
44
44
|
ignore_missing_imports = true
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
markers = [
|
|
48
|
+
"integration: tests that require sibling ecosystem packages to be installed",
|
|
49
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Integration tests — validate contracts with sibling ecosystem packages.
|
|
2
|
+
|
|
3
|
+
These tests verify that heritage-cli's assumptions about the HOARD Python API
|
|
4
|
+
and the entry-point plugin system match reality. They require sibling packages
|
|
5
|
+
to be installed and are skipped gracefully when unavailable.
|
|
6
|
+
|
|
7
|
+
Run with: python -m pytest tests/ -m integration -v
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
# ── HOARD API contract validation ───────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
HOARD_SYMBOLS = {
|
|
17
|
+
"hoard.config.Config": "Config",
|
|
18
|
+
"hoard.config.load_config": "load_config",
|
|
19
|
+
"hoard.cli.run.run_pipeline": "run_pipeline",
|
|
20
|
+
"hoard.cli.run.run_single_phase": "run_single_phase",
|
|
21
|
+
"hoard.review.dashboard.ReviewSession": "ReviewSession",
|
|
22
|
+
"hoard.phases.phase5.run_phase5": "run_phase5",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _hoard_available() -> bool:
|
|
27
|
+
"""Check if the hoard package is importable."""
|
|
28
|
+
try:
|
|
29
|
+
import hoard # noqa: F401
|
|
30
|
+
except ImportError:
|
|
31
|
+
return False
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
hoard_available = _hoard_available()
|
|
36
|
+
requires_hoard = pytest.mark.skipif(
|
|
37
|
+
not hoard_available, reason="HOARD is not installed"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.integration
|
|
42
|
+
class TestHoardContract:
|
|
43
|
+
"""Verify every symbol heritage-cli imports from HOARD exists and matches."""
|
|
44
|
+
|
|
45
|
+
@requires_hoard
|
|
46
|
+
def test_all_symbols_importable(self):
|
|
47
|
+
"""All 6 HOARD symbols heritage-cli relies on are importable."""
|
|
48
|
+
for full_path, name in HOARD_SYMBOLS.items():
|
|
49
|
+
mod_path, attr = full_path.rsplit(".", 1)
|
|
50
|
+
mod = __import__(mod_path, fromlist=[attr])
|
|
51
|
+
obj = getattr(mod, attr)
|
|
52
|
+
assert obj is not None, f"{full_path} imported but is None"
|
|
53
|
+
assert callable(obj) or hasattr(obj, "__init__"), (
|
|
54
|
+
f"{full_path} is not callable/constructable"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@requires_hoard
|
|
58
|
+
def test_config_accepts_expected_kwargs(self):
|
|
59
|
+
"""hoard.config.Config accepts all kwargs heritage-cli passes."""
|
|
60
|
+
from hoard.config import Config
|
|
61
|
+
|
|
62
|
+
# kwargs passed by main.py:run (Phase 1 fallback)
|
|
63
|
+
cfg = Config(
|
|
64
|
+
project_id="test_integration",
|
|
65
|
+
project_name="Test Project",
|
|
66
|
+
jurisdiction="historic_england_cl3",
|
|
67
|
+
workspace_root=Path("/tmp/heritage_test_ws"),
|
|
68
|
+
input_dir=Path("/tmp/heritage_test_input"),
|
|
69
|
+
strict=False,
|
|
70
|
+
extractor="glm-ocr",
|
|
71
|
+
)
|
|
72
|
+
assert cfg is not None
|
|
73
|
+
assert cfg.project_id == "test_integration"
|
|
74
|
+
assert cfg.project_name == "Test Project"
|
|
75
|
+
|
|
76
|
+
@requires_hoard
|
|
77
|
+
def test_config_accepts_minimal_kwargs(self):
|
|
78
|
+
"""hoard.config.Config accepts the minimal kwargs from main.py:review."""
|
|
79
|
+
from hoard.config import Config
|
|
80
|
+
|
|
81
|
+
cfg = Config(
|
|
82
|
+
project_id="test_minimal",
|
|
83
|
+
project_name="test_minimal",
|
|
84
|
+
jurisdiction="historic_england_cl3",
|
|
85
|
+
workspace_root=Path("/tmp/heritage_test_ws"),
|
|
86
|
+
input_dir=Path("/tmp/heritage_test_input"),
|
|
87
|
+
)
|
|
88
|
+
assert cfg is not None
|
|
89
|
+
|
|
90
|
+
@requires_hoard
|
|
91
|
+
def test_load_config_signature(self):
|
|
92
|
+
"""hoard.config.load_config accepts (project, workspace)."""
|
|
93
|
+
from hoard.config import load_config
|
|
94
|
+
|
|
95
|
+
result = load_config("nonexistent_project", Path("/tmp/nonexistent"))
|
|
96
|
+
# Returns Config or None for missing projects
|
|
97
|
+
assert result is None or hasattr(result, "project_id")
|
|
98
|
+
|
|
99
|
+
@requires_hoard
|
|
100
|
+
def test_run_pipeline_signature(self):
|
|
101
|
+
"""hoard.cli.run.run_pipeline accepts a Config."""
|
|
102
|
+
from inspect import signature
|
|
103
|
+
|
|
104
|
+
from hoard.cli.run import run_pipeline
|
|
105
|
+
|
|
106
|
+
sig = signature(run_pipeline)
|
|
107
|
+
params = list(sig.parameters)
|
|
108
|
+
assert "config" in params, (
|
|
109
|
+
f"run_pipeline expects 'config' parameter, got: {params}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@requires_hoard
|
|
113
|
+
def test_run_single_phase_signature(self):
|
|
114
|
+
"""hoard.cli.run.run_single_phase accepts (config, phase, gui_mode)."""
|
|
115
|
+
from inspect import signature
|
|
116
|
+
|
|
117
|
+
from hoard.cli.run import run_single_phase
|
|
118
|
+
|
|
119
|
+
sig = signature(run_single_phase)
|
|
120
|
+
params = list(sig.parameters)
|
|
121
|
+
# HOARD uses 'config' as the parameter name (heritage-cli passes positionally)
|
|
122
|
+
assert "config" in params, (
|
|
123
|
+
f"run_single_phase expects 'config' parameter, got: {params}"
|
|
124
|
+
)
|
|
125
|
+
assert "phase" in params, (
|
|
126
|
+
f"run_single_phase expects 'phase' parameter, got: {params}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@requires_hoard
|
|
130
|
+
def test_review_session_api(self):
|
|
131
|
+
"""ReviewSession has .load(), .total, .run_interactive()."""
|
|
132
|
+
from hoard.config import Config
|
|
133
|
+
from hoard.review.dashboard import ReviewSession
|
|
134
|
+
|
|
135
|
+
cfg = Config(
|
|
136
|
+
project_id="test_review",
|
|
137
|
+
project_name="test_review",
|
|
138
|
+
jurisdiction="historic_england_cl3",
|
|
139
|
+
workspace_root=Path("/tmp/heritage_review_test"),
|
|
140
|
+
input_dir=Path("/tmp/heritage_review_input"),
|
|
141
|
+
)
|
|
142
|
+
session = ReviewSession(cfg)
|
|
143
|
+
assert hasattr(session, "load"), "ReviewSession missing .load()"
|
|
144
|
+
assert hasattr(session, "total"), "ReviewSession missing .total"
|
|
145
|
+
assert hasattr(session, "run_interactive"), (
|
|
146
|
+
"ReviewSession missing .run_interactive()"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@requires_hoard
|
|
150
|
+
def test_run_phase5_signature(self):
|
|
151
|
+
"""hoard.phases.phase5.run_phase5 accepts (config, formats)."""
|
|
152
|
+
from inspect import signature
|
|
153
|
+
|
|
154
|
+
from hoard.phases.phase5 import run_phase5
|
|
155
|
+
|
|
156
|
+
sig = signature(run_phase5)
|
|
157
|
+
params = list(sig.parameters)
|
|
158
|
+
# HOARD uses 'config' as the parameter name (heritage-cli passes positionally)
|
|
159
|
+
assert "config" in params, (
|
|
160
|
+
f"run_phase5 expects 'config' parameter, got: {params}"
|
|
161
|
+
)
|
|
162
|
+
assert "formats" in params, (
|
|
163
|
+
f"run_phase5 expects 'formats' parameter, got: {params}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Entry-point plugin system ───────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.integration
|
|
171
|
+
class TestPluginDiscovery:
|
|
172
|
+
"""Verify the heritage.tools entry-point group is correctly populated."""
|
|
173
|
+
|
|
174
|
+
def test_entry_points_discoverable(self):
|
|
175
|
+
"""All 5 tools are declared as entry points and import to valid modules."""
|
|
176
|
+
from importlib import metadata
|
|
177
|
+
|
|
178
|
+
entry_points = list(metadata.entry_points(group="heritage.tools"))
|
|
179
|
+
|
|
180
|
+
expected_tools = {"hoard", "stratigraph", "trowel", "libby", "dibble"}
|
|
181
|
+
discovered = {ep.name for ep in entry_points}
|
|
182
|
+
|
|
183
|
+
assert expected_tools == discovered, (
|
|
184
|
+
f"Entry point mismatch. Expected {expected_tools}, got {discovered}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def test_all_entry_points_resolve_to_importable_modules(self):
|
|
188
|
+
"""Each entry point module actually imports without error."""
|
|
189
|
+
from importlib import metadata
|
|
190
|
+
|
|
191
|
+
for ep in metadata.entry_points(group="heritage.tools"):
|
|
192
|
+
mod = ep.load()
|
|
193
|
+
assert hasattr(mod, "tool_name"), f"Module for {ep.name} missing tool_name"
|
|
194
|
+
assert hasattr(mod, "dispatch"), f"Module for {ep.name} missing dispatch()"
|
|
195
|
+
assert callable(mod.dispatch), f"dispatch() in {ep.name} is not callable"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ── CLI binary detection ────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.integration
|
|
202
|
+
class TestBinaryDispatch:
|
|
203
|
+
"""Verify that binary-first dispatch works for installed tools."""
|
|
204
|
+
|
|
205
|
+
def _which(self, name: str) -> bool:
|
|
206
|
+
import shutil
|
|
207
|
+
|
|
208
|
+
return shutil.which(name) is not None
|
|
209
|
+
|
|
210
|
+
@pytest.mark.skipif(
|
|
211
|
+
not _hoard_available(),
|
|
212
|
+
reason="HOARD is not installed (binary may still be available)",
|
|
213
|
+
)
|
|
214
|
+
def test_hoard_binary_or_python_available(self):
|
|
215
|
+
"""HOARD is reachable via binary or Python import."""
|
|
216
|
+
import shutil
|
|
217
|
+
|
|
218
|
+
has_binary = shutil.which("hoard") is not None
|
|
219
|
+
has_python = _hoard_available()
|
|
220
|
+
assert has_binary or has_python, (
|
|
221
|
+
"HOARD not found as binary on PATH or as Python import"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def test_tools_list_finds_installed(self):
|
|
225
|
+
"""heritage tools runs without error and reports status."""
|
|
226
|
+
from typer.testing import CliRunner
|
|
227
|
+
|
|
228
|
+
from heritage_cli.main import app
|
|
229
|
+
|
|
230
|
+
runner = CliRunner()
|
|
231
|
+
result = runner.invoke(app, ["tools"])
|
|
232
|
+
assert result.exit_code == 0
|
|
233
|
+
# Should at minimum list hoard in the table
|
|
234
|
+
assert "hoard" in result.stdout.lower()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ── End-to-end: pipeline YAML to orchestrator with real StepKind ────────
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.mark.integration
|
|
241
|
+
class TestPipelineIntegration:
|
|
242
|
+
"""End-to-end pipeline parsing with all supported tool types."""
|
|
243
|
+
|
|
244
|
+
def test_pipeline_example_yaml_parses(self):
|
|
245
|
+
"""The shipped pipeline.example.yaml parses without errors."""
|
|
246
|
+
from heritage_cli.orchestrator import PipelineOrchestrator
|
|
247
|
+
|
|
248
|
+
example = Path(__file__).parent.parent / "pipeline.example.yaml"
|
|
249
|
+
assert example.exists(), "pipeline.example.yaml is missing"
|
|
250
|
+
|
|
251
|
+
orch = PipelineOrchestrator(
|
|
252
|
+
pipeline_path=example,
|
|
253
|
+
project_id="test_proj_integration",
|
|
254
|
+
workspace="/tmp/heritage_integration_ws",
|
|
255
|
+
)
|
|
256
|
+
orch.load()
|
|
257
|
+
|
|
258
|
+
assert len(orch.steps) > 0
|
|
259
|
+
kinds = {s.kind for s in orch.steps}
|
|
260
|
+
# Should contain HOARD and GATE steps at minimum
|
|
261
|
+
from heritage_cli.orchestrator import StepKind
|
|
262
|
+
|
|
263
|
+
assert StepKind.HOARD in kinds, "No HOARD steps in example pipeline"
|
|
264
|
+
assert StepKind.GATE in kinds, "No GATE steps in example pipeline"
|
|
265
|
+
assert StepKind.EXPORT in kinds, "No EXPORT step in example pipeline"
|
|
266
|
+
|
|
267
|
+
def test_state_roundtrip_with_all_statuses(self):
|
|
268
|
+
"""PipelineState persists and restores all 6 status values."""
|
|
269
|
+
import json
|
|
270
|
+
|
|
271
|
+
from heritage_cli.orchestrator import (
|
|
272
|
+
PipelineOrchestrator,
|
|
273
|
+
PipelineStep,
|
|
274
|
+
StepKind,
|
|
275
|
+
StepStatus,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
orch = PipelineOrchestrator(
|
|
279
|
+
pipeline_path=Path("/tmp/test_pipe.yaml"),
|
|
280
|
+
project_id="state_integration_test",
|
|
281
|
+
workspace="/tmp/heritage_state_test",
|
|
282
|
+
)
|
|
283
|
+
# Create a dummy pipeline file so load() doesn't fail
|
|
284
|
+
Path("/tmp/test_pipe.yaml").write_text("steps: []")
|
|
285
|
+
|
|
286
|
+
orch.steps = [
|
|
287
|
+
PipelineStep(id="s1", kind=StepKind.HOARD, status=StepStatus.COMPLETE),
|
|
288
|
+
PipelineStep(id="s2", kind=StepKind.GATE, status=StepStatus.PENDING),
|
|
289
|
+
PipelineStep(id="s3", kind=StepKind.COMMAND, status=StepStatus.FAILED),
|
|
290
|
+
PipelineStep(id="s4", kind=StepKind.LIBBY, status=StepStatus.SKIPPED),
|
|
291
|
+
PipelineStep(id="s5", kind=StepKind.DIBBLE, status=StepStatus.RUNNING),
|
|
292
|
+
PipelineStep(id="s6", kind=StepKind.HOARD, status=StepStatus.BLOCKED),
|
|
293
|
+
]
|
|
294
|
+
orch._save_state()
|
|
295
|
+
assert orch.state_file.exists()
|
|
296
|
+
|
|
297
|
+
restored = orch._load_state()
|
|
298
|
+
assert restored["s1"] == StepStatus.COMPLETE
|
|
299
|
+
assert restored["s2"] == StepStatus.PENDING
|
|
300
|
+
assert restored["s3"] == StepStatus.FAILED
|
|
301
|
+
assert restored["s4"] == StepStatus.SKIPPED
|
|
302
|
+
assert restored["s5"] == StepStatus.RUNNING
|
|
303
|
+
assert restored["s6"] == StepStatus.BLOCKED
|
|
304
|
+
|
|
305
|
+
# Verify JSON structure
|
|
306
|
+
raw = json.loads(orch.state_file.read_text())
|
|
307
|
+
assert "started_at" in raw
|
|
308
|
+
assert "updated_at" in raw
|
|
309
|
+
assert raw["project_id"] == "state_integration_test"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|