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.
Files changed (31) hide show
  1. dapytains-0.0.1a0/MANIFEST.in +1 -0
  2. dapytains-0.0.1a0/PKG-INFO +80 -0
  3. dapytains-0.0.1a0/README.md +51 -0
  4. dapytains-0.0.1a0/dapitains/__init__.py +0 -0
  5. dapytains-0.0.1a0/dapitains/app/__init__.py +0 -0
  6. dapytains-0.0.1a0/dapitains/app/app.py +285 -0
  7. dapytains-0.0.1a0/dapitains/app/database.py +152 -0
  8. dapytains-0.0.1a0/dapitains/app/ingest.py +46 -0
  9. dapytains-0.0.1a0/dapitains/app/navigation.py +135 -0
  10. dapytains-0.0.1a0/dapitains/constants.py +36 -0
  11. dapytains-0.0.1a0/dapitains/errors.py +5 -0
  12. dapytains-0.0.1a0/dapitains/metadata/__init__.py +0 -0
  13. dapytains-0.0.1a0/dapitains/metadata/classes.py +57 -0
  14. dapytains-0.0.1a0/dapitains/metadata/xml_parser.py +98 -0
  15. dapytains-0.0.1a0/dapitains/tei/__init__.py +0 -0
  16. dapytains-0.0.1a0/dapitains/tei/citeStructure.py +329 -0
  17. dapytains-0.0.1a0/dapitains/tei/document.py +481 -0
  18. dapytains-0.0.1a0/dapytains.egg-info/PKG-INFO +80 -0
  19. dapytains-0.0.1a0/dapytains.egg-info/SOURCES.txt +29 -0
  20. dapytains-0.0.1a0/dapytains.egg-info/dependency_links.txt +1 -0
  21. dapytains-0.0.1a0/dapytains.egg-info/requires.txt +7 -0
  22. dapytains-0.0.1a0/dapytains.egg-info/top_level.txt +1 -0
  23. dapytains-0.0.1a0/requirements.txt +7 -0
  24. dapytains-0.0.1a0/setup.cfg +4 -0
  25. dapytains-0.0.1a0/setup.py +26 -0
  26. dapytains-0.0.1a0/tests/test_app.py +159 -0
  27. dapytains-0.0.1a0/tests/test_catalog.py +65 -0
  28. dapytains-0.0.1a0/tests/test_citeStructure.py +135 -0
  29. dapytains-0.0.1a0/tests/test_db_create.py +126 -0
  30. dapytains-0.0.1a0/tests/test_lb.py +188 -0
  31. 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
+ [![Flask Server Tests](https://github.com/distributed-text-services/MyDapytains/actions/workflows/dts-validator.yml/badge.svg)](https://github.com/distributed-text-services/MyDapytains/actions/workflows/dts-validator.yml) [![Test](https://github.com/distributed-text-services/MyDapytains/actions/workflows/test.yml/badge.svg)](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
+ [![Flask Server Tests](https://github.com/distributed-text-services/MyDapytains/actions/workflows/dts-validator.yml/badge.svg)](https://github.com/distributed-text-services/MyDapytains/actions/workflows/dts-validator.yml) [![Test](https://github.com/distributed-text-services/MyDapytains/actions/workflows/test.yml/badge.svg)](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)