snapquery 0.0.2__tar.gz → 0.0.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snapquery
3
- Version: 0.0.2
3
+ Version: 0.0.3
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
@@ -0,0 +1 @@
1
+ __version__ = "0.0.3"
@@ -42,6 +42,12 @@ class SnapQueryCmd(WebserverCmd):
42
42
  action="store_true",
43
43
  help="show the list of available endpoints",
44
44
  )
45
+ parser.add_argument(
46
+ "--limit", type=int, default=None, help="set limit parameter of query"
47
+ )
48
+ parser.add_argument(
49
+ "--namespace", type=str, default="wikidata-examples", help="namespace to filter queries"
50
+ )
45
51
  parser.add_argument("-qn", "--queryName", help="run a named query")
46
52
  parser.add_argument("-f", "--format", type=Format, choices=list(Format))
47
53
  return parser
@@ -61,14 +67,14 @@ class SnapQueryCmd(WebserverCmd):
61
67
  handled = True # Operation handled
62
68
  elif self.args.queryName is not None:
63
69
  nqm = NamedQueryManager()
70
+ namespace=self.args.namespace
64
71
  name = self.args.queryName
65
72
  endpoint_name = self.args.endpointName
66
73
  r_format = self.args.format
67
- sparql_query = nqm.get_sparql(name=name, endpoint_name=endpoint_name)
68
- qlod = nqm.query(endpoint_name=endpoint_name, name=name)
69
- query = Query(name=name, query=sparql_query, lang="sparql")
70
- nqm.format_result(qlod, query, r_format, endpoint_name=endpoint_name)
71
- return handled
74
+ limit=self.args.limit
75
+ formatted_result=nqm.formatted_query(name, namespace=namespace,endpoint_name=endpoint_name, r_format=r_format,limit=limit)
76
+ print(formatted_result)
77
+ return handled
72
78
 
73
79
 
74
80
  def main(argv: list = None):
@@ -4,6 +4,7 @@ Created on 2024-05-03
4
4
  @author: wf
5
5
  """
6
6
  import json
7
+ import re
7
8
  import os
8
9
  from dataclasses import field
9
10
  from pathlib import Path
@@ -14,6 +15,7 @@ from lodstorage.query import Endpoint, EndpointManager, Format, Query
14
15
  from lodstorage.sparql import SPARQL
15
16
  from lodstorage.sql import SQLDB, EntityInfo
16
17
  from lodstorage.yamlable import lod_storable
18
+ from ngwidgets.widgets import Link
17
19
 
18
20
 
19
21
  @lod_storable
@@ -50,6 +52,37 @@ class NamedQuery:
50
52
  """
51
53
  self.query_id = f"{self.namespace}.{self.name}"
52
54
 
55
+ def as_link(self) -> str:
56
+ url = f"/query/{self.namespace}/{self.name}"
57
+ text = self.name
58
+ tooltip = "query details"
59
+ link = Link.create(url, text, tooltip)
60
+ return link
61
+
62
+ @classmethod
63
+ def from_record(cls, record: Dict) -> "NamedQuery":
64
+ """
65
+ Class method to instantiate NamedQuery from a dictionary record.
66
+ """
67
+ return cls(
68
+ namespace=record["namespace"],
69
+ name=record["name"],
70
+ title=record["title"],
71
+ description=record["description"],
72
+ sparql=record["sparql"],
73
+ )
74
+
75
+ def as_viewrecord(self) -> Dict:
76
+ """
77
+ Return a dictionary representing the NamedQuery with keys ordered as Name, Namespace, Title, Description.
78
+ """
79
+ return {
80
+ "name": self.as_link(),
81
+ "namespace": self.namespace,
82
+ "title": self.title,
83
+ "description": self.description,
84
+ }
85
+
53
86
 
54
87
  class NamedQueryManager:
55
88
  """
@@ -72,7 +105,7 @@ class NamedQueryManager:
72
105
  if db_path is None:
73
106
  db_path = NamedQueryManager.get_cache_path()
74
107
  self.debug = debug
75
- self.sql_db = SQLDB(dbname=db_path,check_same_thread=False, debug=debug)
108
+ self.sql_db = SQLDB(dbname=db_path, check_same_thread=False, debug=debug)
76
109
  self.endpoints = EndpointManager.getEndpoints(lang="sparql")
77
110
 
78
111
  @classmethod
@@ -196,6 +229,7 @@ WHERE
196
229
  name: str,
197
230
  namespace: str = "wikidata-examples",
198
231
  endpoint_name: str = "wikidata",
232
+ limit:int=None
199
233
  ):
200
234
  """
