dapytains 0.0.1a0__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.
- dapytains-0.0.1a0/MANIFEST.in +1 -0
- dapytains-0.0.1a0/PKG-INFO +80 -0
- dapytains-0.0.1a0/README.md +51 -0
- dapytains-0.0.1a0/dapitains/__init__.py +0 -0
- dapytains-0.0.1a0/dapitains/app/__init__.py +0 -0
- dapytains-0.0.1a0/dapitains/app/app.py +285 -0
- dapytains-0.0.1a0/dapitains/app/database.py +152 -0
- dapytains-0.0.1a0/dapitains/app/ingest.py +46 -0
- dapytains-0.0.1a0/dapitains/app/navigation.py +135 -0
- dapytains-0.0.1a0/dapitains/constants.py +36 -0
- dapytains-0.0.1a0/dapitains/errors.py +5 -0
- dapytains-0.0.1a0/dapitains/metadata/__init__.py +0 -0
- dapytains-0.0.1a0/dapitains/metadata/classes.py +57 -0
- dapytains-0.0.1a0/dapitains/metadata/xml_parser.py +98 -0
- dapytains-0.0.1a0/dapitains/tei/__init__.py +0 -0
- dapytains-0.0.1a0/dapitains/tei/citeStructure.py +329 -0
- dapytains-0.0.1a0/dapitains/tei/document.py +481 -0
- dapytains-0.0.1a0/dapytains.egg-info/PKG-INFO +80 -0
- dapytains-0.0.1a0/dapytains.egg-info/SOURCES.txt +29 -0
- dapytains-0.0.1a0/dapytains.egg-info/dependency_links.txt +1 -0
- dapytains-0.0.1a0/dapytains.egg-info/requires.txt +7 -0
- dapytains-0.0.1a0/dapytains.egg-info/top_level.txt +1 -0
- dapytains-0.0.1a0/requirements.txt +7 -0
- dapytains-0.0.1a0/setup.cfg +4 -0
- dapytains-0.0.1a0/setup.py +26 -0
- dapytains-0.0.1a0/tests/test_app.py +159 -0
- dapytains-0.0.1a0/tests/test_catalog.py +65 -0
- dapytains-0.0.1a0/tests/test_citeStructure.py +135 -0
- dapytains-0.0.1a0/tests/test_db_create.py +126 -0
- dapytains-0.0.1a0/tests/test_lb.py +188 -0
- dapytains-0.0.1a0/tests/test_tei.py +120 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include requirements.txt
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dapytains
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: A brief description of dapytains
|
|
5
|
+
Home-page: https://github.com/distributed-text-services/MyDapytains
|
|
6
|
+
Author: Thibault Clérice
|
|
7
|
+
Author-email: thibault.clerice@inria.fr
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: saxonche==12.5.0
|
|
14
|
+
Requires-Dist: lxml
|
|
15
|
+
Requires-Dist: flask
|
|
16
|
+
Requires-Dist: flask-sqlalchemy
|
|
17
|
+
Requires-Dist: click
|
|
18
|
+
Requires-Dist: uritemplate
|
|
19
|
+
Requires-Dist: tqdm
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: requires-python
|
|
28
|
+
Dynamic: summary
|
|
29
|
+
|
|
30
|
+
MyDapytains
|
|
31
|
+
===========
|
|
32
|
+
|
|
33
|
+
[](https://github.com/distributed-text-services/MyDapytains/actions/workflows/dts-validator.yml) [](https://github.com/distributed-text-services/MyDapytains/actions/workflows/test.yml)
|
|
34
|
+
|
|
35
|
+
*The name of the library is completely temporary*.
|
|
36
|
+
|
|
37
|
+
This library offers a base python implementation of the following functionalities:
|
|
38
|
+
|
|
39
|
+
- Parsing of machine-actionable citeStructure and citeData to retrieve reference, structure and citable unit metadata within a TEI file
|
|
40
|
+
- Reuse of citeStructure architecture to retrieve and build partial documents, based on provided reference.
|
|
41
|
+
- Support for multiple citeStructure, similar to the ?tree parameter of the DTS Specifications.
|
|
42
|
+
|
|
43
|
+
This library will:
|
|
44
|
+
|
|
45
|
+
- Provide a base implementation of the DTS API, using python as a server-side language
|
|
46
|
+
- Provide some light "caching" features, to avoid reparsing document at query time.
|
|
47
|
+
|
|
48
|
+
## ToDo
|
|
49
|
+
|
|
50
|
+
- Support XSL transformation with mediaType dictionary for outputting different data
|
|
51
|
+
- Add tests to webapp
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## WebApp
|
|
55
|
+
|
|
56
|
+
You can try the webapp using `python -m dapitains.app.app`. It uses test files at the moment.
|
|
57
|
+
|
|
58
|
+
## Guidelines
|
|
59
|
+
|
|
60
|
+
### Document level guidelines
|
|
61
|
+
|
|
62
|
+
1. For TEI document to be accessible in full, no specific requirements are necessary.
|
|
63
|
+
2. For TEI document to have a single citation tree, they must provide at least one element
|
|
64
|
+
at the XPath `/TEI/teiHeader/encodingDesc/refsDecl[@default='true']/citeStructure`.
|
|
65
|
+
3. For TEI document to have multiple citation trees, they must provide at least one element
|
|
66
|
+
at the XPath `/TEI/teiHeader/encodingDesc/refsDecl[@default='true']/citeStructure` and any number of element matching
|
|
67
|
+
the XPath `/TEI/teiHeader/encodingDesc/refsDecl[@n]/citeStructure`, where `@n` holds the citation tree name.
|
|
68
|
+
|
|
69
|
+
We are currently figuring out the Resource level metadata.
|
|
70
|
+
|
|
71
|
+
See one of our test files to check out the minimal requirements: we have one
|
|
72
|
+
[with citeData](./tests/tei/test_citeData_two_levels.xml) and one [with multiple trees](./tests/tei/multiple_tree.xml)
|
|
73
|
+
|
|
74
|
+
### Collection and Resource level guidelines
|
|
75
|
+
|
|
76
|
+
Collection and Resource level guidelines can be provided through file named dts-metadata.xml in each subfolder of a
|
|
77
|
+
given repository. We are currently looking at using external file that would help you ingest metadata, while leaving you
|
|
78
|
+
the option to load up metadata yourself.
|
|
79
|
+
|
|
80
|
+
The current schema for the collection catalog ingestion is available in [./tests/catalog/schema.rng](./tests/catalog/schema.rng).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
MyDapytains
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
[](https://github.com/distributed-text-services/MyDapytains/actions/workflows/dts-validator.yml) [](https://github.com/distributed-text-services/MyDapytains/actions/workflows/test.yml)
|
|
5
|
+
|
|
6
|
+
*The name of the library is completely temporary*.
|
|
7
|
+
|
|
8
|
+
This library offers a base python implementation of the following functionalities:
|
|
9
|
+
|
|
10
|
+
- Parsing of machine-actionable citeStructure and citeData to retrieve reference, structure and citable unit metadata within a TEI file
|
|
11
|
+
- Reuse of citeStructure architecture to retrieve and build partial documents, based on provided reference.
|
|
12
|
+
- Support for multiple citeStructure, similar to the ?tree parameter of the DTS Specifications.
|
|
13
|
+
|
|
14
|
+
This library will:
|
|
15
|
+
|
|
16
|
+
- Provide a base implementation of the DTS API, using python as a server-side language
|
|
17
|
+
- Provide some light "caching" features, to avoid reparsing document at query time.
|
|
18
|
+
|
|
19
|
+
## ToDo
|
|
20
|
+
|
|
21
|
+
- Support XSL transformation with mediaType dictionary for outputting different data
|
|
22
|
+
- Add tests to webapp
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## WebApp
|
|
26
|
+
|
|
27
|
+
You can try the webapp using `python -m dapitains.app.app`. It uses test files at the moment.
|
|
28
|
+
|
|
29
|
+
## Guidelines
|
|
30
|
+
|
|
31
|
+
### Document level guidelines
|
|
32
|
+
|
|
33
|
+
1. For TEI document to be accessible in full, no specific requirements are necessary.
|
|
34
|
+
2. For TEI document to have a single citation tree, they must provide at least one element
|
|
35
|
+
at the XPath `/TEI/teiHeader/encodingDesc/refsDecl[@default='true']/citeStructure`.
|
|
36
|
+
3. For TEI document to have multiple citation trees, they must provide at least one element
|
|
37
|
+
at the XPath `/TEI/teiHeader/encodingDesc/refsDecl[@default='true']/citeStructure` and any number of element matching
|
|
38
|
+
the XPath `/TEI/teiHeader/encodingDesc/refsDecl[@n]/citeStructure`, where `@n` holds the citation tree name.
|
|
39
|
+
|
|
40
|
+
We are currently figuring out the Resource level metadata.
|
|
41
|
+
|
|
42
|
+
See one of our test files to check out the minimal requirements: we have one
|
|
43
|
+
[with citeData](./tests/tei/test_citeData_two_levels.xml) and one [with multiple trees](./tests/tei/multiple_tree.xml)
|
|
44
|
+
|
|
45
|
+
### Collection and Resource level guidelines
|
|
46
|
+
|
|
47
|
+
Collection and Resource level guidelines can be provided through file named dts-metadata.xml in each subfolder of a
|
|
48
|
+
given repository. We are currently looking at using external file that would help you ingest metadata, while leaving you
|
|
49
|
+
the option to load up metadata yourself.
|
|
50
|
+
|
|
51
|
+
The current schema for the collection catalog ingestion is available in [./tests/catalog/schema.rng](./tests/catalog/schema.rng).
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
import uritemplate
|
|
5
|
+
from flask import Flask, request, Response
|
|
6
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
7
|
+
import click
|
|
8
|
+
except ImportError:
|
|
9
|
+
print("This part of the package can only be imported with the web requirements.")
|
|
10
|
+
raise
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import lxml.etree as ET
|
|
14
|
+
from dapitains.tei.document import Document
|
|
15
|
+
from dapitains.errors import InvalidRangeOrder
|
|
16
|
+
from dapitains.app.database import db, Collection, Navigation
|
|
17
|
+
from dapitains.app.navigation import get_nav, get_member_by_path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def inject_json(collection: Collection, templates) -> Dict:
|
|
21
|
+
if collection.resource:
|
|
22
|
+
inj = {
|
|
23
|
+
"collection": templates["collection"].partial({"id": collection.identifier}).uri,
|
|
24
|
+
"document": templates["document"].partial({"resource": collection.identifier}).uri,
|
|
25
|
+
}
|
|
26
|
+
if collection.citeStructure:
|
|
27
|
+
inj["navigation"] = templates["navigation"].partial({"resource": collection.identifier}).uri
|
|
28
|
+
else:
|
|
29
|
+
inj = {"collection": templates["collection"].partial({"id": collection.identifier}).uri}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
**inj,
|
|
33
|
+
"totalParents": collection.total_parents,
|
|
34
|
+
"totalChildren": collection.total_children
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def msg_4xx(string, code=404) -> Response:
|
|
39
|
+
return Response(json.dumps({"message": string}), status=code, mimetype="application/json")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def collection_view(
|
|
43
|
+
identifier: Optional[str],
|
|
44
|
+
nav: str,
|
|
45
|
+
templates: Dict[str, uritemplate.URITemplate]
|
|
46
|
+
) -> Response:
|
|
47
|
+
""" Builds a collection view, regardless of how the parameters are received
|
|
48
|
+
|
|
49
|
+
:param identifier:
|
|
50
|
+
:param nav:
|
|
51
|
+
:param templates:
|
|
52
|
+
"""
|
|
53
|
+
if not identifier:
|
|
54
|
+
coll: Collection = db.session.query(Collection).filter(~Collection.parents.any()).first()
|
|
55
|
+
else:
|
|
56
|
+
coll = Collection.query.where(Collection.identifier==identifier).first()
|
|
57
|
+
if coll is None:
|
|
58
|
+
return msg_4xx("Unknown collection")
|
|
59
|
+
out = coll.json()
|
|
60
|
+
|
|
61
|
+
if nav == 'children':
|
|
62
|
+
members = db.session.query(Collection).filter(
|
|
63
|
+
Collection.parents.any(id=coll.id)
|
|
64
|
+
).all()
|
|
65
|
+
elif nav == 'parents':
|
|
66
|
+
members = db.session.query(Collection).filter(
|
|
67
|
+
Collection.children.any(id=coll.id)
|
|
68
|
+
).all()
|
|
69
|
+
else:
|
|
70
|
+
return msg_4xx(f"nav parameter has a wrong value {nav}", code=400)
|
|
71
|
+
|
|
72
|
+
return Response(json.dumps({
|
|
73
|
+
"@context": "https://distributed-text-services.github.io/specifications/context/1-alpha1.json",
|
|
74
|
+
"dtsVersion": "1-alpha",
|
|
75
|
+
**out,
|
|
76
|
+
"member": [
|
|
77
|
+
member.json(inject=inject_json(member, templates=templates))
|
|
78
|
+
for member in members
|
|
79
|
+
],
|
|
80
|
+
**inject_json(coll, templates=templates)
|
|
81
|
+
}, ), mimetype="application/ld+json", status=200)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def document_view(resource, ref, start, end, tree) -> Response:
|
|
85
|
+
if not resource:
|
|
86
|
+
return msg_4xx("Resource parameter was not provided")
|
|
87
|
+
|
|
88
|
+
collection: Collection = Collection.query.where(Collection.identifier == resource).first()
|
|
89
|
+
if not collection:
|
|
90
|
+
return msg_4xx(f"Unknown resource `{resource}`")
|
|
91
|
+
|
|
92
|
+
nav: Navigation = Navigation.query.where(Navigation.collection_id == collection.id).first()
|
|
93
|
+
if nav is None:
|
|
94
|
+
return msg_4xx(f"The resource `{resource}` does not support navigation")
|
|
95
|
+
|
|
96
|
+
tree = tree or collection.default_tree
|
|
97
|
+
|
|
98
|
+
# Check for forbidden combinations
|
|
99
|
+
if ref or start or end:
|
|
100
|
+
if tree not in nav.references:
|
|
101
|
+
return msg_4xx(f"Unknown tree {tree} for resource `{resource}`")
|
|
102
|
+
elif ref and (start or end):
|
|
103
|
+
return msg_4xx(f"You cannot provide a ref parameter as well as start or end", code=400)
|
|
104
|
+
elif not ref and ((start and not end) or (end and not start)):
|
|
105
|
+
return msg_4xx(f"Range is missing one of its parameters (start or end)", code=400)
|
|
106
|
+
|
|
107
|
+
paths = nav.paths[tree]
|
|
108
|
+
if start and end and (start not in paths or end not in paths):
|
|
109
|
+
return msg_4xx(f"Unknown reference {start} or {end} in the requested tree.", code=404)
|
|
110
|
+
if ref and ref not in paths:
|
|
111
|
+
return msg_4xx(f"Unknown reference {ref} in the requested tree.", code=404)
|
|
112
|
+
|
|
113
|
+
if not ref and not start:
|
|
114
|
+
with open(collection.filepath) as f:
|
|
115
|
+
content = f.read()
|
|
116
|
+
return Response(content, mimetype="application/xml")
|
|
117
|
+
|
|
118
|
+
doc = Document(collection.filepath)
|
|
119
|
+
return Response(
|
|
120
|
+
ET.tostring(doc.get_passage(
|
|
121
|
+
ref_or_start=ref or start,
|
|
122
|
+
end=end,
|
|
123
|
+
tree=tree
|
|
124
|
+
), encoding=str),
|
|
125
|
+
mimetype="application/xml"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def navigation_view(resource, ref, start, end, tree, down, templates: Dict[str, uritemplate.URITemplate]) -> Response:
|
|
130
|
+
if not resource:
|
|
131
|
+
return msg_4xx("Resource parameter was not provided")
|
|
132
|
+
|
|
133
|
+
collection: Collection = Collection.query.where(Collection.identifier == resource).first()
|
|
134
|
+
if not collection:
|
|
135
|
+
return msg_4xx(f"Unknown resource `{resource}`")
|
|
136
|
+
|
|
137
|
+
nav: Navigation = Navigation.query.where(Navigation.collection_id == collection.id).first()
|
|
138
|
+
if nav is None:
|
|
139
|
+
return msg_4xx(f"The resource `{resource}` does not support navigation")
|
|
140
|
+
|
|
141
|
+
tree = tree or collection.default_tree
|
|
142
|
+
|
|
143
|
+
# Check for forbidden combinations
|
|
144
|
+
if ref or start or end:
|
|
145
|
+
if tree not in nav.references:
|
|
146
|
+
return msg_4xx(f"Unknown tree {tree} for resource `{resource}`")
|
|
147
|
+
elif ref and (start or end):
|
|
148
|
+
return msg_4xx(f"You cannot provide a ref parameter as well as start or end", code=400)
|
|
149
|
+
elif not ref and ((start and not end) or (end and not start)):
|
|
150
|
+
return msg_4xx(f"Range is missing one of its parameters (start or end)", code=400)
|
|
151
|
+
|
|
152
|
+
# Start the response
|
|
153
|
+
out = {
|
|
154
|
+
"@context": "https://distributed-text-services.github.io/specifications/context/1-alpha1.json",
|
|
155
|
+
"dtsVersion": "1-alpha",
|
|
156
|
+
"@type": "Navigation",
|
|
157
|
+
"@id": templates["navigation"].expand({
|
|
158
|
+
"ref": ref, "down": down, "start": start, "end": end, "tree": tree
|
|
159
|
+
}),
|
|
160
|
+
"resource": collection.json(inject={k: v.uri for k, v in templates.items()}),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
refs = nav.references[tree]
|
|
164
|
+
paths = nav.paths[tree]
|
|
165
|
+
|
|
166
|
+
# Three first rows of the specs for combination of down/ref/start/end
|
|
167
|
+
if down is None:
|
|
168
|
+
if ref:
|
|
169
|
+
out["ref"] = {"@type": "CitableUnit", **get_member_by_path(refs, paths[ref])}
|
|
170
|
+
elif start and end:
|
|
171
|
+
out["start"] = {"@type": "CitableUnit", **get_member_by_path(refs, paths[start])}
|
|
172
|
+
out["end"] = {"@type": "CitableUnit", **get_member_by_path(refs, paths[end])}
|
|
173
|
+
else:
|
|
174
|
+
return msg_4xx(f"The down query parameter is required when requesting without ref or start/end", code=400)
|
|
175
|
+
return Response(json.dumps(out), mimetype="application/json", status=200)
|
|
176
|
+
elif down == 0 and start and end:
|
|
177
|
+
return msg_4xx(f"The down query parameter cannot be `0` while using start/end", code=400)
|
|
178
|
+
elif down == 0 and not ref:
|
|
179
|
+
return msg_4xx(f"The down query parameter cannot be `0` without using the `ref` parameter", code=400)
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
members, start, end = get_nav(refs=refs, paths=paths, start_or_ref=start or ref, end=end, down=down)
|
|
183
|
+
except InvalidRangeOrder:
|
|
184
|
+
return msg_4xx("End reference comes before start in the document order. Interchange start and end.", code=400)
|
|
185
|
+
except Exception:
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
out["member"] = members
|
|
189
|
+
if end:
|
|
190
|
+
out["start"] = start
|
|
191
|
+
out["end"] = end
|
|
192
|
+
elif start:
|
|
193
|
+
out["ref"] = start
|
|
194
|
+
|
|
195
|
+
return Response(json.dumps(out), mimetype="application/ld+json", status=200)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def create_app(
|
|
199
|
+
app: Flask,
|
|
200
|
+
base_uri: str,
|
|
201
|
+
use_query: bool = False
|
|
202
|
+
) -> (Flask, SQLAlchemy):
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
Initialisation of the DB is up to you
|
|
206
|
+
"""
|
|
207
|
+
navigation_template = uritemplate.URITemplate(base_uri+"/navigation/{?resource}{&ref,start,end,tree,down}")
|
|
208
|
+
collection_template = uritemplate.URITemplate(base_uri+"/collection/{?id}{&nav}")
|
|
209
|
+
document_template = uritemplate.URITemplate(base_uri+"/document/{?resource}{&ref,start,end,tree}")
|
|
210
|
+
|
|
211
|
+
@app.route("/")
|
|
212
|
+
def index_route():
|
|
213
|
+
return Response(
|
|
214
|
+
json.dumps({
|
|
215
|
+
"@context": "https://distributed-text-services.github.io/specifications/context/1-alpha1.json",
|
|
216
|
+
"dtsVersion": "1-alpha",
|
|
217
|
+
"@id": f"{request.url_root}{request.path}",
|
|
218
|
+
"@type": "EntryPoint",
|
|
219
|
+
"collection": collection_template.uri,
|
|
220
|
+
"navigation": navigation_template.uri,
|
|
221
|
+
"document": document_template.uri
|
|
222
|
+
}),
|
|
223
|
+
mimetype="application/ld+json"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
@app.route("/collection/")
|
|
227
|
+
def collection_route():
|
|
228
|
+
resource = request.args.get("id")
|
|
229
|
+
nav = request.args.get("nav", "children")
|
|
230
|
+
|
|
231
|
+
return collection_view(resource, nav, templates={
|
|
232
|
+
"navigation": navigation_template,
|
|
233
|
+
"collection": collection_template,
|
|
234
|
+
"document": document_template,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
@app.route("/navigation/")
|
|
238
|
+
def navigation_route():
|
|
239
|
+
resource = request.args.get("resource")
|
|
240
|
+
ref = request.args.get("ref")
|
|
241
|
+
start = request.args.get("start")
|
|
242
|
+
end = request.args.get("end")
|
|
243
|
+
tree = request.args.get("tree")
|
|
244
|
+
down = request.args.get("down", type=int, default=None)
|
|
245
|
+
|
|
246
|
+
return navigation_view(resource, ref, start, end, tree, down, templates={
|
|
247
|
+
"navigation": navigation_template.partial({"resource": resource}),
|
|
248
|
+
"collection": collection_template.partial({"id": resource}),
|
|
249
|
+
"document": document_template.partial({"resource": resource}),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
@app.route("/document/")
|
|
253
|
+
def document_route():
|
|
254
|
+
resource = request.args.get("resource")
|
|
255
|
+
ref = request.args.get("ref")
|
|
256
|
+
start = request.args.get("start")
|
|
257
|
+
end = request.args.get("end")
|
|
258
|
+
tree = request.args.get("tree")
|
|
259
|
+
return document_view(resource, ref, start, end, tree)
|
|
260
|
+
|
|
261
|
+
return app, db
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
import os
|
|
266
|
+
from dapitains.app.ingest import store_catalog
|
|
267
|
+
from dapitains.metadata.xml_parser import parse
|
|
268
|
+
|
|
269
|
+
app = Flask(__name__)
|
|
270
|
+
_, db = create_app(app, base_uri="http://localhost:5000")
|
|
271
|
+
|
|
272
|
+
basedir = os.path.abspath(os.path.dirname(__file__))
|
|
273
|
+
db_path = os.path.join(basedir, 'app.db')
|
|
274
|
+
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
|
275
|
+
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
276
|
+
|
|
277
|
+
db.init_app(app)
|
|
278
|
+
with app.app_context():
|
|
279
|
+
db.drop_all()
|
|
280
|
+
db.create_all()
|
|
281
|
+
|
|
282
|
+
catalog, _ = parse(f"{basedir}/../../tests/catalog/example-collection.xml")
|
|
283
|
+
store_catalog(catalog)
|
|
284
|
+
|
|
285
|
+
app.run()
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
5
|
+
from sqlalchemy.ext.mutable import MutableDict, Mutable
|
|
6
|
+
from sqlalchemy.types import TypeDecorator, TEXT
|
|
7
|
+
from sqlalchemy import func
|
|
8
|
+
import click
|
|
9
|
+
except ImportError:
|
|
10
|
+
print("This part of the package can only be imported with the web requirements.")
|
|
11
|
+
raise
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Dict, Any
|
|
14
|
+
import dapitains.metadata.classes as abstracts
|
|
15
|
+
import json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CustomKeyJSONDecoder(json.JSONDecoder):
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
super().__init__(object_hook=self.object_hook, *args, **kwargs)
|
|
21
|
+
|
|
22
|
+
def object_hook(self, obj):
|
|
23
|
+
# Only convert 'None' string keys back to None
|
|
24
|
+
return {None if k == 'null' else k: v for k, v in obj.items()}
|
|
25
|
+
|
|
26
|
+
db = SQLAlchemy()
|
|
27
|
+
|
|
28
|
+
parent_child_association = db.Table('parent_child_association',
|
|
29
|
+
db.Column('parent_id', db.Integer, db.ForeignKey('collections.id'), primary_key=True),
|
|
30
|
+
db.Column('child_id', db.Integer, db.ForeignKey('collections.id'), primary_key=True)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class JSONEncoded(TypeDecorator):
|
|
35
|
+
"""Enables JSON storage by encoding and decoding on the fly."""
|
|
36
|
+
impl = TEXT
|
|
37
|
+
|
|
38
|
+
def process_bind_param(self, value, dialect):
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
else:
|
|
42
|
+
return json.dumps(value)
|
|
43
|
+
|
|
44
|
+
def process_result_value(self, value, dialect):
|
|
45
|
+
if value is None:
|
|
46
|
+
return None
|
|
47
|
+
return json.loads(value, cls=CustomKeyJSONDecoder)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Collection(db.Model):
|
|
51
|
+
__tablename__ = 'collections'
|
|
52
|
+
|
|
53
|
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False)
|
|
54
|
+
identifier = db.Column(db.String, nullable=False, unique=True)
|
|
55
|
+
title = db.Column(db.String, nullable=False)
|
|
56
|
+
description = db.Column(db.String, nullable=True)
|
|
57
|
+
resource = db.Column(db.Boolean, default=False)
|
|
58
|
+
filepath = db.Column(db.String, nullable=True)
|
|
59
|
+
dublin_core = db.Column(JSONEncoded, nullable=True)
|
|
60
|
+
extensions = db.Column(JSONEncoded, nullable=True)
|
|
61
|
+
citeStructure = db.Column(JSONEncoded, nullable=True)
|
|
62
|
+
default_tree = db.Column(db.String, nullable=True)
|
|
63
|
+
|
|
64
|
+
# One-to-one relationship with Navigation
|
|
65
|
+
navigation = db.relationship('Navigation', uselist=False, backref='collection', lazy=True)
|
|
66
|
+
|
|
67
|
+
parents = db.relationship(
|
|
68
|
+
'Collection',
|
|
69
|
+
secondary=parent_child_association,
|
|
70
|
+
primaryjoin=id == parent_child_association.c.child_id,
|
|
71
|
+
secondaryjoin=id == parent_child_association.c.parent_id,
|
|
72
|
+
backref='children'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def total_children(self):
|
|
77
|
+
return db.session.query(func.count(parent_child_association.c.child_id)).filter(
|
|
78
|
+
parent_child_association.c.parent_id == self.id
|
|
79
|
+
).scalar()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def total_parents(self):
|
|
83
|
+
return db.session.query(func.count(parent_child_association.c.parent_id)).filter(
|
|
84
|
+
parent_child_association.c.child_id == self.id
|
|
85
|
+
).scalar()
|
|
86
|
+
|
|
87
|
+
def json(self, inject: Optional[Dict[str, Any]] = None):
|
|
88
|
+
data = {
|
|
89
|
+
"@type": "Resource" if self.resource else "Collection",
|
|
90
|
+
"@id": self.identifier,
|
|
91
|
+
"title": self.title,
|
|
92
|
+
**(inject or {})
|
|
93
|
+
}
|
|
94
|
+
if self.description:
|
|
95
|
+
data["description"] = self.description
|
|
96
|
+
if self.resource:
|
|
97
|
+
data["citationTrees"] = []
|
|
98
|
+
if self.citeStructure:
|
|
99
|
+
data["citationTrees"] = [self.citeStructure[self.default_tree]]
|
|
100
|
+
if len(self.citeStructure) >= 1:
|
|
101
|
+
data["citationTrees"][0]["identifier"] = self.default_tree
|
|
102
|
+
for key in self.citeStructure:
|
|
103
|
+
if key != self.default_tree:
|
|
104
|
+
data["citationTrees"].append(self.citeStructure[key])
|
|
105
|
+
self.citeStructure[key]["identifier"] = key
|
|
106
|
+
for tree in data["citationTrees"]:
|
|
107
|
+
tree["@type"] = "CitationTree"
|
|
108
|
+
if self.dublin_core: # ToDo: Fix the way it's presented to adapt to dts view
|
|
109
|
+
data["dublinCore"] = self.dublin_core
|
|
110
|
+
if self.extensions:
|
|
111
|
+
data["extensions"] = self.extensions
|
|
112
|
+
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_class(cls, obj: abstracts.Collection) -> "Collection":
|
|
117
|
+
dublin_core = defaultdict(list)
|
|
118
|
+
for dublin in obj.dublin_core:
|
|
119
|
+
if dublin.language:
|
|
120
|
+
dublin_core[dublin.term].append({"lang": dublin.language, "value": dublin.value})
|
|
121
|
+
else:
|
|
122
|
+
dublin_core[dublin.term].append(dublin.value)
|
|
123
|
+
|
|
124
|
+
extensions = defaultdict(list)
|
|
125
|
+
for exte in obj.extensions:
|
|
126
|
+
if exte.language:
|
|
127
|
+
extensions[exte.term].append({"lang": exte.language, "value": exte.value})
|
|
128
|
+
else:
|
|
129
|
+
extensions[exte.term].append(exte.value)
|
|
130
|
+
|
|
131
|
+
obj = cls(
|
|
132
|
+
identifier=obj.identifier,
|
|
133
|
+
title=obj.title,
|
|
134
|
+
description=obj.description,
|
|
135
|
+
resource=obj.resource,
|
|
136
|
+
filepath=obj.filepath,
|
|
137
|
+
# We are dumping because it's not read or accessible
|
|
138
|
+
dublin_core=dublin_core, #[dub.json() for dub in obj.dublin_core],
|
|
139
|
+
extensions=extensions, # [ext.json() for ext in obj.extension]
|
|
140
|
+
)
|
|
141
|
+
return obj
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Navigation(db.Model):
|
|
145
|
+
__tablename__ = 'navigations'
|
|
146
|
+
|
|
147
|
+
id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False)
|
|
148
|
+
collection_id = db.Column(db.Integer, db.ForeignKey('collections.id'), nullable=False, unique=True)
|
|
149
|
+
|
|
150
|
+
# JSON fields stored as TEXT
|
|
151
|
+
paths = db.Column(JSONEncoded, nullable=False, default={})
|
|
152
|
+
references = db.Column(JSONEncoded, nullable=False, default={})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
from dapitains.app.database import Collection, Navigation, db, parent_child_association
|
|
3
|
+
from dapitains.app.navigation import generate_paths
|
|
4
|
+
from dapitains.metadata.xml_parser import Catalog
|
|
5
|
+
from dapitains.tei.document import Document
|
|
6
|
+
import tqdm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def store_single(catalog: Catalog, keys: Optional[Dict[str, int]]):
|
|
10
|
+
keys = keys or {}
|
|
11
|
+
for identifier, collection in tqdm.tqdm(catalog.objects.items(), desc="Parsing all collections"):
|
|
12
|
+
coll_db = Collection.from_class(collection)
|
|
13
|
+
db.session.add(coll_db)
|
|
14
|
+
db.session.flush()
|
|
15
|
+
keys[coll_db.identifier] = coll_db.id
|
|
16
|
+
if collection.resource:
|
|
17
|
+
doc = Document(collection.filepath)
|
|
18
|
+
if doc.citeStructure:
|
|
19
|
+
references = {
|
|
20
|
+
tree: [ref.json() for ref in obj.find_refs(doc.xml, structure=obj.structure)]
|
|
21
|
+
for tree, obj in doc.citeStructure.items()
|
|
22
|
+
}
|
|
23
|
+
paths = {key: generate_paths(tree) for key, tree in references.items()}
|
|
24
|
+
nav = Navigation(collection_id=coll_db.id, paths=paths, references=references)
|
|
25
|
+
db.session.add(nav)
|
|
26
|
+
coll_db.citeStructure = {
|
|
27
|
+
key: value.structure.json()
|
|
28
|
+
for key, value in doc.citeStructure.items()
|
|
29
|
+
}
|
|
30
|
+
coll_db.default_tree = doc.default_tree
|
|
31
|
+
db.session.add(coll_db)
|
|
32
|
+
db.session.commit()
|
|
33
|
+
|
|
34
|
+
for parent, child in catalog.relationships:
|
|
35
|
+
insert_statement = parent_child_association.insert().values(
|
|
36
|
+
parent_id=keys[parent],
|
|
37
|
+
child_id=keys[child]
|
|
38
|
+
)
|
|
39
|
+
db.session.execute(insert_statement)
|
|
40
|
+
db.session.commit()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def store_catalog(*catalogs):
|
|
44
|
+
keys = {}
|
|
45
|
+
for catalog in catalogs:
|
|
46
|
+
store_single(catalog, keys)
|