Skip to content

Commit 9d31ac3

Browse files
gsoc26: Mapping Conversion Layer (Layer 3), #60 (#63)
* gsoc26: Layer 2 with tests initial commit * gsoc26: Refactor Layer 2 with handler architecture and improved tests * Improvements to Format layer implementation * Review comments resolved under issue #59 * #61: replacing --convert_from & --convert_to with --compression * gsoc26: layer2 complete + implementation for #61 * gsoc2026: mapping layer initial commit * fix: errors from testing fixed. * docs: update README for --format and --compression flags * fix: resolve PR review comments. * fix: remove duplicate import after merging layer2 fixes into layer3 * docs: add Layer 3 mapping conversion flags and examples * fix: move all test data to tests/resources, add round trip IR comparisons * fix: align Quad->Triple->Quad round trip test as required. * fix: fixed review comments * fix: fixed review comments(2) ---------
1 parent b376d40 commit 9d31ac3

10 files changed

Lines changed: 1589 additions & 41 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,11 @@ docker run --rm -v $(pwd):/data dbpedia/databus-python-client download $DOWNLOAD
177177
- `--compression`
178178
- Enables on-the-fly compression format conversion during download. Supported formats: `bz2`, `gz`, `xz`. The source compression is auto-detected from the file extension. Example: `--compression gz` converts all downloaded compressed files to gzip format.
179179
- `--format`
180-
- Enables on-the-fly RDF and tabular format conversion during download (Layer 2). Supported formats: `ntriples` (`nt`), `turtle` (`ttl`), `rdf-xml` (`rdf`, `xml`), `nquads` (`nq`), `trig`, `trix`, `json-ld` (`jsonld`), `csv`, `tsv`. Short aliases shown in brackets. Only the converted output file is kept — the original is deleted after successful conversion. Example: `--format turtle` converts all downloaded RDF triple files to Turtle format.
180+
- Enables on-the-fly RDF and tabular format conversion during download (Layer 2 and Layer 3). Supported formats: `ntriples` (`nt`), `turtle` (`ttl`), `rdf-xml` (`rdf`, `xml`), `nquads` (`nq`), `trig`, `trix`, `json-ld` (`jsonld`), `csv`, `tsv`. Short aliases shown in brackets. Only the converted output file is kept — the original is deleted after successful conversion. Within the same equivalence class (e.g. turtle to ntriples) conversion is lossless. Across classes (e.g. RDF to CSV) some flags below may be required.
181+
- `--graph-name`
182+
- Required when converting RDF triples to a quad format (e.g. turtle to nquads). Assigns all triples to the specified named graph URI. Example: `--format nquads --graph-name https://example.org/mygraph`.
183+
- `--base-uri`
184+
- Required when converting CSV/TSV to RDF triples. Used as the base for constructing subject URIs from CSV row identifiers. Example: `--format ntriples --base-uri https://example.org/data/`.
181185
- `--validate-checksum`
182186
- Validates the checksums of downloaded files against the checksums provided by the Databus. If a checksum does not match, an error is raised and the file is deleted.
183187

