fairagro-middleware-shared 10.0.3.dev26__tar.gz → 11.0.1.dev27__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.
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/.gitignore +0 -3
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/PKG-INFO +1 -1
- fairagro_middleware_shared-11.0.1.dev27/src/middleware/shared/report/__init__.py +19 -0
- fairagro_middleware_shared-11.0.1.dev27/src/middleware/shared/report/emit.py +26 -0
- fairagro_middleware_shared-11.0.1.dev27/src/middleware/shared/report/formats/__init__.py +9 -0
- fairagro_middleware_shared-11.0.1.dev27/src/middleware/shared/report/formats/base.py +15 -0
- fairagro_middleware_shared-11.0.1.dev27/src/middleware/shared/report/formats/jsonld.py +86 -0
- fairagro_middleware_shared-11.0.1.dev27/src/middleware/shared/report/model.py +46 -0
- fairagro_middleware_shared-11.0.1.dev27/tests/unit/test_harvest_report.py +224 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/README.md +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/pyproject.toml +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/common/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/common/models.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/common/rocrate.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/py.typed +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/v1/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/v1/models.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/v2/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/v2/models.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/v3/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/api_models/v3/models.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/config/__init__.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/config/config_base.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/config/config_wrapper.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/config/logging.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/py.typed +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/src/middleware/shared/tracing.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/tests/unit/test_api_models.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/tests/unit/test_config_base.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/tests/unit/test_config_wrapper.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/tests/unit/test_logging.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/tests/unit/test_rocrate.py +0 -0
- {fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/tests/unit/test_tracing.py +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Harvest run report model, serializers, and stdout emission."""
|
|
2
|
+
|
|
3
|
+
from middleware.shared.report.emit import print_report
|
|
4
|
+
from middleware.shared.report.formats.base import ReportSerializer
|
|
5
|
+
from middleware.shared.report.formats.jsonld import (
|
|
6
|
+
FAIRAGRO_HARVEST_REPORT_NS,
|
|
7
|
+
JsonLdReportSerializer,
|
|
8
|
+
)
|
|
9
|
+
from middleware.shared.report.model import FailedRecord, HarvestReport, RepositoryReport
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"FAIRAGRO_HARVEST_REPORT_NS",
|
|
13
|
+
"FailedRecord",
|
|
14
|
+
"HarvestReport",
|
|
15
|
+
"JsonLdReportSerializer",
|
|
16
|
+
"ReportSerializer",
|
|
17
|
+
"RepositoryReport",
|
|
18
|
+
"print_report",
|
|
19
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Stdout emission for harvest reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from middleware.shared.report.formats.base import ReportSerializer
|
|
8
|
+
from middleware.shared.report.formats.jsonld import JsonLdReportSerializer
|
|
9
|
+
from middleware.shared.report.model import HarvestReport
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def print_report(
|
|
15
|
+
report: HarvestReport,
|
|
16
|
+
serializer: ReportSerializer | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Serialize the report and print it to stdout.
|
|
19
|
+
|
|
20
|
+
Serialization or print failures are logged as warnings and do not raise.
|
|
21
|
+
"""
|
|
22
|
+
active_serializer: ReportSerializer = serializer or JsonLdReportSerializer()
|
|
23
|
+
try:
|
|
24
|
+
print(active_serializer.render(report))
|
|
25
|
+
except Exception as exc: # noqa: BLE001 — MUST NOT fail the process (harvest-report)
|
|
26
|
+
logger.warning("Failed to serialise harvest report: %s", exc)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Report serializer protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from middleware.shared.report.model import HarvestReport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReportSerializer(Protocol):
|
|
11
|
+
"""Serialize a harvest report to a string in a specific format."""
|
|
12
|
+
|
|
13
|
+
def render(self, report: HarvestReport) -> str:
|
|
14
|
+
"""Return the serialized report document."""
|
|
15
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""JSON-LD harvest report serializer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from middleware.shared.report.model import FailedRecord, HarvestReport, RepositoryReport
|
|
10
|
+
|
|
11
|
+
FAIRAGRO_HARVEST_REPORT_NS = "https://fairagro.github.io/m4.2_advanced_middleware_api/ns/harvest-report/v1/#"
|
|
12
|
+
|
|
13
|
+
_JSON_LD_CONTEXT = {
|
|
14
|
+
"@vocab": "https://schema.org/",
|
|
15
|
+
"schema": "https://schema.org/",
|
|
16
|
+
"fairagro": FAIRAGRO_HARVEST_REPORT_NS,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _format_iso_duration(seconds: float) -> str:
|
|
21
|
+
"""Serialize a duration value as an ISO 8601 duration string."""
|
|
22
|
+
remainder = f"{seconds:.6f}".rstrip("0").rstrip(".")
|
|
23
|
+
if remainder == "":
|
|
24
|
+
remainder = "0"
|
|
25
|
+
return f"PT{remainder}S"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _format_iso_timestamp(value: datetime) -> str:
|
|
29
|
+
"""Format a UTC datetime value as an ISO 8601 timestamp ending in Z."""
|
|
30
|
+
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _failed_record_to_jsonld(record: FailedRecord) -> dict[str, Any]:
|
|
34
|
+
"""Convert a failed record to its JSON-LD object, omitting unset fields."""
|
|
35
|
+
result: dict[str, Any] = {"fairagro:message": record.message}
|
|
36
|
+
if record.record_id:
|
|
37
|
+
result["fairagro:recordId"] = record.record_id
|
|
38
|
+
if record.url:
|
|
39
|
+
result["fairagro:url"] = record.url
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _repository_to_jsonld(report: RepositoryReport) -> dict[str, Any]:
|
|
44
|
+
"""Convert a repository report to a schema.org EntryPoint JSON-LD object."""
|
|
45
|
+
result: dict[str, Any] = {
|
|
46
|
+
"@type": "schema:EntryPoint",
|
|
47
|
+
"name": report.rdi,
|
|
48
|
+
"identifier": report.rdi,
|
|
49
|
+
"schema:duration": _format_iso_duration(report.duration_seconds),
|
|
50
|
+
"fairagro:harvestId": report.harvest_id,
|
|
51
|
+
"fairagro:skippedDatasets": report.skipped_datasets,
|
|
52
|
+
}
|
|
53
|
+
if report.harvested_datasets is not None:
|
|
54
|
+
result["fairagro:harvestedDatasets"] = report.harvested_datasets
|
|
55
|
+
if report.expected_datasets is not None:
|
|
56
|
+
result["fairagro:expectedDatasets"] = report.expected_datasets
|
|
57
|
+
if report.failed_datasets is not None:
|
|
58
|
+
result["fairagro:failedDatasets"] = report.failed_datasets
|
|
59
|
+
if report.total_studies is not None:
|
|
60
|
+
result["fairagro:totalStudies"] = report.total_studies
|
|
61
|
+
if report.total_assays is not None:
|
|
62
|
+
result["fairagro:totalAssays"] = report.total_assays
|
|
63
|
+
if report.failed_records:
|
|
64
|
+
result["fairagro:failedRecords"] = [_failed_record_to_jsonld(record) for record in report.failed_records]
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class JsonLdReportSerializer:
|
|
69
|
+
"""Render a harvest report as an operator-readable JSON-LD document."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, *, indent: int = 2) -> None:
|
|
72
|
+
"""Configure JSON indentation for operator-readable output."""
|
|
73
|
+
self._indent = indent
|
|
74
|
+
|
|
75
|
+
def render(self, report: HarvestReport) -> str:
|
|
76
|
+
"""Return the report as indented JSON-LD text."""
|
|
77
|
+
document: dict[str, Any] = {
|
|
78
|
+
"@context": _JSON_LD_CONTEXT,
|
|
79
|
+
"@type": "schema:Action",
|
|
80
|
+
"name": report.name,
|
|
81
|
+
"schema:startTime": _format_iso_timestamp(report.start_time),
|
|
82
|
+
"schema:endTime": _format_iso_timestamp(report.end_time),
|
|
83
|
+
"fairagro:harvestDurationSeconds": report.duration_seconds,
|
|
84
|
+
"schema:result": [_repository_to_jsonld(entry) for entry in report.repository_reports],
|
|
85
|
+
}
|
|
86
|
+
return json.dumps(document, ensure_ascii=False, indent=self._indent)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Format-neutral harvest run report domain model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class FailedRecord:
|
|
11
|
+
"""A single dataset that failed during harvesting."""
|
|
12
|
+
|
|
13
|
+
message: str
|
|
14
|
+
record_id: str | None = None
|
|
15
|
+
url: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RepositoryReport: # pylint: disable=too-many-instance-attributes
|
|
20
|
+
"""Execution statistics for a single harvested repository."""
|
|
21
|
+
|
|
22
|
+
rdi: str
|
|
23
|
+
harvest_id: str | None
|
|
24
|
+
duration_seconds: float
|
|
25
|
+
expected_datasets: int | None = None
|
|
26
|
+
harvested_datasets: int | None = None
|
|
27
|
+
failed_datasets: int | None = None
|
|
28
|
+
skipped_datasets: int = 0
|
|
29
|
+
failed_records: tuple[FailedRecord, ...] = ()
|
|
30
|
+
total_studies: int | None = None
|
|
31
|
+
total_assays: int | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class HarvestReport:
|
|
36
|
+
"""Summary statistics for an entire harvest run."""
|
|
37
|
+
|
|
38
|
+
start_time: datetime
|
|
39
|
+
end_time: datetime
|
|
40
|
+
repository_reports: tuple[RepositoryReport, ...] = ()
|
|
41
|
+
name: str = "FAIRagro Harvest Run"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def duration_seconds(self) -> float:
|
|
45
|
+
"""Return the total harvest run duration in seconds."""
|
|
46
|
+
return (self.end_time - self.start_time).total_seconds()
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Unit tests for the shared harvest report library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import fields
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from middleware.shared.report import (
|
|
14
|
+
FAIRAGRO_HARVEST_REPORT_NS,
|
|
15
|
+
FailedRecord,
|
|
16
|
+
HarvestReport,
|
|
17
|
+
JsonLdReportSerializer,
|
|
18
|
+
RepositoryReport,
|
|
19
|
+
print_report,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_START = datetime(2026, 5, 6, 14, 0, 0, tzinfo=UTC)
|
|
23
|
+
_END = datetime(2026, 5, 6, 14, 3, 45, tzinfo=UTC)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sample_repo(**overrides: Any) -> RepositoryReport:
|
|
27
|
+
"""Build a repository report with sensible defaults."""
|
|
28
|
+
values: dict[str, Any] = {
|
|
29
|
+
"rdi": "bonares",
|
|
30
|
+
"harvest_id": "harvest-1",
|
|
31
|
+
"duration_seconds": 12.3,
|
|
32
|
+
"expected_datasets": 100,
|
|
33
|
+
"harvested_datasets": 95,
|
|
34
|
+
"failed_datasets": 5,
|
|
35
|
+
"skipped_datasets": 2,
|
|
36
|
+
"failed_records": (
|
|
37
|
+
FailedRecord(
|
|
38
|
+
message="map failed",
|
|
39
|
+
record_id="frl:123",
|
|
40
|
+
url="https://example.test/frl:123",
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
values.update(overrides)
|
|
45
|
+
return RepositoryReport(**values)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sample_report(
|
|
49
|
+
repositories: tuple[RepositoryReport, ...] | None = None,
|
|
50
|
+
) -> HarvestReport:
|
|
51
|
+
"""Build a harvest run report spanning a fixed UTC window."""
|
|
52
|
+
return HarvestReport(
|
|
53
|
+
start_time=_START,
|
|
54
|
+
end_time=_END,
|
|
55
|
+
repository_reports=((_sample_repo(),) if repositories is None else repositories),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_failed_record_with_optional_identifiers() -> None:
|
|
60
|
+
"""Failed records expose message, record id, and URL when provided."""
|
|
61
|
+
record = FailedRecord(message="boom", record_id="id-1", url="https://x.test")
|
|
62
|
+
assert record.message == "boom"
|
|
63
|
+
assert record.record_id == "id-1"
|
|
64
|
+
assert record.url == "https://x.test"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_failed_record_message_only() -> None:
|
|
68
|
+
"""Optional identifiers are unset when omitted."""
|
|
69
|
+
record = FailedRecord(message="boom")
|
|
70
|
+
assert record.record_id is None
|
|
71
|
+
assert record.url is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_harvest_report_with_one_repository() -> None:
|
|
75
|
+
"""A run report exposes timing and a single repository entry."""
|
|
76
|
+
report = _sample_report()
|
|
77
|
+
assert report.start_time == _START
|
|
78
|
+
assert len(report.repository_reports) == 1
|
|
79
|
+
assert report.repository_reports[0].rdi == "bonares"
|
|
80
|
+
assert report.duration_seconds == (_END - _START).total_seconds()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_harvest_report_with_no_repositories() -> None:
|
|
84
|
+
"""An empty repository list is preserved."""
|
|
85
|
+
report = _sample_report(repositories=())
|
|
86
|
+
assert not report.repository_reports
|
|
87
|
+
assert report.duration_seconds == (_END - _START).total_seconds()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_optional_study_and_assay_counts_set() -> None:
|
|
91
|
+
"""Study and assay totals are available when provided."""
|
|
92
|
+
total_studies = 10
|
|
93
|
+
total_assays = 20
|
|
94
|
+
repo = _sample_repo(total_studies=total_studies, total_assays=total_assays)
|
|
95
|
+
assert repo.total_studies == total_studies
|
|
96
|
+
assert repo.total_assays == total_assays
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_optional_study_and_assay_counts_unset() -> None:
|
|
100
|
+
"""Unset study and assay totals are None, not zero."""
|
|
101
|
+
repo = _sample_repo()
|
|
102
|
+
assert repo.total_studies is None
|
|
103
|
+
assert repo.total_assays is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_model_is_format_neutral() -> None:
|
|
107
|
+
"""The domain model does not embed a serialized document."""
|
|
108
|
+
report = _sample_report()
|
|
109
|
+
assert not hasattr(report, "to_jsonld")
|
|
110
|
+
assert "repository_reports" in {field.name for field in fields(report)}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_jsonld_context_and_types() -> None:
|
|
114
|
+
"""JSON-LD uses schema.org Action with EntryPoint results."""
|
|
115
|
+
document = json.loads(JsonLdReportSerializer().render(_sample_report()))
|
|
116
|
+
assert document["@context"]["@vocab"] == "https://schema.org/"
|
|
117
|
+
assert document["@context"]["schema"] == "https://schema.org/"
|
|
118
|
+
assert document["@context"]["fairagro"] == FAIRAGRO_HARVEST_REPORT_NS
|
|
119
|
+
assert document["@context"]["fairagro"].endswith("/ns/harvest-report/v1/#")
|
|
120
|
+
assert document["@type"] == "schema:Action"
|
|
121
|
+
assert document["schema:result"][0]["@type"] == "schema:EntryPoint"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_jsonld_timestamps_and_durations() -> None:
|
|
125
|
+
"""Timestamps end with Z and durations use ISO 8601 / seconds."""
|
|
126
|
+
report = _sample_report()
|
|
127
|
+
document = json.loads(JsonLdReportSerializer().render(report))
|
|
128
|
+
assert document["schema:startTime"].endswith("Z")
|
|
129
|
+
assert document["schema:endTime"].endswith("Z")
|
|
130
|
+
assert document["fairagro:harvestDurationSeconds"] == report.duration_seconds
|
|
131
|
+
assert document["schema:result"][0]["schema:duration"].startswith("PT")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_jsonld_metrics_and_failed_records() -> None:
|
|
135
|
+
"""Fairagro metrics and nested failed records are emitted."""
|
|
136
|
+
repo = _sample_repo()
|
|
137
|
+
entry = json.loads(JsonLdReportSerializer().render(_sample_report(repositories=(repo,))))["schema:result"][0]
|
|
138
|
+
assert entry["fairagro:harvestId"] == repo.harvest_id
|
|
139
|
+
assert entry["fairagro:expectedDatasets"] == repo.expected_datasets
|
|
140
|
+
assert entry["fairagro:harvestedDatasets"] == repo.harvested_datasets
|
|
141
|
+
assert entry["fairagro:failedDatasets"] == repo.failed_datasets
|
|
142
|
+
assert entry["fairagro:skippedDatasets"] == repo.skipped_datasets
|
|
143
|
+
failed = repo.failed_records[0]
|
|
144
|
+
assert entry["fairagro:failedRecords"] == [
|
|
145
|
+
{
|
|
146
|
+
"fairagro:message": failed.message,
|
|
147
|
+
"fairagro:recordId": failed.record_id,
|
|
148
|
+
"fairagro:url": failed.url,
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_jsonld_optional_study_and_assay_totals() -> None:
|
|
154
|
+
"""Optional study and assay totals appear as fairagro properties."""
|
|
155
|
+
total_studies = 3
|
|
156
|
+
total_assays = 7
|
|
157
|
+
report = _sample_report(repositories=(_sample_repo(total_studies=total_studies, total_assays=total_assays),))
|
|
158
|
+
entry = json.loads(JsonLdReportSerializer().render(report))["schema:result"][0]
|
|
159
|
+
assert entry["fairagro:totalStudies"] == total_studies
|
|
160
|
+
assert entry["fairagro:totalAssays"] == total_assays
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_jsonld_omits_unset_expected_datasets() -> None:
|
|
164
|
+
"""Unset optional counts are omitted rather than null."""
|
|
165
|
+
report = _sample_report(
|
|
166
|
+
repositories=(
|
|
167
|
+
_sample_repo(
|
|
168
|
+
expected_datasets=None,
|
|
169
|
+
harvested_datasets=None,
|
|
170
|
+
failed_datasets=None,
|
|
171
|
+
total_studies=None,
|
|
172
|
+
total_assays=None,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
entry = json.loads(JsonLdReportSerializer().render(report))["schema:result"][0]
|
|
177
|
+
assert "fairagro:expectedDatasets" not in entry
|
|
178
|
+
assert "fairagro:harvestedDatasets" not in entry
|
|
179
|
+
assert "fairagro:failedDatasets" not in entry
|
|
180
|
+
assert "fairagro:totalStudies" not in entry
|
|
181
|
+
assert "fairagro:totalAssays" not in entry
|
|
182
|
+
assert "fairagro:harvestId" in entry
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_jsonld_omits_empty_failed_records() -> None:
|
|
186
|
+
"""Empty failed-record lists are omitted from JSON-LD."""
|
|
187
|
+
report = _sample_report(repositories=(_sample_repo(failed_records=()),))
|
|
188
|
+
entry = json.loads(JsonLdReportSerializer().render(report))["schema:result"][0]
|
|
189
|
+
assert "fairagro:failedRecords" not in entry
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def test_jsonld_empty_result_array() -> None:
|
|
193
|
+
"""A run with no repositories emits an empty result array."""
|
|
194
|
+
document = json.loads(JsonLdReportSerializer().render(_sample_report(repositories=())))
|
|
195
|
+
assert document["schema:result"] == []
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_print_report_writes_jsonld_to_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
|
199
|
+
"""print_report writes a JSON-LD document to stdout."""
|
|
200
|
+
print_report(_sample_report())
|
|
201
|
+
captured = capsys.readouterr()
|
|
202
|
+
document = json.loads(captured.out)
|
|
203
|
+
assert document["@type"] == "schema:Action"
|
|
204
|
+
assert captured.err == ""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_print_report_does_not_raise_on_serialization_failure(
|
|
208
|
+
caplog: pytest.LogCaptureFixture,
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Serialization failures are logged and do not propagate."""
|
|
211
|
+
|
|
212
|
+
class _BoomSerializer:
|
|
213
|
+
def __init__(self) -> None:
|
|
214
|
+
self._fail = True
|
|
215
|
+
|
|
216
|
+
def render(self, report: HarvestReport) -> str:
|
|
217
|
+
if self._fail:
|
|
218
|
+
raise RuntimeError(f"cannot serialise {type(report).__name__}")
|
|
219
|
+
return "{}"
|
|
220
|
+
|
|
221
|
+
with caplog.at_level(logging.WARNING):
|
|
222
|
+
print_report(_sample_report(), serializer=_BoomSerializer())
|
|
223
|
+
|
|
224
|
+
assert any("Failed to serialise harvest report" in message for message in caplog.messages)
|
{fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/README.md
RENAMED
|
File without changes
|
{fairagro_middleware_shared-10.0.3.dev26 → fairagro_middleware_shared-11.0.1.dev27}/pyproject.toml
RENAMED
|
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
|
|
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
|