Skip to content

Commit 3701c23

Browse files
committed
feat: add --graph-mode for Virtuoso .graph sidecars
1 parent e4788a3 commit 3701c23

9 files changed

Lines changed: 634 additions & 145 deletions

File tree

‎databusclient/api/download.py‎

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
"xz": lzma,
4242
}
4343

44+
GRAPH_MODES = {"download-url"}
45+
4446

4547
def _get_download_directory(url: str, localDir: str | None) -> str:
4648
"""Return the local Databus-layout directory for a file URL."""
@@ -55,6 +57,43 @@ def _get_download_directory(url: str, localDir: str | None) -> str:
5557
return base_dir
5658

5759

60+
def _validate_graph_mode(graph_mode: str | None) -> None:
61+
if graph_mode is not None and graph_mode not in GRAPH_MODES:
62+
raise ValueError(
63+
f"Unsupported graph mode: {graph_mode}. "
64+
f"Supported graph modes: {sorted(GRAPH_MODES)}"
65+
)
66+
67+
68+
def _write_graph_sidecars(
69+
final_file_paths: list[str],
70+
source_url: str,
71+
graph_mode: str | None,
72+
) -> None:
73+
"""Write optional graph sidecars for final dataset outputs.
74+
75+
Existing downloads may be reused or transformed by the caller, but once a
76+
final output is considered successfully produced in graph mode, its sidecar
77+
must exist and point back to the source download URL.
78+
"""
79+
if graph_mode is None:
80+
return
81+
82+
if graph_mode == "download-url":
83+
for final_file_path in final_file_paths:
84+
with open(f"{final_file_path}.graph", "w", encoding="utf-8") as f:
85+
f.write(source_url)
86+
87+
88+
def _collect_files(directory: str) -> list[str]:
89+
return sorted(
90+
os.path.join(root, filename)
91+
for root, _dirs, filenames in os.walk(directory)
92+
for filename in filenames
93+
if not filename.endswith(".graph")
94+
)
95+
96+
5897
def _detect_compression_format(filename: str) -> Optional[str]:
5998
"""Detect compression format from file extension.
6099
@@ -342,9 +381,7 @@ def _resolve_checksums_for_urls(file_urls: List[str], databus_key: str | None) -
342381
versions_map: dict = {}
343382
for file_url in file_urls:
344383
try:
345-
host, accountId, groupId, artifactId, versionId, fileId = (
346-
get_databus_id_parts_from_file_url(file_url)
347-
)
384+
host, accountId, groupId, artifactId, versionId, fileId = get_databus_id_parts_from_file_url(file_url)
348385
except Exception:
349386
continue
350387
if versionId is None:
@@ -379,6 +416,7 @@ def _download_file(
379416
convert_format=None,
380417
graph_name=None,
381418
base_uri=None,
419+
graph_mode=None,
382420
validate_checksum: bool = False,
383421
expected_checksum: str | None = None,
384422
manifest_context=None,
@@ -397,9 +435,12 @@ def _download_file(
397435
convert_format: Target RDF/tabular format for on-the-fly conversion.
398436
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
399437
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
438+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
400439
validate_checksum: Whether to validate checksums after downloading.
401440
expected_checksum: The expected checksum of the file.
402441
"""
442+
_validate_graph_mode(graph_mode)
443+
source_url = url
403444
local_dir_was_given = localDir is not None
404445
localDir = _get_download_directory(url, localDir)
405446
if not local_dir_was_given:
@@ -582,6 +623,7 @@ def _download_file(
582623
needs_format_conversion = convert_format is not None
583624

584625
if not should_convert_compression and not needs_format_conversion:
626+
_write_graph_sidecars([filename], source_url, graph_mode)
585627
if manifest_context is not None:
586628
manifest_context.record_file(
587629
url=url,
@@ -602,9 +644,7 @@ def _download_file(
602644
# Source file is uncompressed — compress it directly to
603645
# the target compression format.
604646
target_filepath = filename + COMPRESSION_EXTENSIONS[compression]
605-
print(
606-
f"Compressing {file} -> {os.path.basename(target_filepath)}..."
607-
)
647+
print(f"Compressing {file} -> {os.path.basename(target_filepath)}...")
608648
with open(filename, "rb") as sf:
609649
with COMPRESSION_MODULES[compression].open(
610650
target_filepath, "wb"
@@ -626,6 +666,7 @@ def _download_file(
626666
source_fmt,
627667
compression,
628668
)
669+
_write_graph_sidecars([target_filepath], source_url, graph_mode)
629670
if manifest_context is not None:
630671
manifest_context.record_file(
631672
url=url,
@@ -649,10 +690,12 @@ def _download_file(
649690
file, source_fmt, compression
650691
)
651692
target_filepath = os.path.join(localDir, target_filename)
652-
_convert_compression_format(
653-
filename, target_filepath, source_fmt, compression
654-
)
693+
_convert_compression_format(filename, target_filepath, source_fmt, compression)
694+
final_paths = [target_filepath]
695+
else:
696+
final_paths = [filename]
655697
# No format conversion needed, no further work.
698+
_write_graph_sidecars(final_paths, source_url, graph_mode)
656699
if manifest_context is not None:
657700
manifest_context.record_file(
658701
url=url,
@@ -700,9 +743,7 @@ def _download_file(
700743
get_format_class(source_format_for_mapping)
701744
if source_format_for_mapping else None
702745
)
703-
is_quad_to_triple = (
704-
source_class_for_mapping == "quads" and target_class == "triples"
705-
)
746+
is_quad_to_triple = (source_class_for_mapping == "quads" and target_class == "triples")
706747

707748
if is_quad_to_triple:
708749
# Output directory name = original filename with compression and
@@ -720,6 +761,8 @@ def _download_file(
720761
graph_name=graph_name,
721762
base_uri=base_uri,
722763
)
764+
final_paths = _collect_files(output_dir)
765+
_write_graph_sidecars(final_paths, source_url, graph_mode)
723766

724767
# Delete the original downloaded (possibly compressed) file —
725768
# the split output directory replaces it.
@@ -787,6 +830,10 @@ def _download_file(
787830
shutil.copyfileobj(sf, tf)
788831

789832
os.remove(converted_uncompressed_path)
833+
final_paths = [recompressed_path]
834+
else:
835+
final_paths = [converted_uncompressed_path]
836+
_write_graph_sidecars(final_paths, source_url, graph_mode)
790837
finally:
791838
for temp_path in temp_paths:
792839
if os.path.exists(temp_path):
@@ -803,6 +850,7 @@ def _download_file(
803850
downloaded_at=datetime.now(timezone.utc).isoformat(),
804851
)
805852

853+
806854
def _download_files(
807855
urls: List[str],
808856
localDir: str,
@@ -814,6 +862,7 @@ def _download_files(
814862
convert_format: str = None,
815863
graph_name: str = None,
816864
base_uri: str = None,
865+
graph_mode: str = None,
817866
manifest_context=None,
818867
validate_checksum: bool = False,
819868
checksums: dict | None = None,
@@ -831,6 +880,7 @@ def _download_files(
831880
convert_format: Target RDF/tabular format for on-the-fly conversion.
832881
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
833882
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
883+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
834884
validate_checksum: Whether to validate checksums after downloading.
835885
checksums: Dictionary mapping URLs to their expected checksums.
836886
"""
@@ -849,11 +899,13 @@ def _download_files(
849899
convert_format=convert_format,
850900
graph_name=graph_name,
851901
base_uri=base_uri,
902+
graph_mode=graph_mode,
852903
validate_checksum=validate_checksum,
853904
expected_checksum=expected,
854905
manifest_context=manifest_context,
855906
)
856907

908+
857909
def _get_sparql_query_of_collection(uri: str, databus_key: str | None = None) -> str:
858910
"""Get SPARQL query of collection members from databus collection URI.
859911
@@ -999,6 +1051,7 @@ def _download_collection(
9991051
convert_format: str = None,
10001052
graph_name: str = None,
10011053
base_uri: str = None,
1054+
graph_mode: str = None,
10021055
manifest_context=None,
10031056
validate_checksum: bool = False,
10041057
) -> None:
@@ -1016,6 +1069,7 @@ def _download_collection(
10161069
convert_format: Target RDF/tabular format for on-the-fly conversion.
10171070
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
10181071
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
1072+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
10191073
validate_checksum: Whether to validate checksums after downloading.
10201074
"""
10211075
query = _get_sparql_query_of_collection(uri, databus_key=databus_key)
@@ -1039,6 +1093,7 @@ def _download_collection(
10391093
convert_format=convert_format,
10401094
graph_name=graph_name,
10411095
base_uri=base_uri,
1096+
graph_mode=graph_mode,
10421097
manifest_context=manifest_context,
10431098
validate_checksum=validate_checksum,
10441099
checksums=checksums if checksums else None,
@@ -1056,6 +1111,7 @@ def _download_version(
10561111
convert_format: str = None,
10571112
graph_name: str = None,
10581113
base_uri: str = None,
1114+
graph_mode: str = None,
10591115
manifest_context=None,
10601116
validate_checksum: bool = False,
10611117
) -> None:
@@ -1072,6 +1128,7 @@ def _download_version(
10721128
convert_format: Target RDF/tabular format for on-the-fly conversion.
10731129
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
10741130
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
1131+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
10751132
validate_checksum: Whether to validate checksums after downloading.
10761133
"""
10771134
json_str = fetch_databus_jsonld(uri, databus_key=databus_key)
@@ -1094,6 +1151,7 @@ def _download_version(
10941151
convert_format=convert_format,
10951152
graph_name=graph_name,
10961153
base_uri=base_uri,
1154+
graph_mode=graph_mode,
10971155
manifest_context=manifest_context,
10981156
validate_checksum=validate_checksum,
10991157
checksums=checksums,
@@ -1112,6 +1170,7 @@ def _download_artifact(
11121170
convert_format: str = None,
11131171
graph_name: str = None,
11141172
base_uri: str = None,
1173+
graph_mode: str = None,
11151174
manifest_context=None,
11161175
validate_checksum: bool = False,
11171176
) -> None:
@@ -1129,6 +1188,7 @@ def _download_artifact(
11291188
convert_format: Target RDF/tabular format for on-the-fly conversion.
11301189
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
11311190
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
1191+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
11321192
validate_checksum: Whether to validate checksums after downloading.
11331193
"""
11341194
json_str = fetch_databus_jsonld(uri, databus_key=databus_key)
@@ -1157,6 +1217,7 @@ def _download_artifact(
11571217
convert_format=convert_format,
11581218
graph_name=graph_name,
11591219
base_uri=base_uri,
1220+
graph_mode=graph_mode,
11601221
manifest_context=manifest_context,
11611222
validate_checksum=validate_checksum,
11621223
checksums=checksums,
@@ -1236,6 +1297,7 @@ def _download_group(
12361297
convert_format: str = None,
12371298
graph_name: str = None,
12381299
base_uri: str = None,
1300+
graph_mode: str = None,
12391301
manifest_context=None,
12401302
validate_checksum: bool = False,
12411303
) -> None:
@@ -1253,6 +1315,7 @@ def _download_group(
12531315
convert_format: Target RDF/tabular format for on-the-fly conversion.
12541316
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
12551317
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
1318+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
12561319
validate_checksum: Whether to validate checksums after downloading.
12571320
"""
12581321
json_str = fetch_databus_jsonld(uri, databus_key=databus_key)
@@ -1271,6 +1334,7 @@ def _download_group(
12711334
convert_format=convert_format,
12721335
graph_name=graph_name,
12731336
base_uri=base_uri,
1337+
graph_mode=graph_mode,
12741338
manifest_context=manifest_context,
12751339
validate_checksum=validate_checksum,
12761340
)
@@ -1323,6 +1387,7 @@ def download(
13231387
convert_format=None,
13241388
graph_name=None,
13251389
base_uri=None,
1390+
graph_mode=None,
13261391
validate_checksum: bool = False,
13271392
manifest_context=None,
13281393
) -> None:
@@ -1343,12 +1408,12 @@ def download(
13431408
convert_format: Target RDF/tabular format for on-the-fly conversion.
13441409
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
13451410
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
1411+
graph_mode: Optional graph sidecar mode. Currently supports 'download-url'.
13461412
validate_checksum: Whether to validate checksums after downloading.
13471413
"""
1414+
_validate_graph_mode(graph_mode)
13481415
for databusURI in databusURIs:
1349-
host, account, group, artifact, version, file = (
1350-
get_databus_id_parts_from_file_url(databusURI)
1351-
)
1416+
host, account, group, artifact, version, file = get_databus_id_parts_from_file_url(databusURI)
13521417

13531418
# Determine endpoint per-URI if not explicitly provided
13541419
uri_endpoint = endpoint
@@ -1374,6 +1439,7 @@ def download(
13741439
convert_format,
13751440
graph_name=graph_name,
13761441
base_uri=base_uri,
1442+
graph_mode=graph_mode,
13771443
manifest_context=manifest_context,
13781444
validate_checksum=validate_checksum,
13791445
)
@@ -1398,6 +1464,7 @@ def download(
13981464
convert_format=convert_format,
13991465
graph_name=graph_name,
14001466
base_uri=base_uri,
1467+
graph_mode=graph_mode,
14011468
manifest_context=manifest_context,
14021469
validate_checksum=validate_checksum,
14031470
expected_checksum=expected,
@@ -1415,6 +1482,7 @@ def download(
14151482
convert_format=convert_format,
14161483
graph_name=graph_name,
14171484
base_uri=base_uri,
1485+
graph_mode=graph_mode,
14181486
manifest_context=manifest_context,
14191487
validate_checksum=validate_checksum,
14201488
)
@@ -1434,6 +1502,7 @@ def download(
14341502
convert_format=convert_format,
14351503
graph_name=graph_name,
14361504
base_uri=base_uri,
1505+
graph_mode=graph_mode,
14371506
manifest_context=manifest_context,
14381507
validate_checksum=validate_checksum,
14391508
)
@@ -1453,6 +1522,7 @@ def download(
14531522
convert_format=convert_format,
14541523
graph_name=graph_name,
14551524
base_uri=base_uri,
1525+
graph_mode=graph_mode,
14561526
manifest_context=manifest_context,
14571527
validate_checksum=validate_checksum,
14581528
)
@@ -1494,7 +1564,8 @@ def download(
14941564
convert_format=convert_format,
14951565
graph_name=graph_name,
14961566
base_uri=base_uri,
1567+
graph_mode=graph_mode,
14971568
manifest_context=manifest_context,
14981569
validate_checksum=validate_checksum,
14991570
checksums=checksums if checksums else None,
1500-
)
1571+
)

0 commit comments

Comments
 (0)