From 8c4e5e59681a9e09079652f3a12f0401fb424b65 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Sat, 7 Mar 2026 01:42:16 +0530 Subject: [PATCH 1/4] fix: resolve TTL export alias failure and add RDF notebook example (#355) - Add _format_aliases map in RDFExporter to accept 'ttl', 'nt', 'xml', 'rdf', 'json-ld' as shorthands for canonical format names - Resolve alias at the start of export_to_rdf() before validation, leaving all existing callers unaffected - Add TTL alias demo cell to cookbook/introduction/15_Export.ipynb - Add tests/export/test_rdf_exporter.py covering alias parity, canonical formats, unsupported format error, and file export with format="ttl" Closes #355 Co-Authored-By: Claude Sonnet 4.6 --- cookbook/introduction/15_Export.ipynb | 9 +++- semantica/export/rdf_exporter.py | 10 ++++ tests/export/__init__.py | 0 tests/export/test_rdf_exporter.py | 73 +++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/export/__init__.py create mode 100644 tests/export/test_rdf_exporter.py diff --git a/cookbook/introduction/15_Export.ipynb b/cookbook/introduction/15_Export.ipynb index 0db9d510..3ccdb37d 100644 --- a/cookbook/introduction/15_Export.ipynb +++ b/cookbook/introduction/15_Export.ipynb @@ -178,6 +178,13 @@ "rdf_exporter.export(kg, \"output.ttl\", format=\"turtle\")" ] }, + { + "cell_type": "code", + "source": "# TTL alias: format=\"ttl\" is equivalent to format=\"turtle\"\nrdf_data = {\n \"entities\": [\n {\"id\": \"e1\", \"text\": \"Apple Inc.\", \"type\": \"ORG\", \"confidence\": 0.95},\n {\"id\": \"e2\", \"text\": \"Steve Jobs\", \"type\": \"PERSON\", \"confidence\": 0.97},\n ],\n \"relationships\": [\n {\"source_id\": \"e2\", \"target_id\": \"e1\", \"type\": \"founded_by\", \"confidence\": 0.91},\n ],\n}\n\nrdf_exporter.export(rdf_data, \"output.ttl\", format=\"ttl\")\n\nresult = rdf_exporter.validate_rdf(rdf_data)\nprint(f\"Valid: {result['valid']}\")", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, { "cell_type": "markdown", "metadata": {}, @@ -363,4 +370,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/semantica/export/rdf_exporter.py b/semantica/export/rdf_exporter.py index 67a8a6cc..85f33ebb 100644 --- a/semantica/export/rdf_exporter.py +++ b/semantica/export/rdf_exporter.py @@ -851,6 +851,15 @@ class RDFExporter: # Supported RDF formats self.supported_formats = ["turtle", "rdfxml", "jsonld", "ntriples", "n3"] + # Format aliases (common extensions/shorthands → canonical names) + self._format_aliases = { + "ttl": "turtle", + "nt": "ntriples", + "xml": "rdfxml", + "rdf": "rdfxml", + "json-ld": "jsonld", + } + # Initialize progress tracker self.progress_tracker = get_progress_tracker() @@ -892,6 +901,7 @@ class RDFExporter: ) try: + format = self._format_aliases.get(format.lower(), format.lower()) if format not in self.supported_formats: raise ValidationError( f"Unsupported RDF format: {format}. " diff --git a/tests/export/__init__.py b/tests/export/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/export/test_rdf_exporter.py b/tests/export/test_rdf_exporter.py new file mode 100644 index 00000000..2f662d18 --- /dev/null +++ b/tests/export/test_rdf_exporter.py @@ -0,0 +1,73 @@ +"""Tests for RDFExporter format alias resolution (issue #355).""" + +import pytest +from semantica.export import RDFExporter + + +RDF_DATA = { + "entities": [ + {"id": "e1", "text": "Apple Inc.", "type": "ORG", "confidence": 0.95}, + {"id": "e2", "text": "Steve Jobs", "type": "PERSON", "confidence": 0.97}, + ], + "relationships": [ + {"source_id": "e2", "target_id": "e1", "type": "founded_by", "confidence": 0.91}, + ], +} + + +@pytest.fixture +def exporter(): + return RDFExporter() + + +def test_ttl_alias_produces_same_output_as_turtle(exporter): + """format='ttl' must produce identical output to format='turtle'.""" + result_turtle = exporter.export_to_rdf(RDF_DATA, format="turtle") + result_ttl = exporter.export_to_rdf(RDF_DATA, format="ttl") + assert result_ttl == result_turtle + + +def test_nt_alias_produces_same_output_as_ntriples(exporter): + result_canonical = exporter.export_to_rdf(RDF_DATA, format="ntriples") + result_alias = exporter.export_to_rdf(RDF_DATA, format="nt") + assert result_alias == result_canonical + + +def test_xml_alias_produces_same_output_as_rdfxml(exporter): + result_canonical = exporter.export_to_rdf(RDF_DATA, format="rdfxml") + result_alias = exporter.export_to_rdf(RDF_DATA, format="xml") + assert result_alias == result_canonical + + +def test_rdf_alias_produces_same_output_as_rdfxml(exporter): + result_canonical = exporter.export_to_rdf(RDF_DATA, format="rdfxml") + result_alias = exporter.export_to_rdf(RDF_DATA, format="rdf") + assert result_alias == result_canonical + + +def test_json_ld_alias_produces_same_output_as_jsonld(exporter): + result_canonical = exporter.export_to_rdf(RDF_DATA, format="jsonld") + result_alias = exporter.export_to_rdf(RDF_DATA, format="json-ld") + assert result_alias == result_canonical + + +def test_canonical_formats_unaffected(exporter): + """Existing canonical format names must continue to work.""" + # n3 is listed in supported_formats but has no serializer implementation yet + for fmt in ("turtle", "rdfxml", "jsonld", "ntriples"): + result = exporter.export_to_rdf(RDF_DATA, format=fmt) + assert result is not None and len(result) > 0 + + +def test_unsupported_format_raises(exporter): + from semantica.utils.exceptions import ValidationError + + with pytest.raises(ValidationError): + exporter.export_to_rdf(RDF_DATA, format="parquet") + + +def test_ttl_export_to_file(exporter, tmp_path): + out = tmp_path / "output.ttl" + exporter.export(RDF_DATA, str(out), format="ttl") + assert out.exists() + assert out.stat().st_size > 0 From 34df1964b9680c983f57b46f22299fb5d56f41ce Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Sat, 7 Mar 2026 02:07:44 +0530 Subject: [PATCH 2/4] docs: add PR description and update CHANGELOG for #355 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 6 ++++ pr_description.md | 77 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 pr_description.md diff --git a/CHANGELOG.md b/CHANGELOG.md index afaf9577..4b8c5ba3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- **TTL Export Alias Fix** (PR #355 by @ZohaibHassan16): + - Added `_format_aliases` map in `RDFExporter` so `format="ttl"`, `"nt"`, `"xml"`, `"rdf"`, and `"json-ld"` resolve to their canonical counterparts without breaking existing callers + - Alias resolution applied at the top of `export_to_rdf()` before format validation — zero public API changes + - Added working TTL export cell to `cookbook/introduction/15_Export.ipynb` (Step 3: RDF Export) + - Added `tests/export/test_rdf_exporter.py` with 8 tests covering all aliases, canonical formats, error handling, and file export + - **Incremental/Delta Processing Feature** (PR #349 by @ZohaibHassan16, reviewed and fixed by @KaifAhmad1): - Native delta computation between graph snapshots using SPARQL queries - Delta-aware pipeline execution with `delta_mode` configuration for processing only changed data diff --git a/pr_description.md b/pr_description.md new file mode 100644 index 00000000..bdc64187 --- /dev/null +++ b/pr_description.md @@ -0,0 +1,77 @@ +## Description + +Fixes two gaps that made Turtle/TTL export inaccessible: + +1. `format="ttl"` raised a `ValidationError` because `"ttl"` was not accepted as an alias for `"turtle"` in `RDFExporter`, even though it is the standard file extension. +2. `RDFExporter` was never introduced in `cookbook/introduction/15_Export.ipynb`, so users following the default learning path had no way to discover TTL export. + +## Type of Change + +- [x] Bug fix (non-breaking change which fixes an issue) +- [x] Documentation update + +## Related Issues + +Closes #355 + +## Changes Made + +- `semantica/export/rdf_exporter.py`: Added `_format_aliases` dict in `RDFExporter.__init__()` mapping common shorthands to canonical format names (`ttl→turtle`, `nt→ntriples`, `xml→rdfxml`, `rdf→rdfxml`, `json-ld→jsonld`). Added one-line alias resolution at the top of `export_to_rdf()` before format validation — all existing callers using canonical names are unaffected. +- `cookbook/introduction/15_Export.ipynb`: Added a code cell in Step 3 (RDF Export) demonstrating `format="ttl"` and `validate_rdf()`. +- `tests/export/test_rdf_exporter.py`: New test file with 8 tests covering alias parity, canonical formats, unsupported format error, and file export with `format="ttl"`. + +## Testing + +- [x] Tested locally +- [x] Added tests for new functionality +- [x] Package builds successfully (`python -m build`) + +### Test Commands + +```bash +# Run new RDF exporter tests +pytest tests/export/test_rdf_exporter.py -v + +# Verify the original bug is fixed +python -c " +from semantica.export import RDFExporter +exporter = RDFExporter() +rdf_data = { + 'entities': [ + {'id': 'e1', 'text': 'Apple Inc.', 'type': 'ORG', 'confidence': 0.95}, + {'id': 'e2', 'text': 'Steve Jobs', 'type': 'PERSON', 'confidence': 0.97}, + ], + 'relationships': [ + {'source_id': 'e2', 'target_id': 'e1', 'type': 'founded_by', 'confidence': 0.91}, + ], +} +exporter.export(rdf_data, 'output.ttl', format='ttl') +print('format=ttl works correctly') +" + +# Full test suite +pytest tests/ +``` + +## Documentation + +- [x] Updated relevant documentation +- [x] Added code examples if applicable +- [x] Updated cookbook if adding new examples + +## Breaking Changes + +**Breaking Changes**: No + +All existing callers using canonical format names (`"turtle"`, `"rdfxml"`, `"jsonld"`, `"ntriples"`, `"n3"`) are completely unaffected. The alias map is purely additive. + +## Checklist + +- [x] My code follows the project's style guidelines +- [x] I have performed a self-review of my code +- [x] My changes generate no new warnings +- [x] Package builds successfully + +## Additional Notes + +The alias resolution uses `.lower()` on the input before lookup, so `"TTL"`, `"Ttl"`, etc. also work. No public API signatures were changed. From eb21b851df0ea798ae1a79750b9936def089a83e Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Sat, 7 Mar 2026 02:09:38 +0530 Subject: [PATCH 3/4] docs: update CHANGELOG for #355 and remove pr_description.md Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 +- pr_description.md | 77 ----------------------------------------------- 2 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 pr_description.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b8c5ba3..28eee585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- **TTL Export Alias Fix** (PR #355 by @ZohaibHassan16): +- **TTL Export Alias Fix** (PR #355 by @KaifAhmad1): - Added `_format_aliases` map in `RDFExporter` so `format="ttl"`, `"nt"`, `"xml"`, `"rdf"`, and `"json-ld"` resolve to their canonical counterparts without breaking existing callers - Alias resolution applied at the top of `export_to_rdf()` before format validation — zero public API changes - Added working TTL export cell to `cookbook/introduction/15_Export.ipynb` (Step 3: RDF Export) diff --git a/pr_description.md b/pr_description.md deleted file mode 100644 index bdc64187..00000000 --- a/pr_description.md +++ /dev/null @@ -1,77 +0,0 @@ -## Description - -Fixes two gaps that made Turtle/TTL export inaccessible: - -1. `format="ttl"` raised a `ValidationError` because `"ttl"` was not accepted as an alias for `"turtle"` in `RDFExporter`, even though it is the standard file extension. -2. `RDFExporter` was never introduced in `cookbook/introduction/15_Export.ipynb`, so users following the default learning path had no way to discover TTL export. - -## Type of Change - -- [x] Bug fix (non-breaking change which fixes an issue) -- [x] Documentation update - -## Related Issues - -Closes #355 - -## Changes Made - -- `semantica/export/rdf_exporter.py`: Added `_format_aliases` dict in `RDFExporter.__init__()` mapping common shorthands to canonical format names (`ttl→turtle`, `nt→ntriples`, `xml→rdfxml`, `rdf→rdfxml`, `json-ld→jsonld`). Added one-line alias resolution at the top of `export_to_rdf()` before format validation — all existing callers using canonical names are unaffected. -- `cookbook/introduction/15_Export.ipynb`: Added a code cell in Step 3 (RDF Export) demonstrating `format="ttl"` and `validate_rdf()`. -- `tests/export/test_rdf_exporter.py`: New test file with 8 tests covering alias parity, canonical formats, unsupported format error, and file export with `format="ttl"`. - -## Testing - -- [x] Tested locally -- [x] Added tests for new functionality -- [x] Package builds successfully (`python -m build`) - -### Test Commands - -```bash -# Run new RDF exporter tests -pytest tests/export/test_rdf_exporter.py -v - -# Verify the original bug is fixed -python -c " -from semantica.export import RDFExporter -exporter = RDFExporter() -rdf_data = { - 'entities': [ - {'id': 'e1', 'text': 'Apple Inc.', 'type': 'ORG', 'confidence': 0.95}, - {'id': 'e2', 'text': 'Steve Jobs', 'type': 'PERSON', 'confidence': 0.97}, - ], - 'relationships': [ - {'source_id': 'e2', 'target_id': 'e1', 'type': 'founded_by', 'confidence': 0.91}, - ], -} -exporter.export(rdf_data, 'output.ttl', format='ttl') -print('format=ttl works correctly') -" - -# Full test suite -pytest tests/ -``` - -## Documentation - -- [x] Updated relevant documentation -- [x] Added code examples if applicable -- [x] Updated cookbook if adding new examples - -## Breaking Changes - -**Breaking Changes**: No - -All existing callers using canonical format names (`"turtle"`, `"rdfxml"`, `"jsonld"`, `"ntriples"`, `"n3"`) are completely unaffected. The alias map is purely additive. - -## Checklist - -- [x] My code follows the project's style guidelines -- [x] I have performed a self-review of my code -- [x] My changes generate no new warnings -- [x] Package builds successfully - -## Additional Notes - -The alias resolution uses `.lower()` on the input before lookup, so `"TTL"`, `"Ttl"`, etc. also work. No public API signatures were changed. From 1d96b6f80e7dbda3bfec6f3eed45329877502934 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Sat, 7 Mar 2026 03:03:44 +0530 Subject: [PATCH 4/4] fix: address code review issues from PR #358 (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rdf_exporter.py: add isinstance(format, str) guard before .lower() so non-string inputs (None, int, etc.) raise ValidationError consistently instead of AttributeError; normalize via strip().lower() in one step - 15_Export.ipynb: fix notebook cell using result['valid'] → result['overall_valid'] (validate_rdf() returns overall_valid, not valid); add trailing EOF newline - test_rdf_exporter.py: add tests for non-string format → ValidationError and for overall_valid key presence in validate_rdf() return value Co-Authored-By: Claude Sonnet 4.6 --- cookbook/introduction/15_Export.ipynb | 4 ++-- semantica/export/rdf_exporter.py | 7 ++++++- tests/export/test_rdf_exporter.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/cookbook/introduction/15_Export.ipynb b/cookbook/introduction/15_Export.ipynb index 3ccdb37d..3d6a8c10 100644 --- a/cookbook/introduction/15_Export.ipynb +++ b/cookbook/introduction/15_Export.ipynb @@ -180,7 +180,7 @@ }, { "cell_type": "code", - "source": "# TTL alias: format=\"ttl\" is equivalent to format=\"turtle\"\nrdf_data = {\n \"entities\": [\n {\"id\": \"e1\", \"text\": \"Apple Inc.\", \"type\": \"ORG\", \"confidence\": 0.95},\n {\"id\": \"e2\", \"text\": \"Steve Jobs\", \"type\": \"PERSON\", \"confidence\": 0.97},\n ],\n \"relationships\": [\n {\"source_id\": \"e2\", \"target_id\": \"e1\", \"type\": \"founded_by\", \"confidence\": 0.91},\n ],\n}\n\nrdf_exporter.export(rdf_data, \"output.ttl\", format=\"ttl\")\n\nresult = rdf_exporter.validate_rdf(rdf_data)\nprint(f\"Valid: {result['valid']}\")", + "source": "# TTL alias: format=\"ttl\" is equivalent to format=\"turtle\"\nrdf_data = {\n \"entities\": [\n {\"id\": \"e1\", \"text\": \"Apple Inc.\", \"type\": \"ORG\", \"confidence\": 0.95},\n {\"id\": \"e2\", \"text\": \"Steve Jobs\", \"type\": \"PERSON\", \"confidence\": 0.97},\n ],\n \"relationships\": [\n {\"source_id\": \"e2\", \"target_id\": \"e1\", \"type\": \"founded_by\", \"confidence\": 0.91},\n ],\n}\n\nrdf_exporter.export(rdf_data, \"output.ttl\", format=\"ttl\")\n\nresult = rdf_exporter.validate_rdf(rdf_data)\nprint(f\"Valid: {result['overall_valid']}\")", "metadata": {}, "execution_count": null, "outputs": [] @@ -370,4 +370,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/semantica/export/rdf_exporter.py b/semantica/export/rdf_exporter.py index 85f33ebb..0c3035ca 100644 --- a/semantica/export/rdf_exporter.py +++ b/semantica/export/rdf_exporter.py @@ -901,7 +901,12 @@ class RDFExporter: ) try: - format = self._format_aliases.get(format.lower(), format.lower()) + if not isinstance(format, str): + raise ValidationError( + f"RDF format must be a string, got: {type(format).__name__}" + ) + fmt = format.strip().lower() + format = self._format_aliases.get(fmt, fmt) if format not in self.supported_formats: raise ValidationError( f"Unsupported RDF format: {format}. " diff --git a/tests/export/test_rdf_exporter.py b/tests/export/test_rdf_exporter.py index 2f662d18..e9d5f0c4 100644 --- a/tests/export/test_rdf_exporter.py +++ b/tests/export/test_rdf_exporter.py @@ -71,3 +71,21 @@ def test_ttl_export_to_file(exporter, tmp_path): exporter.export(RDF_DATA, str(out), format="ttl") assert out.exists() assert out.stat().st_size > 0 + + +def test_non_string_format_raises_validation_error(exporter): + """format=None or non-string must raise ValidationError, not AttributeError.""" + from semantica.utils.exceptions import ValidationError + + with pytest.raises(ValidationError): + exporter.export_to_rdf(RDF_DATA, format=None) + + with pytest.raises(ValidationError): + exporter.export_to_rdf(RDF_DATA, format=123) + + +def test_validate_rdf_returns_overall_valid_key(exporter): + """validate_rdf() must return 'overall_valid' key (used in notebook example).""" + result = exporter.validate_rdf(RDF_DATA) + assert "overall_valid" in result + assert isinstance(result["overall_valid"], bool)