mash-core 0.9.0__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.
Files changed (69) hide show
  1. mash/__init__.py +426 -0
  2. mash/core/__init__.py +0 -0
  3. mash/core/catalog/__init__.py +1 -0
  4. mash/core/catalog/api.py +76 -0
  5. mash/core/catalog/frontend/assets/entry.client-Bl1bODN_.js +13 -0
  6. mash/core/catalog/frontend/assets/helpers-CvVoHxcP.js +19 -0
  7. mash/core/catalog/frontend/assets/home-D7SI6u38.js +1 -0
  8. mash/core/catalog/frontend/assets/index-Dh-O-te8.js +26 -0
  9. mash/core/catalog/frontend/assets/manifest-f8f091fe.js +1 -0
  10. mash/core/catalog/frontend/assets/root-BE3ctyoe.css +1 -0
  11. mash/core/catalog/frontend/assets/root-CIOxwHW_.js +1 -0
  12. mash/core/catalog/frontend/favicon.ico +0 -0
  13. mash/core/catalog/frontend/index.html +13 -0
  14. mash/core/catalog/readme.md +3 -0
  15. mash/core/compat/__init__.py +0 -0
  16. mash/core/compat/maplib/__init__.py +1 -0
  17. mash/core/compat/maplib/serialization_helper.py +249 -0
  18. mash/core/config.py +35 -0
  19. mash/core/linkml/__init__.py +2 -0
  20. mash/core/linkml/schema.py +112 -0
  21. mash/core/linkml/shapes.py +49 -0
  22. mash/core/logging.py +47 -0
  23. mash/core/maplib.py +8 -0
  24. mash/core/metadata/__init__.py +1 -0
  25. mash/core/metadata/models.py +291 -0
  26. mash/core/mime.py +50 -0
  27. mash/core/models.py +71 -0
  28. mash/core/ns.py +53 -0
  29. mash/core/profiles/__init__.py +2 -0
  30. mash/core/profiles/mash_profile.py +53 -0
  31. mash/core/profiles/metadata.py +31 -0
  32. mash/core/utilities/__init__.py +3 -0
  33. mash/core/utilities/add_prefix_url.py +34 -0
  34. mash/core/utilities/attributes.py +106 -0
  35. mash/core/utilities/azure.py +35 -0
  36. mash/core/utilities/dataset.py +10 -0
  37. mash/core/utilities/dynamic.py +11 -0
  38. mash/core/utilities/files.py +24 -0
  39. mash/core/utilities/identifiers.py +11 -0
  40. mash/core/utilities/organizations.py +9 -0
  41. mash/core/utilities/profiles.py +23 -0
  42. mash/core/utilities/ranges.py +28 -0
  43. mash/core/utilities/resource_descriptors.py +5 -0
  44. mash/core/utilities/semantic_enum.py +40 -0
  45. mash/core/utilities/user.py +22 -0
  46. mash/core/validation.py +110 -0
  47. mash/orm/__init__.py +4 -0
  48. mash/orm/query/__init__.py +2 -0
  49. mash/orm/query/filters.py +235 -0
  50. mash/orm/query/query.py +149 -0
  51. mash/orm/read.py +413 -0
  52. mash/orm/update.py +253 -0
  53. mash/orm/write.py +186 -0
  54. mash/repositories/__init__.py +2 -0
  55. mash/repositories/mesh/__init__.py +1 -0
  56. mash/repositories/mesh/blob_mesh_repository.py +232 -0
  57. mash/repositories/mesh/filesystem_mesh_repository.py +146 -0
  58. mash/repositories/mesh/mesh_repository.py +52 -0
  59. mash/repositories/mesh/multi_repository_view.py +157 -0
  60. mash/repositories/mesh/repository_collection.py +48 -0
  61. mash/repositories/mesh/utilities.py +69 -0
  62. mash/repositories/models/__init__.py +1 -0
  63. mash/repositories/models/model_registry.py +78 -0
  64. mash/repositories/namespace/__init__.py +1 -0
  65. mash/repositories/namespace/namespace.py +122 -0
  66. mash_core-0.9.0.dist-info/METADATA +198 -0
  67. mash_core-0.9.0.dist-info/RECORD +69 -0
  68. mash_core-0.9.0.dist-info/WHEEL +4 -0
  69. mash_core-0.9.0.dist-info/licenses/LICENSE +21 -0