201
235
  Execute a named SPARQL query using a specified endpoint and return the results.
@@ -204,6 +238,7 @@ WHERE
204
238
  name (str): The name of the named query to execute.
205
239
  namespace (str): The namespace of the named query, default is 'wikidata-examples'.
206
240
  endpoint_name (str): The name of the endpoint to send the SPARQL query to, default is 'wikidata'.
241
+ limit(int): the query limit (if any)
207
242
 
208
243
  Returns:
209
244
  Dict[str, Any]: The results of the SPARQL query in JSON format.
@@ -218,15 +253,52 @@ WHERE
218
253
  msg = f"Invalid endpoint {endpoint_name}"
219
254
  ValueError(msg)
220
255
  endpoint = self.endpoints.get(endpoint_name)
256
+ self.query = Query(name=name, query=sparql_query, lang="sparql",limit=limit)
257
+
221
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}"
222
268
  lod = sparql.queryAsListOfDicts(sparql_query)
223
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
224
296
 
225
297
  def format_result(
226
298
  self,
227
299
  qlod: List[Dict[str, Any]],
228
300
  query: Query,
229
- r_format_str: str,
301
+ r_format: Format,
230
302
  endpoint_name: str = "wikidata",
231
303
  ) -> Optional[str]:
232
304
  """
@@ -235,7 +307,7 @@ WHERE
235
307
  Args:
236
308
  qlod (List[Dict[str, Any]]): The list of dictionaries that represent the query results.
237
309
  query (Query): The query object which contains details like the endpoint and the database.
238
- r_format_str: The format in which to print the results.
310
+ r_format_str(Format): The format in which to print the results.
239
311
  endpoint_name (str): The name of the endpoint to be used, defaults to "wikidata".
240
312
 
241
313
  Returns:
@@ -245,7 +317,6 @@ WHERE
245
317
  endpointConf = self.endpoints.get(endpoint_name, Endpoint.getDefault())
246
318
  query.tryItUrl = endpointConf.website
247
319
  query.database = endpointConf.database
248
- r_format = Format[r_format_str]
249
320
 
250
321
  if r_format == Format.csv:
251
322
  csv_output = CSV.toCSV(qlod)
@@ -0,0 +1,95 @@
1
+ """
2
+ Created on 2024-05-03
3
+
4
+ @author: wf
5
+ """
6
+ import asyncio
7
+ from typing import List
8
+
9
+ from ngwidgets.input_webserver import InputWebSolution
10
+ from ngwidgets.lod_grid import ListOfDictsGrid
11
+ from nicegui import ui
12
+
13
+ from snapquery.snapquery_core import NamedQuery
14
+
15
+
16
+ class NamedQuerySearch:
17
+ """
18
+ search for namedqueries
19
+ """
20
+
21
+ def __init__(self, solution: InputWebSolution):
22
+ self.solution = solution
23
+ self.nqm = self.solution.nqm
24
+ self.search_debounce_task = None
25
+ self.keystroke_time = 0.65 # minimum time in seconds to wait between keystrokes before starting searching
26
+ self.setup_ui()
27
+ self.namespace = "wikidata-examples"
28
+ self.name = ""
29
+
30
+ def setup_ui(self):
31
+ """
32
+ setup my user interface
33
+ """
34
+ with ui.row() as self.search_row:
35
+ self.namespace_search_input = (
36
+ ui.input(label="namespace", on_change=self.on_search_change)
37
+ .bind_value(self, "namespace")
38
+ .props("size=80")
39
+ )
40
+ self.name_search_input = (
41
+ ui.input(label="name", on_change=self.on_search_change)
42
+ .bind_value(self, "name")
43
+ .props("size=80")
44
+ )
45
+ with ui.row() as self.search_result_row:
46
+ self.search_result_grid = ListOfDictsGrid()
47
+
48
+ async def on_search_change(self, _args):
49
+ """
50
+ react on changes in the search input
51
+ """
52
+ # Cancel the existing search task if it's still waiting
53
+ if self.search_debounce_task:
54
+ self.search_debounce_task.cancel()
55
+
56
+ # Create a new task for the new search
57
+ self.search_debounce_task = asyncio.create_task(self.debounced_search())
58
+
59
+ async def debounced_search(self):
60
+ """
61
+ Waits for a period of inactivity and then performs the search.
62
+ """
63
+ try:
64
+ # Wait for the debounce period (keystroke_time)
65
+ await asyncio.sleep(self.keystroke_time)
66
+ sql_query = """SELECT
67
+ *
68
+ FROM NamedQuery
69
+ WHERE name LIKE ? and namespace LIKE? """
70
+ name_like = f"{self.name}%"
71
+ namespace_like = f"{self.namespace}%"
72
+ self.q_lod = self.nqm.sql_db.query(sql_query, (name_like, namespace_like))
73
+ self.show_lod(self.q_lod)
74
+
75
+ except asyncio.CancelledError:
76
+ # The search was cancelled because of new input, so just quietly exit
77
+ pass
78
+ except Exception as ex:
79
+ self.solution.handle_exception(ex)
80
+
81
+ def show_lod(self, q_lod: List):
82
+ """
83
+ show the given list of dicts
84
+ """
85
+ view_lod = []
86
+ for record in self.q_lod:
87
+ nq = NamedQuery.from_record(record)
88
+ vr = nq.as_viewrecord()
89
+ view_lod.append(vr)
90
+ if self.search_result_row:
91
+ with self.search_result_row:
92
+ ui.notify(f"found {len(q_lod)} queries")
93
+ self.search_result_grid.load_lod(view_lod)
94
+ # self.search_result_grid.set_checkbox_selection("#")
95
+ self.search_result_grid.update()
@@ -2,14 +2,15 @@
2
2
  Created on 2024-05-03
3
3
  @author: wf
4
4
  """
