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.
Files changed (27) hide show
  1. {snapquery-0.0.3 → snapquery-0.0.4}/PKG-INFO +4 -2
  2. {snapquery-0.0.3 → snapquery-0.0.4}/pyproject.toml +4 -2
  3. snapquery-0.0.4/snapquery/__init__.py +1 -0
  4. snapquery-0.0.4/snapquery/scholia.py +49 -0
  5. {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_cmd.py +18 -7
  6. {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_core.py +204 -138
  7. {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_view.py +78 -3
  8. {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/snapquery_webserver.py +89 -27
  9. {snapquery-0.0.3 → snapquery-0.0.4}/snapquery/version.py +2 -1
  10. snapquery-0.0.4/tests/test_cmdline.py +75 -0
  11. {snapquery-0.0.3 → snapquery-0.0.4}/tests/test_endpoints.py +1 -0
  12. {snapquery-0.0.3 → snapquery-0.0.4}/tests/test_namedqueries.py +4 -2
  13. {snapquery-0.0.3 → snapquery-0.0.4}/tests/test_restful_api.py +6 -5
  14. snapquery-0.0.4/tests/test_scholia.py +26 -0
  15. snapquery-0.0.4/tests/test_wd_query_parsing.py +90 -0
  16. snapquery-0.0.3/snapquery/__init__.py +0 -1
  17. {snapquery-0.0.3 → snapquery-0.0.4}/.github/workflows/build.yml +0 -0
  18. {snapquery-0.0.3 → snapquery-0.0.4}/.github/workflows/upload-to-pypi.yml +0 -0
  19. {snapquery-0.0.3 → snapquery-0.0.4}/.gitignore +0 -0
  20. {snapquery-0.0.3 → snapquery-0.0.4}/.project +0 -0
  21. {snapquery-0.0.3 → snapquery-0.0.4}/.pydevproject +0 -0
  22. {snapquery-0.0.3 → snapquery-0.0.4}/LICENSE +0 -0
  23. {snapquery-0.0.3 → snapquery-0.0.4}/README.md +0 -0
  24. {snapquery-0.0.3 → snapquery-0.0.4}/scripts/blackisort +0 -0
  25. {snapquery-0.0.3 → snapquery-0.0.4}/scripts/install +0 -0
  26. {snapquery-0.0.3 → snapquery-0.0.4}/scripts/test +0 -0
  27. {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
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.4
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
- # https://pypi.org/project/pyLodStorage/
23
- "pyLodStorage>=0.10.4"
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, Query
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=f"Name of the endpoint to use for queries - use --listEndpoints to list available endpoints",
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", type=str, default="wikidata-examples", help="namespace to filter queries"
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
- formatted_result=nqm.formatted_query(name, namespace=namespace,endpoint_name=endpoint_name, r_format=r_format,limit=limit)
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
- sample_queries = cls.get_samples()
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
- list_of_records, name="NamedQuery", primaryKey="query_id"
274
+ sample_records, name="NamedQuery", primaryKey="query_id"
152
275
  )
153
- nqm.sql_db.createTable(list_of_records, "NamedQuery", withDrop=True)
154
- nqm.sql_db.store(list_of_records, entityInfo)
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 get_sparql(
202
- self,
203
- name: str,
204
- namespace: str = "wikidata-examples",
205
- endpoint_name: str = "wikidata",
206
- ) -> str:
207
- sql_query = f"""SELECT
208
- sparql
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
- sparql_query = query_records[0]["sparql"]
225
- return sparql_query
375
+ record = query_records[0]
376
+ named_query = NamedQuery.from_record(record)
377
+ return named_query
226
378
 
227
- def query(
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
- Execute a named SPARQL query using a specified endpoint and return the results.
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
- Dict[str, Any]: The results of the SPARQL query in JSON format.
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
- sparql_query = self.get_sparql(name, namespace, endpoint_name)
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
- self.query = Query(name=name, query=sparql_query, lang="sparql",limit=limit)
257
-
258
- sparql = SPARQL(endpoint.endpoint)
259
- if limit:
260
- # @TODO - this is to naive for cases where there are
261
- # SPARQL elements hat have a "limit" in the name e.g. "height_limit"
262
- if "limit" in sparql_query or "LIMIT" in sparql_query:
263
- sparql_query = re.sub(
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
- if r_format == Format.csv:
322
- csv_output = CSV.toCSV(qlod)
323
- return csv_output
324
- elif r_format in [Format.latex, Format.github, Format.mediawiki, Format.html]:
325
- doc = query.documentQueryResult(
326
- qlod, tablefmt=str(r_format), floatfmt=".0f"
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 nicegui import ui
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
- from snapquery.snapquery_core import NamedQuery
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 = "wikidata-examples"
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 Query, Format
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
- @app.get("/sparql/{namespace}/{name}")
44
- def sparql(namespace: str, name: str) -> PlainTextResponse:
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
- endpoint_name = "wikidata"
59
- sparql_query = self.nqm.get_sparql(name, namespace, endpoint_name)
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(namespace: str, name: str) -> HTMLResponse:
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(namespace, name)
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(self, namespace: str, name: str):
85
- """ """
86
- endpoint_name = "wikidata"
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
- sparql_query = self.nqm.get_sparql(name, namespace, endpoint_name)
98
- qlod = self.nqm.query(
99
- name=name, namespace=namespace, endpoint_name=endpoint_name
100
- )
101
- query = Query(name=name, query=sparql_query, lang="sparql")
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 = f"""Copyright 2024 contributors. All rights reserved.
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
  from lodstorage.query import EndpointManager
7
8
  from ngwidgets.basetest import Basetest
8
9
 
@@ -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 Manage
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
- lod = nqm.query(name)
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
- html = self.getHtml(path)
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(html)
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