From 4a451f410d1d94c7fead07c1188faaf9908bd34f Mon Sep 17 00:00:00 2001 From: OctoBored <212877535+OctoBored@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:16:29 +0000 Subject: [PATCH 1/5] docs: fix broken star history chart in README The star history chart was broken due to GitHub stargazer API restrictions, so it could no longer be rendered. Update the README to point to a working alternative that uses a different data source requiring no API token. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8b89fd5f..8e729b2d 100644 --- a/README.md +++ b/README.md @@ -1566,11 +1566,11 @@ On-premises deployment · Private cloud · Custom domain implementations · SLA- ## Star History - + - - - Star History Chart + + + Star History Chart From e41993a6bd7b10389991dd222ff260e969033d13 Mon Sep 17 00:00:00 2001 From: Freakz2z Date: Sun, 23 Aug 2026 23:02:15 +0800 Subject: [PATCH 2/5] fix(triplet_store): honor RDF4J repository id --- docs/storage-backends.md | 4 ++-- semantica/triplet_store/rdf4j_store.py | 2 +- tests/triplet_store/test_rdf4j_store.py | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/storage-backends.md b/docs/storage-backends.md index bb05e115..dc211ef0 100644 --- a/docs/storage-backends.md +++ b/docs/storage-backends.md @@ -35,7 +35,7 @@ This page is intentionally conservative: it distinguishes between an adapter exi | FalkorDB | LPG | Yes | Yes | Partial | Partial | Redis-based; provenance depends on node/edge properties, and multi-graph isolation depends on the selected graph name. | | Amazon Neptune | LPG | Yes | Yes | Partial | Partial | Use the property-graph endpoint; AWS auth, VPC, and endpoint configuration can affect local tests. Provenance depends on node/edge properties. | | Apache AGE | LPG | Yes | Yes | Partial | Partial | Runs through PostgreSQL/AGE; Cypher compatibility and property handling can differ from standalone LPG engines. | -| RDF4J | RDF | Yes | Partial | Partial | Partial | Context separation relies on named graphs; triple-level provenance may require reification or graph-level metadata. `RDF4JStore(repository_id=...)` currently has no effect — the constructor always connects to the `"default"` repository regardless of the value passed; track a fix separately. | +| RDF4J | RDF | Yes | Partial | Partial | Partial | Context separation relies on named graphs; triple-level provenance may require reification or graph-level metadata. | | Apache Jena | RDF | Yes | Partial | Partial | Partial | Named graphs are needed for context separation; backend configuration and transaction behavior matter. | | Blazegraph | RDF | Yes | Partial | Partial | Partial | Use quads/named graphs for context; IRI stability and graph naming matter for provenance. | | Anzo | RDF | Yes | Partial | Partial | Partial | Anzo deployments are environment-specific; validate `dataset_uri`/graphmart naming, named-graph support, and provenance mapping. | @@ -107,7 +107,7 @@ from semantica.triplet_store import RDF4JStore store = RDF4JStore( endpoint='http://localhost:8080/rdf4j-server', - repository_id='semantica' # currently has no effect; connects to "default" (see Known limitations) + repository_id='semantica' ) ``` diff --git a/semantica/triplet_store/rdf4j_store.py b/semantica/triplet_store/rdf4j_store.py index 8dad6997..c03b64a8 100644 --- a/semantica/triplet_store/rdf4j_store.py +++ b/semantica/triplet_store/rdf4j_store.py @@ -67,7 +67,7 @@ class RDF4JStore: self.progress_tracker.enabled = True self.endpoint = endpoint.rstrip("/") - self.repository_id = config.get("repository_id", "default") + self.repository_id = repository_id or config.get("repository_id", "default") self.username = config.get("username") self.password = config.get("password") self.timeout = config.get("timeout", 30) diff --git a/tests/triplet_store/test_rdf4j_store.py b/tests/triplet_store/test_rdf4j_store.py index 4ef4f16b..3f630b93 100644 --- a/tests/triplet_store/test_rdf4j_store.py +++ b/tests/triplet_store/test_rdf4j_store.py @@ -22,6 +22,27 @@ def _make_connected_store(): CONSTRUCT_QUERY = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }" +class TestRDF4JStoreInitialization(unittest.TestCase): + def test_explicit_repository_id_selects_repository(self): + response = MagicMock(status_code=200) + + with patch( + "semantica.triplet_store.rdf4j_store.requests.get", + return_value=response, + ) as mock_get: + store = RDF4JStore( + endpoint="http://localhost:8080/rdf4j-server/", + repository_id="semantica", + ) + + self.assertEqual(store.repository_id, "semantica") + mock_get.assert_called_once_with( + "http://localhost:8080/rdf4j-server/repositories/semantica", + timeout=30, + auth=None, + ) + + class TestRDF4JStoreIsConstructQuery(unittest.TestCase): def test_detects_uppercase(self): self.assertTrue(_make_connected_store()._is_construct_query( From de31b43663972037dde2d6aacc9c4a0aa3dd2585 Mon Sep 17 00:00:00 2001 From: Aldrin Joseph Date: Sun, 23 Aug 2026 14:11:35 +0530 Subject: [PATCH 3/5] fix(utils): write console progress only to an interactive stdout ProgressTracker attached ConsoleProgressDisplay unconditionally, so any script or CI job that piped or redirected stdout had one progress bar per stage written into its output, escape sequences included. A plain `python demo.py > out.txt` captured 173 bytes of progress-bar noise around 10 bytes of the program's own output. Console progress is now attached only when stdout is an interactive terminal, when running under Jupyter, or when SEMANTICA_FORCE_PROGRESS is set. FileProgressDisplay is untouched, so progress logging still works in pipelines, and SEMANTICA_DISABLE_PROGRESS keeps its existing meaning and still takes precedence. Both progress environment variables are now documented in the README and the utils reference; SEMANTICA_DISABLE_PROGRESS previously existed only in the reference page. Deviations from the issue: the issue suggested disabling the tracker on non-TTY stdout. This gates the display instead, because disabling the tracker would short-circuit before FileProgressDisplay and take file progress logging down with it, and the ~20 modules that set `progress_tracker.enabled = True` in __init__ would need the property setter taught about TTY state to avoid undoing it. Gating the display leaves both alone. Design note: the claim comment on the issue proposed an `enabled: Optional[bool] = None` constructor opt-in; during implementation the opt-in became SEMANTICA_FORCE_PROGRESS, which needs no signature change and follows the NO_COLOR/FORCE_COLOR convention. Known limitation: TTY detection runs once at tracker construction (the tracker is a process-wide singleton), so a process that redirects stdout after first use needs the env vars to change behaviour. Fixes #1185 --- README.md | 2 + docs/reference/utils.md | 10 +++ semantica/utils/progress_tracker.py | 39 +++++++-- tests/test_progress_tracker_regressions.py | 96 ++++++++++++++++++++++ 4 files changed, 142 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a2646bde..64d2cd7e 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,8 @@ semantica doctor # Config file pass ~/.semantica/config.yaml ``` +**Running in a script or CI?** Progress bars are written only when stdout is an interactive terminal (or a Jupyter notebook), so piping and redirecting stay clean by default. Override with `SEMANTICA_DISABLE_PROGRESS=1` to silence progress everywhere, or `SEMANTICA_FORCE_PROGRESS=1` to keep it when stdout is redirected. `SEMANTICA_DISABLE_PROGRESS` takes precedence. +
If Semantica solves a real problem for you, a star helps others find it. diff --git a/docs/reference/utils.md b/docs/reference/utils.md index 83f9cc97..f7e97789 100644 --- a/docs/reference/utils.md +++ b/docs/reference/utils.md @@ -77,7 +77,17 @@ Most users won't call utils directly: it's the **shared foundation** for all mod export SEMANTICA_LOG_LEVEL=DEBUG export SEMANTICA_LOG_FORMAT=json # "json" | "text" export SEMANTICA_DISABLE_PROGRESS=true + export SEMANTICA_FORCE_PROGRESS=true ``` + + + **Progress bars follow your terminal.** Console progress is written only when + stdout is an interactive terminal (or a Jupyter notebook), so piping or + redirecting output no longer fills logs with progress bars and escape + sequences. Set `SEMANTICA_DISABLE_PROGRESS` to silence progress even in a + terminal, or `SEMANTICA_FORCE_PROGRESS` to keep it when stdout is redirected. + `SEMANTICA_DISABLE_PROGRESS` wins if both are set. + diff --git a/semantica/utils/progress_tracker.py b/semantica/utils/progress_tracker.py index febfb4b9..f27768d4 100644 --- a/semantica/utils/progress_tracker.py +++ b/semantica/utils/progress_tracker.py @@ -64,6 +64,29 @@ def _progress_disabled_from_env() -> bool: "on", ) + +def _progress_forced_from_env() -> bool: + """Return whether console progress is forced on despite a non-interactive stdout.""" + return os.getenv("SEMANTICA_FORCE_PROGRESS", "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + + +def _stdout_is_tty() -> bool: + """Return whether stdout is an interactive terminal. + + Replacement streams do not always implement ``isatty`` and closed streams can + raise, so both cases are treated as non-interactive. + """ + try: + return bool(sys.stdout is not None and sys.stdout.isatty()) + except (AttributeError, ValueError): + return False + + # Try to import IPython for Jupyter support try: from IPython import get_ipython @@ -1040,18 +1063,24 @@ class ProgressTracker: # Create displays self.displays: List[ProgressDisplay] = [] + # Console output only suits an interactive stdout. When output is piped or + # redirected (scripts, CI logs) the progress bars and their escape + # sequences would otherwise drown the program's own output. + console_ok = _stdout_is_tty() or self.is_jupyter or _progress_forced_from_env() + # Always try Jupyter first if available, fallback to console if IPYTHON_AVAILABLE: # Try to detect Jupyter - if available, use it if self.is_jupyter and not self.disable_jupyter_progress: self.displays.append(JupyterProgressDisplay(use_emoji=use_emoji)) # Also add console as fallback for immediate feedback - self.displays.append( - ConsoleProgressDisplay( - use_emoji=use_emoji, update_interval=update_interval + if console_ok: + self.displays.append( + ConsoleProgressDisplay( + use_emoji=use_emoji, update_interval=update_interval + ) ) - ) - else: + elif console_ok: self.displays.append( ConsoleProgressDisplay( use_emoji=use_emoji, update_interval=update_interval diff --git a/tests/test_progress_tracker_regressions.py b/tests/test_progress_tracker_regressions.py index ac4c09da..b42a885b 100644 --- a/tests/test_progress_tracker_regressions.py +++ b/tests/test_progress_tracker_regressions.py @@ -12,6 +12,7 @@ import semantica.utils.progress_tracker as progress_module @pytest.fixture(autouse=True) def reset_progress_singletons(monkeypatch): monkeypatch.delenv("SEMANTICA_DISABLE_PROGRESS", raising=False) + monkeypatch.delenv("SEMANTICA_FORCE_PROGRESS", raising=False) progress_module.ProgressTracker._instance = None progress_module._global_tracker = None yield @@ -19,6 +20,40 @@ def reset_progress_singletons(monkeypatch): progress_module._global_tracker = None +class _FakeStdout: + """Minimal stdout stand-in with controllable TTY reporting.""" + + encoding = "utf-8" + + def __init__(self, tty): + self._tty = tty + self.written = [] + + def isatty(self): + return self._tty + + def write(self, text): + self.written.append(text) + return len(text) + + def flush(self): + pass + + +def _use_stdout(monkeypatch, tty): + """Point sys.stdout at a fake with the given TTY behaviour, outside Jupyter.""" + stream = _FakeStdout(tty=tty) + monkeypatch.setattr(sys, "stdout", stream) + monkeypatch.setattr( + progress_module.ProgressTracker, "_detect_jupyter", lambda *_: False + ) + return stream + + +def _displays_of(tracker, display_cls): + return [d for d in tracker.displays if isinstance(d, display_cls)] + + def _install_tracker_as_singleton(tracker: progress_module.ProgressTracker) -> None: progress_module.ProgressTracker._instance = tracker progress_module._global_tracker = tracker @@ -100,6 +135,67 @@ def test_disable_progress_env_prevents_reenable(monkeypatch): assert tracker.start_tracking(module="core", submodule="test") == "" +def test_console_display_omitted_when_stdout_is_not_a_tty(monkeypatch): + _use_stdout(monkeypatch, tty=False) + + tracker = progress_module.ProgressTracker(use_emoji=False, update_interval=0) + + assert _displays_of(tracker, progress_module.ConsoleProgressDisplay) == [] + + +def test_console_display_present_when_stdout_is_a_tty(monkeypatch): + _use_stdout(monkeypatch, tty=True) + + tracker = progress_module.ProgressTracker(use_emoji=False, update_interval=0) + + assert _displays_of(tracker, progress_module.ConsoleProgressDisplay) + + +def test_file_display_survives_non_tty_stdout(monkeypatch): + _use_stdout(monkeypatch, tty=False) + + tracker = progress_module.ProgressTracker(use_emoji=False, update_interval=0) + + assert _displays_of(tracker, progress_module.FileProgressDisplay) + + +def test_force_progress_env_restores_console_display_on_non_tty(monkeypatch): + monkeypatch.setenv("SEMANTICA_FORCE_PROGRESS", "1") + _use_stdout(monkeypatch, tty=False) + + tracker = progress_module.ProgressTracker(use_emoji=False, update_interval=0) + + assert _displays_of(tracker, progress_module.ConsoleProgressDisplay) + + +def test_disable_progress_env_beats_force_progress_env(monkeypatch): + monkeypatch.setenv("SEMANTICA_DISABLE_PROGRESS", "1") + monkeypatch.setenv("SEMANTICA_FORCE_PROGRESS", "1") + stream = _use_stdout(monkeypatch, tty=False) + + tracker = progress_module.ProgressTracker(use_emoji=False, update_interval=0) + _install_tracker_as_singleton(tracker) + + assert tracker.enabled is False + assert tracker.start_tracking(module="core", submodule="test") == "" + assert stream.written == [] + + +def test_non_tty_stdout_stays_silent_after_module_reenables_tracker(monkeypatch): + stream = _use_stdout(monkeypatch, tty=False) + tracker = progress_module.ProgressTracker(use_emoji=False, update_interval=0) + _install_tracker_as_singleton(tracker) + + # Mirrors the ~20 modules that do `self.progress_tracker.enabled = True`. + tracker.enabled = True + tracking_id = tracker.start_tracking( + module="core", submodule="Semantica", message="Building" + ) + tracker.update_progress(tracking_id, processed=1, total=1, message="Processing") + + assert stream.written == [] + + def test_build_knowledge_base_subprocess_does_not_deadlock(): root = Path(__file__).resolve().parents[1] runtime_dir = root / "test_data" / "runtime" / f"build-regression-{os.getpid()}" From 4c997b501799f2c71e7040187a5c87e972e1fd4e Mon Sep 17 00:00:00 2001 From: Freakz2z Date: Mon, 24 Aug 2026 09:01:22 +0800 Subject: [PATCH 4/5] fix(triplet_store): encode RDF4J repository paths --- docs/reference/triplet_store.md | 2 +- semantica/triplet_store/rdf4j_store.py | 11 ++++--- tests/triplet_store/test_rdf4j_store.py | 44 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/docs/reference/triplet_store.md b/docs/reference/triplet_store.md index ad7a0645..ee5c24e6 100644 --- a/docs/reference/triplet_store.md +++ b/docs/reference/triplet_store.md @@ -182,7 +182,7 @@ for row in result.bindings: store = TripletStore( backend="rdf4j", endpoint="http://localhost:8080/rdf4j-server", - repository_id="semantica", # passed through **config + repository_id="semantica", # selects the remote repository ) ``` diff --git a/semantica/triplet_store/rdf4j_store.py b/semantica/triplet_store/rdf4j_store.py index c03b64a8..d788ab7b 100644 --- a/semantica/triplet_store/rdf4j_store.py +++ b/semantica/triplet_store/rdf4j_store.py @@ -28,7 +28,7 @@ License: MIT import re from typing import Any, Dict, List, Optional -from urllib.parse import urlparse +from urllib.parse import quote, urlparse import requests from rdflib import Graph, Literal @@ -68,6 +68,7 @@ class RDF4JStore: self.endpoint = endpoint.rstrip("/") self.repository_id = repository_id or config.get("repository_id", "default") + self._encoded_repository_id = quote(self.repository_id, safe="") self.username = config.get("username") self.password = config.get("password") self.timeout = config.get("timeout", 30) @@ -79,7 +80,7 @@ class RDF4JStore: """Connect to RDF4J server.""" try: # Test connection - test_url = f"{self.endpoint}/repositories/{self.repository_id}" + test_url = f"{self.endpoint}/repositories/{self._encoded_repository_id}" response = requests.get( test_url, timeout=self.timeout, @@ -100,11 +101,11 @@ class RDF4JStore: def _get_sparql_endpoint(self) -> str: """Get SPARQL query endpoint.""" - return f"{self.endpoint}/repositories/{self.repository_id}" + return f"{self.endpoint}/repositories/{self._encoded_repository_id}" def _get_update_endpoint(self) -> str: """Get SPARQL Update endpoint.""" - return f"{self.endpoint}/repositories/{self.repository_id}/statements" + return f"{self.endpoint}/repositories/{self._encoded_repository_id}/statements" def _is_construct_query(self, query: str) -> bool: """ @@ -163,7 +164,7 @@ class RDF4JStore: """ # RDF4J transaction support transaction_url = ( - f"{self.endpoint}/repositories/{self.repository_id}/transactions" + f"{self.endpoint}/repositories/{self._encoded_repository_id}/transactions" ) try: diff --git a/tests/triplet_store/test_rdf4j_store.py b/tests/triplet_store/test_rdf4j_store.py index 3f630b93..03a9b90b 100644 --- a/tests/triplet_store/test_rdf4j_store.py +++ b/tests/triplet_store/test_rdf4j_store.py @@ -23,6 +23,7 @@ CONSTRUCT_QUERY = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }" class TestRDF4JStoreInitialization(unittest.TestCase): + def test_explicit_repository_id_selects_repository(self): response = MagicMock(status_code=200) @@ -42,6 +43,49 @@ class TestRDF4JStoreInitialization(unittest.TestCase): auth=None, ) + def test_repository_id_is_encoded_as_a_single_url_path_segment(self): + response = MagicMock(status_code=200) + + with patch( + "semantica.triplet_store.rdf4j_store.requests.get", + return_value=response, + ) as mock_get: + store = RDF4JStore( + endpoint="http://localhost:8080/rdf4j-server", + repository_id="team/repo ?#", + ) + + self.assertEqual(store.repository_id, "team/repo ?#") + mock_get.assert_called_once_with( + "http://localhost:8080/rdf4j-server/repositories/team%2Frepo%20%3F%23", + timeout=30, + auth=None, + ) + self.assertEqual( + store._get_sparql_endpoint(), + "http://localhost:8080/rdf4j-server/repositories/team%2Frepo%20%3F%23", + ) + self.assertEqual( + store._get_update_endpoint(), + "http://localhost:8080/rdf4j-server/repositories/" + "team%2Frepo%20%3F%23/statements", + ) + + transaction_response = MagicMock() + transaction_response.headers = {"Location": "/transactions/tx-1"} + with patch( + "semantica.triplet_store.rdf4j_store.requests.post", + return_value=transaction_response, + ) as mock_post: + self.assertEqual(store.begin_transaction(), "tx-1") + + mock_post.assert_called_once_with( + "http://localhost:8080/rdf4j-server/repositories/" + "team%2Frepo%20%3F%23/transactions", + timeout=30, + auth=None, + ) + class TestRDF4JStoreIsConstructQuery(unittest.TestCase): def test_detects_uppercase(self): From 595f08ee303885e076c6d5e008d8a3d51a35a02a Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Mon, 24 Aug 2026 12:49:31 +0530 Subject: [PATCH 5/5] docs: escape & as & in Star History HTML attributes Matches the README's existing convention for query params inside HTML attribute URLs (e.g. the Trendshift badge), per review feedback from Zohaib Hassan and Qodo on this PR. Co-authored-by: OctoBored <212877535+OctoBored@users.noreply.github.com> --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8e729b2d..dea7a87d 100644 --- a/README.md +++ b/README.md @@ -1566,11 +1566,11 @@ On-premises deployment · Private cloud · Custom domain implementations · SLA- ## Star History - + - - - Star History Chart + + + Star History Chart