pyomnigraph 0.2.0__tar.gz → 0.2.2__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.
Files changed (58) hide show
  1. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/PKG-INFO +11 -10
  2. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/README.md +10 -9
  3. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/docs/index.md +1 -1
  4. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/mkdocs.yml +1 -1
  5. pyomnigraph-0.2.2/omnigraph/__init__.py +1 -0
  6. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/omnigraph_cmd.py +6 -1
  7. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/omniserver.py +1 -0
  8. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/rdf_dataset.py +61 -16
  9. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/rdfdump_cmd.py +14 -0
  10. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/examples/datasets.yaml +37 -1
  11. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/examples/servers.yaml +1 -0
  12. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/server_config.py +1 -0
  13. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/blazegraph.py +65 -1
  14. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/graphdb.py +1 -1
  15. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/jena.py +63 -3
  16. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/millenniumdb.py +2 -2
  17. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/oxigraph.py +1 -1
  18. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/qlever.py +66 -0
  19. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/stardog.py +1 -1
  20. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/virtuoso.py +1 -1
  21. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/sparql_server.py +35 -4
  22. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/version.py +1 -1
  23. pyomnigraph-0.2.2/scripts/gov-quickstart +11 -0
  24. pyomnigraph-0.2.2/scripts/quickstart +213 -0
  25. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/test_omniserver.py +54 -0
  26. pyomnigraph-0.2.2/tests/test_rdf_dataset_params.py +58 -0
  27. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/test_rdfdump.py +51 -1
  28. pyomnigraph-0.2.2/tests/test_upload.py +98 -0
  29. pyomnigraph-0.2.0/omnigraph/__init__.py +0 -1
  30. pyomnigraph-0.2.0/scripts/build-stardog-free +0 -35
  31. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/.github/workflows/build.yml +0 -0
  32. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/.github/workflows/upload-to-pypi.yml +0 -0
  33. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/.gitignore +0 -0
  34. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/.project +0 -0
  35. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/.pydevproject +0 -0
  36. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/AGENTS.md +0 -0
  37. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/LICENSE +0 -0
  38. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/basecmd.py +0 -0
  39. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/ominigraph_paths.py +0 -0
  40. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/rdfdump.py +0 -0
  41. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/__init__.py +0 -0
  42. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/examples/endpoints.yaml +0 -0
  43. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/examples/prefixes.yaml +0 -0
  44. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/examples/queries.yaml +0 -0
  45. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/resources/examples/royals.ttl +0 -0
  46. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/servers/__init__.py +0 -0
  47. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/omnigraph/software.py +0 -0
  48. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/pyproject.toml +0 -0
  49. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/scripts/blackisort +0 -0
  50. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/scripts/doc +0 -0
  51. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/scripts/install +0 -0
  52. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/scripts/release +0 -0
  53. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/scripts/test +0 -0
  54. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/__init__.py +0 -0
  55. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/basetest.py +0 -0
  56. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/test_qlever.py +0 -0
  57. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/test_software_list.py +0 -0
  58. {pyomnigraph-0.2.0 → pyomnigraph-0.2.2}/tests/test_sparql_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyomnigraph
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: pyomnigraph: Unified Python interface for multiple graph databases (SPARQL, Neo4j, Weaviate)
5
5
  Project-URL: Home, https://github.com/WolfgangFahl/omnigraph
6
6
  Project-URL: Documentation, https://wiki.bitplan.com/index.php/omnigraph
@@ -83,16 +83,16 @@ This leads to:
83
83
 
84
84
  ### Supported Triple Stores
85
85
 
86
- | Database | Status | Strengths |
87
- |----------|--------|-----------|
86
+ | Database | Status | Notes |
87
+ |----------|--------|-------|
88
88
  | **Apache Jena** | 🟢 Working | Robust, standards compliant |
89
89
  | **Blazegraph** | 🟢 Working | High performance, easy setup |
90
- | **GraphDB** | 🟢 Working | Enterprise features, reasoning - Free Version tested|
90
+ | **Oxigraph** | 🟢 Working | Rust-based, embedded, fast |
91
91
  | **QLever** | 🟢 Working | Extremely fast queries |
