pyCEURsemantify 0.0.1__py3-none-any.whl
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.
- ceursemantify/__init__.py +7 -0
- ceursemantify/ceursemantify_cmd.py +174 -0
- ceursemantify/convert.py +204 -0
- ceursemantify/embed.py +84 -0
- ceursemantify/endpoint.py +72 -0
- ceursemantify/generator.py +278 -0
- ceursemantify/jsonld.py +197 -0
- ceursemantify/loctime.py +132 -0
- ceursemantify/mapper.py +178 -0
- ceursemantify/model.py +176 -0
- ceursemantify/pages.py +818 -0
- ceursemantify/resources/ceurspt_mapping.yaml +85 -0
- ceursemantify/resources/context.yaml +102 -0
- ceursemantify/resources/header.html +45 -0
- ceursemantify/resources/queries.yaml +164 -0
- ceursemantify/resources/site.yaml +17 -0
- ceursemantify/site.py +322 -0
- ceursemantify/volume_html.py +275 -0
- pyceursemantify-0.0.1.dist-info/METADATA +117 -0
- pyceursemantify-0.0.1.dist-info/RECORD +23 -0
- pyceursemantify-0.0.1.dist-info/WHEEL +4 -0
- pyceursemantify-0.0.1.dist-info/entry_points.txt +2 -0
- pyceursemantify-0.0.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2026-10-01
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from argparse import ArgumentParser, Namespace
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from basemkit.base_cmd import BaseCmd
|
|
13
|
+
|
|
14
|
+
import ceursemantify
|
|
15
|
+
from ceursemantify.endpoint import SptEndpoint
|
|
16
|
+
from ceursemantify.generator import Generator
|
|
17
|
+
from ceursemantify.site import SiteConfig, SiteReport, VolumeRanges
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Version:
|
|
22
|
+
"""
|
|
23
|
+
Version information for pyCEURsemantify
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
name: str = "pyCEURsemantify"
|
|
27
|
+
version: str = ceursemantify.__version__
|
|
28
|
+
date: str = "2026-10-01"
|
|
29
|
+
updated: str = "2026-10-13"
|
|
30
|
+
description: str = "Semantification of the CEUR-WS publication workflow"
|
|
31
|
+
authors: str = "Wolfgang Fahl"
|
|
32
|
+
doc_url: str = "https://github.com/ceurws/semantify"
|
|
33
|
+
chat_url: str = "https://github.com/ceurws/semantify/discussions"
|
|
34
|
+
cm_url: str = "https://github.com/ceurws/semantify"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CeurSemantifyCmd(BaseCmd):
|
|
38
|
+
"""
|
|
39
|
+
command line interface of pyCEURsemantify:
|
|
40
|
+
generate volume pages, rebuild year pages, serve the endpoint
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
"""
|
|
45
|
+
constructor
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(Version())
|
|
48
|
+
|
|
49
|
+
def add_arguments(self, parser: ArgumentParser) -> None:
|
|
50
|
+
"""
|
|
51
|
+
add the arguments to the given parser
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
parser: the argument parser
|
|
55
|
+
"""
|
|
56
|
+
super().add_arguments(parser)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"volumes",
|
|
59
|
+
nargs="*",
|
|
60
|
+
help="volume numbers and ranges to generate, e.g. 3889 3887-3912",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-y",
|
|
64
|
+
"--year",
|
|
65
|
+
type=int,
|
|
66
|
+
action="append",
|
|
67
|
+
dest="years",
|
|
68
|
+
help="rebuild the page of the given year from the volumes present (can be given several times)",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--regenerate",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="with --year: fetch and write the volumes present for the year again before rebuilding its page",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--embed",
|
|
77
|
+
nargs="+",
|
|
78
|
+
metavar="PATH",
|
|
79
|
+
help="embed the JSON-LD into the given volume pages of the proceedings chairs (index.html files or"
|
|
80
|
+
" volume directories) and write index.jsonld next to them",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument("--progress", action="store_true", help="show a progress bar while generating volumes")
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--serve",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="load all JSON-LD files into memory and serve them as SPARQL endpoint",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument("--host", default="127.0.0.1", help="host of the endpoint (default: %(default)s)")
|
|
89
|
+
parser.add_argument("--port", type=int, default=9987, help="port of the endpoint (default: %(default)s)")
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-o",
|
|
92
|
+
"--output",
|
|
93
|
+
default=".",
|
|
94
|
+
help="directory that holds the year directories (default: current directory)",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument("--config", help="site configuration yaml file (default: the packaged site.yaml)")
|
|
97
|
+
|
|
98
|
+
def show_report(self, title: str, report: SiteReport, args: Namespace) -> None:
|
|
99
|
+
"""
|
|
100
|
+
show the given report: problems always, written files if verbose
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
title: what the report is about
|
|
104
|
+
report: the report
|
|
105
|
+
args: the parsed arguments
|
|
106
|
+
"""
|
|
107
|
+
for problem in report.problems:
|
|
108
|
+
print(problem, file=sys.stderr)
|
|
109
|
+
if args.verbose:
|
|
110
|
+
for path in report.written:
|
|
111
|
+
print(path)
|
|
112
|
+
if not args.quiet:
|
|
113
|
+
print(f"{title}: {len(report.written)} files, {len(report.problems)} problems, {report.seconds:.1f} s")
|
|
114
|
+
|
|
115
|
+
def handle_args(self, args: Namespace) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
handle the parsed arguments
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
args: the parsed arguments
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
bool: True if the arguments were handled
|
|
124
|
+
"""
|
|
125
|
+
handled = super().handle_args(args)
|
|
126
|
+
if not handled:
|
|
127
|
+
config = SiteConfig.of_yaml(args.config)
|
|
128
|
+
generator = Generator.of_output(args.output, config)
|
|
129
|
+
if args.embed:
|
|
130
|
+
report = generator.embed_volumes(args.embed, progress=args.progress)
|
|
131
|
+
self.show_report(f"{len(args.embed)} volume pages", report, args)
|
|
132
|
+
handled = True
|
|
133
|
+
if args.volumes:
|
|
134
|
+
numbers = VolumeRanges.numbers(args.volumes)
|
|
135
|
+
report = generator.generate_volumes(numbers, progress=args.progress)
|
|
136
|
+
self.show_report(f"{len(numbers)} volumes", report, args)
|
|
137
|
+
handled = True
|
|
138
|
+
for year in args.years or []:
|
|
139
|
+
if args.regenerate:
|
|
140
|
+
report = generator.regenerate_year(year, progress=args.progress)
|
|
141
|
+
else:
|
|
142
|
+
report = generator.rebuild_year(year)
|
|
143
|
+
self.show_report(f"year {year}", report, args)
|
|
144
|
+
handled = True
|
|
145
|
+
if args.serve:
|
|
146
|
+
endpoint = SptEndpoint(generator.site)
|
|
147
|
+
load_report = endpoint.load()
|
|
148
|
+
if not args.quiet:
|
|
149
|
+
print(
|
|
150
|
+
f"endpoint: {load_report.files} files, {load_report.triples} triples, "
|
|
151
|
+
f"{load_report.seconds:.1f} s, http://{args.host}:{args.port}/npq/{endpoint.name}"
|
|
152
|
+
)
|
|
153
|
+
endpoint.serve(host=args.host, port=args.port)
|
|
154
|
+
handled = True
|
|
155
|
+
return handled
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
159
|
+
"""
|
|
160
|
+
command line entry point
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
argv: the command line arguments
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
int: the exit code
|
|
167
|
+
"""
|
|
168
|
+
cmd = CeurSemantifyCmd()
|
|
169
|
+
exit_code = cmd.run(argv)
|
|
170
|
+
return exit_code
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
sys.exit(main())
|
ceursemantify/convert.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2026-10-01
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from datetime import date
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Convert:
|
|
15
|
+
"""
|
|
16
|
+
conversions of source values to the values of the research model
|
|
17
|
+
|
|
18
|
+
Each conversion is named in resources/ceurspt_mapping.yaml and raises a
|
|
19
|
+
ValueError for a value it can not convert.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def text(cls, value: Any) -> str:
|
|
24
|
+
"""
|
|
25
|
+
normalize whitespace: line breaks and runs of blanks become one blank
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
value: the source text
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: the single line text
|
|
32
|
+
"""
|
|
33
|
+
text = " ".join(str(value).split())
|
|
34
|
+
return text
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def integer(cls, value: Any) -> int:
|
|
38
|
+
"""
|
|
39
|
+
convert the given value to an integer
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
value: the source value
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
int: the integer
|
|
46
|
+
"""
|
|
47
|
+
integer = int(value)
|
|
48
|
+
return integer
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def volume_number(cls, value: Any) -> int:
|
|
52
|
+
"""
|
|
53
|
+
get the volume number of a volume identifier such as Vol-3889
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
value: the volume identifier
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
int: the volume number
|
|
60
|
+
"""
|
|
61
|
+
match = re.fullmatch(r"Vol[-.]?(\d+)", str(value).strip())
|
|
62
|
+
if not match:
|
|
63
|
+
raise ValueError(f"not a volume identifier: {value}")
|
|
64
|
+
volume_number = int(match.group(1))
|
|
65
|
+
return volume_number
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def iso_date(cls, value: Any) -> str:
|
|
69
|
+
"""
|
|
70
|
+
check that the given value is an ISO date
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
value: the source date
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str: the date as YYYY-MM-DD
|
|
77
|
+
"""
|
|
78
|
+
iso_date = date.fromisoformat(str(value).strip()).isoformat()
|
|
79
|
+
return iso_date
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def last_path_parts(cls, value: Any, marker: str) -> str:
|
|
83
|
+
"""
|
|
84
|
+
get the part of the path of an url that follows the given marker
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
value: the url or an identifier without url
|
|
88
|
+
marker: the path segment the identifier follows e.g. /pid/
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: the identifier
|
|
92
|
+
"""
|
|
93
|
+
identifier = str(value).strip()
|
|
94
|
+
path = urlparse(identifier).path
|
|
95
|
+
if marker in path:
|
|
96
|
+
identifier = path.split(marker, 1)[1]
|
|
97
|
+
return identifier
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def wikidata_id(cls, value: Any) -> str:
|
|
101
|
+
"""
|
|
102
|
+
get the Q-identifier of a Wikidata entity url or identifier
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
value: the entity url or Q-identifier
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
str: the Q-identifier
|
|
109
|
+
"""
|
|
110
|
+
wikidata_id = cls.last_path_parts(value, "/entity/")
|
|
111
|
+
if not re.fullmatch(r"Q\d+", wikidata_id):
|
|
112
|
+
raise ValueError(f"not a Wikidata identifier: {value}")
|
|
113
|
+
return wikidata_id
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def dblp_person_id(cls, value: Any) -> str:
|
|
117
|
+
"""
|
|
118
|
+
get the dblp person identifier of a dblp person url
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
value: the url e.g. https://dblp.org/pid/32/7870
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
str: the identifier e.g. 32/7870
|
|
125
|
+
"""
|
|
126
|
+
dblp_person_id = cls.last_path_parts(value, "/pid/")
|
|
127
|
+
return dblp_person_id
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def dblp_record_id(cls, value: Any) -> str:
|
|
131
|
+
"""
|
|
132
|
+
get the dblp publication identifier of a dblp record url
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value: the url e.g. https://dblp.org/rec/conf/semtab/X24
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
str: the identifier e.g. conf/semtab/X24
|
|
139
|
+
"""
|
|
140
|
+
dblp_record_id = cls.last_path_parts(value, "/rec/")
|
|
141
|
+
return dblp_record_id
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def file_stem(cls, value: Any) -> str:
|
|
145
|
+
"""
|
|
146
|
+
get the file name without extension of an url
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
value: the url e.g. https://ceur-ws.org/Vol-3889/paper0.pdf
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: the stem e.g. paper0
|
|
153
|
+
"""
|
|
154
|
+
path = urlparse(str(value)).path
|
|
155
|
+
file_stem = os.path.splitext(os.path.basename(path))[0]
|
|
156
|
+
if not file_stem:
|
|
157
|
+
raise ValueError(f"no file name in: {value}")
|
|
158
|
+
return file_stem
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def page_part(cls, value: Any, index: int) -> Optional[str]:
|
|
162
|
+
"""
|
|
163
|
+
get the first or last page of a page range such as 1-11
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
value: the page range or a single page
|
|
167
|
+
index: 0 for the first page, -1 for the last page
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Optional[str]: the page
|
|
171
|
+
"""
|
|
172
|
+
parts = [part.strip() for part in re.split(r"[-–—]+", str(value)) if part.strip()]
|
|
173
|
+
if not parts:
|
|
174
|
+
raise ValueError(f"no pages in: {value}")
|
|
175
|
+
page_part = parts[index]
|
|
176
|
+
return page_part
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def page_start(cls, value: Any) -> Optional[str]:
|
|
180
|
+
"""
|
|
181
|
+
get the first page of a page range
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
value: the page range
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Optional[str]: the first page
|
|
188
|
+
"""
|
|
189
|
+
page_start = cls.page_part(value, 0)
|
|
190
|
+
return page_start
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def page_end(cls, value: Any) -> Optional[str]:
|
|
194
|
+
"""
|
|
195
|
+
get the last page of a page range
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
value: the page range
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Optional[str]: the last page
|
|
202
|
+
"""
|
|
203
|
+
page_end = cls.page_part(value, -1)
|
|
204
|
+
return page_end
|
ceursemantify/embed.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2026-10-13
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from ceursemantify.pages import Fence, PageState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Embedder:
|
|
13
|
+
"""
|
|
14
|
+
embed the JSON-LD into the volume page of the proceedings chair: the
|
|
15
|
+
state comment before the html element, the alternate link and the fence
|
|
16
|
+
in the head; the markup of the chair is otherwise untouched
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
alternate_link = '<link rel="alternate" type="application/ld+json" href="index.jsonld">'
|
|
20
|
+
state_pattern = re.compile(r"<!-- CEURSTATE=[^\n]*? -->\n?")
|
|
21
|
+
fence_pattern = re.compile(rf"<!--\n{Fence.fence}json-ld\n.*?\n{Fence.fence}\n-->\n?", re.DOTALL)
|
|
22
|
+
link_pattern = re.compile(re.escape(alternate_link) + r"\n?")
|
|
23
|
+
# the state comment goes after the doctype, before the html element otherwise
|
|
24
|
+
doctype_pattern = re.compile(r"<!DOCTYPE[^>]*>\n?", re.IGNORECASE)
|
|
25
|
+
html_pattern = re.compile(r"<html[\s>]", re.IGNORECASE)
|
|
26
|
+
head_pattern = re.compile(r"<head[^>]*>\n?", re.IGNORECASE)
|
|
27
|
+
|
|
28
|
+
def __init__(self, state: PageState):
|
|
29
|
+
"""
|
|
30
|
+
constructor
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
state: the state the page announces
|
|
34
|
+
"""
|
|
35
|
+
self.state = state
|
|
36
|
+
|
|
37
|
+
def strip(self, html_text: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
remove the embedded parts from the given page
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
html_text: the html of a volume page
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: the html without state comment, alternate link and fence
|
|
46
|
+
"""
|
|
47
|
+
stripped = self.state_pattern.sub("", html_text)
|
|
48
|
+
stripped = self.fence_pattern.sub("", stripped)
|
|
49
|
+
stripped = self.link_pattern.sub("", stripped)
|
|
50
|
+
return stripped
|
|
51
|
+
|
|
52
|
+
def embed(self, html_text: str, jsonld_text: str, source: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
embed the given JSON-LD into the given page, replacing an earlier embedding
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
html_text: the html of a volume page
|
|
58
|
+
jsonld_text: the pretty printed JSON-LD
|
|
59
|
+
source: what the JSON-LD was derived from e.g. the url of the volume
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
str: the html with the JSON-LD embedded
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: if the page has no html or no head element
|
|
66
|
+
"""
|
|
67
|
+
stripped = self.strip(html_text)
|
|
68
|
+
html_match = self.html_pattern.search(stripped)
|
|
69
|
+
head_match = self.head_pattern.search(stripped)
|
|
70
|
+
if html_match is None or head_match is None:
|
|
71
|
+
raise ValueError("no html or head element to embed into")
|
|
72
|
+
doctype_match = self.doctype_pattern.match(stripped)
|
|
73
|
+
state_at = doctype_match.end() if doctype_match else html_match.start()
|
|
74
|
+
state_comment = self.state.as_html(source, state="embedded")
|
|
75
|
+
inserted = f"{self.alternate_link}\n{Fence.as_html(jsonld_text)}\n"
|
|
76
|
+
embedded = (
|
|
77
|
+
stripped[:state_at]
|
|
78
|
+
+ state_comment
|
|
79
|
+
+ "\n"
|
|
80
|
+
+ stripped[state_at : head_match.end()]
|
|
81
|
+
+ inserted
|
|
82
|
+
+ stripped[head_match.end() :]
|
|
83
|
+
)
|
|
84
|
+
return embedded
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2026-10-01
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from lodstorage.npq_endpoint import NpqEndpoint
|
|
11
|
+
from rdflib import Graph
|
|
12
|
+
|
|
13
|
+
from ceursemantify.site import Site
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LoadReport:
|
|
18
|
+
"""
|
|
19
|
+
what was loaded into the endpoint
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
files: int = 0
|
|
23
|
+
triples: int = 0
|
|
24
|
+
seconds: float = 0.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SptEndpoint:
|
|
28
|
+
"""
|
|
29
|
+
single point of truth endpoint: the JSON-LD files of the site loaded
|
|
30
|
+
into memory on startup and served by the NPQ endpoint of pyLoDStorage,
|
|
31
|
+
see https://cr.bitplan.com/index.php/NPQ-FakeSparql
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, site: Site, name: str = "ceur-ws"):
|
|
35
|
+
"""
|
|
36
|
+
constructor
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
site: the directory tree with the JSON-LD files
|
|
40
|
+
name: the name the graph is served under: /npq/<name>
|
|
41
|
+
"""
|
|
42
|
+
self.site = site
|
|
43
|
+
self.name = name
|
|
44
|
+
self.graph = Graph()
|
|
45
|
+
|
|
46
|
+
def load(self) -> LoadReport:
|
|
47
|
+
"""
|
|
48
|
+
load all JSON-LD files of the site into the graph
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
LoadReport: number of files and triples and the time used
|
|
52
|
+
"""
|
|
53
|
+
report = LoadReport()
|
|
54
|
+
start = time.perf_counter()
|
|
55
|
+
for path in self.site.jsonld_paths():
|
|
56
|
+
self.graph.parse(path, format="json-ld")
|
|
57
|
+
report.files += 1
|
|
58
|
+
report.triples = len(self.graph)
|
|
59
|
+
report.seconds = time.perf_counter() - start
|
|
60
|
+
return report
|
|
61
|
+
|
|
62
|
+
def serve(self, host: str = "127.0.0.1", port: int = 9987) -> None:
|
|
63
|
+
"""
|
|
64
|
+
serve the graph over the SPARQL protocol
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
host: the host to listen on
|
|
68
|
+
port: the port to listen on
|
|
69
|
+
"""
|
|
70
|
+
endpoint = NpqEndpoint()
|
|
71
|
+
endpoint.add_graph(self.name, self.graph)
|
|
72
|
+
endpoint.run(host=host, port=port)
|