5
- from fastapi import Request, Header,HTTPException
6
- from fastapi.responses import HTMLResponse
7
- from lodstorage.query import Query
5
+ from fastapi import HTTPException
6
+ from fastapi.responses import HTMLResponse, PlainTextResponse
7
+ from lodstorage.query import Query, Format
8
8
  from ngwidgets.input_webserver import InputWebserver, InputWebSolution, WebserverConfig
9
9
  from nicegui import app
10
10
  from nicegui.client import Client
11
11
 
12
12
  from snapquery.snapquery_core import NamedQueryManager
13
+ from snapquery.snapquery_view import NamedQuerySearch
13
14
  from snapquery.version import Version
14
15
 
15
16
 
@@ -39,8 +40,27 @@ class SnapQueryWebServer(InputWebserver):
39
40
  InputWebserver.__init__(self, config=SnapQueryWebServer.get_config())
40
41
  self.nqm = NamedQueryManager.from_samples()
41
42
 
42
- @app.get("/query/{namespace}/{name}", response_class=HTMLResponse)
43
- def query(namespace: str, name: str):
43
+ @app.get("/sparql/{namespace}/{name}")
44
+ def sparql(namespace: str, name: str) -> PlainTextResponse:
45
+ """
46
+ Gets a SPARQL query by name within a specified namespace
47
+
48
+ Args:
49
+ namespace (str): The namespace identifying the group or category of the query.
50
+ name (str): The specific name of the query to be executed.
51
+
52
+ Returns:
53
+ HTMLResponse: The plain text SPARQL code
54
+
55
+ Raises:
56
+ HTTPException: If the query cannot be found or fails to execute.
57
+ """
58
+ endpoint_name = "wikidata"
59
+ sparql_query = self.nqm.get_sparql(name, namespace, endpoint_name)
60
+ return PlainTextResponse(sparql_query)
61
+
62
+ @app.get("/query/{namespace}/{name}")
63
+ def query(namespace: str, name: str) -> HTMLResponse:
44
64
  """
45
65
  Executes a SPARQL query by name within a specified namespace, formats the results, and returns them as an HTML response.
46
66
 
@@ -54,22 +74,23 @@ class SnapQueryWebServer(InputWebserver):
54
74
  Raises:
55
75
  HTTPException: If the query cannot be found or fails to execute.
56
76
  """
57
- content=self.query(namespace,name)
77
+ content = self.query(namespace, name)
58
78
  if not content:
59
79
  raise HTTPException(status_code=500, detail="Could not create result")
60
80
 
61
81
  # Return the content as an HTML response
62
82
  return HTMLResponse(content)
63
-
64
- def query(self,namespace:str,name:str):
83
+
84
+ def query(self, namespace: str, name: str):
85
+ """ """
65
86
  endpoint_name = "wikidata"
66
87
  # content negotiation
67
88
  # Determine response format by extension in the name or Accept header
