snapquery 0.2.0__tar.gz → 0.2.1__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 (118) hide show
  1. {snapquery-0.2.0 → snapquery-0.2.1}/PKG-INFO +1 -1
  2. snapquery-0.2.1/snapquery/__init__.py +1 -0
  3. snapquery-0.2.1/snapquery/github_access.py +119 -0
  4. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/endpoints.yaml +25 -0
  5. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/endpoints_optional.yaml +0 -24
  6. snapquery-0.2.1/snapquery/sib_sparql_examples.py +167 -0
  7. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_cmd.py +59 -16
  8. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_core.py +1 -1
  9. snapquery-0.2.1/tests/test_sib_examples.py +74 -0
  10. snapquery-0.2.0/snapquery/__init__.py +0 -1
  11. snapquery-0.2.0/snapquery/github_access.py +0 -46
  12. snapquery-0.2.0/tests/test_sib_examples.py +0 -35
  13. {snapquery-0.2.0 → snapquery-0.2.1}/.github/workflows/build.yml +0 -0
  14. {snapquery-0.2.0 → snapquery-0.2.1}/.github/workflows/upload-to-pypi.yml +0 -0
  15. {snapquery-0.2.0 → snapquery-0.2.1}/.gitignore +0 -0
  16. {snapquery-0.2.0 → snapquery-0.2.1}/.project +0 -0
  17. {snapquery-0.2.0 → snapquery-0.2.1}/.pydevproject +0 -0
  18. {snapquery-0.2.0 → snapquery-0.2.1}/LICENSE +0 -0
  19. {snapquery-0.2.0 → snapquery-0.2.1}/README.md +0 -0
  20. {snapquery-0.2.0 → snapquery-0.2.1}/docs/index.md +0 -0
  21. {snapquery-0.2.0 → snapquery-0.2.1}/mkdocs.yml +0 -0
  22. {snapquery-0.2.0 → snapquery-0.2.1}/pyproject.toml +0 -0
  23. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/blackisort +0 -0
  24. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/doc +0 -0
  25. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/fixdomain +0 -0
  26. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/install +0 -0
  27. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/qlv +0 -0
  28. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/release +0 -0
  29. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/restore_db +0 -0
  30. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/test +0 -0
  31. {snapquery-0.2.0 → snapquery-0.2.1}/scripts/test_queries +0 -0
  32. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/authorization.py +0 -0
  33. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/basequeryview.py +0 -0
  34. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/ceurws.py +0 -0
  35. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/dblp.py +0 -0
  36. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/error_filter.py +0 -0
  37. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/execution.py +0 -0
  38. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/graph.py +0 -0
  39. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/models/__init__.py +0 -0
  40. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/models/person.py +0 -0
  41. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/models/sparql_components.py +0 -0
  42. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/mwlogin.py +0 -0
  43. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/namespace_stats_view.py +0 -0
  44. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/orcid.py +0 -0
  45. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/params_view.py +0 -0
  46. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/person_selector.py +0 -0
  47. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/pid.py +0 -0
  48. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/pid_lookup.py +0 -0
  49. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/prefix_merger.py +0 -0
  50. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/qimport.py +0 -0
  51. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/qimport_view.py +0 -0
  52. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/qlever.py +0 -0
  53. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/query_annotate.py +0 -0
  54. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/query_selector.py +0 -0
  55. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/resources/sparql_language.json +0 -0
  56. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/WikiProject_LSEThesisProject.wiki +0 -0
  57. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/ceur-ws.json +0 -0
  58. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/dblp.json +0 -0
  59. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/federated-query-examples.json +0 -0
  60. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/graphs.yaml +0 -0
  61. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/meta_query.yaml +0 -0
  62. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/orkg.json +0 -0
  63. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/qlever.json +0 -0
  64. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/qlever_performance.json +0 -0
  65. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/query_stats.yaml +0 -0
  66. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/scholia.json +0 -0
  67. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/snapquery.json +0 -0
  68. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-WikidataThesisToolkit.json +0 -0
  69. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-WikidataThesisToolkitAlt.json +0 -0
  70. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-examples.json +0 -0
  71. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-short-urls.json +0 -0
  72. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/scholia.py +0 -0
  73. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_view.py +0 -0
  74. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_webserver.py +0 -0
  75. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/sparql_analyzer.py +0 -0
  76. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/stats_view.py +0 -0
  77. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/version.py +0 -0
  78. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/wd_page_query_extractor.py +0 -0
  79. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/wd_short_url.py +0 -0
  80. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery.puml +0 -0
  81. {snapquery-0.2.0 → snapquery-0.2.1}/snapquery.yaml +0 -0
  82. {snapquery-0.2.0 → snapquery-0.2.1}/tests/__init__.py +0 -0
  83. {snapquery-0.2.0 → snapquery-0.2.1}/tests/query_extractors/__init__.py +0 -0
  84. {snapquery-0.2.0 → snapquery-0.2.1}/tests/query_extractors/sparql_standard_queries.py +0 -0
  85. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/ask_unsupported.txt +0 -0
  86. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/invalid_literal.txt +0 -0
  87. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/invalid_regex_func.txt +0 -0
  88. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/memory_limit.txt +0 -0
  89. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/timeout.txt +0 -0
  90. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/triplydb/parser_error.txt +0 -0
  91. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/duplicate_prefix.txt +0 -0
  92. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/invalid_iri_declaration.txt +0 -0
  93. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/invalid_sparql_syntax.txt +0 -0
  94. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/timeout.txt +0 -0
  95. {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/undefined_prefix.txt +0 -0
  96. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_authorization.py +0 -0
  97. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_ceurws.py +0 -0
  98. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_cmdline.py +0 -0
  99. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_endpoints.py +0 -0
  100. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_error_filter.py +0 -0
  101. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_graph.py +0 -0
  102. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_import.py +0 -0
  103. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_namedqueries.py +0 -0
  104. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_oauth.py +0 -0
  105. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_orcid.py +0 -0
  106. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_pids.py +0 -0
  107. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_prefix_merger.py +0 -0
  108. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_qlever.py +0 -0
  109. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_query_annotate.py +0 -0
  110. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_query_execution.py +0 -0
  111. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_query_name.py +0 -0
  112. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_restful_api.py +0 -0
  113. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_scholia.py +0 -0
  114. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_scholia_q.py +0 -0
  115. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_sparql_analyzer.py +0 -0
  116. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_sparql_components.py +0 -0
  117. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_wd_query_parsing.py +0 -0
  118. {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_wd_short_url.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snapquery
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Just Query wikidata and other SPARQL endpoints by query name - a frontend to Introduce Named Queries and Named Query Middleware to wikidata and other SPARQL endpoints
5
5
  Project-URL: Home, https://github.com/WolfgangFahl/snapquery
6
6
  Project-URL: Documentation, https://wiki.bitplan.com/index.php/snapquery
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
@@ -0,0 +1,119 @@
1
+ """
2
+ GitHub API client for accessing repository contents.
3
+
4
+ Created on 2025-12-02
5
+ @author: wf
6
+ """
7
+ import json
8
+ from pathlib import Path
9
+ from typing import List, Dict, Any, Optional, Union
10
+
11
+ import requests
12
+
13
+
14
+ class GitHub:
15
+ """
16
+ A simple GitHub API client for accessing repository contents.
17
+ """
18
+
19
+ def __init__(self, owner: str, repo: str, token: Optional[str] = None, session: Optional[requests.Session] = None):
20
+ """
21
+ Initialize GitHub client.
22
+
23
+ Args:
24
+ owner: Repository owner (username or organization)
25
+ repo: Repository name
26
+ token: Optional GitHub API token for authentication
27
+ session: Optional custom requests.Session
28
+ """
29
+ self.owner = owner
30
+ self.repo = repo
31
+ self.token = token
32
+ self.base_url = f"https://api.github.com/repos/{owner}/{repo}"
33
+ # Use provided token or read from file (compatible with GitHubApi)
34
+ self.token = token if token is not None else self._read_token()
35
+
36
+ # Use custom session or create new one
37
+ self.session = session or requests.Session()
38
+
39
+
40
+
41
+ def _headers(self) -> Dict[str, str]:
42
+ headers = {"Accept": "application/vnd.github.v3+json"}
43
+ if self.token:
44
+ headers["Authorization"] = f"token {self.token}"
45
+ return headers
46
+
47
+ def _read_token(self) -> Optional[str]:
48
+ """
49
+ Read GitHub token from ~/.github/access_token.json
50
+ (compatible with GitHubApi token storage format).
51
+
52
+ Returns:
53
+ GitHub token or None if not found
54
+ """
55
+ token_path = Path.home() / ".github" / "access_token.json"
56
+ if token_path.exists():
57
+ try:
58
+ with open(token_path, 'r') as f:
59
+ data = json.load(f)
60
+ return data.get("access_token")
61
+ except (json.JSONDecodeError, OSError):
62
+ pass
63
+ return None
64
+
65
+
66
+ def get_contents(self, path: str = "") -> Union[List[Dict[str, Any]], Dict[str, Any]]:
67
+ """
68
+ Get contents of a directory or file from the repository.
69
+
70
+ Args:
71
+ path: Path within the repository
72
+
73
+ Returns:
74
+ List of dictionaries for directories, or content for files
75
+ """
76
+ url = f"{self.base_url}/contents/{path}"
77
+ response = self.session.get(url, headers=self._headers(), timeout=30)
78
+ response.raise_for_status()
79
+ return response.json()
80
+
81
+ def list_files_recursive(self, path: str = "", suffix: Optional[str] = None) -> List[Dict[str, Any]]:
82
+ """
83
+ Recursively list files under a given path. Optionally filter by file suffix.
84
+
85
+ Args:
86
+ path: starting path within the repository
87
+ suffix: optional filename suffix filter, e.g., ".ttl"
88
+
89
+ Returns:
90
+ A flat list of GitHub content item dicts for files.
91
+ """
92
+ items = self.get_contents(path)
93
+ if isinstance(items, dict):
94
+ items = [items]
95
+
96
+ files: List[Dict[str, Any]] = []
97
+ for item in items:
98
+ item_type = item.get("type")
99
+ item_path = item.get("path", "")
100
+ if item_type == "file":
101
+ if suffix is None or item_path.endswith(suffix):
102
+ files.append(item)
103
+ elif item_type == "dir":
104
+ files.extend(self.list_files_recursive(item_path, suffix=suffix))
105
+ return files
106
+
107
+ def download(self, download_url: str) -> str:
108
+ """
109
+ Download raw file content via a download_url.
110
+
111
+ Args:
112
+ download_url: The GitHub-provided raw download URL
113
+
114
+ Returns:
115
+ The text content of the file
116
+ """
117
+ response = self.session.get(download_url, headers=self._headers(), timeout=30)
118
+ response.raise_for_status()
119
+ return response.text
@@ -76,6 +76,31 @@ endpoints:
76
76
  - blazegraph
77
77
  - commons
78
78
 
79
+ wikidata-qlever:
80
+ description: QLever Freiburg Wikidata.
81
+ lang: sparql
82
+ method: POST
83
+ database: qlever
84
+ endpoint: https://qlever.dev/api/wikidata
85
+ website: https://qlever.dev/wikidata
86
+ calls_per_minute: 60
87
+ prefix_sets:
88
+ - rdf
89
+ - wikidata
90
+ - commons
91
+
92
+ wikidata-qlever-dbis:
93
+ description: QLever RWTH Wikidata.
94
+ lang: sparql
95
+ method: POST
96
+ database: qlever
97
+ endpoint: https://qlever-api.wikidata.dbis.rwth-aachen.de/
98
+ website: https://qlever.wikidata.dbis.rwth-aachen.de/wikidata
99
+ calls_per_minute: 60
100
+ prefix_sets:
101
+ - rdf
102
+ - wikidata
103
+
79
104
  dblp:
80
105
  description: DBLP official (qlever - reliable).
81
106
  lang: sparql
@@ -26,30 +26,6 @@ endpoints:
26
26
  - rdf
27
27
  - wikidata
28
28
 
29
- wikidata-qlever:
30
- description: QLever Freiburg Wikidata.
31
- lang: sparql
32
- method: POST
33
- database: qlever
34
- endpoint: https://qlever.cs.uni-freiburg.de/api/wikidata
35
- website: https://qlever.cs.uni-freiburg.de/wikidata
36
- calls_per_minute: 60
37
- prefix_sets:
38
- - rdf
39
- - wikidata
40
- - commons
41
-
42
- wikidata-qlever-dbis:
43
- description: QLever RWTH Wikidata.
44
- lang: sparql
45
- method: POST
46
- database: qlever
47
- endpoint: https://qlever-api.wikidata.dbis.rwth-aachen.de/
48
- website: https://qlever.wikidata.dbis.rwth-aachen.de/wikidata
49
- calls_per_minute: 60
50
- prefix_sets:
51
- - rdf
52
- - wikidata
53
29
 
54
30
  wikidata-scatter:
55
31
  description: Scatter experimental.
@@ -0,0 +1,167 @@
1
+ """
2
+ SIB SPARQL Examples Fetcher
3
+ Refactored to use a reusable GitHub API client and improved rdflib parsing.
4
+
5
+ Handles nested /examples/<service>/<NNN>.ttl structure.
6
+ Extracts sh:select, rdfs:comment, schema:target/labels as NamedQuery.
7
+ Exports to YAML (user request).
8
+
9
+ Modified: 2025-12-02 by wf
10
+ """
11
+
12
+ import os
13
+ from typing import Optional, List, Dict, Any, Set
14
+
15
+ import rdflib
16
+
17
+ from snapquery.github_access import GitHub
18
+ from snapquery.snapquery_core import NamedQuery, NamedQueryManager, NamedQuerySet
19
+
20
+ class SibSparqlExamples:
21
+ """
22
+ Fetch & parse SIB sparql-examples from GitHub API (remote, no clone).
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ nqm: NamedQueryManager,
28
+ github: Optional[GitHub] = None,
29
+ debug: bool = False,
30
+ ):
31
+ self.nqm = nqm
32
+ self.github = github or GitHub(owner="sib-swiss", repo="sparql-examples", token=os.getenv("GITHUB_TOKEN"))
33
+ self.named_query_set = NamedQuerySet(
34
+ domain="sparql-examples.sib.swiss",
35
+ namespace="sib-examples",
36
+ target_graph_name="various", # varies per query (schema:target)
37
+ )
38
+ self.debug = debug
39
+ self.cache: Dict[str, str] = {} # TTL content cache
40
+ self.stored_queries: List[NamedQuery] = []
41
+
42
+ def collect_ttl_items(self, path: str = "examples") -> List[Dict[str, Any]]:
43
+ """Recursively collect all .ttl file items under given path using GitHub client."""
44
+ ttl_files= self.github.list_files_recursive(path, suffix=".ttl")
45
+ return ttl_files
46
+
47
+ def get_ttl_content(self, download_url: str) -> str:
48
+ """Fetch TTL content from GitHub download_url, with cache."""
49
+ if download_url in self.cache:
50
+ return self.cache[download_url]
51
+ content = self.github.download(download_url)
52
+ self.cache[download_url] = content
53
+ return content
54
+
55
+ def get_html_url(self, file_path: str) -> str:
56
+ """
57
+ Construct GitHub HTML URL for a file path.
58
+
59
+ Args:
60
+ file_path: Path like "examples/Bgee/001.ttl"
61
+
62
+ Returns:
63
+ URL like "https://github.com/sib-swiss/sparql-examples/blob/main/examples/Bgee/001.ttl"
64
+ """
65
+ url=f"https://github.com/{self.github.owner}/{self.github.repo}/blob/main/{file_path}"
66
+ return url
67
+
68
+ def parse_ttl_to_namedqueries(self, ttl_content: str, ttl_path: str) -> List[NamedQuery]:
69
+ """
70
+ Parse TTL → NamedQuery using rdflib + SHACL shapes.
71
+ Matches your Bgee sample exactly.
72
+ """
73
+ parsed_queries: List[NamedQuery] = []
74
+ try:
75
+ g = rdflib.Graph()
76
+ g.parse(data=ttl_content, format="turtle")
77
+
78
+ # path e.g. "examples/Bgee/001.ttl" → endpoint_path="Bgee/001"
79
+ endpoint_path = ttl_path.replace("examples/", "").rstrip(".ttl")
80
+ html_url = self.get_html_url(ttl_path)
81
+
82
+ # Fixed SPARQL: direct sh:select on sh:SPARQLSelectExecutable (matches sample)
83
+ qtext = """
84
+ PREFIX sh: <http://www.w3.org/ns/shacl#>
85
+ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
86
+ PREFIX schema: <http://schema.org/>
87
+
88
+ SELECT ?s ?query ?comment ?label WHERE {
89
+ ?s a sh:SPARQLSelectExecutable ;
90
+ sh:select ?query .
91
+ OPTIONAL { ?s rdfs:comment ?comment }
92
+ OPTIONAL { ?s rdfs:label ?label }
93
+ OPTIONAL { ?s schema:name ?label } # Fallback label
94
+ }
95
+ """
96
+
97
+ rows = list(g.query(qtext))
98
+ if not rows:
99
+ if self.debug:
100
+ print(f"No SHACL queries in {ttl_path}")
101
+ return []
102
+
103
+ seen_queries: Set[str] = set()
104
+ fallback_title = endpoint_path.split("/")[-1].replace("_", " ").title()
105
+
106
+ for i, row in enumerate(rows):
107
+ query_str = str(row.query)
108
+ if not query_str or query_str in seen_queries:
109
+ continue
110
+ seen_queries.add(query_str)
111
+
112
+ comment = str(row.comment) if row.comment else ""
113
+ label = str(row.label) if row.label else fallback_title
114
+
115
+ title = label
116
+ description = comment or f"SIB example: {endpoint_path} ({title})"
117
+
118
+ # Name: "Bgee:001" → slugify to "bgee-001"
119
+ name_suffix = f"_{i+1}" if len(rows) > 1 else ""
120
+ sub_name = endpoint_path.replace("/", ":") + name_suffix
121
+ name = sub_name # No duplicate prefix!
122
+
123
+ base_url = f"https://sparql-examples.sib.swiss/{endpoint_path.replace('/', '/')}{name_suffix}"
124
+
125
+ nq = NamedQuery(
126
+ domain=self.named_query_set.domain,
127
+ namespace=self.named_query_set.namespace,
128
+ name=name,
129
+ url=html_url, # GitHub source
130
+ title=title,
131
+ description=description,
132
+ sparql=query_str,
133
+ )
134
+ parsed_queries.append(nq)
135
+
136
+ except Exception as e:
137
+ if self.debug:
138
+ print(f"Error parsing {ttl_path}: {e}")
139
+ return parsed_queries
140
+
141
+ def extract_queries(self, limit: Optional[int] = None, debug_print: bool = False) -> List[NamedQuery]:
142
+ """
143
+ Main: Fetch/parse/store all TTL → NamedQuery/DB/Set.
144
+ """
145
+ self.stored_queries.clear()
146
+ ttl_items = self.collect_ttl_items("examples")
147
+ if limit:
148
+ ttl_items = ttl_items[:limit]
149
+
150
+ for item in ttl_items:
151
+ if debug_print:
152
+ print(f"Processing {item['path']}...")
153
+ content = self.get_ttl_content(item["download_url"])
154
+ nqs = self.parse_ttl_to_namedqueries(content, item["path"])
155
+ for nq in nqs:
156
+ self.nqm.add_and_store(nq) # FIXED: stores to DB + QueryDetails
157
+ self.named_query_set.add(nq) # FIXED: add (not add_query)
158
+ self.stored_queries.append(nq)
159
+ return self.stored_queries
160
+
161
+
162
+ def save_to_yaml(self, filepath: str) -> Dict[str, Any]:
163
+ """
164
+ Export stored queries to a YAML file.
165
+ """
166
+ self.named_query_set.save_to_yaml_file(filepath)
167
+
@@ -3,7 +3,7 @@ Created on 2024-05-03
3
3
 
4
4
  @author: wf
5
5
  """
6
-
6
+ from tqdm import tqdm
7
7
  import logging
8
8
  import sys
9
9
  from argparse import ArgumentParser
@@ -87,6 +87,12 @@ class SnapQueryCmd(WebserverCmd):
87
87
  action=StoreDictKeyPair,
88
88
  help="query parameters as Key-value pairs in the format key1=value1,key2=value2",
89
89
  )
90
+ parser.add_argument(
91
+ "--progress",
92
+ action="store_true",
93
+ help="show progress bars when testing queries (--testQueries)",
94
+ )
95
+
90
96
  parser.add_argument(
91
97
  "--domain",
92
98
  type=str,
@@ -144,6 +150,56 @@ class SnapQueryCmd(WebserverCmd):
144
150
  self.args.func(self.args)
145
151
  return self.args
146
152
 
153
+ def handle_test_queries(self):
154
+ """
155
+ Handle the --testQueries option by executing queries against endpoints.
156
+ The endpoint is the outer loop, queries are the inner loop.
157
+ """
158
+ # Determine which endpoints to use
159
+ if self.args.endpointName:
160
+ endpoint_names = [self.args.endpointName]
161
+ else:
162
+ endpoint_names = list(self.nqm.endpoints.keys())
163
+
164
+ # Get all queries to test
165
+ queries = self.nqm.get_all_queries(domain=self.args.domain, namespace=self.args.namespace)
166
+
167
+ # Create execution instance
168
+ execution = Execution(self.nqm, debug=self.args.debug)
169
+
170
+ # Outer loop: endpoints
171
+ endpoint_iter = tqdm(endpoint_names, desc="Testing endpoints") if self.args.progress else endpoint_names
172
+ for endpoint_name in endpoint_iter:
173
+ # Inner loop: queries
174
+ query_iter = tqdm(queries, desc=f"Queries for {endpoint_name}", leave=False) if self.args.progress else queries
175
+ for i, nq in enumerate(query_iter, start=1):
176
+ execution.execute(
177
+ nq,
178
+ endpoint_name=endpoint_name,
179
+ context=self.args.context,
180
+ title=f"{endpoint_name}::query {i:3}/{len(queries)}",
181
+ prefix_merger=QueryPrefixMerger.get_by_name(self.args.prefix_merger),
182
+ )
183
+
184
+ def handle_test_queries_no_progress_version(self):
185
+ if self.args.endpointName:
186
+ endpoint_names = [self.args.endpointName]
187
+ else:
188
+ endpoint_names = list(self.nqm.endpoints.keys())
189
+ queries = self.nqm.get_all_queries(domain=self.args.domain, namespace=self.args.namespace)
190
+ execution = Execution(self.nqm, debug=self.args.debug)
191
+ query_iter = tqdm(queries, desc="Testing queries") if self.args.progress else queries
192
+ for i, nq in enumerate(query_iter, start=1):
193
+ for endpoint_name in endpoint_names:
194
+ execution.execute(
195
+ nq,
196
+ endpoint_name=endpoint_name,
197
+ context=self.args.context,
198
+ title=f"query {i:3}/{len(queries)}::{endpoint_name}",
199
+ prefix_merger=QueryPrefixMerger.get_by_name(self.args.prefix_merger),
200
+ )
201
+
202
+
147
203
  def handle_args(self, args) -> bool:
148
204
  """
149
205
  handle the command line args
@@ -174,21 +230,8 @@ class SnapQueryCmd(WebserverCmd):
174
230
  print(f"{namespace}:{count}")
175
231
  handled = True
176
232
  elif self.args.testQueries:
177
- if self.args.endpointName:
178
- endpoint_names = [self.args.endpointName]
179
- else:
180
- endpoint_names = list(nqm.endpoints.keys())
181
- queries = self.nqm.get_all_queries(domain=self.args.domain, namespace=self.args.namespace)
182
- execution = Execution(self.nqm, debug=self.args.debug)
183
- for i, nq in enumerate(queries, start=1):
184
- for endpoint_name in endpoint_names:
185
- execution.execute(
186
- nq,
187
- endpoint_name=endpoint_name,
188
- context=self.args.context,
189
- title=f"query {i:3}/{len(queries)}::{endpoint_name}",
190
- prefix_merger=QueryPrefixMerger.get_by_name(self.args.prefix_merger),
191
- )
233
+ self.handle_test_queries()
234
+ handled = True
192
235
  elif self.args.queryName is not None or self.args.query_id is not None:
193
236
  if self.args.query_id is not None:
194
237
  query_name = QueryName.from_query_id(self.args.query_id)
@@ -728,7 +728,7 @@ class NamedQueryManager:
728
728
  for source_class, pk in [
729
729
  (NamedQuery, "query_id"),
730
730
  (QueryStats, "stats_id"),
731
- (QueryDetails, "quer_id"),
731
+ (QueryDetails, "query_id"),
732
732
  ]:
733
733
  # Fetch sample records from the specified class
734
734
  sample_records = cls.get_sample_records(source_class=source_class)
@@ -0,0 +1,74 @@
1
+ """
2
+ Test SIB SPARQL Examples fetching.
3
+ Verifies integration between SIB fetcher and Snapquery Core.
4
+
5
+ Created on 2025-12-02
6
+ @author: wf
7
+ """
8
+ import os
9
+ import unittest
10
+ from basemkit.basetest import Basetest
11
+ from snapquery.snapquery_core import NamedQueryManager
12
+ from snapquery.sib_sparql_examples import SibSparqlExamples
13
+
14
+ class TestSibExamples(Basetest):
15
+ """
16
+ Test retrieving SIB SPARQL examples using GitHub cache/api.
17
+ """
18
+
19
+ def setUp(self, debug=True, profile=True):
20
+ Basetest.setUp(self, debug=debug, profile=profile)
21
+
22
+ @unittest.skipIf(Basetest.inPublicCI(), "avoid github rate limit in CI")
23
+ def test_sib_examples_fetch_and_store(self):
24
+ """
25
+ Test retrieving SIB examples, populating the DB, and exporting to YAML.
26
+ """
27
+ db_path = "/tmp/sib_examples.db"
28
+ yaml_path = "/tmp/sib_examples.yaml"
29
+
30
+ if os.path.exists(db_path):
31
+ os.remove(db_path)
32
+ if os.path.exists(yaml_path):
33
+ os.remove(yaml_path)
34
+
35
+ nqm = NamedQueryManager.from_samples(db_path=db_path)
36
+ sib_fetcher = SibSparqlExamples(nqm, debug=self.debug)
37
+
38
+ # Limit for testing efficiency
39
+ limit = 7 # if self.inPublicCI() else None
40
+ if self.debug:
41
+ print(f"Fetching SIB examples (limit={limit})...")
42
+
43
+ loaded_queries = sib_fetcher.extract_queries(limit=limit, debug_print=self.debug)
44
+
45
+ self.assertTrue(len(loaded_queries) > 0, "Should have loaded at least one query")
46
+ self.assertEqual(len(sib_fetcher.named_query_set), len(loaded_queries))
47
+
48
+ # Verify SQL Database Storage
49
+ records = nqm.sql_db.query(
50
+ """
51
+ SELECT count(*) as count
52
+ FROM NamedQuery
53
+ WHERE namespace=? AND domain=?
54
+ """,
55
+ (sib_fetcher.named_query_set.namespace, sib_fetcher.named_query_set.domain)
56
+ )
57
+ db_count = records[0]['count']
58
+ self.assertEqual(db_count, len(loaded_queries), "DB count should match loaded count")
59
+
60
+ # Verify YAML Export
61
+ if self.debug:
62
+ print(f"Exporting to {yaml_path}...")
63
+ sib_fetcher.save_to_yaml(yaml_path)
64
+
65
+ self.assertTrue(os.path.exists(yaml_path))
66
+
67
+ # Optional: Read back to verify YamlAble structure
68
+ with open(yaml_path, 'r') as f:
69
+ content = f.read()
70
+ self.assertIn("sib-examples", content)
71
+ self.assertIn("queries:", content)
72
+
73
+ if self.debug:
74
+ print(f"Successfully processed {db_count} queries.")
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
@@ -1,46 +0,0 @@
1
- """
2
- GitHub API client for accessing repository contents.
3
-
4
- Created on 2025-12-02
5
- @author: wf
6
- """
7
- import requests
8
-
9
- class GitHub:
10
- """
11
- A simple GitHub API client for accessing repository contents.
12
- """
13
-
14
- def __init__(self, owner: str, repo: str, token: str = None):
15
- """
16
- Initialize GitHub client.
17
-
18
- Args:
19
- owner: Repository owner (username or organization)
20
- repo: Repository name
21
- token: Optional GitHub API token for authentication
22
- """
23
- self.owner = owner
24
- self.repo = repo
25
- self.token = token
26
- self.base_url = f"https://api.github.com/repos/{owner}/{repo}"
27
-
28
- def get_contents(self, path: str = ""):
29
- """
30
- Get contents of a directory or file from the repository.
31
-
32
- Args:
33
- path: Path within the repository
34
-
35
- Returns:
36
- List of dictionaries for directories, or content for files
37
- """
38
- url = f"{self.base_url}/contents/{path}"
39
- headers = {"Accept": "application/vnd.github.v3+json"}
40
-
41
- if self.token:
42
- headers["Authorization"] = f"token {self.token}"
43
-
44
- response = requests.get(url, headers=headers)
45
- response.raise_for_status()
46
- return response.json()
@@ -1,35 +0,0 @@
1
- """
2
-
3
- SIB Swiss Institute of Bioinformatics
4
-
5
- sparql examples
6
-
7
- @author wf
8
- """
9
- import unittest
10
-
11
- from basemkit.basetest import Basetest
12
- from rdflib import Graph
13
-
14
-
15
- class TestSibExamples(Basetest):
16
- """
17
- Test for Issue #59
18
- https://github.com/WolfgangFahl/snapquery/issues/59
19
- https://github.com/sib-swiss/sparql-examples/
20
-
21
- Snap query should consider using the sparql-examples style of encoding queries as their own entities as the basis of it's data interchange
22
- """
23
-
24
- def setUp(self, debug=False, profile=True):
25
- Basetest.setUp(self, debug=debug, profile=profile)
26
-
27
-
28
- @unittest.skip("needs github clone to work -postpone")
29
- def testBgee(self):
30
- """
31
- test a single example
32
- """
33
- g= Graph().parse("examples/Bgee/001.ttl", format="turtle")
34
- query = g.value(None, g.namespace("sh")["select"]) # Gets sh:select literal
35
- print(query) # Full SPARQL text
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
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes