fairagro-middleware-api-client 9.0.1.dev13__tar.gz → 9.1.1.dev14__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_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/.gitignore +3 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/PKG-INFO +1 -1
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/src/middleware/api_client/api_client.py +18 -20
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/unit/test_client.py +46 -19
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/README.md +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/example_client_config.yaml +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/pyproject.toml +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/spec/harvest-client/design.md +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/spec/harvest-client/spec.md +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/src/middleware/api_client/__init__.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/src/middleware/api_client/config.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/src/middleware/api_client/models.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/src/middleware/api_client/py.typed +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/conftest.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/integration/conftest.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/integration/test_create_arcs.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/unit/test_api_client_config.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/unit/test_client_config.py +0 -0
- {fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/tests/unit/test_retry_logic.py +0 -0
|
@@ -15,6 +15,7 @@ import httpx
|
|
|
15
15
|
from pydantic import BaseModel, ValidationError
|
|
16
16
|
|
|
17
17
|
from middleware.shared.api_models.common.models import HarvestStatus as SharedHarvestStatus
|
|
18
|
+
from middleware.shared.api_models.common.rocrate import RoCratePayload
|
|
18
19
|
from middleware.shared.api_models.v3.models import (
|
|
19
20
|
CreateArcRequest,
|
|
20
21
|
CreateHarvestRequest,
|
|
@@ -295,7 +296,7 @@ class ApiClient:
|
|
|
295
296
|
seen_identifiers: set[str] = set()
|
|
296
297
|
|
|
297
298
|
async def submit_one(arc_item: dict[str, Any]) -> None:
|
|
298
|
-
request = SubmitHarvestArcRequest(arc=arc_item)
|
|
299
|
+
request = SubmitHarvestArcRequest(arc=self._validate_rocrate(arc_item))
|
|
299
300
|
await self._post(f"v3/harvests/{harvest_id}/arcs", request)
|
|
300
301
|
|
|
301
302
|
async for arc in arcs:
|
|
@@ -305,7 +306,7 @@ class ApiClient:
|
|
|
305
306
|
if identifier in seen_identifiers:
|
|
306
307
|
logger.error(
|
|
307
308
|
"Duplicate ARC identifier '%s' in harvest %s — "
|
|
308
|
-
"
|
|
309
|
+
"several ARCs share the same identifier (client-side data error).",
|
|
309
310
|
identifier,
|
|
310
311
|
harvest_id,
|
|
311
312
|
)
|
|
@@ -504,23 +505,20 @@ class ApiClient:
|
|
|
504
505
|
# ------------------------------------------------------------------
|
|
505
506
|
|
|
506
507
|
@staticmethod
|
|
507
|
-
def
|
|
508
|
-
"""
|
|
508
|
+
def _validate_rocrate(arc_content: dict[str, Any]) -> RoCratePayload:
|
|
509
|
+
"""Validate a RO-Crate dict before sending it to the API."""
|
|
510
|
+
try:
|
|
511
|
+
return RoCratePayload.model_validate(arc_content)
|
|
512
|
+
except ValidationError as e:
|
|
513
|
+
raise ApiClientError(f"Invalid RO-Crate JSON: {e}") from e
|
|
509
514
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
for item in graph:
|
|
518
|
-
if isinstance(item, dict) and item.get("@id") == "./":
|
|
519
|
-
identifier = item.get("identifier")
|
|
520
|
-
if isinstance(identifier, list):
|
|
521
|
-
identifier = identifier[0] if identifier else None
|
|
522
|
-
return str(identifier) if identifier else None
|
|
523
|
-
return None
|
|
515
|
+
@staticmethod
|
|
516
|
+
def _extract_identifier_from_rocrate(arc_content: dict[str, Any]) -> str | None:
|
|
517
|
+
"""Return the RO-Crate identifier when the payload is structurally valid."""
|
|
518
|
+
try:
|
|
519
|
+
return RoCratePayload.model_validate(arc_content).identifier
|
|
520
|
+
except ValidationError:
|
|
521
|
+
return None
|
|
524
522
|
|
|
525
523
|
@classmethod
|
|
526
524
|
def _serialize_arc(cls, arc: "ARC | dict[str, Any] | str") -> dict[str, Any]:
|
|
@@ -575,7 +573,7 @@ class ApiClient:
|
|
|
575
573
|
"""
|
|
576
574
|
logger.info("Creating/updating ARC for RDI: %s", rdi)
|
|
577
575
|
serialized = self._serialize_arc(arc)
|
|
578
|
-
request = CreateArcRequest(rdi=rdi, arc=serialized)
|
|
576
|
+
request = CreateArcRequest(rdi=rdi, arc=self._validate_rocrate(serialized))
|
|
579
577
|
data = await self._post("v3/arcs", request)
|
|
580
578
|
return self._parse_arc_response(data)
|
|
581
579
|
|
|
@@ -710,7 +708,7 @@ class ApiClient:
|
|
|
710
708
|
:class:`ArcResult` with the result of the operation.
|
|
711
709
|
"""
|
|
712
710
|
serialized = self._serialize_arc(arc)
|
|
713
|
-
request = SubmitHarvestArcRequest(arc=serialized)
|
|
711
|
+
request = SubmitHarvestArcRequest(arc=self._validate_rocrate(serialized))
|
|
714
712
|
data = await self._post(f"v3/harvests/{harvest_id}/arcs", request)
|
|
715
713
|
return self._parse_arc_response(data)
|
|
716
714
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import http
|
|
5
|
+
import json
|
|
5
6
|
import ssl
|
|
6
7
|
from collections.abc import AsyncGenerator
|
|
7
8
|
from pathlib import Path
|
|
@@ -40,6 +41,15 @@ _ARC_RESPONSE = {
|
|
|
40
41
|
"events": [],
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
|
|
45
|
+
def _rocrate_dict(identifier: str = "mock-arc") -> dict[str, Any]:
|
|
46
|
+
"""Minimal valid RO-Crate payload for client tests."""
|
|
47
|
+
return {
|
|
48
|
+
"@context": "https://w3id.org/ro/crate/1.1/context",
|
|
49
|
+
"@graph": [{"@id": "./", "identifier": identifier}],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
43
53
|
_HARVEST_RESPONSE: dict[str, str | None | dict] = {
|
|
44
54
|
"client_id": "test-client",
|
|
45
55
|
"message": "Harvest created",
|
|
@@ -229,7 +239,7 @@ async def test_create_or_update_arc_with_dict(client_config: Config) -> None:
|
|
|
229
239
|
return_value=httpx.Response(http.HTTPStatus.OK, json=_ARC_RESPONSE)
|
|
230
240
|
)
|
|
231
241
|
async with ApiClient(client_config) as client:
|
|
232
|
-
response = await client.create_or_update_arc(rdi="test-rdi", arc=
|
|
242
|
+
response = await client.create_or_update_arc(rdi="test-rdi", arc=_rocrate_dict())
|
|
233
243
|
assert isinstance(response, ArcResult)
|
|
234
244
|
|
|
235
245
|
|
|
@@ -241,7 +251,7 @@ async def test_create_or_update_arc_with_json_string(client_config: Config) -> N
|
|
|
241
251
|
return_value=httpx.Response(http.HTTPStatus.OK, json=_ARC_RESPONSE)
|
|
242
252
|
)
|
|
243
253
|
async with ApiClient(client_config) as client:
|
|
244
|
-
response = await client.create_or_update_arc(rdi="test-rdi", arc=
|
|
254
|
+
response = await client.create_or_update_arc(rdi="test-rdi", arc=json.dumps(_rocrate_dict()))
|
|
245
255
|
assert route.called
|
|
246
256
|
assert isinstance(response, ArcResult)
|
|
247
257
|
assert response.arc_id == "arc-123"
|
|
@@ -252,7 +262,7 @@ async def test_create_or_update_arc_with_invalid_json_string(client_config: Conf
|
|
|
252
262
|
"""Test create_or_update_arc with an invalid JSON string."""
|
|
253
263
|
async with ApiClient(client_config) as client:
|
|
254
264
|
with pytest.raises(ApiClientError, match="Invalid JSON string provided for ARC"):
|
|
255
|
-
await client.create_or_update_arc(rdi="test-rdi", arc='{"
|
|
265
|
+
await client.create_or_update_arc(rdi="test-rdi", arc='{"@context":')
|
|
256
266
|
|
|
257
267
|
|
|
258
268
|
@pytest.mark.asyncio
|
|
@@ -290,6 +300,14 @@ async def test_create_or_update_arc_invalid_response(client_config: Config) -> N
|
|
|
290
300
|
)
|
|
291
301
|
async with ApiClient(client_config) as client:
|
|
292
302
|
with pytest.raises(ApiClientError, match="Invalid ARC response"):
|
|
303
|
+
await client.create_or_update_arc(rdi="test-rdi", arc=_rocrate_dict("mock"))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.asyncio
|
|
307
|
+
async def test_create_or_update_arc_invalid_rocrate(client_config: Config) -> None:
|
|
308
|
+
"""Test create_or_update_arc rejects structurally invalid RO-Crate JSON."""
|
|
309
|
+
async with ApiClient(client_config) as client:
|
|
310
|
+
with pytest.raises(ApiClientError, match="Invalid RO-Crate JSON"):
|
|
293
311
|
await client.create_or_update_arc(rdi="test-rdi", arc={"id": "mock"})
|
|
294
312
|
|
|
295
313
|
|
|
@@ -301,7 +319,7 @@ async def test_create_or_update_arc_sends_correct_headers(client_config: Config)
|
|
|
301
319
|
return_value=httpx.Response(http.HTTPStatus.OK, json=_ARC_RESPONSE)
|
|
302
320
|
)
|
|
303
321
|
async with ApiClient(client_config) as client:
|
|
304
|
-
await client.create_or_update_arc(rdi="test", arc=
|
|
322
|
+
await client.create_or_update_arc(rdi="test", arc=_rocrate_dict())
|
|
305
323
|
|
|
306
324
|
assert route.called
|
|
307
325
|
req = route.calls.last.request
|
|
@@ -499,7 +517,7 @@ async def test_submit_arc_in_harvest(client_config: Config) -> None:
|
|
|
499
517
|
return_value=httpx.Response(http.HTTPStatus.OK, json=_ARC_RESPONSE)
|
|
500
518
|
)
|
|
501
519
|
async with ApiClient(client_config) as client:
|
|
502
|
-
response = await client.submit_arc_in_harvest("harvest-456", arc=
|
|
520
|
+
response = await client.submit_arc_in_harvest("harvest-456", arc=_rocrate_dict())
|
|
503
521
|
assert isinstance(response, ArcResult)
|
|
504
522
|
assert response.arc_id == "arc-123"
|
|
505
523
|
|
|
@@ -513,6 +531,14 @@ async def test_submit_arc_in_harvest_invalid_response(client_config: Config) ->
|
|
|
513
531
|
)
|
|
514
532
|
async with ApiClient(client_config) as client:
|
|
515
533
|
with pytest.raises(ApiClientError, match="Invalid ARC response"):
|
|
534
|
+
await client.submit_arc_in_harvest("harvest-456", arc=_rocrate_dict("mock"))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@pytest.mark.asyncio
|
|
538
|
+
async def test_submit_arc_in_harvest_invalid_rocrate(client_config: Config) -> None:
|
|
539
|
+
"""Test submit_arc_in_harvest rejects structurally invalid RO-Crate JSON."""
|
|
540
|
+
async with ApiClient(client_config) as client:
|
|
541
|
+
with pytest.raises(ApiClientError, match="Invalid RO-Crate JSON"):
|
|
516
542
|
await client.submit_arc_in_harvest("harvest-456", arc={"id": "mock"})
|
|
517
543
|
|
|
518
544
|
|
|
@@ -524,7 +550,7 @@ async def test_submit_arc_in_harvest_with_json_string(client_config: Config) ->
|
|
|
524
550
|
return_value=httpx.Response(http.HTTPStatus.OK, json=_ARC_RESPONSE)
|
|
525
551
|
)
|
|
526
552
|
async with ApiClient(client_config) as client:
|
|
527
|
-
response = await client.submit_arc_in_harvest("harvest-456", arc=
|
|
553
|
+
response = await client.submit_arc_in_harvest("harvest-456", arc=json.dumps(_rocrate_dict()))
|
|
528
554
|
assert route.called
|
|
529
555
|
assert isinstance(response, ArcResult)
|
|
530
556
|
assert response.arc_id == "arc-123"
|
|
@@ -535,7 +561,7 @@ async def test_submit_arc_in_harvest_with_invalid_json_string(client_config: Con
|
|
|
535
561
|
"""Test submit_arc_in_harvest with an invalid JSON string."""
|
|
536
562
|
async with ApiClient(client_config) as client:
|
|
537
563
|
with pytest.raises(ApiClientError, match="Invalid JSON string provided for ARC"):
|
|
538
|
-
await client.submit_arc_in_harvest("harvest-456", arc='{"
|
|
564
|
+
await client.submit_arc_in_harvest("harvest-456", arc='{"@context":')
|
|
539
565
|
|
|
540
566
|
|
|
541
567
|
# ---------------------------------------------------------------------------
|
|
@@ -564,7 +590,7 @@ async def test_harvest_arcs_success(client_config: Config) -> None:
|
|
|
564
590
|
return_value=httpx.Response(http.HTTPStatus.OK, json=completed_response)
|
|
565
591
|
)
|
|
566
592
|
|
|
567
|
-
arcs = _arc_gen(
|
|
593
|
+
arcs = _arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2"), _rocrate_dict("arc-3"))
|
|
568
594
|
async with ApiClient(client_config) as client:
|
|
569
595
|
result = await client.harvest_arcs("test-rdi", arcs, expected_datasets=3)
|
|
570
596
|
|
|
@@ -589,7 +615,7 @@ async def test_harvest_arcs_success_with_parallelism(client_config: Config) -> N
|
|
|
589
615
|
return_value=httpx.Response(http.HTTPStatus.OK, json=completed_response)
|
|
590
616
|
)
|
|
591
617
|
|
|
592
|
-
arcs = _arc_gen(
|
|
618
|
+
arcs = _arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2"), _rocrate_dict("arc-3"))
|
|
593
619
|
async with ApiClient(client_config) as client:
|
|
594
620
|
result = await client.harvest_arcs("test-rdi", arcs)
|
|
595
621
|
|
|
@@ -614,7 +640,7 @@ async def test_harvest_arcs_uses_config_default_concurrency(client_config: Confi
|
|
|
614
640
|
return_value=httpx.Response(http.HTTPStatus.OK, json=completed_response)
|
|
615
641
|
)
|
|
616
642
|
|
|
617
|
-
arcs = _arc_gen(
|
|
643
|
+
arcs = _arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2"), _rocrate_dict("arc-3"))
|
|
618
644
|
async with ApiClient(client_config) as client:
|
|
619
645
|
result = await client.harvest_arcs("test-rdi", arcs)
|
|
620
646
|
|
|
@@ -664,7 +690,8 @@ async def test_harvest_arcs_continues_on_item_error(client_config: Config) -> No
|
|
|
664
690
|
)
|
|
665
691
|
|
|
666
692
|
async with ApiClient(client_config) as client:
|
|
667
|
-
|
|
693
|
+
arcs = _arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2"), _rocrate_dict("arc-3"))
|
|
694
|
+
result = await client.harvest_arcs("test-rdi", arcs)
|
|
668
695
|
|
|
669
696
|
assert isinstance(result, HarvestResult)
|
|
670
697
|
assert route_submit.call_count == EXPECTED_ARC_UPLOADS
|
|
@@ -672,7 +699,7 @@ async def test_harvest_arcs_continues_on_item_error(client_config: Config) -> No
|
|
|
672
699
|
assert not cancel_route.called
|
|
673
700
|
assert len(result.errors) == 1
|
|
674
701
|
assert result.errors[0].error_type == HarvestErrorType.SUBMISSION_FAILED
|
|
675
|
-
assert result.errors[0].arc_id
|
|
702
|
+
assert result.errors[0].arc_id == "arc-1"
|
|
676
703
|
assert "HTTP error 400" in result.errors[0].message
|
|
677
704
|
|
|
678
705
|
|
|
@@ -698,7 +725,7 @@ async def test_harvest_arcs_cancels_on_catastrophic_error(client_config: Config)
|
|
|
698
725
|
|
|
699
726
|
async with ApiClient(client_config) as client:
|
|
700
727
|
with pytest.raises(ApiClientError, match="HTTP error 409"):
|
|
701
|
-
await client.harvest_arcs("test-rdi", _arc_gen(
|
|
728
|
+
await client.harvest_arcs("test-rdi", _arc_gen(_rocrate_dict("arc-1")))
|
|
702
729
|
|
|
703
730
|
assert fail_route.called
|
|
704
731
|
|
|
@@ -760,8 +787,8 @@ async def test_harvest_arcs_with_json_string(client_config: Config) -> None:
|
|
|
760
787
|
)
|
|
761
788
|
|
|
762
789
|
arcs = _arc_gen(
|
|
763
|
-
|
|
764
|
-
|
|
790
|
+
json.dumps(_rocrate_dict("arc-1-string")),
|
|
791
|
+
_rocrate_dict("arc-2-dict"),
|
|
765
792
|
ARC.from_arc_investigation(ArcInvestigation.create(identifier="test", title="Test")),
|
|
766
793
|
)
|
|
767
794
|
async with ApiClient(client_config) as client:
|
|
@@ -807,7 +834,7 @@ async def test_harvest_arcs_cancel_failure_does_not_mask_original_error(client_c
|
|
|
807
834
|
|
|
808
835
|
async with ApiClient(client_config) as client:
|
|
809
836
|
with pytest.raises(ApiClientError, match="HTTP error 409"):
|
|
810
|
-
await client.harvest_arcs("test-rdi", _arc_gen(
|
|
837
|
+
await client.harvest_arcs("test-rdi", _arc_gen(_rocrate_dict("arc-1")))
|
|
811
838
|
|
|
812
839
|
|
|
813
840
|
@pytest.mark.asyncio
|
|
@@ -855,7 +882,7 @@ async def test_harvest_arcs_all_exceptions_retrieved_when_multiple_tasks_catastr
|
|
|
855
882
|
with pytest.raises(ApiClientError, match="HTTP error 409"):
|
|
856
883
|
await client.harvest_arcs(
|
|
857
884
|
"test-rdi",
|
|
858
|
-
_arc_gen(
|
|
885
|
+
_arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2"), _rocrate_dict("arc-3")),
|
|
859
886
|
)
|
|
860
887
|
|
|
861
888
|
assert fail_route.called
|
|
@@ -892,7 +919,7 @@ async def test_harvest_arcs_500_is_submission_failed(client_config: Config) -> N
|
|
|
892
919
|
async with ApiClient(client_config) as client:
|
|
893
920
|
result = await client.harvest_arcs(
|
|
894
921
|
"test-rdi",
|
|
895
|
-
_arc_gen(
|
|
922
|
+
_arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2"), _rocrate_dict("arc-3")),
|
|
896
923
|
)
|
|
897
924
|
|
|
898
925
|
assert complete_route.called
|
|
@@ -929,7 +956,7 @@ async def test_harvest_arcs_502_is_submission_failed(client_config: Config) -> N
|
|
|
929
956
|
)
|
|
930
957
|
|
|
931
958
|
async with ApiClient(client_config) as client:
|
|
932
|
-
result = await client.harvest_arcs("test-rdi", _arc_gen(
|
|
959
|
+
result = await client.harvest_arcs("test-rdi", _arc_gen(_rocrate_dict("arc-1"), _rocrate_dict("arc-2")))
|
|
933
960
|
|
|
934
961
|
assert complete_route.called
|
|
935
962
|
assert not fail_route.called
|
{fairagro_middleware_api_client-9.0.1.dev13 → fairagro_middleware_api_client-9.1.1.dev14}/README.md
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
|