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.
- {snapquery-0.2.0 → snapquery-0.2.1}/PKG-INFO +1 -1
- snapquery-0.2.1/snapquery/__init__.py +1 -0
- snapquery-0.2.1/snapquery/github_access.py +119 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/endpoints.yaml +25 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/endpoints_optional.yaml +0 -24
- snapquery-0.2.1/snapquery/sib_sparql_examples.py +167 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_cmd.py +59 -16
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_core.py +1 -1
- snapquery-0.2.1/tests/test_sib_examples.py +74 -0
- snapquery-0.2.0/snapquery/__init__.py +0 -1
- snapquery-0.2.0/snapquery/github_access.py +0 -46
- snapquery-0.2.0/tests/test_sib_examples.py +0 -35
- {snapquery-0.2.0 → snapquery-0.2.1}/.github/workflows/build.yml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/.github/workflows/upload-to-pypi.yml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/.gitignore +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/.project +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/.pydevproject +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/LICENSE +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/README.md +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/docs/index.md +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/mkdocs.yml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/pyproject.toml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/blackisort +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/doc +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/fixdomain +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/install +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/qlv +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/release +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/restore_db +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/test +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/scripts/test_queries +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/authorization.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/basequeryview.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/ceurws.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/dblp.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/error_filter.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/execution.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/graph.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/models/__init__.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/models/person.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/models/sparql_components.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/mwlogin.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/namespace_stats_view.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/orcid.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/params_view.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/person_selector.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/pid.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/pid_lookup.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/prefix_merger.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/qimport.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/qimport_view.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/qlever.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/query_annotate.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/query_selector.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/resources/sparql_language.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/WikiProject_LSEThesisProject.wiki +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/ceur-ws.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/dblp.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/federated-query-examples.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/graphs.yaml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/meta_query.yaml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/orkg.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/qlever.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/qlever_performance.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/query_stats.yaml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/scholia.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/snapquery.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-WikidataThesisToolkit.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-WikidataThesisToolkitAlt.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-examples.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-short-urls.json +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/scholia.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_view.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/snapquery_webserver.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/sparql_analyzer.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/stats_view.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/version.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/wd_page_query_extractor.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery/wd_short_url.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery.puml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/snapquery.yaml +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/__init__.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/query_extractors/__init__.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/query_extractors/sparql_standard_queries.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/ask_unsupported.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/invalid_literal.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/invalid_regex_func.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/memory_limit.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/timeout.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/triplydb/parser_error.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/duplicate_prefix.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/invalid_iri_declaration.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/invalid_sparql_syntax.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/timeout.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/undefined_prefix.txt +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_authorization.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_ceurws.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_cmdline.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_endpoints.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_error_filter.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_graph.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_import.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_namedqueries.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_oauth.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_orcid.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_pids.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_prefix_merger.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_qlever.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_query_annotate.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_query_execution.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_query_name.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_restful_api.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_scholia.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_scholia_q.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_sparql_analyzer.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_sparql_components.py +0 -0
- {snapquery-0.2.0 → snapquery-0.2.1}/tests/test_wd_query_parsing.py +0 -0
- {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.
|
|
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
|
-
|
|
178
|
-
|
|
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, "
|
|
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
|
|
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
|
{snapquery-0.2.0 → snapquery-0.2.1}/snapquery/samples/wikidata-WikidataThesisToolkitAlt.json
RENAMED
|
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
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/ask_unsupported.txt
RENAMED
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/invalid_literal.txt
RENAMED
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/qlever/invalid_regex_func.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/triplydb/parser_error.txt
RENAMED
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/duplicate_prefix.txt
RENAMED
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/invalid_iri_declaration.txt
RENAMED
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/invalid_sparql_syntax.txt
RENAMED
|
File without changes
|
|
File without changes
|
{snapquery-0.2.0 → snapquery-0.2.1}/tests/resources/error_messages/wdqs/undefined_prefix.txt
RENAMED
|
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
|