snapquery 0.2.3__tar.gz → 0.2.5__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.
- {snapquery-0.2.3 → snapquery-0.2.5}/PKG-INFO +5 -5
- {snapquery-0.2.3 → snapquery-0.2.5}/pyproject.toml +4 -4
- snapquery-0.2.5/snapquery/__init__.py +1 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/github_access.py +25 -10
- snapquery-0.2.5/snapquery/qlever.py +187 -0
- snapquery-0.2.5/snapquery/query_parameterizer.py +246 -0
- snapquery-0.2.5/snapquery/query_set_cmd.py +435 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/query_set_tool.py +11 -15
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/endpoints.yaml +19 -2
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/hartig-eqs-2025-12-16.yaml +3033 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/qlever-issues-2025-12-05.yaml +6219 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/scholia-2025-12-04.yaml +9324 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/scholia-2025-12-16.yaml +9324 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/scholia-qlever-2025-12-16.yaml +14907 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/sib-2025-12-04.yaml +24822 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/wikidata-examples-2025-12-04.yaml +8715 -0
- snapquery-0.2.5/snapquery/samples/query-sets/yaml/wikidata-short-urls-2025-12-06.yaml +1541 -0
- snapquery-0.2.5/snapquery/scholia.py +161 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/sib_sparql_examples.py +13 -7
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/snapquery_cmd.py +11 -3
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/snapquery_webserver.py +8 -2
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/sparql_analyzer.py +1 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/version.py +1 -1
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/wd_page_query_extractor.py +41 -15
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/wd_short_url.py +13 -10
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_cmdline.py +4 -2
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_endpoints.py +3 -3
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_qlever.py +18 -36
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_query_set_tool.py +4 -3
- snapquery-0.2.5/tests/test_scholia.py +123 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_sib_examples.py +10 -6
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_sparql_analyzer.py +1 -1
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_wd_query_parsing.py +3 -3
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_wd_short_url.py +4 -1
- snapquery-0.2.3/snapquery/__init__.py +0 -1
- snapquery-0.2.3/snapquery/qlever.py +0 -111
- snapquery-0.2.3/snapquery/query_set_cmd.py +0 -168
- snapquery-0.2.3/snapquery/scholia.py +0 -107
- snapquery-0.2.3/tests/test_scholia.py +0 -57
- {snapquery-0.2.3 → snapquery-0.2.5}/.github/workflows/build.yml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/.github/workflows/upload-to-pypi.yml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/.gitignore +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/.project +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/.pydevproject +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/CITATION.cff +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/LICENSE +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/README.md +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/docs/index.md +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/mkdocs.yml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/blackisort +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/doc +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/fixdomain +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/install +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/qlv +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/release +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/restore_db +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/test +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/scripts/test_queries +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/authorization.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/basequeryview.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/ceurws.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/dblp.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/error_filter.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/execution.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/graph.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/models/__init__.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/models/person.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/models/sparql_components.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/mwlogin.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/namespace_stats_view.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/orcid.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/params_view.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/person_selector.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/pid.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/pid_lookup.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/prefix_merger.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/query_annotate.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/query_selector.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/query_set_tool_view.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/resources/sparql_language.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/WikiProject_LSEThesisProject.wiki +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/ceur-ws.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/dblp.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/endpoints_optional.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/federated-query-examples.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/graphs.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/meta_query.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/orkg.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/qlever.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/qlever_performance.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/ceur-ws.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/dblp.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/federated-query-examples.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/orkg.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/qlever.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/qlever_performance.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/scholia.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/snapquery.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/wikidata-WikidataThesisToolkit.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/wikidata-WikidataThesisToolkitAlt.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/wikidata-examples.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query-sets/yaml/wikidata-short-urls.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/query_stats.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/scholia.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/snapquery.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/wikidata-WikidataThesisToolkit.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/wikidata-WikidataThesisToolkitAlt.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/wikidata-examples.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/samples/wikidata-short-urls.json +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/snapquery_core.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/snapquery_view.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery/stats_view.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery.puml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/snapquery.yaml +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/__init__.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/action_stats.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/query_extractors/__init__.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/query_extractors/sparql_standard_queries.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/qlever/ask_unsupported.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/qlever/invalid_literal.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/qlever/invalid_regex_func.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/qlever/memory_limit.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/qlever/timeout.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/triplydb/parser_error.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/wdqs/duplicate_prefix.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/wdqs/invalid_iri_declaration.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/wdqs/invalid_sparql_syntax.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/wdqs/timeout.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/resources/error_messages/wdqs/undefined_prefix.txt +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_authorization.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_ceurws.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_error_filter.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_graph.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_namedqueries.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_oauth.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_orcid.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_pids.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_prefix_merger.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_query_annotate.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_query_execution.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_query_name.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_restful_api.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_scholia_q.py +0 -0
- {snapquery-0.2.3 → snapquery-0.2.5}/tests/test_sparql_components.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snapquery
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: Just Query wikidata and other SPARQL endpoints by query name - a frontend to Introduce Named Queries and Named Query Middleware to wikidata and other SPARQL endpoints
|
|
5
5
|
Project-URL: Home, https://github.com/WolfgangFahl/snapquery
|
|
6
6
|
Project-URL: Documentation, https://wiki.bitplan.com/index.php/snapquery
|
|
@@ -26,14 +26,14 @@ Classifier: Topic :: Software Development :: User Interfaces
|
|
|
26
26
|
Requires-Python: >=3.10
|
|
27
27
|
Requires-Dist: mwoauth>=0.4.0
|
|
28
28
|
Requires-Dist: nameparser>=1.1.3
|
|
29
|
-
Requires-Dist: ngwidgets>=0.30.
|
|
29
|
+
Requires-Dist: ngwidgets>=0.30.3
|
|
30
30
|
Requires-Dist: nicegui[plotly]==2.24.2
|
|
31
31
|
Requires-Dist: pandas>=2.3.3
|
|
32
32
|
Requires-Dist: py-3rdparty-mediawiki>=0.18.1
|
|
33
33
|
Requires-Dist: py-ez-wikidata>=0.3.2
|
|
34
|
-
Requires-Dist: pybasemkit>=0.1.
|
|
35
|
-
Requires-Dist: pylodstorage>=0.18.
|
|
36
|
-
Requires-Dist: pyopensourceprojects>=0.
|
|
34
|
+
Requires-Dist: pybasemkit>=0.1.6
|
|
35
|
+
Requires-Dist: pylodstorage>=0.18.6
|
|
36
|
+
Requires-Dist: pyopensourceprojects>=0.4.1
|
|
37
37
|
Requires-Dist: python-slugify>=8.0.4
|
|
38
38
|
Requires-Dist: ratelimit>=2.2.1
|
|
39
39
|
Requires-Dist: requests
|
|
@@ -22,11 +22,11 @@ dependencies = [
|
|
|
22
22
|
# https://pypi.org/project/pybasemkit/
|
|
23
23
|
# https://github.com/WolfgangFahl/pybasemkit
|
|
24
24
|
# Python base module kit: YAML/JSON I/O, structured logging, CLI tooling, shell execution, and remote pydevd debug support.
|
|
25
|
-
"pybasemkit>=0.1.
|
|
25
|
+
"pybasemkit>=0.1.6",
|
|
26
26
|
# https://github.com/WolfgangFahl/nicegui_widgets
|
|
27
|
-
"ngwidgets>=0.30.
|
|
27
|
+
"ngwidgets>=0.30.3",
|
|
28
28
|
# https://pypi.org/project/pyLodStorage/
|
|
29
|
-
"pyLodStorage>=0.18.
|
|
29
|
+
"pyLodStorage>=0.18.6",
|
|
30
30
|
# https://pypi.org/project/py-3rdparty-mediawiki/
|
|
31
31
|
"py-3rdparty-mediawiki>=0.18.1",
|
|
32
32
|
"wikitextparser",
|
|
@@ -51,7 +51,7 @@ dependencies = [
|
|
|
51
51
|
# https://pypi.org/project/nameparser/
|
|
52
52
|
"nameparser>=1.1.3",
|
|
53
53
|
# https://github.com/WolfgangFahl/pyOpenSourceProjects
|
|
54
|
-
"pyOpenSourceProjects>=0.
|
|
54
|
+
"pyOpenSourceProjects>=0.4.1"
|
|
55
55
|
]
|
|
56
56
|
|
|
57
57
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.5"
|
|
@@ -4,9 +4,10 @@ GitHub API client for accessing repository contents.
|
|
|
4
4
|
Created on 2025-12-02
|
|
5
5
|
@author: wf
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import json
|
|
8
|
-
from
|
|
9
|
-
from typing import
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
11
|
|
|
11
12
|
import requests
|
|
12
13
|
|
|
@@ -16,28 +17,38 @@ class GitHub:
|
|
|
16
17
|
A simple GitHub API client for accessing repository contents.
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
|
-
def __init__(
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
owner: str,
|
|
23
|
+
repo: str,
|
|
24
|
+
branch: Optional[str] = None,
|
|
25
|
+
token: Optional[str] = None,
|
|
26
|
+
session: Optional[requests.Session] = None
|
|
27
|
+
):
|
|
20
28
|
"""
|
|
21
29
|
Initialize GitHub client.
|
|
22
30
|
|
|
23
31
|
Args:
|
|
24
32
|
owner: Repository owner (username or organization)
|
|
25
33
|
repo: Repository name
|
|
34
|
+
branch: Optional specific branch or commit SHA (default: default repo branch)
|
|
26
35
|
token: Optional GitHub API token for authentication
|
|
27
36
|
session: Optional custom requests.Session
|
|
28
37
|
"""
|
|
29
38
|
self.owner = owner
|
|
30
39
|
self.repo = repo
|
|
31
|
-
self.
|
|
40
|
+
self.branch = branch
|
|
41
|
+
|
|
42
|
+
|
|
32
43
|
self.base_url = f"https://api.github.com/repos/{owner}/{repo}"
|
|
33
|
-
|
|
44
|
+
|
|
45
|
+
# Use provided token or read from file
|
|
34
46
|
self.token = token if token is not None else self._read_token()
|
|
35
47
|
|
|
36
48
|
# Use custom session or create new one
|
|
37
49
|
self.session = session or requests.Session()
|
|
38
50
|
|
|
39
51
|
|
|
40
|
-
|
|
41
52
|
def _headers(self) -> Dict[str, str]:
|
|
42
53
|
headers = {"Accept": "application/vnd.github.v3+json"}
|
|
43
54
|
if self.token:
|
|
@@ -55,14 +66,13 @@ class GitHub:
|
|
|
55
66
|
token_path = Path.home() / ".github" / "access_token.json"
|
|
56
67
|
if token_path.exists():
|
|
57
68
|
try:
|
|
58
|
-
with open(token_path,
|
|
69
|
+
with open(token_path, "r") as f:
|
|
59
70
|
data = json.load(f)
|
|
60
71
|
return data.get("access_token")
|
|
61
72
|
except (json.JSONDecodeError, OSError):
|
|
62
73
|
pass
|
|
63
74
|
return None
|
|
64
75
|
|
|
65
|
-
|
|
66
76
|
def get_contents(self, path: str = "") -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
|
67
77
|
"""
|
|
68
78
|
Get contents of a directory or file from the repository.
|
|
@@ -74,10 +84,15 @@ class GitHub:
|
|
|
74
84
|
List of dictionaries for directories, or content for files
|
|
75
85
|
"""
|
|
76
86
|
url = f"{self.base_url}/contents/{path}"
|
|
77
|
-
|
|
87
|
+
params = {}
|
|
88
|
+
if self.branch:
|
|
89
|
+
params["ref"] = self.branch
|
|
90
|
+
|
|
91
|
+
response = self.session.get(url, headers=self._headers(), params=params, timeout=30)
|
|
78
92
|
response.raise_for_status()
|
|
79
93
|
return response.json()
|
|
80
94
|
|
|
95
|
+
|
|
81
96
|
def list_files_recursive(self, path: str = "", suffix: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
82
97
|
"""
|
|
83
98
|
Recursively list files under a given path. Optionally filter by file suffix.
|
|
@@ -116,4 +131,4 @@ class GitHub:
|
|
|
116
131
|
"""
|
|
117
132
|
response = self.session.get(download_url, headers=self._headers(), timeout=30)
|
|
118
133
|
response.raise_for_status()
|
|
119
|
-
return response.text
|
|
134
|
+
return response.text
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2024-06-20
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
from bs4 import BeautifulSoup
|
|
11
|
+
from osprojects.osproject import OsProject, Ticket
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
from snapquery.snapquery_core import NamedQuery, NamedQuerySet
|
|
15
|
+
from snapquery.wd_short_url import ShortUrl
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QLeverUrl(ShortUrl):
|
|
19
|
+
"""
|
|
20
|
+
Handles operations related to QLever short URLs.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, short_url: str):
|
|
24
|
+
super().__init__(short_url, scheme="https", netloc="qlever.cs.uni-freiburg.de")
|
|
25
|
+
self.sparql = None
|
|
26
|
+
|
|
27
|
+
def read_query(self) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Read a query from a QLever short URL.
|
|
30
|
+
It follows redirects and parses the HTML to find the query in the textarea.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str: The SPARQL query extracted from the short URL.
|
|
34
|
+
"""
|
|
35
|
+
# Ensure we have the final URL (handles redirects)
|
|
36
|
+
self.fetch_final_url(self.short_url)
|
|
37
|
+
|
|
38
|
+
if self.url:
|
|
39
|
+
try:
|
|
40
|
+
# Assuming ShortUrl or its parent has a get_content or similar method.
|
|
41
|
+
# If not, specific request logic would go here.
|
|
42
|
+
# Using get_wikitext as per your snippet pattern, assuming it returns page content.
|
|
43
|
+
text = self.get_wikitext(self.url)
|
|
44
|
+
if text:
|
|
45
|
+
soup = BeautifulSoup(text, "html.parser")
|
|
46
|
+
query_element = soup.find("textarea", {"id": "query"})
|
|
47
|
+
if query_element and query_element.text:
|
|
48
|
+
self.sparql = query_element.text.strip()
|
|
49
|
+
except Exception as ex:
|
|
50
|
+
self.error = ex
|
|
51
|
+
return self.sparql
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class QLever:
|
|
55
|
+
"""
|
|
56
|
+
Handle https://github.com/ad-freiburg/qlever specifics
|
|
57
|
+
and interaction with QLever instances.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
with_progress: bool = True,
|
|
63
|
+
debug: bool = False,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
constructor
|
|
67
|
+
"""
|
|
68
|
+
self.url = "https://github.com/ad-freiburg/qlever"
|
|
69
|
+
self.with_progress = with_progress
|
|
70
|
+
self.debug = debug
|
|
71
|
+
# Regex pattern to find URLs starting with the specified prefix
|
|
72
|
+
self.wd_url_pattern = re.compile(r"https://(?:qlever\.cs\.uni-freiburg\.de|qlever\.dev)/wikidata/[A-Za-z0-9]+")
|
|
73
|
+
self.osproject = OsProject.fromUrl(self.url)
|
|
74
|
+
|
|
75
|
+
def wd_urls_for_ticket(self, ticket: Ticket) -> List[str]:
|
|
76
|
+
"""
|
|
77
|
+
Extracts and returns all URLs from a ticket's body and comments
|
|
78
|
+
that match the QLever Wikidata pattern.
|
|
79
|
+
"""
|
|
80
|
+
extracted_urls = []
|
|
81
|
+
|
|
82
|
+
# Extract URLs from the ticket body
|
|
83
|
+
if ticket.body:
|
|
84
|
+
found_urls = self.wd_url_pattern.findall(ticket.body)
|
|
85
|
+
extracted_urls.extend(found_urls)
|
|
86
|
+
|
|
87
|
+
# Fetch and extract URLs from comments
|
|
88
|
+
comments = self.osproject.getComments(ticket.number)
|
|
89
|
+
for comment in comments:
|
|
90
|
+
if "body" in comment:
|
|
91
|
+
found_urls = self.wd_url_pattern.findall(comment["body"])
|
|
92
|
+
extracted_urls.extend(found_urls)
|
|
93
|
+
|
|
94
|
+
# distinct list
|
|
95
|
+
ticket_urls = list(set(extracted_urls))
|
|
96
|
+
return ticket_urls
|
|
97
|
+
|
|
98
|
+
def get_issues_query_set(self, limit: int = None, progress: bool = False) -> NamedQuerySet:
|
|
99
|
+
"""
|
|
100
|
+
Orchestrates the retrieval of tickets, extraction of URLs,
|
|
101
|
+
resolving of SPARQL queries, and creation of a NamedQuerySet.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
limit (int): Max number of tickets to process.
|
|
105
|
+
progress (bool): Whether to show a CLI progress bar.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
NamedQuerySet: The populated query set.
|
|
109
|
+
"""
|
|
110
|
+
# Get the full dictionary
|
|
111
|
+
tickets_map = self.osproject.getAllTickets(limit=limit)
|
|
112
|
+
items_iterable = tickets_map.items()
|
|
113
|
+
|
|
114
|
+
if progress:
|
|
115
|
+
iterator = tqdm(
|
|
116
|
+
items_iterable, total=len(tickets_map), desc="Scanning tickets for QLever URLs", unit="ticket"
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
iterator = items_iterable
|
|
120
|
+
|
|
121
|
+
ticket_dict = {}
|
|
122
|
+
|
|
123
|
+
# 1. Scan tickets for URLs
|
|
124
|
+
for i, (t_id, ticket) in enumerate(iterator):
|
|
125
|
+
msg = f"fetching short urls for ticket {i}:#{t_id} {ticket.title}"
|
|
126
|
+
if self.debug:
|
|
127
|
+
print(msg)
|
|
128
|
+
urls = self.wd_urls_for_ticket(ticket)
|
|
129
|
+
if urls:
|
|
130
|
+
ticket_dict[ticket] = urls
|
|
131
|
+
|
|
132
|
+
# 2. Convert to NamedQuerySet
|
|
133
|
+
# (This resolves the short URLs to actual SPARQL)
|
|
134
|
+
nq_set = self.named_queries_for_tickets(ticket_dict)
|
|
135
|
+
return nq_set
|
|
136
|
+
|
|
137
|
+
def named_queries_for_tickets(self, ticket_dict: Dict[Ticket, List[str]]) -> NamedQuerySet:
|
|
138
|
+
"""
|
|
139
|
+
Create named queries for each ticket's extracted URLs.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
ticket_dict (dict): Dictionary mapping tickets to a list of URLs.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
NamedQuerySet: A set of named queries generated from the URLs.
|
|
146
|
+
"""
|
|
147
|
+
named_query_set = NamedQuerySet(
|
|
148
|
+
domain="qlever.dev",
|
|
149
|
+
namespace="issues.wikidata",
|
|
150
|
+
target_graph_name="wikidata",
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
total_urls = sum(len(urls) for urls in ticket_dict.values())
|
|
154
|
+
|
|
155
|
+
# We might want progress here if there are many URLs to resolve
|
|
156
|
+
if self.with_progress and total_urls > 0:
|
|
157
|
+
pbar = tqdm(total=total_urls, desc="Resolving QLever URLs", unit="url")
|
|
158
|
+
else:
|
|
159
|
+
pbar = None
|
|
160
|
+
|
|
161
|
+
for ticket, urls in ticket_dict.items():
|
|
162
|
+
for i, url in enumerate(urls, 1):
|
|
163
|
+
# Resolve the Short URL
|
|
164
|
+
qlever_url = QLeverUrl(url)
|
|
165
|
+
sparql = qlever_url.read_query()
|
|
166
|
+
|
|
167
|
+
if sparql:
|
|
168
|
+
# Construct the NamedQuery
|
|
169
|
+
query = NamedQuery(
|
|
170
|
+
domain=named_query_set.domain,
|
|
171
|
+
name=f"Issue{ticket.number}_query{i}",
|
|
172
|
+
namespace=named_query_set.namespace,
|
|
173
|
+
url=url,
|
|
174
|
+
sparql=sparql,
|
|
175
|
+
title=f"QLever Issue #{ticket.number} Query {i}",
|
|
176
|
+
description=ticket.title,
|
|
177
|
+
comment=f"Source: Ticket {ticket.url}, QLever Link: {url}",
|
|
178
|
+
)
|
|
179
|
+
named_query_set.queries.append(query)
|
|
180
|
+
|
|
181
|
+
if pbar:
|
|
182
|
+
pbar.update(1)
|
|
183
|
+
|
|
184
|
+
if pbar:
|
|
185
|
+
pbar.close()
|
|
186
|
+
|
|
187
|
+
return named_query_set
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2025-12-16
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import textwrap
|
|
9
|
+
from typing import Optional, Dict, Any, List
|
|
10
|
+
|
|
11
|
+
from lodstorage.params import Param
|
|
12
|
+
from lodstorage.query import Query
|
|
13
|
+
from snapquery.snapquery_core import NamedQuerySet, NamedQuery
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QueryParameterizer:
|
|
17
|
+
"""
|
|
18
|
+
Parameterizer for Scholia Query Sets.
|
|
19
|
+
Transforms Jinja2-based Scholia templates into standard
|
|
20
|
+
LoDStorage Parameterized Queries.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Replacement for Scholia's label macro
|
|
24
|
+
LABEL_SERVICE = 'SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }'
|
|
25
|
+
|
|
26
|
+
# Defaults for specific Scholia Aspects
|
|
27
|
+
# derived from the prompt list
|
|
28
|
+
ASPECT_DEFAULTS = {
|
|
29
|
+
"author": "Q80", # Tim Berners-Lee
|
|
30
|
+
"authors": "Q80",
|
|
31
|
+
"award": "Q616568", # Turing Award
|
|
32
|
+
"catalogue": "Q2463660", # British Library catalogue
|
|
33
|
+
"chemical": "Q18216", # Aspirin
|
|
34
|
+
"cito": "Q1054942", # citation intent
|
|
35
|
+
"clinical": "Q12136", # Common Cold (disease context)
|
|
36
|
+
"complex": "Q417724", # Protein complex
|
|
37
|
+
"countries": "Q183", # Germany
|
|
38
|
+
"country": "Q183",
|
|
39
|
+
"dataset": "Q1172284", # Data set
|
|
40
|
+
"disease": "Q12136", # Common Cold
|
|
41
|
+
"event": "Q2020153", # WWW Conference
|
|
42
|
+
"gene": "Q7191", # p53
|
|
43
|
+
"index": "Q1", # Universe (generic fallback)
|
|
44
|
+
"language": "Q1860", # English
|
|
45
|
+
"lexeme": "Q151326", # Word
|
|
46
|
+
"license": "Q20007257", # CC BY 4.0
|
|
47
|
+
"location": "Q64", # Berlin
|
|
48
|
+
"ontology": "Q324254", # Ontology
|
|
49
|
+
"organization": "Q95", # Google
|
|
50
|
+
"organizations": "Q95",
|
|
51
|
+
"pathway": "Q26487", # Metabolic pathway
|
|
52
|
+
"podcast": "Q24634210", # The Ozymandias Project
|
|
53
|
+
"printer": "Q11226", # Printer (the device/person)
|
|
54
|
+
"project": "Q5562725", # WikiProject
|
|
55
|
+
"property": "P31", # Instance of
|
|
56
|
+
"protein": "Q11060", # Green fluorescent protein
|
|
57
|
+
"publisher": "Q203019", # Elsevier
|
|
58
|
+
"retraction": "Q45263695", # Retracted paper example
|
|
59
|
+
"series": "Q245209", # Book series
|
|
60
|
+
"software": "Q28865", # Python
|
|
61
|
+
"sponsor": "Q95", # Google
|
|
62
|
+
"taxon": "Q140", # Lion
|
|
63
|
+
"topic": "Q21198", # Computer Science
|
|
64
|
+
"topics": "Q21198",
|
|
65
|
+
"use": "Q1",
|
|
66
|
+
"uses": "Q1",
|
|
67
|
+
"venue": "Q180445", # Nature (Journal)
|
|
68
|
+
"venues": "Q180445",
|
|
69
|
+
"wikiproject": "Q5562725",
|
|
70
|
+
"work": "Q13442814", # Scientific Article
|
|
71
|
+
"works": "Q13442814"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def __init__(self, debug: bool = False):
|
|
75
|
+
self.debug = debug
|
|
76
|
+
|
|
77
|
+
def _infer_param_details(self, var_name: str, aspect: Optional[str] = None) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Heuristics to determine type and default value based on variable name
|
|
80
|
+
and the query aspect (category).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
var_name: The variable name found in the jinja template (e.g. 'q', 'target').
|
|
84
|
+
aspect: The scholia aspect derived from the query name (e.g. 'venue', 'author').
|
|
85
|
+
"""
|
|
86
|
+
# Global Default
|
|
87
|
+
details = {"type": "string", "default": "Q80"}
|
|
88
|
+
|
|
89
|
+
# 1. Identify Type based on variable name
|
|
90
|
+
if var_name in ['q', 'target', 'item', 'person', 'work', 'venue', 'gene', 'disease']:
|
|
91
|
+
details["type"] = "WikidataItem"
|
|
92
|
+
|
|
93
|
+
# 2. Assign Default Value based on Aspect
|
|
94
|
+
if aspect and aspect in self.ASPECT_DEFAULTS:
|
|
95
|
+
details["default"] = self.ASPECT_DEFAULTS[aspect]
|
|
96
|
+
|
|
97
|
+
# 3. Fallback defaults if aspect is missing or generic
|
|
98
|
+
elif var_name == 'person':
|
|
99
|
+
details["default"] = "Q80" # Tim Berners-Lee
|
|
100
|
+
elif var_name == 'work':
|
|
101
|
+
details["default"] = "Q13442814"
|
|
102
|
+
else:
|
|
103
|
+
details["default"] = "Q80"
|
|
104
|
+
|
|
105
|
+
elif var_name in ['p', 'property']:
|
|
106
|
+
details["type"] = "WikidataProperty"
|
|
107
|
+
details["default"] = "P31"
|
|
108
|
+
elif var_name in ['limit']:
|
|
109
|
+
details["type"] = "int"
|
|
110
|
+
details["default"] = 10
|
|
111
|
+
elif var_name in ['lang', 'language']:
|
|
112
|
+
details["type"] = "string"
|
|
113
|
+
details["default"] = "en"
|
|
114
|
+
|
|
115
|
+
return details
|
|
116
|
+
|
|
117
|
+
def get_queries_by_aspect(self,query_list:List[Query])->Dict[str,List[Query]]:
|
|
118
|
+
"""
|
|
119
|
+
get queries by aspect
|
|
120
|
+
"""
|
|
121
|
+
queries_by_aspect = {}
|
|
122
|
+
for query in query_list:
|
|
123
|
+
aspect=self.get_aspect(query.name)
|
|
124
|
+
if aspect in self.ASPECT_DEFAULTS:
|
|
125
|
+
if aspect not in queries_by_aspect:
|
|
126
|
+
queries_by_aspect[aspect] = []
|
|
127
|
+
queries_by_aspect[aspect].append(query)
|
|
128
|
+
return queries_by_aspect
|
|
129
|
+
|
|
130
|
+
def store_query_list(self,query_list:List[Query],tag:str,path:str="/tmp/queries"):
|
|
131
|
+
"""
|
|
132
|
+
Store query list sorted by aspect into separate YAML files.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
query_list (List[Query]): List of Query objects with aspect metadata.
|
|
136
|
+
tag (str): Tag to include in filename.
|
|
137
|
+
path (str): Directory path where YAML files will be stored.
|
|
138
|
+
"""
|
|
139
|
+
queries_by_aspect=self.get_queries_by_aspect(query_list)
|
|
140
|
+
# Create directory if it doesn't exist
|
|
141
|
+
os.makedirs(path, exist_ok=True)
|
|
142
|
+
|
|
143
|
+
# Store each aspect's queries in a separate YAML file
|
|
144
|
+
for aspect, queries in sorted(queries_by_aspect.items()):
|
|
145
|
+
filename = f"{tag}_{aspect}_queries.yaml"
|
|
146
|
+
filepath = os.path.join(path, filename)
|
|
147
|
+
|
|
148
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
149
|
+
for query in queries:
|
|
150
|
+
# work around https://github.com/yaml/pyyaml/issues/411
|
|
151
|
+
if query.sparql:
|
|
152
|
+
clean_sparql = textwrap.dedent(query.sparql).strip()
|
|
153
|
+
yaml_str=query.to_yaml()
|
|
154
|
+
f.write(yaml_str)
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def parameterize_query_set(self, query_set: NamedQuerySet) -> List[Query]:
|
|
159
|
+
"""
|
|
160
|
+
Parameterize the given query set to produce a list of LoDStorage queries.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
query_set (NamedQuerySet): The set of named queries to transform.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List[Query]: A list of transformed LoDStorage Query objects.
|
|
167
|
+
"""
|
|
168
|
+
query_list = []
|
|
169
|
+
for nq in query_set.queries:
|
|
170
|
+
query = self.parameterize_query(nq)
|
|
171
|
+
query_list.append(query)
|
|
172
|
+
return query_list
|
|
173
|
+
|
|
174
|
+
def get_aspect(self,name:str)->str:
|
|
175
|
+
aspect = None
|
|
176
|
+
if name:
|
|
177
|
+
# Split by hyphen first, as that is the standard separator in the example
|
|
178
|
+
parts = re.split(r'[-_]', name)
|
|
179
|
+
if parts:
|
|
180
|
+
aspect = parts[0].lower()
|
|
181
|
+
return aspect
|
|
182
|
+
|
|
183
|
+
def parameterize_query(self, nq: NamedQuery) -> Query:
|
|
184
|
+
"""
|
|
185
|
+
Parameterize a single named query.
|
|
186
|
+
|
|
187
|
+
1. Identifies the "Aspect" from the query name (e.g. 'venue-...' -> 'venue').
|
|
188
|
+
2. Cleans Jinja2 specific imports.
|
|
189
|
+
3. Replaces Scholia Macros with standard SPARQL.
|
|
190
|
+
4. Detects variables and creates Param objects using aspect-specific defaults.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
nq (NamedQuery): The named query input.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Query: The transformed LoDStorage Query object.
|
|
197
|
+
"""
|
|
198
|
+
# 0. Determine Aspect
|
|
199
|
+
# Naming convention: {aspect}-{subname} or {aspect}_{subname}
|
|
200
|
+
# e.g., "venue-curation_missing-author-item" -> "venue"
|
|
201
|
+
aspect=self.get_aspect(nq.name)
|
|
202
|
+
|
|
203
|
+
raw_sparql = nq.sparql
|
|
204
|
+
|
|
205
|
+
# 1. Remove Jinja imports
|
|
206
|
+
# e.g., {% import 'sparql-helpers.sparql' as sparql_helpers -%}
|
|
207
|
+
txt = re.sub(r'\{%\s*import\s*.*?%\}', '', raw_sparql, flags=re.DOTALL)
|
|
208
|
+
|
|
209
|
+
# 2. Replace Label/Description Macros
|
|
210
|
+
# Matches: {{ sparql_helpers.labels(["?mol"], languages) }}
|
|
211
|
+
txt = re.sub(
|
|
212
|
+
r"\{\{\s*sparql_helpers\.(labels|descriptions).*?\}\}",
|
|
213
|
+
self.LABEL_SERVICE,
|
|
214
|
+
txt,
|
|
215
|
+
flags=re.DOTALL
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# 3. Detect Variables to build Param list
|
|
219
|
+
found_vars = set(re.findall(r"\{\{\s*([a-zA-Z0-9_]+)\s*\}\}", txt))
|
|
220
|
+
found_vars.discard('sparql_helpers')
|
|
221
|
+
|
|
222
|
+
# Create lodstorage Query object
|
|
223
|
+
clean_sparql = txt.strip()
|
|
224
|
+
|
|
225
|
+
query_obj = Query(
|
|
226
|
+
name=nq.name,
|
|
227
|
+
query=clean_sparql,
|
|
228
|
+
title=nq.title,
|
|
229
|
+
description=nq.description,
|
|
230
|
+
lang='sparql'
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# 4. Attach Parameters with Aspect context
|
|
234
|
+
param_list = []
|
|
235
|
+
for var in sorted(found_vars):
|
|
236
|
+
details = self._infer_param_details(var, aspect=aspect)
|
|
237
|
+
p = Param(
|
|
238
|
+
name=var,
|
|
239
|
+
type=details["type"],
|
|
240
|
+
default_value=details["default"]
|
|
241
|
+
)
|
|
242
|
+
param_list.append(p)
|
|
243
|
+
|
|
244
|
+
query_obj.param_list = param_list
|
|
245
|
+
|
|
246
|
+
return query_obj
|