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.
Files changed (22) hide show
  1. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/PKG-INFO +1 -1
  2. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/README.md +4 -1
  3. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/pyproject.toml +6 -1
  4. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/PKG-INFO +1 -1
  5. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/SOURCES.txt +1 -0
  6. heritage_cli-1.0.2/tests/test_integration.py +309 -0
  7. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/setup.cfg +0 -0
  8. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/__init__.py +0 -0
  9. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/__init__.py +0 -0
  10. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/dibble.py +0 -0
  11. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/hoard.py +0 -0
  12. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/libby.py +0 -0
  13. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/stratigraph.py +0 -0
  14. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/commands/trowel.py +0 -0
  15. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/main.py +0 -0
  16. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli/orchestrator.py +0 -0
  17. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/dependency_links.txt +0 -0
  18. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/entry_points.txt +0 -0
  19. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/requires.txt +0 -0
  20. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/src/heritage_cli.egg-info/top_level.txt +0 -0
  21. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/tests/test_orchestrator.py +0 -0
  22. {heritage_cli-1.0.1 → heritage_cli-1.0.2}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: heritage-cli
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Unified CLI for the heritage science open-source ecosystem — orchestrates HOARD, StratiGraph, Trowel, Libby, Dibble, and other tools
5
5
  Author: Mark Bouck
6
6
  License: MIT
@@ -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.1"
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
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: heritage-cli
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Unified CLI for the heritage science open-source ecosystem — orchestrates HOARD, StratiGraph, Trowel, Libby, Dibble, and other tools
5
5
  Author: Mark Bouck
6
6
  License: MIT
@@ -15,5 +15,6 @@ src/heritage_cli/commands/hoard.py
15
15
  src/heritage_cli/commands/libby.py
16
16
  src/heritage_cli/commands/stratigraph.py
17
17
  src/heritage_cli/commands/trowel.py
18
+ tests/test_integration.py
18
19
  tests/test_orchestrator.py
19
20
  tests/test_smoke.py
@@ -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