snapquery 0.0.3__tar.gz → 0.0.4__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.0.3 → snapquery-0.0.4}/PKG-INFO +4 -2
- {snapquery-0.0.3 → snapquery-0.0.4}/pyproject.toml +4 -2
- snapquery-0.0.4/snapquery/__init__.py +1 -0
- snapquery-0.0.4/snapquery/scholia.py +49 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_cmd.py +18 -7
- {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_core.py +204 -138
- {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_view.py +78 -3
- {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_webserver.py +89 -27
- {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/version.py +2 -1
- snapquery-0.0.4/tests/test_cmdline.py +75 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/tests/test_endpoints.py +1 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/tests/test_namedqueries.py +4 -2
- {snapquery-0.0.3 → snapquery-0.0.4}/tests/test_restful_api.py +6 -5
- snapquery-0.0.4/tests/test_scholia.py +26 -0
- snapquery-0.0.4/tests/test_wd_query_parsing.py +90 -0
- snapquery-0.0.3/snapquery/__init__.py +0 -1
- {snapquery-0.0.3 → snapquery-0.0.4}/.github/workflows/build.yml +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/.github/workflows/upload-to-pypi.yml +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/.gitignore +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/.project +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/.pydevproject +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/LICENSE +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/README.md +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/scripts/blackisort +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/scripts/install +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/scripts/test +0 -0
- {snapquery-0.0.3 → snapquery-0.0.4}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: snapquery
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: snapquery: Introduce Named Queries and Named Query Middleware to wikidata
|
|
5
5
|
Project-URL: Home, https://github.com/WolfgangFahl/snapquery
|
|
6
6
|
Project-URL: Documentation, https://wiki.bitplan.com/index.php/snapquery
|
|
@@ -25,7 +25,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
25
25
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
26
26
|
Requires-Python: >=3.9
|
|
27
27
|
Requires-Dist: ngwidgets>=0.14.2
|
|
28
|
-
Requires-Dist: pylodstorage>=0.10.
|
|
28
|
+
Requires-Dist: pylodstorage>=0.10.5
|
|
29
|
+
Requires-Dist: requests
|
|
30
|
+
Requires-Dist: wikitextparser
|
|
29
31
|
Provides-Extra: test
|
|
30
32
|
Requires-Dist: green; extra == 'test'
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
@@ -19,8 +19,10 @@ license = {text = "Apache-2.0"}
|
|
|
19
19
|
dependencies = [
|
|
20
20
|
# https://github.com/WolfgangFahl/nicegui_widgets
|
|
21
21
|
"ngwidgets>=0.14.2",
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# https://pypi.org/project/pyLodStorage/
|
|
23
|
+
"pyLodStorage>=0.10.5",
|
|
24
|
+
"wikitextparser",
|
|
25
|
+
"requests"
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.4"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2024-05-04
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
import requests
|
|
7
|
+
from snapquery.snapquery_core import NamedQueryManager
|
|
8
|
+
class ScholiaQueries:
|
|
9
|
+
"""
|
|
10
|
+
scholia queries
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def get(cls,db_path:str=None,debug:bool=False,limit:int=None):
|
|
15
|
+
nqm = NamedQueryManager.from_samples(db_path=db_path)
|
|
16
|
+
repository_url = "https://api.github.com/repos/WDscholia/scholia/contents/scholia/app/templates"
|
|
17
|
+
headers = {'Accept': 'application/vnd.github.v3+json'}
|
|
18
|
+
response = requests.get(repository_url, headers=headers)
|
|
19
|
+
response.raise_for_status() # Ensure we notice bad responses
|
|
20
|
+
|
|
21
|
+
exclude = ["author_topics", "event_proceedings", "publisher_editors", "works_topics"]
|
|
22
|
+
lod=[]
|
|
23
|
+
file_list_json=response.json()
|
|
24
|
+
for i,file_info in enumerate(file_list_json,start=1):
|
|
25
|
+
file_name = file_info['name']
|
|
26
|
+
if file_name.endswith('.sparql') and file_name[:-7] not in exclude:
|
|
27
|
+
file_response = requests.get(file_info['download_url'])
|
|
28
|
+
file_response.raise_for_status()
|
|
29
|
+
query_str = file_response.text
|
|
30
|
+
name=file_name[:-7]
|
|
31
|
+
record={
|
|
32
|
+
"namespace":"scholia",
|
|
33
|
+
"name": name,
|
|
34
|
+
"url": file_info['download_url'],
|
|
35
|
+
"title": name,
|
|
36
|
+
"sparql":query_str
|
|
37
|
+
}
|
|
38
|
+
lod.append(record)
|
|
39
|
+
if debug:
|
|
40
|
+
if i % 80 == 0:
|
|
41
|
+
print(f"{i}")
|
|
42
|
+
print(".", end='', flush=True)
|
|
43
|
+
if limit and len(lod)>=limit:
|
|
44
|
+
break
|
|
45
|
+
if debug:
|
|
46
|
+
print(f"found {len(lod)} scholia queries")
|
|
47
|
+
nqm.store(lod)
|
|
48
|
+
return nqm
|
|
49
|
+
|
|
@@ -3,10 +3,11 @@ Created on 2024-05-03
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import sys
|
|
7
8
|
from argparse import ArgumentParser
|
|
8
9
|
|
|
9
|
-
from lodstorage.query import EndpointManager, Format
|
|
10
|
+
from lodstorage.query import EndpointManager, Format
|
|
10
11
|
from ngwidgets.cmd import WebserverCmd
|
|
11
12
|
|
|
12
13
|
from snapquery.snapquery_core import NamedQueryManager
|
|
@@ -34,7 +35,7 @@ class SnapQueryCmd(WebserverCmd):
|
|
|
34
35
|
"-en",
|
|
35
36
|
"--endpointName",
|
|
36
37
|
default="wikidata",
|
|
37
|
-
help=
|
|
38
|
+
help="Name of the endpoint to use for queries - use --listEndpoints to list available endpoints",
|
|
38
39
|
)
|
|
39
40
|
parser.add_argument(
|
|
40
41
|
"-le",
|
|
@@ -46,7 +47,10 @@ class SnapQueryCmd(WebserverCmd):
|
|
|
46
47
|
"--limit", type=int, default=None, help="set limit parameter of query"
|
|
47
48
|
)
|
|
48
49
|
parser.add_argument(
|
|
49
|
-
"--namespace",
|
|
50
|
+
"--namespace",
|
|
51
|
+
type=str,
|
|
52
|
+
default="wikidata-examples",
|
|
53
|
+
help="namespace to filter queries",
|
|
50
54
|
)
|
|
51
55
|
parser.add_argument("-qn", "--queryName", help="run a named query")
|
|
52
56
|
parser.add_argument("-f", "--format", type=Format, choices=list(Format))
|
|
@@ -66,13 +70,20 @@ class SnapQueryCmd(WebserverCmd):
|
|
|
66
70
|
print(endpoint)
|
|
67
71
|
handled = True # Operation handled
|
|
68
72
|
elif self.args.queryName is not None:
|
|
69
|
-
nqm = NamedQueryManager()
|
|
70
|
-
namespace=self.args.namespace
|
|
73
|
+
nqm = NamedQueryManager.from_samples()
|
|
74
|
+
namespace = self.args.namespace
|
|
71
75
|
name = self.args.queryName
|
|
72
76
|
endpoint_name = self.args.endpointName
|
|
73
77
|
r_format = self.args.format
|
|
74
|
-
limit=self.args.limit
|
|
75
|
-
|
|
78
|
+
limit = self.args.limit
|
|
79
|
+
qb = nqm.get_query(
|
|
80
|
+
name=name, namespace=namespace, endpoint_name=endpoint_name, limit=limit
|
|
81
|
+
)
|
|
82
|
+
if r_format==Format.raw:
|
|
83
|
+
formatted_result=qb.raw_query()
|
|
84
|
+
else:
|
|
85
|
+
qlod = qb.get_lod()
|
|
86
|
+
formatted_result = qb.format_result(qlod=qlod, r_format=r_format)
|
|
76
87
|
print(formatted_result)
|
|
77
88
|
return handled
|
|
78
89
|
|
|
@@ -3,9 +3,11 @@ Created on 2024-05-03
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import json
|
|
7
|
-
import re
|
|
8
8
|
import os
|
|
9
|
+
import re
|
|
10
|
+
import requests
|
|
9
11
|
from dataclasses import field
|
|
10
12
|
from pathlib import Path
|
|
11
13
|
from typing import Any, Dict, List, Optional
|
|
@@ -38,13 +40,15 @@ class NamedQuery:
|
|
|
38
40
|
namespace: str
|
|
39
41
|
# name/id
|
|
40
42
|
name: str
|
|
41
|
-
# one line title
|
|
42
|
-
title: str
|
|
43
|
-
# multiline description
|
|
44
|
-
description: str
|
|
45
43
|
# sparql query (to be hidden later)
|
|
46
44
|
sparql: str
|
|
47
|
-
|
|
45
|
+
# the url of the source code of the query
|
|
46
|
+
url: Optional[str]=None
|
|
47
|
+
# one line title
|
|
48
|
+
title: Optional[str]=None
|
|
49
|
+
# multiline description
|
|
50
|
+
description: Optional[str]=None
|
|
51
|
+
|
|
48
52
|
def __post_init__(self):
|
|
49
53
|
"""
|
|
50
54
|
Post-initialization processing to construct a unique identifier for the query
|
|
@@ -53,6 +57,9 @@ class NamedQuery:
|
|
|
53
57
|
self.query_id = f"{self.namespace}.{self.name}"
|
|
54
58
|
|
|
55
59
|
def as_link(self) -> str:
|
|
60
|
+
"""
|
|
61
|
+
get me as a link
|
|
62
|
+
"""
|
|
56
63
|
url = f"/query/{self.namespace}/{self.name}"
|
|
57
64
|
text = self.name
|
|
58
65
|
tooltip = "query details"
|
|
@@ -68,9 +75,21 @@ class NamedQuery:
|
|
|
68
75
|
namespace=record["namespace"],
|
|
69
76
|
name=record["name"],
|
|
70
77
|
title=record["title"],
|
|
78
|
+
url=record["url"],
|
|
71
79
|
description=record["description"],
|
|
72
80
|
sparql=record["sparql"],
|
|
73
81
|
)
|
|
82
|
+
|
|
83
|
+
def as_record(self)->Dict:
|
|
84
|
+
record = {
|
|
85
|
+
"namespace": self.namespace,
|
|
86
|
+
"name": self.name,
|
|
87
|
+
"url": self.url,
|
|
88
|
+
"title": self.title,
|
|
89
|
+
"description": self.description,
|
|
90
|
+
"sparql": self.sparql,
|
|
91
|
+
}
|
|
92
|
+
return record
|
|
74
93
|
|
|
75
94
|
def as_viewrecord(self) -> Dict:
|
|
76
95
|
"""
|
|
@@ -81,9 +100,123 @@ class NamedQuery:
|
|
|
81
100
|
"namespace": self.namespace,
|
|
82
101
|
"title": self.title,
|
|
83
102
|
"description": self.description,
|
|
103
|
+
"url": self.url
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
|
|
107
|
+
class QueryBundle:
|
|
108
|
+
"""
|
|
109
|
+
Bundles a named query, a query, and an endpoint into a single manageable object, facilitating the execution of SPARQL queries.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
named_query (NamedQuery): The named query object, which includes metadata about the query.
|
|
113
|
+
query (Query): The actual query object that contains the SPARQL query string.
|
|
114
|
+
endpoint (Endpoint): The endpoint object where the SPARQL query should be executed.
|
|
115
|
+
sparql (SPARQL): A SPARQL service object initialized with the endpoint URL.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, named_query: NamedQuery, query: Query, endpoint: Endpoint):
|
|
119
|
+
"""
|
|
120
|
+
Initializes a new instance of the QueryBundle class.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
named_query (NamedQuery): An instance of NamedQuery that provides a named reference to the query.
|
|
124
|
+
query (Query): An instance of Query containing the SPARQL query string.
|
|
125
|
+
endpoint (Endpoint): An instance of Endpoint representing the SPARQL endpoint URL.
|
|
126
|
+
"""
|
|
127
|
+
self.named_query = named_query
|
|
128
|
+
self.query = query
|
|
129
|
+
self.endpoint = endpoint
|
|
130
|
+
self.sparql = SPARQL(endpoint.endpoint)
|
|
131
|
+
|
|
132
|
+
def raw_query(self,resultFormat, mime_type:str=None, timeout:float=10.0):
|
|
133
|
+
"""
|
|
134
|
+
returns raw result of the endpoint
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
resultFormat(str): format of the result
|
|
138
|
+
mime_type(str): mime_type to use (if any)
|
|
139
|
+
timeout(float): timeout in seconds
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
raw result of the query
|
|
143
|
+
"""
|
|
144
|
+
params = {"query": self.query.query, "format": resultFormat}
|
|
145
|
+
payload = {}
|
|
146
|
+
if mime_type:
|
|
147
|
+
headers = {"Accept": mime_type}
|
|
148
|
+
else:
|
|
149
|
+
headers = {}
|
|
150
|
+
endpoint_url = self.endpoint.endpoint
|
|
151
|
+
method = self.endpoint.method
|
|
152
|
+
response = requests.request(
|
|
153
|
+
method, endpoint_url, headers=headers, data=payload, params=params,timeout=timeout
|
|
154
|
+
)
|
|
155
|
+
return response.text
|
|
156
|
+
|
|
157
|
+
def get_lod(self) -> List[dict]:
|
|
158
|
+
"""
|
|
159
|
+
Executes the stored query using the SPARQL service and returns the results as a list of dictionaries.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List[dict]: A list where each dictionary represents a row of results from the SPARQL query.
|
|
163
|
+
"""
|
|
164
|
+
lod = self.sparql.queryAsListOfDicts(self.query.query)
|
|
165
|
+
return lod
|
|
166
|
+
|
|
167
|
+
def format_result(
|
|
168
|
+
self,
|
|
169
|
+
qlod: List[Dict[str, Any]] = None,
|
|
170
|
+
r_format: Format = Format.json,
|
|
171
|
+
) -> Optional[str]:
|
|
172
|
+
"""
|
|
173
|
+
Formats the query results based on the specified format and prints them.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
qlod (List[Dict[str, Any]]): The list of dictionaries that represent the query results.
|
|
177
|
+
query (Query): The query object which contains details like the endpoint and the database.
|
|
178
|
+
r_format(Format): The format in which to print the results.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Optional[str]: The formatted string representation of the query results, or None if printed directly.
|
|
182
|
+
"""
|
|
183
|
+
if qlod is None:
|
|
184
|
+
qlod = self.get_lod()
|
|
185
|
+
if r_format is None:
|
|
186
|
+
r_format = Format.json
|
|
187
|
+
if r_format == Format.csv:
|
|
188
|
+
csv_output = CSV.toCSV(qlod)
|
|
189
|
+
return csv_output
|
|
190
|
+
elif r_format in [Format.latex, Format.github, Format.mediawiki, Format.html]:
|
|
191
|
+
doc = self.query.documentQueryResult(
|
|
192
|
+
qlod, tablefmt=str(r_format), floatfmt=".0f"
|
|
193
|
+
)
|
|
194
|
+
return doc.asText()
|
|
195
|
+
elif r_format == Format.json:
|
|
196
|
+
return json.dumps(qlod, indent=2, sort_keys=True, default=str)
|
|
197
|
+
return None # In case no format is matched or needed
|
|
198
|
+
|
|
199
|
+
def set_limit(self, limit: int = None):
|
|
200
|
+
"""
|
|
201
|
+
set the limit of my query
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
limit(int): the limit to set - default: None
|
|
205
|
+
"""
|
|
206
|
+
if limit:
|
|
207
|
+
sparql_query = self.query.query
|
|
208
|
+
# @TODO - this is too naive for cases where
|
|
209
|
+
# there are SPARQL elements hat have a "limit" in the name e.g. "height_limit"
|
|
210
|
+
# or if there is a LIMIT in a subquery
|
|
211
|
+
if "limit" in sparql_query or "LIMIT" in sparql_query:
|
|
212
|
+
sparql_query = re.sub(
|
|
213
|
+
r"(limit|LIMIT)\s+(\d+)", f"LIMIT {limit}", sparql_query
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
sparql_query += f"\nLIMIT {limit}"
|
|
217
|
+
self.query.query = sparql_query
|
|
218
|
+
|
|
219
|
+
|
|
87
220
|
class NamedQueryManager:
|
|
88
221
|
"""
|
|
89
222
|
Manages the storage, retrieval, and execution of named SPARQL queries.
|
|
@@ -136,23 +269,33 @@ class NamedQueryManager:
|
|
|
136
269
|
nqm = NamedQueryManager(debug=debug)
|
|
137
270
|
path_obj = Path(db_path)
|
|
138
271
|
if not path_obj.exists() or path_obj.stat().st_size == 0:
|
|
139
|
-
|
|
140
|
-
list_of_records = []
|
|
141
|
-
for _query_name, named_query in sample_queries.items():
|
|
142
|
-
record = {
|
|
143
|
-
"namespace": named_query.namespace,
|
|
144
|
-
"name": named_query.name,
|
|
145
|
-
"title": named_query.title,
|
|
146
|
-
"description": named_query.description,
|
|
147
|
-
"sparql": named_query.sparql,
|
|
148
|
-
}
|
|
149
|
-
list_of_records.append(record)
|
|
272
|
+
sample_records=cls.get_sample_records()
|
|
150
273
|
entityInfo = EntityInfo(
|
|
151
|
-
|
|
274
|
+
sample_records, name="NamedQuery", primaryKey="query_id"
|
|
152
275
|
)
|
|
153
|
-
nqm.sql_db.createTable(
|
|
154
|
-
nqm.sql_db.store(
|
|
276
|
+
nqm.sql_db.createTable(sample_records, "NamedQuery", withDrop=True)
|
|
277
|
+
nqm.sql_db.store(sample_records, entityInfo)
|
|
155
278
|
return nqm
|
|
279
|
+
|
|
280
|
+
def store(self,lod):
|
|
281
|
+
"""
|
|
282
|
+
store the given list of dicts
|
|
283
|
+
"""
|
|
284
|
+
sample_records=NamedQueryManager.get_sample_records()
|
|
285
|
+
entityInfo = EntityInfo(
|
|
286
|
+
sample_records, name="NamedQuery", primaryKey="query_id"
|
|
287
|
+
)
|
|
288
|
+
self.sql_db.store(lod, entityInfo,fixNone=True,replace=True)
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def get_sample_records(cls):
|
|
292
|
+
sample_queries = cls.get_samples()
|
|
293
|
+
list_of_records = []
|
|
294
|
+
for _query_name, named_query in sample_queries.items():
|
|
295
|
+
record=named_query.as_record()
|
|
296
|
+
list_of_records.append(record)
|
|
297
|
+
return list_of_records
|
|
298
|
+
|
|
156
299
|
|
|
157
300
|
@classmethod
|
|
158
301
|
def get_samples(cls) -> dict[str, NamedQuery]:
|
|
@@ -163,6 +306,7 @@ class NamedQueryManager:
|
|
|
163
306
|
"wikidata-examples.cats": NamedQuery(
|
|
164
307
|
namespace="wikidata-examples",
|
|
165
308
|
name="cats",
|
|
309
|
+
url="https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/queries/examples#Cats",
|
|
166
310
|
title="Cats on Wikidata",
|
|
167
311
|
description="This query retrieves all items classified under 'house cat' (Q146) on Wikidata.",
|
|
168
312
|
sparql="""
|
|
@@ -176,6 +320,7 @@ WHERE {
|
|
|
176
320
|
"wikidata-examples.horses": NamedQuery(
|
|
177
321
|
namespace="wikidata-examples",
|
|
178
322
|
name="horses",
|
|
323
|
+
url="https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/queries/examples#Horses_(showing_some_info_about_them)",
|
|
179
324
|
title="Horses on Wikidata",
|
|
180
325
|
description="This query retrieves information about horses, including parents, gender, and approximate birth and death years.",
|
|
181
326
|
sparql="""
|
|
@@ -198,14 +343,20 @@ ORDER BY ?horse
|
|
|
198
343
|
}
|
|
199
344
|
return samples
|
|
200
345
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
name
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
346
|
+
def lookup(self, name: str, namespace: str) -> NamedQuery:
|
|
347
|
+
"""
|
|
348
|
+
lookup the named query for the given name and namespace
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
name (str): The name of the named query to execute.
|
|
353
|
+
namespace (str): The namespace of the named query, default is 'wikidata-examples'.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
NamedQuery: the named query
|
|
357
|
+
"""
|
|
358
|
+
sql_query = """SELECT
|
|
359
|
+
*
|
|
209
360
|
FROM
|
|
210
361
|
NamedQuery
|
|
211
362
|
WHERE
|
|
@@ -221,18 +372,19 @@ WHERE
|
|
|
221
372
|
msg = f"multiple entries ({query_count}) for name '{name}' and namespace '{namespace}'"
|
|
222
373
|
raise ValueError(msg)
|
|
223
374
|
|
|
224
|
-
|
|
225
|
-
|
|
375
|
+
record = query_records[0]
|
|
376
|
+
named_query = NamedQuery.from_record(record)
|
|
377
|
+
return named_query
|
|
226
378
|
|
|
227
|
-
def
|
|
379
|
+
def get_query(
|
|
228
380
|
self,
|
|
229
381
|
name: str,
|
|
230
382
|
namespace: str = "wikidata-examples",
|
|
231
383
|
endpoint_name: str = "wikidata",
|
|
232
|
-
limit:int=None
|
|
233
|
-
):
|
|
384
|
+
limit: int = None,
|
|
385
|
+
) -> QueryBundle:
|
|
234
386
|
"""
|
|
235
|
-
|
|
387
|
+
get the query for the given parameters
|
|
236
388
|
|
|
237
389
|
Args:
|
|
238
390
|
name (str): The name of the named query to execute.
|
|
@@ -241,114 +393,28 @@ WHERE
|
|
|
241
393
|
limit(int): the query limit (if any)
|
|
242
394
|
|
|
243
395
|
Returns:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
Raises:
|
|
247
|
-
ValueError: If no named query matches the given name and namespace.
|
|
248
|
-
Exception: If the SPARQL query execution fails or the endpoint returns an error.
|
|
396
|
+
QueryBundle: named_query, query and endpoint
|
|
249
397
|
"""
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if not endpoint_name in self.endpoints:
|
|
398
|
+
named_query = self.lookup(name, namespace)
|
|
399
|
+
if endpoint_name not in self.endpoints:
|
|
253
400
|
msg = f"Invalid endpoint {endpoint_name}"
|
|
254
401
|
ValueError(msg)
|
|
255
402
|
endpoint = self.endpoints.get(endpoint_name)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
r"(limit|LIMIT)\s+(\d+)", f"LIMIT {limit}", sparql_query
|
|
265
|
-
)
|
|
266
|
-
else:
|
|
267
|
-
sparql_query += f"\nLIMIT {limit}"
|
|
268
|
-
lod = sparql.queryAsListOfDicts(sparql_query)
|
|
269
|
-
return lod
|
|
270
|
-
|
|
271
|
-
def formatted_query(self,
|
|
272
|
-
name: str,
|
|
273
|
-
namespace: str = "wikidata-examples",
|
|
274
|
-
endpoint_name: str = "wikidata",
|
|
275
|
-
r_format: Format = Format.html,
|
|
276
|
-
limit:int=None)->str:
|
|
277
|
-
"""
|
|
278
|
-
Execute a named SPARQL query using a specified endpoint and return the results formatted .
|
|
279
|
-
|
|
280
|
-
Args:
|
|
281
|
-
name (str): The name of the named query to execute.
|
|
282
|
-
namespace (str): The namespace of the named query, default is 'wikidata-examples'.
|
|
283
|
-
endpoint_name (str): The name of the endpoint to send the SPARQL query to, default is 'wikidata'.
|
|
284
|
-
r_format: (Format): the format to result should be returned in
|
|
285
|
-
limit(int): the query limit (if any)
|
|
286
|
-
Returns:
|
|
287
|
-
str: The results of the SPARQL query in the given format.
|
|
288
|
-
|
|
289
|
-
Raises:
|
|
290
|
-
ValueError: If no named query matches the given name and namespace.
|
|
291
|
-
Exception: If the SPARQL query execution fails or the endpoint returns an error.
|
|
292
|
-
"""
|
|
293
|
-
qlod = self.query(endpoint_name=endpoint_name, name=name,namespace=namespace,limit=limit)
|
|
294
|
-
formatted_result=self.format_result(qlod=qlod, query=self.query, r_format=r_format, endpoint_name=endpoint_name)
|
|
295
|
-
return formatted_result
|
|
296
|
-
|
|
297
|
-
def format_result(
|
|
298
|
-
self,
|
|
299
|
-
qlod: List[Dict[str, Any]],
|
|
300
|
-
query: Query,
|
|
301
|
-
r_format: Format,
|
|
302
|
-
endpoint_name: str = "wikidata",
|
|
303
|
-
) -> Optional[str]:
|
|
304
|
-
"""
|
|
305
|
-
Formats the query results based on the specified format and prints them.
|
|
306
|
-
|
|
307
|
-
Args:
|
|
308
|
-
qlod (List[Dict[str, Any]]): The list of dictionaries that represent the query results.
|
|
309
|
-
query (Query): The query object which contains details like the endpoint and the database.
|
|
310
|
-
r_format_str(Format): The format in which to print the results.
|
|
311
|
-
endpoint_name (str): The name of the endpoint to be used, defaults to "wikidata".
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
Optional[str]: The formatted string representation of the query results, or None if printed directly.
|
|
315
|
-
"""
|
|
316
|
-
# Retrieve endpoint configuration using the provided endpoint name
|
|
403
|
+
sparql_query = named_query.sparql
|
|
404
|
+
query = Query(
|
|
405
|
+
name=name,
|
|
406
|
+
query=sparql_query,
|
|
407
|
+
lang="sparql",
|
|
408
|
+
endpoint=endpoint.endpoint,
|
|
409
|
+
limit=limit,
|
|
410
|
+
)
|
|
317
411
|
endpointConf = self.endpoints.get(endpoint_name, Endpoint.getDefault())
|
|
318
412
|
query.tryItUrl = endpointConf.website
|
|
319
413
|
query.database = endpointConf.database
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
return doc.asText()
|
|
329
|
-
elif r_format == Format.json:
|
|
330
|
-
return json.dumps(qlod, indent=2, sort_keys=True, default=str)
|
|
331
|
-
return None # In case no format is matched or needed
|
|
332
|
-
|
|
333
|
-
def print_result(
|
|
334
|
-
self,
|
|
335
|
-
qlod: List[Dict[str, Any]],
|
|
336
|
-
query: Query,
|
|
337
|
-
r_format_str: str,
|
|
338
|
-
endpoint_name: str = "wikidata",
|
|
339
|
-
) -> None:
|
|
340
|
-
"""
|
|
341
|
-
Prints the formatted results of a query.
|
|
342
|
-
|
|
343
|
-
Args:
|
|
344
|
-
qlod (List[Dict[str, Any]]): The list of dictionaries that represent the query results.
|
|
345
|
-
query (Query): The query object which contains details such as the endpoint and the database.
|
|
346
|
-
r_format_str: The format in which to print the results.
|
|
347
|
-
endpoint_name (str): The name of the endpoint to be used, defaults to "wikidata".
|
|
348
|
-
|
|
349
|
-
Returns:
|
|
350
|
-
None
|
|
351
|
-
"""
|
|
352
|
-
result_str = self.format_result(qlod, query, r_format_str, endpoint_name)
|
|
353
|
-
if result_str is not None:
|
|
354
|
-
print(result_str)
|
|
414
|
+
query_bundle = QueryBundle(
|
|
415
|
+
named_query=named_query,
|
|
416
|
+
query=query,
|
|
417
|
+
endpoint=endpoint
|
|
418
|
+
)
|
|
419
|
+
query_bundle.set_limit(limit)
|
|
420
|
+
return query_bundle
|
|
@@ -3,14 +3,89 @@ Created on 2024-05-03
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import asyncio
|
|
7
8
|
from typing import List
|
|
8
9
|
|
|
9
10
|
from ngwidgets.input_webserver import InputWebSolution
|
|
10
11
|
from ngwidgets.lod_grid import ListOfDictsGrid
|
|
11
|
-
from
|
|
12
|
+
from ngwidgets.widgets import Link
|
|
13
|
+
from nicegui import ui, run, background_tasks
|
|
14
|
+
|
|
15
|
+
from snapquery.snapquery_core import NamedQuery, QueryBundle
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NamedQueryView:
|
|
19
|
+
"""
|
|
20
|
+
display a named Query
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
solution: InputWebSolution,
|
|
26
|
+
query_bundle: QueryBundle,
|
|
27
|
+
r_format_str: str = "html",
|
|
28
|
+
):
|
|
29
|
+
self.solution = solution
|
|
30
|
+
self.nqm = self.solution.nqm
|
|
31
|
+
self.query_bundle = query_bundle
|
|
32
|
+
self.r_format_str = r_format_str
|
|
33
|
+
self.load_task = None
|
|
34
|
+
self.limit = 200
|
|
35
|
+
self.timeout = 20.0
|
|
36
|
+
self.setup_ui()
|
|
37
|
+
|
|
38
|
+
def setup_ui(self):
|
|
39
|
+
"""
|
|
40
|
+
setup my user interface
|
|
41
|
+
"""
|
|
42
|
+
nq = self.query_bundle.named_query
|
|
43
|
+
url = self.query_bundle.query.tryItUrl
|
|
44
|
+
text = nq.title
|
|
45
|
+
tooltip = "try it!"
|
|
46
|
+
link = Link.create(url, text, tooltip, target="_blank")
|
|
47
|
+
with self.solution.container:
|
|
48
|
+
with ui.row() as self.query_parms_row:
|
|
49
|
+
ui.number(label="limit").bind_value(self, "limit")
|
|
50
|
+
ui.number(label="time out").bind_value(self, "timeout")
|
|
51
|
+
with ui.row() as self.query_row:
|
|
52
|
+
self.try_it_link = ui.html(link)
|
|
53
|
+
ui.label(nq.description)
|
|
54
|
+
ui.button(icon="play_arrow", on_click=self.run_query)
|
|
55
|
+
self.grid_row = ui.row()
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
async def load_query_results(self):
|
|
59
|
+
"""
|
|
60
|
+
(re) load the query results
|
|
61
|
+
"""
|
|
62
|
+
self.query_bundle.set_limit(int(self.limit))
|
|
63
|
+
lod = await run.io_bound(self.query_bundle.get_lod)
|
|
64
|
+
self.grid_row.clear()
|
|
65
|
+
with self.grid_row:
|
|
66
|
+
self.lod_grid = ListOfDictsGrid()
|
|
67
|
+
self.lod_grid.load_lod(lod)
|
|
68
|
+
self.grid_row.update()
|
|
69
|
+
|
|
70
|
+
async def run_query(self, _args):
|
|
71
|
+
"""
|
|
72
|
+
run the current query
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def cancel_running():
|
|
76
|
+
if self.load_task:
|
|
77
|
+
self.load_task.cancel()
|
|
12
78
|
|
|
13
|
-
|
|
79
|
+
self.grid_row.clear()
|
|
80
|
+
with self.grid_row:
|
|
81
|
+
ui.spinner()
|
|
82
|
+
self.grid_row.update()
|
|
83
|
+
# cancel task still running
|
|
84
|
+
cancel_running()
|
|
85
|
+
# cancel task if it takes too long
|
|
86
|
+
ui.timer(self.timeout, lambda: cancel_running(), once=True)
|
|
87
|
+
# run task in background
|
|
88
|
+
self.load_task = background_tasks.create(self.load_query_results())
|
|
14
89
|
|
|
15
90
|
|
|
16
91
|
class NamedQuerySearch:
|
|
@@ -24,7 +99,7 @@ class NamedQuerySearch:
|
|
|
24
99
|
self.search_debounce_task = None
|
|
25
100
|
self.keystroke_time = 0.65 # minimum time in seconds to wait between keystrokes before starting searching
|
|
26
101
|
self.setup_ui()
|
|
27
|
-
self.namespace = "
|
|
102
|
+
self.namespace = ""
|
|
28
103
|
self.name = ""
|
|
29
104
|
|
|
30
105
|
def setup_ui(self):
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
Created on 2024-05-03
|
|
3
3
|
@author: wf
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from fastapi import HTTPException
|
|
6
7
|
from fastapi.responses import HTMLResponse, PlainTextResponse
|
|
7
|
-
from lodstorage.query import
|
|
8
|
+
from lodstorage.query import Format
|
|
8
9
|
from ngwidgets.input_webserver import InputWebserver, InputWebSolution, WebserverConfig
|
|
9
|
-
from nicegui import app
|
|
10
|
+
from nicegui import app, ui
|
|
10
11
|
from nicegui.client import Client
|
|
11
12
|
|
|
12
13
|
from snapquery.snapquery_core import NamedQueryManager
|
|
13
|
-
from snapquery.snapquery_view import NamedQuerySearch
|
|
14
|
+
from snapquery.snapquery_view import NamedQuerySearch, NamedQueryView
|
|
14
15
|
from snapquery.version import Version
|
|
15
16
|
|
|
16
17
|
|
|
@@ -40,33 +41,68 @@ class SnapQueryWebServer(InputWebserver):
|
|
|
40
41
|
InputWebserver.__init__(self, config=SnapQueryWebServer.get_config())
|
|
41
42
|
self.nqm = NamedQueryManager.from_samples()
|
|
42
43
|
|
|
43
|
-
@
|
|
44
|
-
def
|
|
44
|
+
@ui.page("/query/{namespace}/{name}")
|
|
45
|
+
async def query_page(
|
|
46
|
+
client: Client,
|
|
47
|
+
namespace: str,
|
|
48
|
+
name: str,
|
|
49
|
+
endpoint_name: str = "wikidata",
|
|
50
|
+
limit: int = None,
|
|
51
|
+
format: str = "html",
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
show the query page for the given namespace and name
|
|
55
|
+
"""
|
|
56
|
+
return await self.page(
|
|
57
|
+
client,
|
|
58
|
+
SnapQuerySolution.query_page,
|
|
59
|
+
namespace=namespace,
|
|
60
|
+
name=name,
|
|
61
|
+
endpoint_name=endpoint_name,
|
|
62
|
+
limit=limit,
|
|
63
|
+
r_format_str=format,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@app.get("/api/sparql/{namespace}/{name}")
|
|
67
|
+
def sparql(
|
|
68
|
+
namespace: str,
|
|
69
|
+
name: str,
|
|
70
|
+
endpoint_name: str = "wikidata",
|
|
71
|
+
limit: int = None,
|
|
72
|
+
) -> PlainTextResponse:
|
|
45
73
|
"""
|
|
46
74
|
Gets a SPARQL query by name within a specified namespace
|
|
47
75
|
|
|
48
76
|
Args:
|
|
49
77
|
namespace (str): The namespace identifying the group or category of the query.
|
|
50
78
|
name (str): The specific name of the query to be executed.
|
|
51
|
-
|
|
79
|
+
endpoint_name(str): the name of the endpoint to use
|
|
80
|
+
limit(int): a limit to set, default=None
|
|
52
81
|
Returns:
|
|
53
82
|
HTMLResponse: The plain text SPARQL code
|
|
54
83
|
|
|
55
84
|
Raises:
|
|
56
85
|
HTTPException: If the query cannot be found or fails to execute.
|
|
57
86
|
"""
|
|
58
|
-
|
|
59
|
-
sparql_query =
|
|
87
|
+
qb = self.nqm.get_query(name, namespace, endpoint_name, limit)
|
|
88
|
+
sparql_query = qb.query.query
|
|
60
89
|
return PlainTextResponse(sparql_query)
|
|
61
90
|
|
|
62
|
-
@app.get("/query/{namespace}/{name}")
|
|
63
|
-
def query(
|
|
91
|
+
@app.get("/api/query/{namespace}/{name}")
|
|
92
|
+
def query(
|
|
93
|
+
namespace: str,
|
|
94
|
+
name: str,
|
|
95
|
+
endpoint_name: str = "wikidata",
|
|
96
|
+
limit: int = None,
|
|
97
|
+
) -> HTMLResponse:
|
|
64
98
|
"""
|
|
65
99
|
Executes a SPARQL query by name within a specified namespace, formats the results, and returns them as an HTML response.
|
|
66
100
|
|
|
67
101
|
Args:
|
|
68
102
|
namespace (str): The namespace identifying the group or category of the query.
|
|
69
103
|
name (str): The specific name of the query to be executed.
|
|
104
|
+
endpoint_name(str): the name of the endpoint to use
|
|
105
|
+
limit(int): a limit to set, default=None
|
|
70
106
|
|
|
71
107
|
Returns:
|
|
72
108
|
HTMLResponse: The HTML formatted response containing the results of the query execution.
|
|
@@ -74,16 +110,34 @@ class SnapQueryWebServer(InputWebserver):
|
|
|
74
110
|
Raises:
|
|
75
111
|
HTTPException: If the query cannot be found or fails to execute.
|
|
76
112
|
"""
|
|
77
|
-
content = self.query(
|
|
113
|
+
content = self.query(
|
|
114
|
+
namespace, name, endpoint_name=endpoint_name, limit=limit
|
|
115
|
+
)
|
|
78
116
|
if not content:
|
|
79
117
|
raise HTTPException(status_code=500, detail="Could not create result")
|
|
80
118
|
|
|
81
119
|
# Return the content as an HTML response
|
|
82
120
|
return HTMLResponse(content)
|
|
83
121
|
|
|
84
|
-
def query(
|
|
85
|
-
|
|
86
|
-
|
|
122
|
+
def query(
|
|
123
|
+
self,
|
|
124
|
+
namespace: str,
|
|
125
|
+
name: str,
|
|
126
|
+
endpoint_name: str = "wikidata",
|
|
127
|
+
limit: int = None,
|
|
128
|
+
) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Queries an external API to retrieve data based on a given namespace and name.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
namespace (str): The namespace to which the query belongs. It helps in categorizing the data.
|
|
134
|
+
name (str): The name identifier of the data to be queried.
|
|
135
|
+
endpoint_name (str): The name of the endpoint to be used for the query. Defaults to 'wikidata'.
|
|
136
|
+
limit(int): the limit for the query default: None
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
str: the content retrieved
|
|
140
|
+
"""
|
|
87
141
|
# content negotiation
|
|
88
142
|
# Determine response format by extension in the name or Accept header
|
|
89
143
|
if "." in name:
|
|
@@ -92,24 +146,16 @@ class SnapQueryWebServer(InputWebserver):
|
|
|
92
146
|
else:
|
|
93
147
|
r_format_str = "html"
|
|
94
148
|
|
|
95
|
-
# Retrieve the SPARQL query string using the namespace and name.
|
|
96
149
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
150
|
+
r_format = Format[r_format_str]
|
|
151
|
+
qb = self.nqm.get_query(name, namespace, endpoint_name, limit)
|
|
152
|
+
qlod = qb.get_lod()
|
|
153
|
+
content = qb.format_result(qlod, r_format)
|
|
154
|
+
return content
|
|
102
155
|
except Exception as e:
|
|
103
156
|
# Handling specific exceptions can be more detailed based on what nqm.get_sparql and nqm.query can raise
|
|
104
157
|
raise HTTPException(status_code=404, detail=str(e))
|
|
105
158
|
|
|
106
|
-
# Format the results and generate HTML content
|
|
107
|
-
r_format = Format[r_format_str]
|
|
108
|
-
content = self.nqm.format_result(
|
|
109
|
-
qlod, query, r_format, endpoint_name=endpoint_name
|
|
110
|
-
)
|
|
111
|
-
return content
|
|
112
|
-
|
|
113
159
|
|
|
114
160
|
class SnapQuerySolution(InputWebSolution):
|
|
115
161
|
"""
|
|
@@ -150,3 +196,19 @@ class SnapQuerySolution(InputWebSolution):
|
|
|
150
196
|
svg display
|
|
151
197
|
"""
|
|
152
198
|
await self.setup_content_div(self.setup_ui)
|
|
199
|
+
|
|
200
|
+
async def query_page(
|
|
201
|
+
self,
|
|
202
|
+
namespace: str,
|
|
203
|
+
name: str,
|
|
204
|
+
endpoint_name: str = "wikidata",
|
|
205
|
+
limit: int = None,
|
|
206
|
+
r_format_str: str = "html",
|
|
207
|
+
):
|
|
208
|
+
def show():
|
|
209
|
+
query_bundle = self.nqm.get_query(name, namespace, endpoint_name, limit)
|
|
210
|
+
self.named_query_view = NamedQueryView(
|
|
211
|
+
self, query_bundle=query_bundle, r_format_str=r_format_str
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
await self.setup_content_div(show)
|
|
@@ -3,6 +3,7 @@ Created on 2024-05-01
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
|
|
8
9
|
import snapquery
|
|
@@ -26,7 +27,7 @@ class Version:
|
|
|
26
27
|
chat_url = "https://github.com/WolfgangFahl/snapquery/discussions"
|
|
27
28
|
cm_url = "https://github.com/WolfgangFahl/snapquery"
|
|
28
29
|
|
|
29
|
-
license =
|
|
30
|
+
license = """Copyright 2024 contributors. All rights reserved.
|
|
30
31
|
|
|
31
32
|
Licensed under the Apache License 2.0
|
|
32
33
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2024-05-04
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from io import StringIO
|
|
10
|
+
|
|
11
|
+
from ngwidgets.basetest import Basetest
|
|
12
|
+
|
|
13
|
+
from snapquery import snapquery_cmd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestCommandLine(Basetest):
|
|
17
|
+
"""
|
|
18
|
+
test the command line handling
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def setUp(self, debug=False, profile=True):
|
|
22
|
+
Basetest.setUp(self, debug=debug, profile=profile)
|
|
23
|
+
|
|
24
|
+
def capture_stdout(self, callback, *args, **kwargs):
|
|
25
|
+
"""
|
|
26
|
+
Executes a callback with given arguments and captures stdout.
|
|
27
|
+
"""
|
|
28
|
+
capture = StringIO()
|
|
29
|
+
original_stdout = sys.stdout
|
|
30
|
+
sys.stdout = capture
|
|
31
|
+
try:
|
|
32
|
+
callback(*args, **kwargs)
|
|
33
|
+
return capture.getvalue()
|
|
34
|
+
finally:
|
|
35
|
+
sys.stdout = original_stdout
|
|
36
|
+
|
|
37
|
+
def test_list_endpoints(self):
|
|
38
|
+
"""
|
|
39
|
+
test listing endpoints
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def run_cmd():
|
|
44
|
+
snapquery_cmd.main(["--listEndpoints"])
|
|
45
|
+
|
|
46
|
+
# Capture the output of running the command
|
|
47
|
+
output = self.capture_stdout(run_cmd)
|
|
48
|
+
if self.debug:
|
|
49
|
+
print(output)
|
|
50
|
+
self.assertTrue("wikidata:https://query.wikidata.org" in output)
|
|
51
|
+
|
|
52
|
+
def test_namedquery(self):
|
|
53
|
+
"""
|
|
54
|
+
test a named query via command line
|
|
55
|
+
"""
|
|
56
|
+
limit = 10
|
|
57
|
+
|
|
58
|
+
def run_cmd():
|
|
59
|
+
snapquery_cmd.main(["-d", "-qn", "cats", "--limit", f"{limit}"])
|
|
60
|
+
|
|
61
|
+
# Capture the output of running the command
|
|
62
|
+
output = self.capture_stdout(run_cmd)
|
|
63
|
+
if self.debug:
|
|
64
|
+
print(output)
|
|
65
|
+
# Parse the JSON output
|
|
66
|
+
result = json.loads(output)
|
|
67
|
+
# Check that the result has exactly 10 entries
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
len(result), limit, "The number of results should be exactly 10."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check that each entry has 'item' and 'itemLabel'
|
|
73
|
+
for entry in result:
|
|
74
|
+
self.assertIn("item", entry, "Each entry must include an 'item'.")
|
|
75
|
+
self.assertIn("itemLabel", entry, "Each entry must include an 'itemLabel'.")
|
|
@@ -3,6 +3,7 @@ Created on 2024-05-03
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import json
|
|
7
8
|
|
|
8
9
|
from ngwidgets.basetest import Basetest
|
|
@@ -12,7 +13,7 @@ from snapquery.snapquery_core import NamedQueryManager
|
|
|
12
13
|
|
|
13
14
|
class TestNamedQueryManager(Basetest):
|
|
14
15
|
"""
|
|
15
|
-
test the named query
|
|
16
|
+
test the named query Manager
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
def setUp(self, debug=False, profile=True):
|
|
@@ -26,7 +27,8 @@ class TestNamedQueryManager(Basetest):
|
|
|
26
27
|
nqm = NamedQueryManager.from_samples(db_path=db_path)
|
|
27
28
|
for name, ex_count in [("x-invalid", -1), ("cats", 223)]:
|
|
28
29
|
try:
|
|
29
|
-
|
|
30
|
+
query_bundle = nqm.get_query(name)
|
|
31
|
+
lod = query_bundle.get_lod()
|
|
30
32
|
if self.debug:
|
|
31
33
|
print(f"{name}:")
|
|
32
34
|
print(json.dumps(lod, default=str, indent=2))
|
|
@@ -3,6 +3,7 @@ Created on 2024-05-03
|
|
|
3
3
|
|
|
4
4
|
@author: wf
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from ngwidgets.webserver_test import WebserverTest
|
|
7
8
|
|
|
8
9
|
from snapquery.snapquery_cmd import SnapQueryCmd
|
|
@@ -32,7 +33,7 @@ class TestRestFulApi(WebserverTest):
|
|
|
32
33
|
test the plaintext SPARQL API
|
|
33
34
|
"""
|
|
34
35
|
for example in ["cats", "horses"]:
|
|
35
|
-
path = f"/sparql/wikidata-examples/{example}"
|
|
36
|
+
path = f"/api/sparql/wikidata-examples/{example}?limit=10"
|
|
36
37
|
sparql_query = self.getHtml(path)
|
|
37
38
|
if self.debug:
|
|
38
39
|
print(sparql_query)
|
|
@@ -42,8 +43,8 @@ class TestRestFulApi(WebserverTest):
|
|
|
42
43
|
"""
|
|
43
44
|
test the RESTFul Query API
|
|
44
45
|
"""
|
|
45
|
-
for r_format in ["mediawiki", "github", "latex", "html"]:
|
|
46
|
-
path = f"/query/wikidata-examples/cats.{r_format}"
|
|
47
|
-
|
|
46
|
+
for r_format in ["mediawiki", "github", "latex", "html", "json"]:
|
|
47
|
+
path = f"/api/query/wikidata-examples/cats.{r_format}?limit=10"
|
|
48
|
+
result = self.getHtml(path)
|
|
48
49
|
if self.debug:
|
|
49
|
-
print(
|
|
50
|
+
print(result)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2024-05-04
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
from ngwidgets.basetest import Basetest
|
|
7
|
+
from snapquery.scholia import ScholiaQueries
|
|
8
|
+
class TestScholia(Basetest):
|
|
9
|
+
"""
|
|
10
|
+
test scholia queries
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def setUp(self, debug=True, profile=True):
|
|
14
|
+
Basetest.setUp(self, debug=debug, profile=profile)
|
|
15
|
+
|
|
16
|
+
def test_scholia_queries(self):
|
|
17
|
+
"""
|
|
18
|
+
test retrieving scholia queries
|
|
19
|
+
"""
|
|
20
|
+
if self.inPublicCI():
|
|
21
|
+
db_path = "/tmp/scholia_queries.db"
|
|
22
|
+
limit=10
|
|
23
|
+
else:
|
|
24
|
+
db_path=None
|
|
25
|
+
limit=None
|
|
26
|
+
nqm=ScholiaQueries.get(db_path,debug=self.debug,limit=limit)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2024-05-04
|
|
3
|
+
|
|
4
|
+
@author: tholzheim
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pprint
|
|
8
|
+
import re
|
|
9
|
+
import requests
|
|
10
|
+
import wikitextparser as wtp
|
|
11
|
+
from wikitextparser import Section, Template
|
|
12
|
+
from ngwidgets.basetest import Basetest
|
|
13
|
+
from snapquery.snapquery_core import NamedQuery, NamedQueryManager
|
|
14
|
+
|
|
15
|
+
class TestWdQueryParsing(Basetest):
|
|
16
|
+
"""
|
|
17
|
+
test wikidata query parsing
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def setUp(self, debug=True, profile=True):
|
|
21
|
+
Basetest.setUp(self, debug=debug, profile=profile)
|
|
22
|
+
self.base_url = "https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/queries/examples"
|
|
23
|
+
|
|
24
|
+
def _get_examples_wikitext(self) -> str:
|
|
25
|
+
"""Get wiki text with SPARQL query examples"""
|
|
26
|
+
res = requests.get(f"{self.base_url}?action=raw")
|
|
27
|
+
return res.text
|
|
28
|
+
|
|
29
|
+
def _extract_query_from_section(self, section: Section) -> NamedQuery:
|
|
30
|
+
"""
|
|
31
|
+
Extract named query from section
|
|
32
|
+
"""
|
|
33
|
+
query = self._get_sparql_template(section.templates)
|
|
34
|
+
named_query = None
|
|
35
|
+
if query:
|
|
36
|
+
desc = section.plain_text()
|
|
37
|
+
title = self._sanitize_title(section.title).strip()
|
|
38
|
+
named_query = NamedQuery(
|
|
39
|
+
namespace="wikidata-examples",
|
|
40
|
+
name=title,
|
|
41
|
+
title=title,
|
|
42
|
+
description=desc,
|
|
43
|
+
url=self.base_url,
|
|
44
|
+
sparql=query.arguments[0].value,
|
|
45
|
+
)
|
|
46
|
+
return named_query
|
|
47
|
+
|
|
48
|
+
def _sanitize_title(self, section_title: str) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Sanitize section title by removing translate tags
|
|
51
|
+
"""
|
|
52
|
+
pattern = r"""<translate><!--T:\d+-->(?P<name>[\w\d\s.\-,\\=<>/|{}()"'&]+)<\/translate>"""
|
|
53
|
+
match = re.search(pattern, section_title)
|
|
54
|
+
if match:
|
|
55
|
+
title = match.group("name")
|
|
56
|
+
else:
|
|
57
|
+
title = section_title
|
|
58
|
+
return title
|
|
59
|
+
|
|
60
|
+
def _get_sparql_template(self, templates: list[Template]) -> Template:
|
|
61
|
+
"""
|
|
62
|
+
Get sparql template of the list of templates
|
|
63
|
+
If the list contains more than one sparql template None is returned.
|
|
64
|
+
"""
|
|
65
|
+
queries = []
|
|
66
|
+
for template in templates:
|
|
67
|
+
if template.name == "SPARQL":
|
|
68
|
+
queries.append(template)
|
|
69
|
+
# ToDo: Handle sections with multiple queries
|
|
70
|
+
return queries[0] if len(queries) == 1 else None
|
|
71
|
+
|
|
72
|
+
def test_wikidata_examples_query_extraction(self):
|
|
73
|
+
"""
|
|
74
|
+
test extraction and parsing of wikidata example queries to NamedQueries
|
|
75
|
+
|
|
76
|
+
Queries can be found at https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/queries/examples
|
|
77
|
+
"""
|
|
78
|
+
wikitext = self._get_examples_wikitext()
|
|
79
|
+
parsed = wtp.parse(wikitext)
|
|
80
|
+
lod = []
|
|
81
|
+
for section in parsed.sections:
|
|
82
|
+
nq = self._extract_query_from_section(section)
|
|
83
|
+
if nq:
|
|
84
|
+
lod.append(nq.as_record())
|
|
85
|
+
if self.debug:
|
|
86
|
+
pprint.pprint(nq)
|
|
87
|
+
if self.debug:
|
|
88
|
+
print(f"found {len(lod)} queries")
|
|
89
|
+
nqm = NamedQueryManager.from_samples()
|
|
90
|
+
nqm.store(lod)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|