92
- | **Virtuoso** | 🛑 In Progress | Mature, SQL integration |
93
- | **MillenniumDB** | 🛑 In Progress | Property Graph + RDF; SPARQL 1.1 (RDF) and MQL (PG); import-first workflow |
94
- | **Oxigraph** | 🛑 In Progress | Rust-based, embedded |
95
- | **Stardog** | 🛑 Planned | Knowledge graphs, reasoning |
92
+ | **GraphDB** | 🛑 Disabled | License required (free version available) |
93
+ | **MillenniumDB** | 🛑 Disabled | Import-first workflow, Property Graph + RDF |
94
+ | **Virtuoso** | 🛑 Disabled | Permissions issue (#30) |
95
+ | **Stardog** | 🛑 Planned | License required |
96
96
 
97
97
  Whether you're building a semantic web application, conducting research, or evaluating different triple stores, pyomnigraph eliminates the complexity of working with multiple graph database systems.
98
98
 
@@ -103,10 +103,11 @@ omnigraph omnigraph --list --include-inactive --doc-format github
103
103
  | Active | Name | Container Name | Wikidata | Image | Port | Test Port | Dataset | User |
104
104
  |----------|-------------------------------------|----------------------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------|--------|-------------|-----------|--------|
105
105
  | 🟢️ | [blazegraph](http://localhost:9898) | blazegraph-omnigraph | [Q20127748](https://www.wikidata.org/wiki/Q20127748) | [lyrasis/blazegraph:2.1.5](https://hub.docker.com/r/lyrasis/blazegraph) | 9898 | 7898 | kb | |
106
- | 🟢 | [graphdb](http://localhost:7200) | graphdb-omnigraph | [Q58425577](https://www.wikidata.org/wiki/Q58425577) | [ontotext/graphdb:9.11.2-se](https://hub.docker.com/r/ontotext/graphdb) | 7200 | 7700 | repo1 | |
107
106
  | 🟢️ | [jena](http://localhost:3030) | jena-omnigraph | [Q109376461](https://www.wikidata.org/wiki/Q109376461) | [stain/jena-fuseki:latest](https://hub.docker.com/r/stain/jena-fuseki) | 3030 | 7030 | ds | admin |
108
- | 🛑 | [oxigraph](http://localhost:7878) | oxigraph-omnigraph | [Q118980507](https://www.wikidata.org/wiki/Q118980507) | [oxigraph/oxigraph:latest](https://hub.docker.com/r/oxigraph/oxigraph) | 7878 | 7378 | default | |
107
+ | 🟢️ | [oxigraph](http://localhost:7878) | oxigraph-omnigraph | [Q118980507](https://www.wikidata.org/wiki/Q118980507) | [oxigraph/oxigraph:latest](https://hub.docker.com/r/oxigraph/oxigraph) | 7878 | 7378 | default | |
109
108
  | 🟢️ | [qlever](http://localhost:7019) | qlever-omnigraph | [Q111016295](https://www.wikidata.org/wiki/Q111016295) | [adfreiburg/qlever:latest](https://hub.docker.com/r/adfreiburg/qlever) | 7019 | 7819 | olympics | |
109
+ | 🛑 | [graphdb](http://localhost:7200) | graphdb-omnigraph | [Q58425577](https://www.wikidata.org/wiki/Q58425577) | [ontotext/graphdb:11.2.0](https://hub.docker.com/r/ontotext/graphdb) | 7200 | 7700 | repo1 | |
110
+ | 🛑 | [millenniumdb](http://localhost:1234)| millenniumdb-omnigraph| [Q118954995](https://www.wikidata.org/wiki/Q118954995) | [imfd/millenniumdb](https://hub.docker.com/r/imfd/millenniumdb) | 1234 | 1734 | default | |
110
111
  | 🛑 | [stardog](http://localhost:5820) | stardog-omnigraph | [Q91147741](https://www.wikidata.org/wiki/Q91147741) | [stardog/stardog:latest](https://hub.docker.com/r/stardog/stardog) | 5820 | 5320 | mydb | admin |
111
112
  | 🛑 | [virtuoso](http://localhost:8890) | virtuoso-omnigraph | [Q7935239](https://www.wikidata.org/wiki/Q7935239) | [openlink/virtuoso-opensource-7:latest](https://hub.docker.com/r/openlink/virtuoso-opensource-7) | 8890 | 8390 | KB | dba |
112
113
 
@@ -37,16 +37,16 @@ This leads to:
37
37
 
38
38
  ### Supported Triple Stores
39
39
 
40
- | Database | Status | Strengths |
41
- |----------|--------|-----------|
40
+ | Database | Status | Notes |
41
+ |----------|--------|-------|
42
42
  | **Apache Jena** | 🟢 Working | Robust, standards compliant |
43
43
  | **Blazegraph** | 🟢 Working | High performance, easy setup |
44
- | **GraphDB** | 🟢 Working | Enterprise features, reasoning - Free Version tested|
44
+ | **Oxigraph** | 🟢 Working | Rust-based, embedded, fast |
45
45
  | **QLever** | 🟢 Working | Extremely fast queries |
46
- | **Virtuoso** | 🛑 In Progress | Mature, SQL integration |
47
- | **MillenniumDB** | 🛑 In Progress | Property Graph + RDF; SPARQL 1.1 (RDF) and MQL (PG); import-first workflow |
48
- | **Oxigraph** | 🛑 In Progress | Rust-based, embedded |
49
- | **Stardog** | 🛑 Planned | Knowledge graphs, reasoning |
46
+ | **GraphDB** | 🛑 Disabled | License required (free version available) |
47
+ | **MillenniumDB** | 🛑 Disabled | Import-first workflow, Property Graph + RDF |
48
+ | **Virtuoso** | 🛑 Disabled | Permissions issue (#30) |
49
+ | **Stardog** | 🛑 Planned | License required |
50
50
 
51
51
  Whether you're building a semantic web application, conducting research, or evaluating different triple stores, pyomnigraph eliminates the complexity of working with multiple graph database systems.
52
52
 
@@ -57,10 +57,11 @@ omnigraph omnigraph --list --include-inactive --doc-format github
57
57
  | Active | Name | Container Name | Wikidata | Image | Port | Test Port | Dataset | User |
58
58
  |----------|-------------------------------------|----------------------|--------------------------------------------------------|--------------------------------------------------------------------------------------------------|--------|-------------|-----------|--------|
59
59
  | 🟢️ | [blazegraph](http://localhost:9898) | blazegraph-omnigraph | [Q20127748](https://www.wikidata.org/wiki/Q20127748) | [lyrasis/blazegraph:2.1.5](https://hub.docker.com/r/lyrasis/blazegraph) | 9898 | 7898 | kb | |
60
- | 🟢 | [graphdb](http://localhost:7200) | graphdb-omnigraph | [Q58425577](https://www.wikidata.org/wiki/Q58425577) | [ontotext/graphdb:9.11.2-se](https://hub.docker.com/r/ontotext/graphdb) | 7200 | 7700 | repo1 | |
61
60
  | 🟢️ | [jena](http://localhost:3030) | jena-omnigraph | [Q109376461](https://www.wikidata.org/wiki/Q109376461) | [stain/jena-fuseki:latest](https://hub.docker.com/r/stain/jena-fuseki) | 3030 | 7030 | ds | admin |
62
- | 🛑 | [oxigraph](http://localhost:7878) | oxigraph-omnigraph | [Q118980507](https://www.wikidata.org/wiki/Q118980507) | [oxigraph/oxigraph:latest](https://hub.docker.com/r/oxigraph/oxigraph) | 7878 | 7378 | default | |
61
+ | 🟢️ | [oxigraph](http://localhost:7878) | oxigraph-omnigraph | [Q118980507](https://www.wikidata.org/wiki/Q118980507) | [oxigraph/oxigraph:latest](https://hub.docker.com/r/oxigraph/oxigraph) | 7878 | 7378 | default | |
63
62
  | 🟢️ | [qlever](http://localhost:7019) | qlever-omnigraph | [Q111016295](https://www.wikidata.org/wiki/Q111016295) | [adfreiburg/qlever:latest](https://hub.docker.com/r/adfreiburg/qlever) | 7019 | 7819 | olympics | |
63
+ | 🛑 | [graphdb](http://localhost:7200) | graphdb-omnigraph | [Q58425577](https://www.wikidata.org/wiki/Q58425577) | [ontotext/graphdb:11.2.0](https://hub.docker.com/r/ontotext/graphdb) | 7200 | 7700 | repo1 | |
64
+ | 🛑 | [millenniumdb](http://localhost:1234)| millenniumdb-omnigraph| [Q118954995](https://www.wikidata.org/wiki/Q118954995) | [imfd/millenniumdb](https://hub.docker.com/r/imfd/millenniumdb) | 1234 | 1734 | default | |
64
65
  | 🛑 | [stardog](http://localhost:5820) | stardog-omnigraph | [Q91147741](https://www.wikidata.org/wiki/Q91147741) | [stardog/stardog:latest](https://hub.docker.com/r/stardog/stardog) | 5820 | 5320 | mydb | admin |
65
66
  | 🛑 | [virtuoso](http://localhost:8890) | virtuoso-omnigraph | [Q7935239](https://www.wikidata.org/wiki/Q7935239) | [openlink/virtuoso-opensource-7:latest](https://hub.docker.com/r/openlink/virtuoso-opensource-7) | 8890 | 8390 | KB | dba |
66
67
 
@@ -1,4 +1,4 @@
1
- # omnigraph API Documentation
1
+ # pyomnigraph API Documentation
2
2
 
3
3
  ::: omnigraph
4
4
  options:
@@ -1,4 +1,4 @@
1
- site_name: omnigraph API Documentation
1
+ site_name: pyomnigraph API Documentation
2
2
  theme:
3
3
  name: material
4
4
  plugins:
@@ -0,0 +1 @@
1
+ __version__ = "0.2.2"
@@ -123,6 +123,10 @@ class OmnigraphCmd(BaseCmd):
123
123
  """
124
124
  if dataset is None:
125
125
  server.config.dumps_dir = self.ogp.examples_dir
126
+ elif dataset.rdf_file:
127
+ # If rdf_file is specified, set dumps_dir to the parent directory of the file
128
+ rdf_file_path = Path(dataset.rdf_file).expanduser().resolve()
129
+ server.config.dumps_dir = rdf_file_path.parent
126
130
  else:
127
131
  server.config.dumps_dir = self.ogp.dumps_dir / dataset.id
128
132
 
@@ -167,7 +171,8 @@ class OmnigraphCmd(BaseCmd):
167
171
  handled = False
168
172
  if cmds:
169
173
  for cmd in cmds:
170
- if cmd == "load":
174
+ # issue #38: upload needs the per-dataset dumps_dir just like load
175
+ if cmd in ("load", "upload"):
171
176
  cmd_iterator = self.load_iterator(server)
172
177
  else:
173
178
  cmd_iterator = iter([None]) # Single iteration
@@ -70,6 +70,7 @@ class OmniServer:
70
70
  "start": lambda s: ServerCmd(title("start", s), s.start),
71
71
  "status": lambda s: ServerCmd(title("status", s), s.status_info),
72
72
  "stop": lambda s: ServerCmd(title("stop", s), s.stop),
73
+ "upload": lambda s: ServerCmd(title("native bulk upload dumps", s), s.upload_dump_files),
73
74
  "webui": lambda s: ServerCmd(title("webui", s), s.webui),
74
75
  }
75
76
  return server_cmds
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
8
8
  from typing import Dict, Optional
9
9
 
10
10
  from basemkit.yamlable import lod_storable
11
+ from lodstorage.params import Params
11
12
  from lodstorage.query import Query
12
13
  from lodstorage.sparql import SPARQL
13
14
 
@@ -19,38 +20,80 @@ class RdfDataset:
19
20
  """
20
21
 
21
22
  name: str # Human-readable dataset name
22
- base_url: str # Base URL e.g. for tryit
23
- endpoint_url: str # SPARQL endpoint URL
23
+ base_url: Optional[str] = None # Base URL e.g. for tryit
24
+ endpoint_url: Optional[str] = None # SPARQL endpoint URL
24
25
  description: Optional[str] = None # Optional dataset description
25
26
  database: Optional[str] = "jena" # the database type of the endpoint
26
27
  expected_solutions: Optional[int] = None # Expected number of solutions
27
28
  select_pattern: str = "?s ?p ?o" # Basic Graph Pattern for queries
28
29
  construct_template: Optional[str] = field(default="?s ?p ?o")
30
+ params: Optional[Dict[str, str]] = None # npq parameters for {{ name }} templates (issue #36)
29
31
  prefix_sets: Optional[list] = field(default_factory=list)
30
32
  active: Optional[bool] = False
33
+ rdf_file: Optional[str] = None # Path to local RDF file (alternative to endpoint_url)
31
34
  # fields to be configured by post_init
32
35
  id: Optional[str] = field(default=None)
33
36
  count_query: Optional[Query] = field(default=None)
34
37
  select_query: Optional[Query] = field(default=None)
35
38
  sparql: Optional[SPARQL] = field(default=None)
36
39
 
40
+ def apply_params(self, params_dict: Optional[Dict[str, str]] = None) -> None:
41
+ """
42
+ Apply npq parameters to select_pattern and construct_template.
43
+
44
+ {{ name }} templates are replaced with the values of the merged
45
+ params: the dataset's own params overridden by the given ones.
46
+ Substitution always starts from the raw templates, so CLI overrides
47
+ can be re-applied after YAML defaults. The blanket Params audit is
48
+ off - it rejects quoted VALUES lists (see issue #36); values stem
49
+ from local configuration and CLI.
50
+
51
+ Args:
52
+ params_dict: additional parameter values overriding self.params
53
+ """
54
+ # preserve the raw templates on first call
55
+ if not hasattr(self, "raw_templates"):
56
+ self.raw_templates = {
57
+ "select_pattern": self.select_pattern,
58
+ "construct_template": self.construct_template,
59
+ }
60
+ merged = dict(self.params) if self.params else {}
61
+ if params_dict:
62
+ merged.update(params_dict)
63
+ if merged:
64
+ for attr, raw_template in self.raw_templates.items():
65
+ if raw_template:
66
+ params = Params(raw_template, with_audit=False)
67
+ if params.has_params:
68
+ params.set(merged)
69
+ setattr(self, attr, params.apply_parameters())
70
+
71
+ def build_queries(self) -> None:
72
+ """
73
+ (Re)build count_query and select_query from the current select_pattern.
74
+ Only initializes SPARQL-related fields if endpoint_url is provided.
75
+ """
76
+ if self.endpoint_url:
77
+ self.count_query = Query(
78
+ name=f"{self.name}_count",
79
+ query=f"SELECT (COUNT(*) AS ?count) WHERE {{ {self.select_pattern} }}",
80
+ endpoint=self.endpoint_url,
81
+ description=f"Count query for {self.name}",
82
+ )
83
+ self.select_query = Query(
84
+ name=f"{self.name}_select",
85
+ query=f"SELECT * WHERE {{ {self.select_pattern} }}",
86
+ endpoint=self.endpoint_url,
87
+ description=f"Select query for {self.name}",
88
+ )
89
+ self.sparql = SPARQL(self.endpoint_url)
90
+
37
91
  def __post_init__(self):
38
92
  """
39
- Generate count_query and construct_pattern from select_pattern.
93
+ Apply npq params and generate the queries from select_pattern.
40
94
  """
41
- self.count_query = Query(
42
- name=f"{self.name}_count",
43
- query=f"SELECT (COUNT(*) AS ?count) WHERE {{ {self.select_pattern} }}",
44
- endpoint=self.endpoint_url,
45
- description=f"Count query for {self.name}",
46
- )
47
- self.select_query = Query(
48
- name=f"{self.name}_select",
49
- query=f"SELECT * WHERE {{ {self.select_pattern} }}",
50
- endpoint=self.endpoint_url,
51
- description=f"Select query for {self.name}",
52
- )
53
- self.sparql = SPARQL(self.endpoint_url)
95
+ self.apply_params()
96
+ self.build_queries()
54
97
 
55
98
  @property
56
99
  def full_name(self):
@@ -66,6 +109,8 @@ class RdfDataset:
66
109
  Number of solutions available from the count query
67
110
  """
68
111
  count = self.sparql.getValue(self.count_query.query, "count")
112
+ # issue #37: getValue returns the raw literal which may be a str
113
+ count = int(count)
69
114
  return count
70
115
 
71
116
  def getTryItUrl(self, database: str = "blazegraph") -> str:
@@ -10,6 +10,8 @@ import os
10
10
  import webbrowser
11
11
  from argparse import ArgumentParser, Namespace
12
12
 
13
+ from basemkit.argparse_action import StoreDictKeyPair
14
+
13
15
  from omnigraph.basecmd import BaseCmd
14
16
  from omnigraph.rdf_dataset import RdfDataset, RdfDatasets
15
17
  from omnigraph.rdfdump import RdfDumpDownloader
@@ -61,6 +63,13 @@ class RdfDumpCmd(BaseCmd):
61
63
  default=None,
62
64
  help="Maximum number of solutions/triples to download (uses dataset expected_solutions if not specified)",
63
65
  )
66
+ parser.add_argument(
67
+ "--param",
68
+ action=StoreDictKeyPair,
69
+ dest="params",
70
+ metavar="KEY=VALUE,KEY=VALUE...",
71
+ help="npq parameter values overriding the dataset's params (issue #36)",
72
+ )
64
73
  parser.add_argument("--no-progress", action="store_true", help="Disable progress bar")
65
74
  parser.add_argument("--output-path", default=".", help="Path for dump files")
66
75
  parser.add_argument("--tryit", action="store_true", help="open the try it! URL [default: %(default)s]")
@@ -97,6 +106,11 @@ class RdfDumpCmd(BaseCmd):
97
106
  """
98
107
  super().handle_args(args)
99
108
  datasets = self.datasets
109
+ if getattr(args, "params", None):
110
+ # apply CLI npq parameter overrides and rebuild the queries
111
+ for dataset in datasets.values():
112
+ dataset.apply_params(args.params)
113
+ dataset.build_queries()
100
114
  if self.args.about:
101
115
  self.about()
102
116
 
@@ -65,4 +65,40 @@ datasets:
65
65
  ?pName ?p4Name ?o4Name
66
66
  # Optional dataset description
67
67
  description: "W2306 postal code coordinates"
68
- active: false
68
+ active: false
69
+ papers_of_my_network_wikidata:
70
+ # npq (named parameterized query) example - issue #36
71
+ # ORCID is the join key; {{ orcids }} is the VALUES scaling knob;
72
+ # endpoint is late-bound - own copy answers completely and fastest,
73
+ # see the endpoint matrix in the issue
74
+ name: "Papers of my network (wikidata)"
75
+ base_url: "https://qlever-api.wikidata.dbis.rwth-aachen.de"
76
+ endpoint_url: "https://qlever-api.wikidata.dbis.rwth-aachen.de/"
77
+ database: "qlever"
78
+ select_pattern: |
79
+ VALUES ?orcid { {{ orcids }} }
80
+ ?author <http://www.wikidata.org/prop/direct/P496> ?orcid .
81
+ ?work <http://www.wikidata.org/prop/direct/P50> ?author .
82
+ ?work ?p ?o
83
+ construct_template: "?work ?p ?o"
84
+ params:
85
+ orcids: '"0000-0002-0821-6995" "0000-0003-2533-6363" "0000-0001-9879-3827" "0000-0001-6324-7164"'
86
+ description: "Papers authored by the co-author network, seeded by ORCIDs"
87
+ active: false
88
+ papers_of_my_network_dblp:
89
+ # npq companion - same {{ orcids }} param drives the dblp graph
90
+ name: "Papers of my network (dblp)"
91
+ base_url: "https://sparql.dblp.org"
92
+ endpoint_url: "https://sparql.dblp.org/sparql"
93
+ database: "qlever"
94
+ select_pattern: |
95
+ VALUES ?orcid { {{ orcids }} }
96
+ BIND(IRI(CONCAT("https://orcid.org/", ?orcid)) AS ?orcid_uri)
97
+ ?author <https://dblp.org/rdf/schema#orcid> ?orcid_uri .
98
+ ?paper <https://dblp.org/rdf/schema#authoredBy> ?author .
99
+ ?paper ?p ?o
100
+ construct_template: "?paper ?p ?o"
101
+ params:
102
+ orcids: '"0000-0002-0821-6995" "0000-0003-2533-6363" "0000-0001-9879-3827" "0000-0001-6324-7164"'
103
+ description: "Papers authored by the co-author network in dblp, same ORCID seed"
104
+ active: false
@@ -14,6 +14,7 @@ servers:
14
14
  port: 9898
15
15
  test_port: 7898
16
16
  host: "localhost"
17
+ # docker_bind: "0.0.0.0" # Optional: Uncomment to allow external access (default: "127.0.0.1" for localhost-only, recommended for security)
17
18
  prefix_sets: ["rdf", "blazegraph","gov","wikidata"]
18
19
  active: true
19
20
  graphdb:
@@ -161,6 +161,7 @@ class ServerConfig:
161
161
  has_license: bool = False
162
162
  protocol: str = "http"
163
163
  host: str = "localhost"
164
+ docker_bind: str = "127.0.0.1" # Docker port bind address (default: localhost-only for security)
164
165
  rdf_format: str = "turtle"
165
166
  auth_user: Optional[str] = None
166
167
  auth_password: Optional[str] = None
@@ -8,6 +8,8 @@ from dataclasses import dataclass
8
8
  from pathlib import Path
9
9
  import re
10
10
  import os
11
+ import shutil
12
+
11
13
  from omnigraph.server_config import ServerLifecycleState, ServerStatus
12
14
  from omnigraph.sparql_server import ServerConfig, ServerEnv, SparqlServer
13
15
 
@@ -42,7 +44,7 @@ class BlazegraphConfig(ServerConfig):
42
44
  f"docker run -d --name {self.container_name} "
43
45
  f"-e BLAZEGRAPH_UID={os.getuid()} "
44
46
  f"-e BLAZEGRAPH_GID={os.getgid()} "
45
- f"-p {self.port}:8080 "
47
+ f"-p {self.docker_bind}:{self.port}:8080 "
46
48
  f"-v {data_dir}/RWStore.properties:/RWStore.properties "
47
49
  f"-v {data_dir}:/data "
48
50
  f"{self.image}"
@@ -94,6 +96,68 @@ class Blazegraph(SparqlServer):
94
96
  rwstore_path.write_text(props)
95
97
 
96
98
 
99
+ def get_dataloader_xml(self, container_path: str) -> str:
100
+ """
101
+ Build the DataLoader servlet properties XML for the given
102
+ container-side file or directory path.
103
+
104
+ Args:
105
+ container_path: path as seen inside the container (mounted /data)
106
+
107
+ Returns:
108
+ the properties XML document
109
+ """
110
+ xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
111
+ <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
112
+ <properties>
113
+ <entry key="namespace">{self.config.dataset}</entry>
114
+ <entry key="propertyFile">/RWStore.properties</entry>
115
+ <entry key="fileOrDirs">{container_path}</entry>
116
+ <entry key="-durableQueues">true</entry>
117
+ </properties>"""
118
+ return xml
119
+
120
+ def upload_dump_files(self, file_pattern: str = None) -> int:
121
+ """
122
+ Bulk-load dump files via Blazegraph's REST DataLoader servlet, which
123
+ reads the files from the server's own filesystem (the mounted /data
124
+ directory) instead of pushing them through HTTP request bodies.
125
+
126
+ Args:
127
+ file_pattern: Glob pattern for dump files
128
+
129
+ Returns:
130
+ Number of files loaded successfully
131
+ """
132
+ container_name = self.config.container_name
133
+ files = self.get_dump_files(file_pattern)
134
+ loaded_count = 0
135
+ if not files:
136
+ self.log.log("⚠️", container_name, f"No dump files found for pattern: {file_pattern}")
137
+ else:
138
+ # stage the dumps under data_dir so the container sees them at /data/dumps
139
+ stage_dir = Path(self.config.base_data_dir) / "dumps"
140
+ stage_dir.mkdir(parents=True, exist_ok=True)
141
+ for file in files:
142
+ target = stage_dir / file.name
143
+ if not target.exists():
144
+ shutil.copy2(file, target)
145
+ xml = self.get_dataloader_xml("/data/dumps")
146
+ response = self.make_request(
147
+ "POST",
148
+ self.config.dataloader_url,
149
+ headers={"Content-Type": "application/xml"},
150
+ data=xml,
151
+ timeout=self.config.upload_timeout,
152
+ )
153
+ if response.success:
154
+ loaded_count = len(files)
155
+ self.log.log("✅", container_name, f"DataLoader loaded {loaded_count} file(s)")
156
+ else:
157
+ error_msg = str(response.error) if response.error else f"HTTP {response.response.status_code}"
158
+ self.log.log("❌", container_name, f"DataLoader failed: {error_msg}")
159
+ return loaded_count
160
+
97
161
  def status(self) -> ServerStatus:
98
162
  """
99
163
  Get server status information.
@@ -60,7 +60,7 @@ class GraphDBConfig(ServerConfig):
60
60
  # 4. Construct Command
61
61
  docker_run_command = (
62
62
  f"docker run {env_str} -d --name {self.container_name} "
63
- f"-p 127.0.0.1:{self.port}:7200 "
63
+ f"-p {self.docker_bind}:{self.port}:7200 "
64
64
  f"-v {data_dir}:/opt/graphdb/home "
65
65
  f"{target_image}"
66
66
  )
@@ -7,6 +7,8 @@ Apache Jena SPARQL support
7
7
  """
8
8
 
9
9
  from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import List
10
12
 
11
13
  from omnigraph.server_config import ServerLifecycleState, ServerStatus
12
14
  from omnigraph.sparql_server import ServerConfig, ServerEnv, SparqlServer
@@ -48,7 +50,7 @@ class JenaConfig(ServerConfig):
48
50
  env = f"{env} -e ADMIN_PASSWORD={self.auth_password}"
49
51
  docker_run_command = (
50
52
  f"docker run {self.docker_user_flag} {env} -d --name {self.container_name} "
51
- f"-p {self.port}:3030 "
53
+ f"-p {self.docker_bind}:{self.port}:3030 "
52
54
  f"-v {data_dir}:/fuseki "
53
55
  f"{self.image}"
54
56
  )
@@ -96,7 +98,65 @@ class Jena(SparqlServer):
96
98
  Returns:
97
99
  Tuple of (response, exception)
98
100
  """
99
- result,error=self.execute_update_query_with_post(update_query)
100
- return result,error
101
+ result, error = self.execute_update_query_with_post(update_query)
102
+ return result, error
103
+
104
+ def get_tdbloader_command(self, files: List[Path]) -> str:
105
+ """
106
+ Build the tdb2.tdbloader docker command for the given dump files.
107
+
108
+ The loader classes ship inside fuseki-server.jar, so the server's own
109
+ image is reused with an overridden entrypoint. The TDB2 dataset is
110
+ single-writer - the server must be stopped while the loader runs.
111
+
112
+ Args:
113
+ files: dump files to load (from the dumps directory)
114
+
115
+ Returns:
116
+ the docker run command string
117
+ """
118
+ loc = f"/fuseki/databases/{self.config.dataset}"
119
+ dumps_dir = Path(self.config.dumps_dir)
120
+ file_args = " ".join(f"/dumps/{file.name}" for file in files)
121
+ command = (
122
+ f"docker run --rm {self.config.docker_user_flag} --entrypoint java "
123
+ f"-v {self.config.base_data_dir}:/fuseki "
124
+ f"-v {dumps_dir}:/dumps "
125
+ f"{self.config.image} "
126
+ f"-cp /jena-fuseki/fuseki-server.jar tdb2.tdbloader "
127
+ f"--loc {loc} {file_args}"
128
+ )
129
+ return command
130
+
131
+ def upload_dump_files(self, file_pattern: str = None) -> int:
132
+ """
133
+ Bulk-load dump files with tdb2.tdbloader directly into the TDB2
134
+ database files - the native path for the file-backed store, avoiding
135
+ the HTTP single-POST transaction that faults the mmap'd node table
136
+ (issue #25).
137
+
138
+ Args:
139
+ file_pattern: Glob pattern for dump files
140
+
141
+ Returns:
142
+ Number of files loaded successfully
143
+ """
144
+ container_name = self.config.container_name
145
+ files = self.get_dump_files(file_pattern)
146
+ loaded_count = 0
147
+ if not files:
148
+ self.log.log("⚠️", container_name, f"No dump files found for pattern: {file_pattern}")
149
+ else:
150
+ self.stop()
151
+ loader_cmd = self.get_tdbloader_command(files)
152
+ shell_result = self.run_shell_command(
153
+ loader_cmd,
154
+ success_msg=f"tdb2.tdbloader loaded {len(files)} file(s)",
155
+ error_msg="tdb2.tdbloader failed",
156
+ )
157
+ if shell_result.success:
158
+ loaded_count = len(files)
159
+ self.start()
160
+ return loaded_count
101
161
 
102
162
 
@@ -62,8 +62,8 @@ class MillenniumDBConfig(ServerConfig):
62
62
 
63
63
  docker_run_command = (
64
64
  f"docker run -d --name {self.container_name} "
65
- f"-p {self.sparql_port}:1234 "
66
- f"-p {self.web_port}:4321 "
65
+ f"-p {self.docker_bind}:{self.sparql_port}:1234 "
66
+ f"-p {self.docker_bind}:{self.web_port}:4321 "
67
67
  f"-v {data_dir}:/data "
68
68
  f"{self.image} "
69
69
  f"mdb-server {db_path}"
@@ -47,7 +47,7 @@ class OxigraphConfig(ServerConfig):
47
47
  """
48
48
  docker_run_command = (
49
49
  f"docker run {self.docker_user_flag} -d --name {self.container_name} "
50
- f"-p {self.port}:7878 "
50
+ f"-p {self.docker_bind}:{self.port}:7878 "
51
51
  f"-v {data_dir}:/data "
52
52
  f"{self.image} serve --bind 0.0.0.0:7878 --location /data"
53
53
  )
@@ -5,6 +5,7 @@ Created on 2025-05-28
5
5
  """
6
6
 
7
7
  import os
8
+ import shutil
8
9
  from configparser import ConfigParser, ExtendedInterpolation
9
10
  from dataclasses import dataclass
10
11
  from pathlib import Path
@@ -238,6 +239,71 @@ class QLever(SparqlServer):
238
239
 
239
240
  return None
240
241
 
242
+ def get_index_commands(self, files: List[Path]) -> List[str]:
243
+ """
244
+ Build the qlever CLI commands to (re)index the given dump files.
245
+
246
+ QLever loads bulk data by building its index from files - there is no
247
+ native HTTP bulk-write path; the SPARQL INSERT route is only suitable
248
+ for small increments.
249
+
250
+ Args:
251
+ files: dump files staged in the data directory
252
+
253
+ Returns:
254
+ list of shell commands to run in the data directory
255
+ """
256
+ commands = [
257
+ "qlever stop",
258
+ "qlever index --overwrite-existing",
259
+ f"qlever start --server-container {self.config.container_name}",
260
+ ]
261
+ return commands
262
+
263
+ def upload_dump_files(self, file_pattern: str = None) -> int:
264
+ """
265
+ Bulk-load dump files by rebuilding the QLever index from them -
266
+ the native path; the turtle-to-INSERT conversion of upload_request
267
+ is unsuitable for bulk data.
268
+
269
+ Args:
270
+ file_pattern: Glob pattern for dump files
271
+
272
+ Returns:
273
+ Number of files loaded successfully
274
+ """
275
+ container_name = self.config.container_name
276
+ files = self.get_dump_files(file_pattern)
277
+ loaded_count = 0
278
+ if not files:
279
+ self.log.log("⚠️", container_name, f"No dump files found for pattern: {file_pattern}")
280
+ return loaded_count
281
+ data_dir = Path(self.config.data_dir)
282
+ qleverfile_path = data_dir / "Qleverfile"
283
+ qlever_file = QLeverfile.ofFile(qleverfile_path)
284
+ if qlever_file is None:
285
+ self.log.log("❌", container_name, f"no Qleverfile in {data_dir} - run start (setup-config) first")
286
+ return loaded_count
287
+ # stage the dumps in the data directory and register them as input files
288
+ for file in files:
289
+ target = data_dir / file.name
290
+ if not target.exists():
291
+ shutil.copy2(file, target)
292
+ input_files = " ".join(file.name for file in files)
293
+ qlever_file.set("index", "INPUT_FILES", input_files)
294
+ qlever_file.save()
295
+ ok = True
296
+ for command in self.get_index_commands(files):
297
+ shell_result = self.run_shell_command(f"cd {data_dir};{command}")
298
+ if not shell_result.success:
299
+ self.log.log("❌", container_name, f"failed: {command}")
300
+ ok = False
301
+ break
302
+ if ok:
303
+ loaded_count = len(files)
304
+ self.log.log("✅", container_name, f"index rebuilt from {loaded_count} file(s)")
305
+ return loaded_count
306
+
241
307
  def upload_request(self, file_content: bytes) -> Response:
242
308
  """Upload request for QLever using SPARQL INSERT statements."""
243
309
  turtle_data = file_content.decode("utf-8")
@@ -62,7 +62,7 @@ class StardogConfig(ServerConfig):
62
62
  # 4. Construct Command using target_image
63
63
  docker_run_command = (
64
64
  f"docker run {self.docker_user_flag}{env_str} -d --name {self.container_name} "
65
- f"-p {self.port}:5820 "
65
+ f"-p {self.docker_bind}:{self.port}:5820 "
66
66
  f"-v {data_dir}:/var/opt/stardog "
67
67
  f"{target_image}"
68
68
  )