@@ -296,6 +300,24 @@ databusclient download https://databus.dbpedia.org/dbpedia/mappings/mappingbased
296300
databusclient download https://databus.dbpedia.org/dbpedia/mappings/mappingbased-literals/2022.12.01/mappingbased-literals_lang=az.ttl.bz2 --format ntriples --compression gz
297301
```
298302
303+
**Download with Mapping Conversion (Layer 3)**: convert across format classes — between RDF triples, RDF quads, and tabular data.
304+
```bash
305+
# RDF Triples -> RDF Quads (requires --graph-name)
306+
databusclient download https://databus.dbpedia.org/dbpedia/mappings/mappingbased-literals/2022.12.01/mappingbased-literals_lang=az.ttl.bz2 --format nquads --graph-name https://example.org/mygraph
307+
308+
# RDF Quads -> RDF Triples (splits into one file per named graph, in a subdirectory)
309+
databusclient download https://databus.dbpedia.org/dbpedia/mappings/mappingbased-literals/2022.12.01/mappingbased-literals_lang=az.nq --format turtle
310+
311+
# RDF Triples -> CSV (produces a companion .meta.json preserving datatypes/language tags)
312+
databusclient download https://databus.dbpedia.org/dbpedia/mappings/mappingbased-literals/2022.12.01/mappingbased-literals_lang=az.ttl.bz2 --format csv
313+
314+
# CSV -> RDF Triples (requires --base-uri; lossless if companion .meta.json is present)
315+
databusclient download https://databus.dbpedia.org/dbpedia/some-tabular-dataset/2022.12.01/data.csv --format ntriples --base-uri https://example.org/data/
316+
317+
# RDF Quads -> CSV (adds a 'graph' column)
318+
databusclient download https://databus.dbpedia.org/dbpedia/mappings/mappingbased-literals/2022.12.01/mappingbased-literals_lang=az.nq --format csv
319+
```
320+
299321
<a id="cli-deploy"></a>
300322
### Deploy
301323

databusclient/api/download.py

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@
1818
get_databus_id_parts_from_file_url,
1919
compute_sha256_and_length,
2020
)
21-
from databusclient.filehandling.format import convert_file, get_converted_filename
21+
from databusclient.filehandling.format import (
22+
convert_file,
23+
get_converted_filename,
24+
normalize_format,
25+
get_format_class,
26+
detect_format_from_filename,
27+
FORMAT_TO_EXTENSION,
28+
)
2229

2330
# Compression format mappings
2431
COMPRESSION_EXTENSIONS = {
@@ -316,6 +323,8 @@ def _download_file(
316323
client_id=None,
317324
compression=None,
318325
convert_format=None,
326+
graph_name=None,
327+
base_uri=None,
319328
validate_checksum: bool = False,
320329
expected_checksum: str | None = None,
321330
) -> None:
@@ -331,6 +340,8 @@ def _download_file(
331340
compression: Target compression format for on-the-fly conversion.
332341
Source compression is auto-detected from the file extension.
333342
convert_format: Target RDF/tabular format for on-the-fly conversion.
343+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
344+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
334345
validate_checksum: Whether to validate checksums after downloading.
335346
expected_checksum: The expected checksum of the file.
336347
"""
@@ -555,10 +566,6 @@ def _download_file(
555566
# already matches target format, skip decompression and conversion
556567
# entirely — no work needed for the format part.
557568
if needs_format_conversion and source_compression is not None:
558-
from databusclient.filehandling.format import (
559-
detect_format_from_filename,
560-
normalize_format,
561-
)
562569
detected_input_format = detect_format_from_filename(file)
563570
normalized_target = normalize_format(convert_format)
564571
if detected_input_format == normalized_target:
@@ -599,10 +606,57 @@ def _download_file(
599606

600607
conversion_input_path = temp_decompressed_path
601608

602-
# Convert format on uncompressed input.
609+
# Determine whether this is a Quad -> Triple (Layer 3) conversion.
610+
# This direction produces multiple output files (one per named
611+
# graph) written into a subdirectory, rather than a single file —
612+
# so it is handled separately from the standard single-file path
613+
# below (no recompression, no single-file delete-and-replace).
614+
normalized_convert_format = normalize_format(convert_format)
615+
target_class = get_format_class(normalized_convert_format)
616+
source_format_for_mapping = detect_format_from_filename(conversion_input_path)
617+
source_class_for_mapping = (
618+
get_format_class(source_format_for_mapping)
619+
if source_format_for_mapping else None
620+
)
621+
is_quad_to_triple = (
622+
source_class_for_mapping == "quads" and target_class == "triples"
623+
)
624+
625+
if is_quad_to_triple:
626+
# Output directory name = original filename with compression and
627+
# format extensions stripped (e.g. "data.nq.gz" -> "data").
628+
output_stem = get_converted_filename(file, convert_format)
629+
target_ext = FORMAT_TO_EXTENSION.get(normalized_convert_format, "")
630+
if target_ext and output_stem.lower().endswith(target_ext):
631+
output_stem = output_stem[: -len(target_ext)]
632+
output_dir = os.path.join(localDir, output_stem)
633+
634+
convert_file(
635+
conversion_input_path,
636+
output_dir,
637+
convert_format,
638+
graph_name=graph_name,
639+
base_uri=base_uri,
640+
)
641+
642+
# Delete the original downloaded (possibly compressed) file —
643+
# the split output directory replaces it.
644+
if os.path.exists(filename):
645+
os.remove(filename)
646+
print(f"Removed original file: {os.path.basename(filename)}")
647+
return
648+
649+
# Standard single-output-file path (Layer 2, and the remaining
650+
# Layer 3 directions: Triple<->Quad, Triple<->TSD, Quad->TSD).
603651
converted_basename = get_converted_filename(file, convert_format)
604652
converted_uncompressed_path = os.path.join(localDir, converted_basename)
605-
convert_file(conversion_input_path, converted_uncompressed_path, convert_format)
653+
convert_file(
654+
conversion_input_path,
655+
converted_uncompressed_path,
656+
convert_format,
657+
graph_name=graph_name,
658+
base_uri=base_uri,
659+
)
606660

607661
# Delete the original downloaded file after successful format conversion,
608662
# unless the converted output is the same file (same format, same path).
@@ -612,12 +666,18 @@ def _download_file(
612666
print(f"Removed original file: {os.path.basename(filename)}")
613667

614668
# Recompress converted output when needed.
669+
# Three cases:
670+
# 1. Source was compressed + --compression given -> use target compression
671+
# 2. Source was compressed, no --compression given -> recompress with original
672+
# 3. Source was NOT compressed + --compression given -> compress the output
673+
# 4. Source was NOT compressed, no --compression given -> no compression
615674
if source_compression is not None:
616675
if should_convert_compression and compression:
617676
final_compression = compression
618677
else:
619678
final_compression = source_compression
620-
elif should_convert_compression and compression:
679+
elif compression:
680+
# Source was uncompressed but user explicitly requested --compression
621681
final_compression = compression
622682
else:
623683
final_compression = None
@@ -651,6 +711,8 @@ def _download_files(
651711
client_id: str = None,
652712
compression: str = None,
653713
convert_format: str = None,
714+
graph_name: str = None,
715+
base_uri: str = None,
654716
validate_checksum: bool = False,
655717
checksums: dict | None = None,
656718
) -> None:
@@ -665,6 +727,8 @@ def _download_files(
665727
client_id: Client ID for token exchange.
666728
compression: Target compression format for on-the-fly conversion.
667729
convert_format: Target RDF/tabular format for on-the-fly conversion.
730+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
731+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
668732
validate_checksum: Whether to validate checksums after downloading.
669733
checksums: Dictionary mapping URLs to their expected checksums.
670734
"""
@@ -681,11 +745,12 @@ def _download_files(
681745
client_id=client_id,
682746
compression=compression,
683747
convert_format=convert_format,
748+
graph_name=graph_name,
749+
base_uri=base_uri,
684750
validate_checksum=validate_checksum,
685751
expected_checksum=expected,
686752
)
687753

688-
689754
def _get_sparql_query_of_collection(uri: str, databus_key: str | None = None) -> str:
690755
"""Get SPARQL query of collection members from databus collection URI.
691756
@@ -829,6 +894,8 @@ def _download_collection(
829894
client_id: str = None,
830895
compression: str = None,
831896
convert_format: str = None,
897+
graph_name: str = None,
898+
base_uri: str = None,
832899
validate_checksum: bool = False,
833900
) -> None:
834901
"""Download all files in a databus collection.
@@ -843,6 +910,8 @@ def _download_collection(
843910
client_id: Client ID for token exchange.
844911
compression: Target compression format for on-the-fly conversion.
845912
convert_format: Target RDF/tabular format for on-the-fly conversion.
913+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
914+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
846915
validate_checksum: Whether to validate checksums after downloading.
847916
"""
848917
query = _get_sparql_query_of_collection(uri, databus_key=databus_key)
@@ -864,6 +933,8 @@ def _download_collection(
864933
client_id=client_id,
865934
compression=compression,
866935
convert_format=convert_format,
936+
graph_name=graph_name,
937+
base_uri=base_uri,
867938
validate_checksum=validate_checksum,
868939
checksums=checksums if checksums else None,
869940
)
@@ -878,6 +949,8 @@ def _download_version(
878949
client_id: str = None,
879950
compression: str = None,
880951
convert_format: str = None,
952+
graph_name: str = None,
953+
base_uri: str = None,
881954
validate_checksum: bool = False,
882955
) -> None:
883956
"""Download all files in a databus artifact version.
@@ -891,6 +964,8 @@ def _download_version(
891964
client_id: Client ID for token exchange.
892965
compression: Target compression format for on-the-fly conversion.
893966
convert_format: Target RDF/tabular format for on-the-fly conversion.
967+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
968+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
894969
validate_checksum: Whether to validate checksums after downloading.
895970
"""
896971
json_str = fetch_databus_jsonld(uri, databus_key=databus_key)
@@ -911,6 +986,8 @@ def _download_version(
911986
client_id=client_id,
912987
compression=compression,
913988
convert_format=convert_format,
989+
graph_name=graph_name,
990+
base_uri=base_uri,
914991
validate_checksum=validate_checksum,
915992
checksums=checksums,
916993
)
@@ -926,6 +1003,8 @@ def _download_artifact(
9261003
client_id: str = None,
9271004
compression: str = None,
9281005
convert_format: str = None,
1006+
graph_name: str = None,
1007+
base_uri: str = None,
9291008
validate_checksum: bool = False,
9301009
) -> None:
9311010
"""Download files in a databus artifact.
@@ -940,6 +1019,8 @@ def _download_artifact(
9401019
client_id: Client ID for token exchange.
9411020
compression: Target compression format for on-the-fly conversion.
9421021
convert_format: Target RDF/tabular format for on-the-fly conversion.
1022+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
1023+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
9431024
validate_checksum: Whether to validate checksums after downloading.
9441025
"""
9451026
json_str = fetch_databus_jsonld(uri, databus_key=databus_key)
@@ -966,6 +1047,8 @@ def _download_artifact(
9661047
client_id=client_id,
9671048
compression=compression,
9681049
convert_format=convert_format,
1050+
graph_name=graph_name,
1051+
base_uri=base_uri,
9691052
validate_checksum=validate_checksum,
9701053
checksums=checksums,
9711054
)
@@ -1042,6 +1125,8 @@ def _download_group(
10421125
client_id: str = None,
10431126
compression: str = None,
10441127
convert_format: str = None,
1128+
graph_name: str = None,
1129+
base_uri: str = None,
10451130
validate_checksum: bool = False,
10461131
) -> None:
10471132
"""Download files in a databus group.
@@ -1056,6 +1141,8 @@ def _download_group(
10561141
client_id: Client ID for token exchange.
10571142
compression: Target compression format for on-the-fly conversion.
10581143
convert_format: Target RDF/tabular format for on-the-fly conversion.
1144+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
1145+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
10591146
validate_checksum: Whether to validate checksums after downloading.
10601147
"""
10611148
json_str = fetch_databus_jsonld(uri, databus_key=databus_key)
@@ -1072,6 +1159,8 @@ def _download_group(
10721159
client_id=client_id,
10731160
compression=compression,
10741161
convert_format=convert_format,
1162+
graph_name=graph_name,
1163+
base_uri=base_uri,
10751164
validate_checksum=validate_checksum,
10761165
)
10771166

@@ -1121,6 +1210,8 @@ def download(
11211210
client_id="vault-token-exchange",
11221211
compression=None,
11231212
convert_format=None,
1213+
graph_name=None,
1214+
base_uri=None,
11241215
validate_checksum: bool = False,
11251216
) -> None:
11261217
"""Download datasets from databus.
@@ -1136,8 +1227,10 @@ def download(
11361227
auth_url: Keycloak token endpoint URL. Default is "https://auth.dbpedia.org/realms/dbpedia/protocol/openid-connect/token".
11371228
client_id: Client ID for token exchange. Default is "vault-token-exchange".
11381229
compression: Target compression format for on-the-fly conversion (supported: bz2, gz, xz).
1139-
Source compression is auto-detected from the file extension.
1230+
Source compression is auto-detected from the file extension.
11401231
convert_format: Target RDF/tabular format for on-the-fly conversion.
1232+
graph_name: Named graph URI for Triple -> Quad conversion (Layer 3).
1233+
base_uri: Base URI for CSV -> Triple conversion (Layer 3).
11411234
validate_checksum: Whether to validate checksums after downloading.
11421235
"""
11431236
for databusURI in databusURIs:
@@ -1167,6 +1260,8 @@ def download(
11671260
client_id,
11681261
compression,
11691262
convert_format,
1263+
graph_name=graph_name,
1264+
base_uri=base_uri,
11701265
validate_checksum=validate_checksum,
11711266
)
11721267
elif file is not None:
@@ -1188,6 +1283,8 @@ def download(
11881283
client_id=client_id,
11891284
compression=compression,
11901285
convert_format=convert_format,
1286+
graph_name=graph_name,
1287+
base_uri=base_uri,
11911288
validate_checksum=validate_checksum,
11921289
expected_checksum=expected,
11931290
)
@@ -1202,6 +1299,8 @@ def download(
12021299
client_id=client_id,
12031300
compression=compression,
12041301
convert_format=convert_format,
1302+
graph_name=graph_name,
1303+
base_uri=base_uri,
12051304
validate_checksum=validate_checksum,
12061305
)
12071306
elif artifact is not None:
@@ -1218,6 +1317,8 @@ def download(
12181317
client_id=client_id,
12191318
compression=compression,
12201319
convert_format=convert_format,
1320+
graph_name=graph_name,
1321+
base_uri=base_uri,
12211322
validate_checksum=validate_checksum,
12221323
)
12231324
elif group is not None and group != "collections":
@@ -1234,6 +1335,8 @@ def download(
12341335
client_id=client_id,
12351336
compression=compression,
12361337
convert_format=convert_format,
1338+
graph_name=graph_name,
1339+
base_uri=base_uri,
12371340
validate_checksum=validate_checksum,
12381341
)
12391342
elif account is not None:
@@ -1272,6 +1375,8 @@ def download(
12721375
client_id=client_id,
12731376
compression=compression,
12741377
convert_format=convert_format,
1378+
graph_name=graph_name,
1379+
base_uri=base_uri,
12751380
validate_checksum=validate_checksum,
12761381
checksums=checksums if checksums else None,
12771382
)

0 commit comments

Comments
 (0)