Coverage for src/time_agnostic_library/support.py: 99%
96 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-10 10:04 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-10 10:04 +0000
1# SPDX-FileCopyrightText: 2021-2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
6import json
7import re
8from datetime import datetime, timezone
9from functools import lru_cache
10from pathlib import Path
12CONFIG_PATH = "./config.json"
14_TRIPLE_LEN = 3
16_NT_TERM_RE = re.compile(
17 r"<([^>]+)>"
18 r'|"((?:[^"\\]|\\.)*)"\^\^<([^>]+)>'
19 r'|"((?:[^"\\]|\\.)*)"@([a-zA-Z][\w-]*)'
20 r'|"((?:[^"\\]|\\.)*)"'
21 r"|(_:\S+)",
22 re.DOTALL,
23)
26def _nt_match_to_n3(match: re.Match) -> str:
27 if match.group(1) is not None:
28 return f"<{match.group(1)}>"
29 if match.group(2) is not None:
30 return f'"{match.group(2)}"^^<{match.group(3)}>'
31 if match.group(4) is not None:
32 return f'"{match.group(4)}"@{match.group(5)}'
33 if match.group(6) is not None:
34 return f'"{match.group(6)}"'
35 return match.group(7)
38def generate_config_file(
39 config_path: str = CONFIG_PATH,
40 dataset_urls: list | None = None,
41 dataset_dirs: list | None = None,
42 *,
43 dataset_is_quadstore: bool = True,
44 provenance_urls: list | None = None,
45 provenance_dirs: list | None = None,
46 provenance_is_quadstore: bool = True,
47 blazegraph_full_text_search: bool = False,
48 fuseki_full_text_search: bool = False,
49 virtuoso_full_text_search: bool = False,
50 graphdb_connector_name: str = "",
51 qlever_full_text_search: bool = False,
52) -> dict:
53 if provenance_dirs is None:
54 provenance_dirs = []
55 if provenance_urls is None:
56 provenance_urls = []
57 if dataset_dirs is None:
58 dataset_dirs = []
59 if dataset_urls is None:
60 dataset_urls = []
61 config = {
62 "dataset": {
63 "triplestore_urls": dataset_urls,
64 "file_paths": dataset_dirs,
65 "is_quadstore": dataset_is_quadstore,
66 },
67 "provenance": {
68 "triplestore_urls": provenance_urls,
69 "file_paths": provenance_dirs,
70 "is_quadstore": provenance_is_quadstore,
71 },
72 "blazegraph_full_text_search": str(blazegraph_full_text_search).lower(),
73 "fuseki_full_text_search": str(fuseki_full_text_search).lower(),
74 "virtuoso_full_text_search": str(virtuoso_full_text_search).lower(),
75 "graphdb_connector_name": graphdb_connector_name,
76 "qlever_full_text_search": str(qlever_full_text_search).lower(),
77 }
78 with Path(config_path).open("w", encoding="utf-8") as f:
79 json.dump(config, f)
80 return config
83@lru_cache(maxsize=4096)
84def _cached_parse(time_string: str) -> datetime:
85 if time_string.endswith("Z"):
86 time_string = time_string[:-1] + "+00:00"
87 time = datetime.fromisoformat(time_string)
88 if time.tzinfo is None:
89 return time.replace(tzinfo=timezone.utc)
90 return time.astimezone(timezone.utc)
93def convert_to_datetime(
94 time_string: str | None, *, stringify: bool = False
95) -> datetime | str | None:
96 if time_string and time_string != "None":
97 time = _cached_parse(time_string)
98 if stringify:
99 return time.isoformat()
100 return time
101 return None
104def _strip_literal_datatype(n3: str) -> str:
105 if not n3.startswith('"'):
106 return n3
107 i = 1
108 while i < len(n3):
109 if n3[i] == "\\":
110 i += 2
111 continue
112 if n3[i] == '"':
113 rest = n3[i + 1 :]
114 if rest.startswith("@"):
115 return n3
116 return n3[: i + 1]
117 i += 1
118 return n3
121def _to_nt_sorted_list(quads) -> list | None:
122 if quads is None:
123 return None
124 lines = set()
125 for q in quads:
126 parts = [_strip_literal_datatype(el) for el in q[:3]]
127 lines.add(" ".join(parts))
128 return sorted(lines)
131def _to_dict_of_nt_sorted_lists(dictionary: dict) -> dict:
132 result = {}
133 for key, value in dictionary.items():
134 if isinstance(value, set):
135 result[key] = _to_nt_sorted_list(value)
136 else:
137 result.setdefault(key, {})
138 for snapshot, quad_set in value.items():
139 result[key][snapshot] = _to_nt_sorted_list(quad_set)
140 return result
143def _nt_list_to_quad_set(nt_list: list[str]) -> set[tuple[str, ...]]:
144 result = set()
145 for line in nt_list:
146 if not line.strip():
147 continue
148 matches = list(_NT_TERM_RE.finditer(line))
149 if len(matches) >= _TRIPLE_LEN:
150 result.add(tuple(_nt_match_to_n3(m) for m in matches[:3]))
151 return result
154def _to_dict_of_quad_sets(dictionary: dict) -> dict:
155 result = {}
156 for key, value in dictionary.items():
157 if isinstance(value, list):
158 result[key] = _nt_list_to_quad_set(value)
159 else:
160 result.setdefault(key, {})
161 for snapshot, triples in value.items():
162 result[key][snapshot] = _nt_list_to_quad_set(triples)
163 return result