68
- if '.' in name:
69
- r_format = name.split('.')[-1]
70
- name = name[:name.rfind('.')]
89
+ if "." in name:
90
+ r_format_str = name.split(".")[-1]
91
+ name = name[: name.rfind(".")]
71
92
  else:
72
- r_format = "html"
93
+ r_format_str = "html"
73
94
 
74
95
  # Retrieve the SPARQL query string using the namespace and name.
75
96
  try:
@@ -83,6 +104,7 @@ class SnapQueryWebServer(InputWebserver):
83
104
  raise HTTPException(status_code=404, detail=str(e))
84
105
 
85
106
  # Format the results and generate HTML content
107
+ r_format = Format[r_format_str]
86
108
  content = self.nqm.format_result(
87
109
  qlod, query, r_format, endpoint_name=endpoint_name
88
110
  )
@@ -104,3 +126,27 @@ class SnapQuerySolution(InputWebSolution):
104
126
  client (Client): The client instance this context is associated with.
105
127
  """
106
128
  super().__init__(webserver, client) # Call to the superclass constructor
129
+ self.nqm = self.webserver.nqm
130
+ self.endpoint_name = "wikidata"
131
+
132
+ def configure_settings(self):
133
+ """
134
+ add additional settings
135
+ """
136
+ self.add_select("default Endpoint", list(self.nqm.endpoints.keys())).bind_value(
137
+ self, "endpoint_name"
138
+ )
139
+
140
+ def setup_ui(self):
141
+ """
142
+ setup my user interface
143
+ """
144
+ self.search = NamedQuerySearch(self)
145
+
146
+ async def home(
147
+ self,
148
+ ):
149
+ """Generates the home page with a selection of examples and
150
+ svg display
151
+ """
152
+ await self.setup_content_div(self.setup_ui)
@@ -17,7 +17,7 @@ class Version:
17
17
  name = "snapquery"
18
18
  version = snapquery.__version__
19
19
  date = "2024-05-03"
20
- updated = "2024-05-03"
20
+ updated = "2024-05-04"
21
21
  description = "Introduce Named Queries and Named Query Middleware to wikidata"
22
22
 
23
23
  authors = "Wolfgang Fahl"
@@ -3,21 +3,22 @@ Created on 2024-05-03
3
3
 
4
4
  @author: wf
5
5
  """
6
- from snapquery.snapquery_webserver import SnapQueryWebServer
7
- from snapquery.snapquery_cmd import SnapQueryCmd
8
6
  from ngwidgets.webserver_test import WebserverTest
9
7
 
8
+ from snapquery.snapquery_cmd import SnapQueryCmd
9
+ from snapquery.snapquery_webserver import SnapQueryWebServer
10
10
 
11
- class TestRestFullApi(WebserverTest):
11
+
12
+ class TestRestFulApi(WebserverTest):
12
13
  """
13
14
  test endpoint handling according to https://github.com/WolfgangFahl/snapquery/issues/1
14
15
  """
16
+
15
17
  def setUp(self, debug=True, profile=True):
16
18
  server_class = SnapQueryWebServer
17
19
  cmd_class = SnapQueryCmd
18
20
  WebserverTest.setUp(self, server_class, cmd_class, debug=debug, profile=profile)
19
21
 
20
-
21
22
  def testDemoWebserver(self):
22
23
  """
23
24
  test API docs access
@@ -25,10 +26,24 @@ class TestRestFullApi(WebserverTest):
25
26
  # self.debug=True
26
27
  html = self.getHtml("/docs")
27
28
  self.assertTrue("Swagger" in html)
28
-
29
+
30
+ def testSparqlApi(self):
31
+ """
32
+ test the plaintext SPARQL API
33
+ """
34
+ for example in ["cats", "horses"]:
35
+ path = f"/sparql/wikidata-examples/{example}"
36
+ sparql_query = self.getHtml(path)
37
+ if self.debug:
38
+ print(sparql_query)
39
+ self.assertTrue("SELECT" in sparql_query)
40
+
29
41
  def testQueryApi(self):
30
- for r_format in ["mediawiki","github","latex","html"]:
31
- path=f"/query/wikidata-examples/cats.{r_format}"
32
- html=self.getHtml(path)
42
+ """
43
+ test the RESTFul Query API
44
+ """
45
+ for r_format in ["mediawiki", "github", "latex", "html"]:
46
+ path = f"/query/wikidata-examples/cats.{r_format}"
47
+ html = self.getHtml(path)
33
48
  if self.debug:
34
49
  print(html)
@@ -1 +0,0 @@
1
- __version__ = "0.0.2"
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