dapytains 0.0.1a0__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.
dapitains/__init__.py ADDED
File without changes
File without changes
dapitains/app/app.py ADDED
@@ -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)
@@ -0,0 +1,135 @@
1
+ from typing import List, Dict, Any, Optional, Tuple
2
+ from dapitains.errors import InvalidRangeOrder
3
+
4
+
5
+ def get_member_by_path(data: List[Dict[str, Any]], path: List[int]) -> Optional[Dict[str, Any]]:
6
+ """
7
+ Retrieve the member at the specified path in the nested data structure.
8
+
9
+ :param data: The nested data structure (list of dictionaries).
10
+ :param path: A list of indices that represent the path to the desired member.
11
+ :return: The member at the specified path, or None if the path is invalid.
12
+ """
13
+ current_level = data
14
+
15
+ path_copy = [] + path
16
+ while path_copy:
17
+ index = path_copy.pop(0)
18
+ try:
19
+ current_level = current_level[index]
20
+ if 'members' in current_level and path_copy:
21
+ current_level = current_level['members']
22
+ except (IndexError, KeyError):
23
+ return None
24
+
25
+ return current_level
26
+
27
+
28
+ def strip_members(obj: Dict[str, Any], add_type: bool = False) -> Dict[str, Any]:
29
+ obj = {k: v for k, v in obj.items() if k != "members"}
30
+ if add_type:
31
+ obj["@type"] = "CitableUnit"
32
+ return obj
33
+
34
+
35
+ def generate_paths(data: List[Dict[str, Any]], path: Optional[List[int]] = None) -> Dict[str, List[int]]:
36
+ """
37
+ Generate a dictionary mapping each 'ref' in a nested data structure to its path.
38
+
39
+ The path is represented as a list of indices that show how to access each 'ref'
40
+ in the nested structure.
41
+
42
+ :param data: The nested data structure (list of dictionaries). Each dictionary
43
+ can have a 'ref' and/or 'members' key.
44
+ :param path: A list of indices representing the current path in the nested data
45
+ structure. Used internally for recursion. Defaults to None for the
46
+ initial call.
47
+ :return: A dictionary where each key is a 'ref' and each value is a list of indices
48
+ representing the path to that 'ref' in the nested structure.
49
+ """
50
+ if path is None:
51
+ path = []
52
+
53
+ paths = {}
54
+
55
+ def recurse(items, current_path):
56
+ for index, item in enumerate(items):
57
+ ref = item.get('identifier')
58
+ if ref:
59
+ # Record the path for the current reference
60
+ paths[ref] = current_path + [index]
61
+
62
+ members = item.get('members')
63
+ if members:
64
+ # Recurse into the 'members' list
65
+ recurse(members, current_path + [index])
66
+
67
+ recurse(data, [])
68
+ return paths
69
+
70
+
71
+ def get_nav(
72
+ refs: List[Dict[str, Any]],
73
+ paths: Dict[str, List[int]],
74
+ start_or_ref: Optional[str] = None,
75
+ end: Optional[str] = None,
76
+ down: Optional[int] = 1
77
+ ) -> Tuple[List[Dict[str, Any]], Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
78
+ """ Given a references set and a path set, provide the CitableUnit from start to end at down level.
79
+
80
+ """
81
+
82
+ paths_index = list(paths.keys())
83
+ start_index, end_index = None, len(paths_index)
84
+
85
+ if end:
86
+ # For end, as end is inclusive, we check for the last partial match
87
+ # (ie, if Mark is [1], we want everything starting
88
+ # by [1].)
89
+ end_index = paths_index.index(end)
90
+ len_end = len(paths[end])
91
+ for idx, reference in enumerate(paths_index[end_index+1:]):
92
+ if paths[reference][:len_end] == paths[end]:
93
+ end_index = end_index+idx
94
+ else:
95
+ break
96
+
97
+ if start_or_ref:
98
+ start_index = paths_index.index(start_or_ref)
99
+ if not end:
100
+ if down == 0:
101
+ end_index = len(paths_index)
102
+ else:
103
+ for index, reference in enumerate(paths_index[start_index+1:]):
104
+ if len(paths[start_or_ref]) == len(paths[reference]):
105
+ end_index = index + start_index
106
+ if start_index > end_index:
107
+ raise InvalidRangeOrder
108
+
109
+ paths = dict(list(paths.items())[start_index:end_index+1])
110
+
111
+ current_level = []
112
+ start_path, end_path = None, None
113
+ if start_or_ref:
114
+ start_path = paths[start_or_ref]
115
+ current_level.append(len(start_path))
116
+ if end:
117
+ end_path = paths[end]
118
+ current_level.append(len(end_path))
119
+
120
+ current_level = max(current_level) if current_level else 0
121
+
122
+ if down == 0:
123
+ paths = {key: value for key, value in paths.items() if len(value) == current_level}
124
+ elif down == -1:
125
+ paths = {key: value for key, value in paths.items() if current_level <= len(value)}
126
+ else:
127
+ paths = {key: value for key, value in paths.items() if current_level <= len(value) <= down + current_level}
128
+
129
+ return (
130
+ [
131
+ strip_members(get_member_by_path(refs, path), add_type=True) for path in paths.values()
132
+ ],
133
+ strip_members(get_member_by_path(refs, start_path), add_type=True) if start_path else None,
134
+ strip_members(get_member_by_path(refs, end_path), add_type=True) if end_path else None
135
+ )
dapitains/constants.py ADDED
@@ -0,0 +1,36 @@
1
+ import logging
2
+ import os
3
+
4
+ try:
5
+ saxon_version = os.getenv("pysaxon", "HE")
6
+ saxon_license = os.getenv("pysaxon_license", "")
7
+ logging.info(f"Using SaxonLib {saxon_version}")
8
+ if saxon_version == "HE":
9
+ import saxonche as saxonlib
10
+ PROCESSOR = saxonlib.PySaxonProcessor()
11
+ elif saxon_version == "PE":
12
+ import saxoncpe as saxonlib
13
+ PROCESSOR = saxonlib.PySaxonProcessor(license=saxon_license)
14
+ elif saxon_version == "PE":
15
+ import saxoncee as saxonlib
16
+ PROCESSOR = saxonlib.PySaxonProcessor(license=saxon_license)
17
+ except ImportError:
18
+ print("Unable to import the required PySaxonC version, resorting to PySaxonC-HE")
19
+ import saxonche as saxonlib
20
+ PROCESSOR = saxonlib.PySaxonProcessor()
21
+
22
+ PROCESSOR.set_configuration_property("indent-space", "0")
23
+
24
+
25
+
26
+
27
+ def get_xpath_proc(elem: saxonlib.PyXdmNode) -> saxonlib.PyXPathProcessor:
28
+ """ Builds an XPath processor around a given element, with the default TEI namespace
29
+
30
+ :param elem: An XML node, root or not
31
+ :return: XPathProccesor
32
+ """
33
+ xpath = PROCESSOR.new_xpath_processor()
34
+ xpath.declare_namespace("", "http://www.tei-c.org/ns/1.0")
35
+ xpath.set_context(xdm_item=elem)
36
+ return xpath
dapitains/errors.py ADDED
@@ -0,0 +1,5 @@
1
+ class UnknownTreeName(Exception):
2
+ """This exception is raised when a requested tree is unknown """
3
+
4
+ class InvalidRangeOrder(Exception):
5
+ """Error raised when a range is in the wrong order (start > end) """
File without changes