mash/__init__.py ADDED
@@ -0,0 +1,426 @@
1
+ _SHAPE_NS = "urn:graph:shapes"
2
+ _ONTO_NS = "urn:graph:ontology"
3
+
4
+
5
+ class _STATIC:
6
+ maplib_compatible_mimetypes = [
7
+ "application/n-triples",
8
+ "text/turtle",
9
+ "application/rdf+xml",
10
+ ]
11
+
12
+
13
+ import atexit
14
+ import os
15
+ import shutil
16
+ from enum import StrEnum
17
+ from pathlib import Path
18
+ from tempfile import TemporaryDirectory, mkdtemp
19
+ from typing import Literal
20
+ from warnings import warn
21
+
22
+ from cachetools import LRUCache
23
+ from maplib import Model
24
+
25
+ from mash.core.config import Config
26
+ from mash.core.logging import color_logger
27
+ from mash.repositories.mesh.mesh_repository import MeshRepository
28
+ from mash.repositories.mesh.repository_collection import RepositoryCollection
29
+ from mash.repositories.namespace.namespace import (
30
+ DictBackedNamespaceRepository,
31
+ NamespaceRepository,
32
+ )
33
+
34
+ logger = color_logger()
35
+
36
+
37
+ class _MashImpl:
38
+ namespaces: NamespaceRepository
39
+ config: Config
40
+
41
+ _static_repo_collection: RepositoryCollection
42
+ _static_repo_view: MeshRepository
43
+ _live_repo_view: MeshRepository
44
+ _live_repo_collection: RepositoryCollection
45
+
46
+ def __init__(self):
47
+ self.reset()
48
+
49
+ def reset(self):
50
+ self.config = Config()
51
+ self._static_repo_collection = RepositoryCollection()
52
+ self._live_repo_collection = RepositoryCollection()
53
+ self.reset_temp_directory()
54
+ self.namespaces = DictBackedNamespaceRepository()
55
+
56
+ def repositories_ordered(self, default=True, non_default=True) -> list[MeshRepository]:
57
+ pass
58
+
59
+ @property
60
+ def active_repository(self) -> MeshRepository:
61
+ return self._live_repo_collection.active_repository()
62
+
63
+ def reset_temp_directory(self):
64
+ if hasattr(self, "_tmpdir_cls") and isinstance(self._tmpdir_cls, TemporaryDirectory):
65
+ self._tmpdir_cls.cleanup()
66
+
67
+ tempdir = self.config.data_storage_directory / "temp"
68
+ if not tempdir.exists():
69
+ tempdir.mkdir(parents=True)
70
+ self._tmpdir_cls = TemporaryDirectory(
71
+ "mash",
72
+ delete=False,
73
+ dir=tempdir,
74
+ )
75
+ self._tmpdir = self._tmpdir_cls.name
76
+
77
+ def static_repository(self):
78
+ return self._static_repo_view
79
+
80
+ def add_repository(self, key, repository: MeshRepository):
81
+ self._live_repo_collection[key] = repository
82
+
83
+ def remove_repository(self, key):
84
+ self._live_repo_collection.pop(key)
85
+
86
+
87
+ _mash = _MashImpl()
88
+
89
+
90
+ def _get_impl() -> _MashImpl:
91
+ return _mash
92
+
93
+
94
+ from mash.core.metadata import models
95
+ from mash.repositories.models import ModelRegistry, find_semantic_models_in_module
96
+
97
+ _model_registry = ModelRegistry()
98
+
99
+
100
+ def clean_up_tempdir():
101
+ _mash._tmpdir_cls.cleanup()
102
+
103
+
104
+ atexit.register(clean_up_tempdir)
105
+
106
+
107
+ from mash.core.metadata.models import SemanticModel
108
+ from mash.core.models import IRI, Predicate, ReverseRelation
109
+ from mash.orm import instantiate, serialize
110
+
111
+ # Repository stuff below, could make into optional package
112
+
113
+
114
+ def get_active_repository() -> MeshRepository:
115
+ return _mash.active_repository
116
+
117
+
118
+ def _where_is_it(arg: models.SemanticModel, exclude: list):
119
+ for repository in _mash._live_repo_collection.values():
120
+ if repository in exclude:
121
+ continue
122
+ res = repository.has_data(arg.identifier)
123
+ if res:
124
+ return repository
125
+
126
+
127
+ def repository_add(key: str, repository: MeshRepository, ignore_if_exists=False):
128
+ if ignore_if_exists and key in _mash._live_repo_collection:
129
+ return False
130
+
131
+ _mash.add_repository(key, repository)
132
+ return True
133
+
134
+
135
+ def repository_remove(key: str):
136
+ _mash.remove_repository(key)
137
+
138
+
139
+ def _ActiveRepository() -> None:
140
+ """Vanguard statement to represent the active repostory"""
141
+ return None
142
+
143
+
144
+ def write_data(data: bytes, resource: models.Distribution | str, repo=_ActiveRepository()):
145
+ if repo is None:
146
+ repo = _mash.active_repository
147
+
148
+ with repo.open(resource, mode="wb") as file:
149
+ file.write(data)
150
+
151
+
152
+ def copy(
153
+ instances: list[models.SemanticModel] | models.SemanticModel,
154
+ dest: MeshRepository,
155
+ source: MeshRepository | Literal["any"] = _ActiveRepository(),
156
+ filter_mimetype=None,
157
+ ):
158
+ """Copies all metadata and data-data contained in a set of instances from one repository to another"""
159
+ if not isinstance(instances, list):
160
+ instances = [instances]
161
+
162
+ if source is None:
163
+ source = _mash.active_repository
164
+
165
+ # 1. Copy metadata
166
+ dest.update_metadata(*instances)
167
+
168
+ # 2. Copy data-data
169
+ for instance in instances:
170
+ is_distribution = isinstance(instance, models.Distribution) or issubclass(instance.__class__, models.Distribution)
171
+
172
+ if source == "any":
173
+ if isinstance(instance, models.Dataset):
174
+ _dist = instance.distributions[0]
175
+ else:
176
+ _dist = instance
177
+ source_repository = _where_is_it(_dist, exclude=[dest])
178
+
179
+ else:
180
+ source_repository = source
181
+
182
+ if source_repository is None:
183
+ # This means the object doesn't exist anywhere yet, so there is no data to copy
184
+ return
185
+
186
+ if is_distribution:
187
+ _copy_dist_data(instance, source_repository, dest)
188
+ return
189
+
190
+ elif isinstance(instance, models.Dataset):
191
+ for dist in instance.distributions:
192
+ if filter_mimetype and dist.mime_type != filter_mimetype:
193
+ continue
194
+ _copy_dist_data(dist, source_repository, dest)
195
+
196
+
197
+ def _copy_dist_data(distribution: models.Distribution, source_repo: MeshRepository, dest_repo: MeshRepository):
198
+ try:
199
+ with source_repo.open(distribution) as source_fp:
200
+ with dest_repo.open(distribution, "wb") as dest_fp:
201
+ shutil.copyfileobj(source_fp, dest_fp)
202
+ except FileNotFoundError:
203
+ logger.warning("No data found for distribution")
204
+
205
+
206
+ def save(*args: models.SemanticModel, repo: MeshRepository = _ActiveRepository()):
207
+ """Updates or saves a model in active storage"""
208
+ if repo is None:
209
+ repo = _mash.active_repository
210
+ repo.update_metadata(*args)
211
+
212
+
213
+ def fetch(identifier: str, repo: MeshRepository | Literal["any"] = _ActiveRepository()) -> models.SemanticModel | None:
214
+ """Fetches a resource from any repository"""
215
+ if repo is None:
216
+ return _mash._live_repo_view.fetch(identifier, repo="active")
217
+
218
+ if repo == "any":
219
+ return _mash._live_repo_view.fetch(identifier, repo="any")
220
+
221
+ return repo.fetch(identifier)
222
+
223
+
224
+ def safe_fetch(identifier: str, repo: MeshRepository | Literal["any"] = _ActiveRepository()) -> models.SemanticModel | None:
225
+ """Fetches a resource from any repository, returning None on error"""
226
+ try:
227
+ return fetch(identifier, repo)
228
+ except Exception as e:
229
+ logger.warning(f"Failed to fetch {identifier} from repository {repo}: {e}")
230
+ return None
231
+
232
+
233
+ def delete(*args: models.SemanticModel, repo=_ActiveRepository()):
234
+ """Deletes metadata and data attached to models from temporary and local folders"""
235
+ if repo is None:
236
+ repo = _mash.active_repository
237
+
238
+ repo.delete(*args)
239
+
240
+
241
+ def metadata_graph(format="ntriples") -> Model:
242
+ """Fetch metadata from all repositories and return a common RDf graph with all of it"""
243
+ return _mash._live_repo_view.metadata_graph()
244
+
245
+
246
+ class data:
247
+ def repositories_with_data(distribution_id) -> list[MeshRepository]:
248
+ live_repos = _mash._live_repo_collection.values()
249
+ return [r for r in live_repos if r.has_data(distribution_id)]
250
+
251
+ def exists_locally(distribution_id) -> bool:
252
+ return known_repository("local").has_data(distribution_id)
253
+
254
+
255
+ from mash.core.utilities.dataset import distribution_with_mimetype
256
+
257
+
258
+ def read_data(
259
+ source: models.Distribution | models.Dataset | str, mime_type: str = None, repo: MeshRepository | Literal["any"] = _ActiveRepository()
260
+ ) -> bytes:
261
+ """Fetches data from any repository"""
262
+ if repo is None:
263
+ repo = _mash.active_repository
264
+
265
+ # Resolve distribution
266
+ if isinstance(source, str):
267
+ source = fetch(source, repo)
268
+ if source is None:
269
+ raise FileNotFoundError("The distribution was not found in provided repo")
270
+
271
+ if isinstance(source, models.Dataset):
272
+ if mime_type is None:
273
+ raise ValueError("When trying to fetch data from a dataset, the attribute mime_type is required")
274
+
275
+ source = distribution_with_mimetype(source, mime_type)
276
+ if source is None:
277
+ raise ValueError("No distribution found with provided mime_type")
278
+
279
+ distribution_id = source.identifier
280
+
281
+ # Fetch data
282
+ if repo != "any":
283
+ with repo.open(distribution_id) as file:
284
+ return file.read()
285
+
286
+ with _mash._live_repo_view.open(distribution_id, mode="rb", repo="any") as file:
287
+ return file.read()
288
+
289
+
290
+ from mash.repositories.mesh.filesystem_mesh_repository import FilesystemMeshRepository
291
+ from mash.repositories.mesh.multi_repository_view import MultiRepositoryView
292
+
293
+
294
+ def add_local_repository(key, ignore_if_exists=False):
295
+ path: Path = _mash.config.data_storage_directory / key
296
+ path.mkdir(exist_ok=True)
297
+ if repository_add(
298
+ key,
299
+ FilesystemMeshRepository(
300
+ path,
301
+ ),
302
+ ignore_if_exists=ignore_if_exists,
303
+ ):
304
+ logger.info(f"Added local repository '{key}' at {path}")
305
+ else:
306
+ logger.info(f"Repository with key '{key}' already exists")
307
+
308
+
309
+ def _setup_default_repositories():
310
+ _mash._static_repo_view = MultiRepositoryView(_mash._static_repo_collection, read_only=True)
311
+ _mash._live_repo_collection["static"] = _mash._static_repo_view
312
+ _mash._live_repo_view = MultiRepositoryView(_mash._live_repo_collection)
313
+ _mash.reset_temp_directory()
314
+ temp_repo = FilesystemMeshRepository(Path(_mash._tmpdir))
315
+ _mash.add_repository("temp", temp_repo)
316
+ _mash._live_repo_collection.activate_layer(temp_repo)
317
+
318
+ storage_dir = _mash.config.data_storage_directory
319
+ if not storage_dir.exists():
320
+ storage_dir.mkdir()
321
+
322
+ _mash.add_repository(
323
+ "local",
324
+ FilesystemMeshRepository(
325
+ _mash.config.data_storage_directory,
326
+ ),
327
+ )
328
+
329
+
330
+ from mash.repositories.mesh.utilities import load_configured_blob_repositories
331
+
332
+ _setup_default_repositories()
333
+ load_configured_blob_repositories()
334
+
335
+
336
+ class _UseRepositoryImpl:
337
+ """
338
+ Activates a repository as the default one,
339
+ works as both a context manager and a function.
340
+ """
341
+
342
+ def __init__(self, repo: MeshRepository | str):
343
+ if isinstance(repo, str):
344
+ repo = _mash._live_repo_collection[repo]
345
+
346
+ _mash._live_repo_collection.activate_layer(repo)
347
+
348
+ def __enter__(self):
349
+ """Doesn't need to do anything"""
350
+
351
+ def __exit__(self, exc_type, exc_val, exc_tb):
352
+ _mash._live_repo_collection.pop_layer()
353
+
354
+
355
+ use_repository = _UseRepositoryImpl
356
+
357
+
358
+ def known_repository(kind: Literal["temp", "local", "static", "default"]) -> MeshRepository:
359
+ if kind == "default":
360
+ return _mash.active_repository
361
+
362
+ return _mash._live_repo_collection[kind]
363
+
364
+
365
+ from mash.core.maplib import model_from_dataset, serialize_model_to_dataset
366
+ from mash.core.validation import validate_dataset
367
+ from mash.orm.query import query, strict_query
368
+
369
+
370
+ def ns():
371
+ return _mash.namespaces
372
+
373
+
374
+ from mash.core.profiles import MashProfile, MetadataProfile
375
+
376
+
377
+ def globally_use_profile(profile: MashProfile):
378
+ _mash.namespaces.add_layer(profile.namespaces)
379
+ _model_registry.add_layer(profile.models)
380
+ if profile.static_resources is not None:
381
+ _mash._static_repo_collection[profile.identifier] = profile.static_resources
382
+
383
+
384
+ globally_use_profile(MetadataProfile)
385
+
386
+
387
+ def get_repository(key: str) -> MeshRepository | None:
388
+ return _mash._live_repo_collection.get(key)
389
+
390
+
391
+ K3_DIRECTORY = os.getenv("K3_DIRECTORY", None)
392
+
393
+
394
+ def k3(filename) -> Path:
395
+ """This is a utility to make it easy to write and read files from K3_DIRECTORY
396
+ rather than the working directory
397
+ """
398
+ try:
399
+ k3_directory = Path(os.environ["K3_DIRECTORY"])
400
+ assert k3_directory.exists(), "The K3_DIRECTORY pointed to must exist"
401
+ work_dir = k3_directory
402
+ if not work_dir.exists():
403
+ os.mkdir(work_dir)
404
+ logger.info("Created directory " + str(work_dir))
405
+ except KeyError:
406
+ raise ValueError("Your must have the environment K3_DIRECTORY set pointing to a safe folder for storage of sensitive data")
407
+
408
+ return work_dir / filename
409
+
410
+
411
+ import importlib
412
+
413
+ for extension in _mash.config.extensions_enabled:
414
+ importlib.import_module(extension)
415
+
416
+
417
+ def _reset():
418
+ clean_up_tempdir()
419
+ _mash.reset()
420
+ _model_registry.reset()
421
+ _setup_default_repositories()
422
+ load_configured_blob_repositories()
423
+ globally_use_profile(MetadataProfile)
424
+ for extension in _mash.config.extensions_enabled:
425
+ _mod = importlib.import_module(extension)
426
+ _mod.module_enable()
mash/core/__init__.py ADDED
File without changes
@@ -0,0 +1 @@
1
+ from .api import app
@@ -0,0 +1,76 @@
1
+ import os
2
+ import webbrowser
3
+ from contextlib import asynccontextmanager
4
+
5
+ from cachetools import LRUCache, cached
6
+ from fastapi import APIRouter, FastAPI
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.staticfiles import StaticFiles
9
+
10
+ import mash
11
+ from mash import models
12
+ from mash.core.utilities.user import get_current_agent
13
+ from mash.orm import filters
14
+
15
+ HOST = ""
16
+ DATASET_PREFIX = "/dataset"
17
+ GRID_MODEL_PREFIX = "/models"
18
+ SERVICE_PREFIX = "/service"
19
+
20
+
21
+ @asynccontextmanager
22
+ async def lifespan(app: FastAPI):
23
+ if os.getenv("DEV_WP") == "true":
24
+ webbrowser.open_new_tab("http://localhost:8234/catalog")
25
+ yield
26
+
27
+
28
+ app = FastAPI(title="Mash API", lifespan=lifespan)
29
+
30
+ app.add_middleware(
31
+ CORSMiddleware,
32
+ allow_origins=["*"],
33
+ allow_credentials=True,
34
+ allow_methods=["*"],
35
+ allow_headers=["*"],
36
+ )
37
+
38
+ catalog = models.Catalog(
39
+ identifier="urn:mash:local_catalog",
40
+ title="Mash data catalog",
41
+ description="An automatically generated catalog of resources found in Mash",
42
+ publisher=get_current_agent(),
43
+ contact=models.ContactGroup(identifier="mail@example.com", name="Example", mail="mail@example.com"),
44
+ datasets=[f"{HOST}{DATASET_PREFIX}/{d.identifier}" for d in mash.query(filters.isKind(models.Dataset))],
45
+ dataservices=[f"{HOST}{SERVICE_PREFIX}/{d.identifier}" for d in mash.query(filters.isKind(models.DataService))],
46
+ )
47
+
48
+
49
+ app.mount(
50
+ "/catalog",
51
+ StaticFiles(directory="mash/core/catalog/frontend", html=True),
52
+ name="Data catalog UI",
53
+ )
54
+ app.mount("/assets", StaticFiles(directory="mash/core/catalog/frontend/assets"))
55
+
56
+
57
+ @app.get("/dcat/catalog", tags=["Catalog"])
58
+ def describe_data_catalog() -> models.Catalog:
59
+ return catalog
60
+
61
+
62
+ datasets = APIRouter(prefix=DATASET_PREFIX, tags=["Datasets"])
63
+
64
+
65
+ @datasets.get("")
66
+ @cached(cache=LRUCache(8))
67
+ def list_datasets() -> list[models.Dataset]:
68
+ return mash.query(filters.isKind(models.Dataset))
69
+
70
+
71
+ @datasets.get("/{id}")
72
+ def describe_dataset(id: str) -> models.Dataset:
73
+ return mash.query(filters.isKind(models.Dataset), filters.hasIdentifier(id))[0]
74
+
75
+
76
+ app.include_router(datasets)