ansys-bdm-api 0.5.dev0__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.
@@ -0,0 +1,602 @@
1
+ # Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ from io import BufferedIOBase, RawIOBase
18
+ from pathlib import Path
19
+ import shutil
20
+ from types import TracebackType
21
+ from typing import TYPE_CHECKING, Protocol
22
+
23
+ from ansys.bdm.api.entity_handle import EntityHandle
24
+ from ansys.bdm.api.ientity_writer import IEntityWriter
25
+ from ansys.bdm.api.recursive_dictionary import (
26
+ RecursiveDictionaryOfEntityHandles,
27
+ get_and_create_nested_dict,
28
+ validate_path_component,
29
+ )
30
+ from ansys.bdm.api.storage_exceptions import NotFoundInLocalStorageRootError
31
+
32
+ if TYPE_CHECKING:
33
+ from os import PathLike
34
+
35
+ import ansys.bdm.api.iasync_storage_scope
36
+
37
+
38
+ class IReadStorageScope(Protocol):
39
+ """
40
+ Represents access to a read only Blob Data Management system.
41
+ Allows consumers to produce and consume :class:`EntityHandle` instances.
42
+
43
+ This is the synchronous version of the :class:`IAsyncReadStorageScope` interface.
44
+ """
45
+
46
+ def __enter__(self) -> "IReadStorageScope":
47
+ """
48
+ Track the set of files that are stored centrally.
49
+ """
50
+ # deliberately not implemented
51
+ ...
52
+
53
+ def __exit__(
54
+ self,
55
+ __exc_type: type[BaseException] | None, # noqa: PYI063
56
+ __exc_value: BaseException | None,
57
+ __traceback: TracebackType | None,
58
+ ) -> None:
59
+ """
60
+ Remove all files from local storage that are not stored centrally.
61
+ """
62
+ # deliberately not implemented
63
+ ...
64
+
65
+ def get_cached(self, entity: EntityHandle) -> Path:
66
+ """
67
+ Realize the data to a local filesystem if needed and returns a path to the cached or original file.
68
+
69
+ The :class:`EntityHandle` is intended to represent an immutable value. The file
70
+ returned by this call may point to a cached or even the original file. Callers
71
+ must not modify the file on disk or undefined behavior, including class 3 errors,
72
+ may occur. If the caller needs to modify the file, consider using
73
+ :func:`get_copy()`, or copying the file before modifying it.
74
+
75
+ Parameters
76
+ ----------
77
+ entity: EntityHandle
78
+ The handle to the data to realize
79
+
80
+ Returns
81
+ -------
82
+ Path
83
+ The path to the contents of the EntityHandle as realized locally. The caller
84
+ MUST NOT modify the returned path.
85
+ """
86
+ # deliberately not implemented
87
+ ...
88
+
89
+ def get_copy(self, entity: EntityHandle, destination: Path) -> None:
90
+ """
91
+ Realize the data to a local filesystem by writing to a specified file.
92
+
93
+ The caller is free to modify the written file. The caller is responsible
94
+ for deleting the generated file. If the destination path is within the storage root
95
+ then the copy at that location will be deleted when the storage scope context is closed.
96
+
97
+ Parameters
98
+ ----------
99
+ entity: EntityHandle
100
+ The handle to the data to realize
101
+ destination: Path
102
+ The path to the file to write
103
+ """
104
+ # deliberately not implemented
105
+ ...
106
+
107
+ def get_stream(self, entity: EntityHandle) -> RawIOBase:
108
+ """
109
+ Open the EntityHandle contents for reading as a stream.
110
+
111
+ The returned stream MUST NOT be writable. The returned stream MAY be seekable.
112
+
113
+ Parameters
114
+ ----------
115
+ entity: EntityHandle
116
+ The handle to the data to realize
117
+
118
+ Returns
119
+ -------
120
+ RawIOBase
121
+ The stream which, when read, will return the contents of the EntityHandle.
122
+
123
+ Raises
124
+ ------
125
+
126
+ CannotGenerateStreamForDirectoryError
127
+ If the entity requested is a collection
128
+ """
129
+ # deliberately not implemented
130
+ ...
131
+
132
+ def get_bytes(self, entity: EntityHandle) -> bytes:
133
+ """
134
+ Return the content of the referenced blob.
135
+
136
+ Parameters
137
+ ----------
138
+ entity: EntityHandle
139
+ The handle to the data to realize
140
+
141
+ Returns
142
+ -------
143
+ bytes
144
+ the contents of the EntityHandle.
145
+
146
+ Raises
147
+ ------
148
+
149
+ CannotGenerateStreamForDirectoryError
150
+ If the entity requested is a collection
151
+ """
152
+ # deliberately not implemented
153
+ ...
154
+
155
+ def get_text(self, entity: EntityHandle, encoding: str | None = None) -> str:
156
+ """
157
+ Return the content of the referenced blob.
158
+
159
+ Parameters
160
+ ----------
161
+ entity: EntityHandle
162
+ The handle to the data to realize
163
+ encoding: Optional[str]
164
+ The name of an encoding. When this argument is not None the bytes
165
+ of ``entity`` will be read using that encoding.
166
+
167
+ Returns
168
+ -------
169
+ str
170
+ the contents of the EntityHandle in text form with the encoding determined
171
+ in order of preference by:
172
+ - the encoding argument if not None
173
+ - the encoding field of the entity argument if set
174
+ - the BOM of the referenced entity if it contains one; or
175
+ - UTF-8
176
+
177
+ Raises
178
+ ------
179
+
180
+ CannotGenerateStreamForDirectoryError
181
+ If the entity requested is a collection
182
+ """
183
+ # deliberately not implemented
184
+ ...
185
+
186
+ def get_children(self, entity: EntityHandle) -> list[EntityHandle]:
187
+ """
188
+ Return the entities contained in this entity.
189
+
190
+ Parameters
191
+ ----------
192
+
193
+ entity: EntityHandle
194
+ The entity to query
195
+
196
+
197
+ Returns
198
+ -------
199
+
200
+ List[EntityHandle]
201
+ The list of entities that are children of the provided entity
202
+
203
+ Raises
204
+ ------
205
+
206
+ NotADirectoryError
207
+ If the requested entity is not a directory
208
+
209
+ """
210
+ # deliberately not implemented
211
+ ...
212
+
213
+ def get_child(self, entity: EntityHandle, child_name: str) -> EntityHandle:
214
+ """
215
+ Return the entity contained in this entity with a given name.
216
+
217
+ Parameters
218
+ ----------
219
+
220
+ entity: EntityHandle
221
+ The entity to query
222
+ child_name: str
223
+ The child to search for
224
+
225
+ Returns
226
+ -------
227
+
228
+ EntityHandle
229
+ The :class:`EntityHandle` requested
230
+
231
+ Raises
232
+ ------
233
+
234
+ NotADirectoryError
235
+ If the requested entity is not a directory
236
+ EntityNotFoundInBlobStorageError
237
+ If the requested entity is not found
238
+ """
239
+ # deliberately not implemented
240
+ ...
241
+
242
+ def get_parent(self, entity: EntityHandle) -> EntityHandle | None:
243
+ """
244
+ Return the entity containing in this entity.
245
+
246
+ Parameters
247
+ ----------
248
+
249
+ entity: EntityHandle
250
+ The entity to query
251
+
252
+ Returns
253
+ -------
254
+
255
+ Optional[EntityHandle]
256
+ The :class:`EntityHandle` requested, or None if the entity is stored at the root of its scope.
257
+
258
+ Raises
259
+ ------
260
+
261
+ EntityNotFoundInBlobStorageError
262
+ If the requested entity is not found.
263
+ """
264
+ # deliberately not implemented
265
+ ...
266
+
267
+ def get_unreferenced_entities(
268
+ self,
269
+ context: str,
270
+ live_handles: list[EntityHandle],
271
+ ) -> list[EntityHandle]:
272
+ """
273
+ Return a list of all entities within the context that are not referenced by the given live entity handles.
274
+
275
+ Parameters
276
+ ----------
277
+
278
+ context: str
279
+ A label for storage scopes that enables them to be categorized and configured given
280
+ a common system wide configuration.
281
+ It defines the bounds over which methods supporting garbage collection are constrained.
282
+
283
+ live_handles: list[EntityHandle]
284
+ A list of handles that are expected to refer to blobs.
285
+
286
+ Returns
287
+ -------
288
+
289
+ list[EntityHandle]
290
+ The list of all entities within the context that are not referenced by the given live entity handles.
291
+
292
+ Raises
293
+ ------
294
+
295
+ ValueError
296
+ If the live_handles do not belong to the provided context.
297
+ """
298
+ # deliberately not implemented
299
+ ...
300
+
301
+ @property
302
+ def asynchronous(
303
+ self,
304
+ ) -> "ansys.bdm.api.iasync_storage_scope.IAsyncReadStorageScope":
305
+ """
306
+ Return a object with an asynchronous interface to the same scope.
307
+ """
308
+ # deliberately not implemented
309
+ ...
310
+
311
+ def _copy_nested_dictionary(
312
+ self,
313
+ nested_dict: RecursiveDictionaryOfEntityHandles,
314
+ current_path: Path,
315
+ glob: str | None,
316
+ ) -> None:
317
+ for key, value in nested_dict.items():
318
+ validate_path_component(key)
319
+ dest_path = current_path / key
320
+ if isinstance(value, EntityHandle):
321
+ if not glob or dest_path.match(glob):
322
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
323
+ self.get_copy(value, dest_path)
324
+ else:
325
+ if not glob or dest_path.match(glob):
326
+ dest_path.mkdir(parents=True, exist_ok=True)
327
+ # we cannot stop iterating. even if the directory does not match glob,
328
+ # there may be files within it that do match glob.
329
+ # Don't create the directory if it doesn't match, though, to avoid orphaned directories that would need
330
+ # to be cleaned up later.
331
+ self._copy_nested_dictionary(value, dest_path, glob)
332
+
333
+ def get_copy_from_dictionary(
334
+ self,
335
+ root: "PathLike[str]",
336
+ source: RecursiveDictionaryOfEntityHandles,
337
+ glob: str | None = None,
338
+ ) -> None:
339
+ """
340
+ Create a recursive directory structure containing copies of entities from source.
341
+
342
+ This method creates a directory structure on the file system at root containing
343
+ copies of the entities in source. If glob is None, all files at or within source
344
+ are copied to root. Note that file and directory names used when creating the
345
+ directory structure at root are always taken from the keys in source, not from
346
+ the original_name attribute of the EntityHandle objects.
347
+
348
+ Parameters
349
+ ----------
350
+ root : PathLike[str]
351
+ The root directory to create the directory structure in.
352
+ source : RecursiveDictionaryOfEntityHandles
353
+ A recursive nested dictionary of EntityHandle objects.
354
+ glob : str | None, optional
355
+ A glob pattern to filter which files and directories to copy. Behaves
356
+ identically to Path.match() and is interpreted as relative to root using
357
+ the keys in source as the path components. Default is None.
358
+
359
+ Raises
360
+ ------
361
+ NotADirectoryError
362
+ If root points to an existing file.
363
+ """
364
+ # deliberately not implemented
365
+ abs_dest_path = Path(root).resolve()
366
+ if abs_dest_path.is_file():
367
+ raise NotADirectoryError("destination path must be a directory")
368
+ if abs_dest_path.is_dir():
369
+ shutil.rmtree(abs_dest_path)
370
+ abs_dest_path.mkdir(parents=True)
371
+ # An implementation must perform matching against source not the file system. Callers expect the minimum number
372
+ # of copies between BDM and root. An implementation which copied the whole of source to root then deleted
373
+ # non-matching entities is not acceptable.
374
+ self._copy_nested_dictionary(source, abs_dest_path, glob)
375
+
376
+
377
+ class IStorageScope(IReadStorageScope, Protocol):
378
+ """
379
+ Represents access to a Blob Data Management system. Allows consumers
380
+ to produce and consume :class:`EntityHandle` instances.
381
+
382
+ This is the synchronous version of the :class:`IAsyncStorageScope` interface.
383
+
384
+ On dispose, files within the :func:`storage_root` that have not been passed to
385
+ :func:`store()` will be deleted.
386
+ """
387
+
388
+ def __enter__(self) -> "IStorageScope":
389
+ """
390
+ Track the set of files that are stored centrally.
391
+ """
392
+ # deliberately not implemented
393
+ ...
394
+
395
+ def store(
396
+ self,
397
+ from_: "PathLike[str]",
398
+ mime_type: str | None = None,
399
+ encoding: str | None = None,
400
+ ) -> EntityHandle:
401
+ """
402
+ Create an :class:`EntityHandle` from a file on disk.
403
+
404
+ Many blob handler implementations will try to optimize performance and the file contents
405
+ may not be immediately read from disk. To avoid class 3 type errors, the file MUST NOT be
406
+ externally modified after calling this method.
407
+
408
+ Parameters
409
+ ----------
410
+
411
+ from_: PathLike[str]
412
+ The local file on disk which contains the contents for the generated :class:`EntityHandle`
413
+ mime_type: Optional[str]
414
+ Mime type of this file, if known. If None is passed in, the IAsyncStorageScope SHOULD
415
+ use file extension to determine the mime type.
416
+ encoding: Optional[str]
417
+ The Internet Assigned Numbers Authority (IANA) registered encoding name used for textual data.
418
+ This MAY be None if not known and SHOULD NOT be set for binary mime types.
419
+
420
+ Returns
421
+ -------
422
+ An :class:`EntityHandle` that rerpesents the contents of the read file at the moment
423
+ this method is invoked
424
+ """
425
+ # deliberately not implemented
426
+ ...
427
+
428
+ def store_stream(
429
+ self,
430
+ from_: RawIOBase | BufferedIOBase | bytes,
431
+ relative_location: Path | None = None,
432
+ mime_type: str | None = None,
433
+ encoding: str | None = None,
434
+ ) -> EntityHandle:
435
+ """
436
+ Fully reads a passed in stream and returns a handle for the given content.
437
+
438
+ Parameters
439
+ ----------
440
+
441
+ from_ : Union[RawIOBase, BufferedIOBase, bytes]
442
+ The stream or in-memory bytes from which the new entity will be created
443
+ relative_location : Optional[Path]
444
+ The nominal relative path of the entity. The path is relative to the storage_root of
445
+ the scope. No data is expected at this location.
446
+ The filename from this path will be used as the original name of the entity.
447
+ If this parameter is not set then the implementation will generate a unique location.
448
+ mime_type: Optional[str]
449
+ Mime type of this file, if known. If None is passed in, the IAsyncStorageScope SHOULD
450
+ use file extension to determine the mime type.
451
+ encoding: Optional[str]
452
+ The Internet Assigned Numbers Authority (IANA) registered encoding name used for textual data.
453
+ This MAY be None if not known and SHOULD NOT be set for binary mime types.
454
+
455
+ Returns
456
+ -------
457
+
458
+ EntityHandle
459
+ The :class:`EntityHandle` that represents the passed in data.
460
+ """
461
+ # deliberately not implemented
462
+ ...
463
+
464
+ def begin_store(
465
+ self,
466
+ relative_location: Path | None = None,
467
+ mime_type: str | None = None,
468
+ encoding: str | None = None,
469
+ ) -> IEntityWriter:
470
+ """
471
+ Create an :class:`IEntityWriter` by writing to a stream object.
472
+
473
+ Parameters
474
+ ----------
475
+
476
+ relative_location : Optional[Path]
477
+ The nominal relative path of the entity. The path is relative to the storage_root of
478
+ the scope. No data is expected at this location.
479
+ The filename from this path will be used as the original name of the entity.
480
+ If this parameter is not set then the implementation will generate a unique location.
481
+ mime_type: Optional[str]
482
+ Mime type of this file, if known. If None is passed in, the IAsyncStorageScope SHOULD
483
+ use file extension to determine the mime type.
484
+ encoding: Optional[str]
485
+ The Internet Assigned Numbers Authority (IANA) registered encoding name used for textual data.
486
+ This MAY be None if not known and SHOULD NOT be set for binary mime types.
487
+
488
+ Returns
489
+ -------
490
+
491
+ IEntityWriter
492
+ An object which allows you to write to a stream and retrieve the handle.
493
+ """
494
+ # deliberately not implemented
495
+ ...
496
+
497
+ def get_storage_root(self) -> Path:
498
+ """
499
+ A local filesystem directory that can be used to stage files.
500
+
501
+ Since this folder is managed by the BDM implementation, there may be performance
502
+ benefits to storing files here.
503
+
504
+ This directory usually is lazy instantiated on the first call to this function.
505
+ """
506
+ # deliberately not implemented
507
+ ...
508
+
509
+ @property
510
+ def stored_entities(self) -> list[EntityHandle]:
511
+ """
512
+ The set of :class:`EntityHandle` instances that have been stored via this scope.
513
+ """
514
+ # deliberately not implemented
515
+ ...
516
+
517
+ def destroy(self, *entities: EntityHandle) -> None:
518
+ """
519
+ Delete the entities referenced by the handle.
520
+
521
+ As :class:`EntityHandle` instances are intended to be simple immutable structures
522
+ that can be passed across process and service boundaries, their lifespan must be managed
523
+ by an external system. This call allows the resources associated with a BlobHandle
524
+ to be removed.
525
+
526
+ Parameters
527
+ ----------
528
+
529
+ *entities: EntityHandle
530
+ The entities to delete
531
+ """
532
+ # deliberately not implemented
533
+ ...
534
+
535
+ @property
536
+ def asynchronous(self) -> "ansys.bdm.api.iasync_storage_scope.IAsyncStorageScope":
537
+ """
538
+ Return an object with an asynchronous interface to the same scope.
539
+ """
540
+ # deliberately not implemented
541
+ ...
542
+
543
+ def store_to_dictionary(
544
+ self,
545
+ root: "PathLike[str]",
546
+ glob: str | None = None,
547
+ ) -> RecursiveDictionaryOfEntityHandles:
548
+ """
549
+ Create a recursive nested dictionary of EntityHandles from a directory.
550
+
551
+ This method creates a recursive nested dictionary containing EntityHandle objects for files and nested
552
+ dictionaries for directories. If glob is None, all files at or within root are included in the result.
553
+
554
+ Parameters
555
+ ----------
556
+ root : PathLike[str]
557
+ The root directory to store to a nested dictionary.
558
+ glob : str | None, optional
559
+ A glob pattern to filter which files and directories to include in the result. Behaves identically to
560
+ Path.rglob() and is interpreted as relative to root. Default is None.
561
+
562
+ Returns
563
+ -------
564
+ RecursiveDictionaryOfEntityHandles
565
+ A recursive nested dictionary of EntityHandle objects.
566
+
567
+ Raises
568
+ ------
569
+ FileNotFoundError
570
+ If root does not exist.
571
+ NotADirectoryError
572
+ If root is a file.
573
+ NotFoundInLocalStorageRootError
574
+ If root is not within the storage root of storage scope.
575
+ """
576
+ abs_root_path = Path(root).resolve()
577
+
578
+ storage_root = self.get_storage_root()
579
+ try:
580
+ abs_root_path.relative_to(storage_root)
581
+ except ValueError as e:
582
+ raise NotFoundInLocalStorageRootError(
583
+ f"root path is not within the storage root. storage_root={storage_root}, root={root}",
584
+ ) from e
585
+
586
+ if not abs_root_path.exists():
587
+ raise FileNotFoundError(f"root path does not exist: {root}")
588
+
589
+ if abs_root_path.is_file():
590
+ raise NotADirectoryError(f"root path is a file: {root}")
591
+
592
+ result_dict = RecursiveDictionaryOfEntityHandles()
593
+ for path in abs_root_path.rglob(glob or "*"):
594
+ subdirs = path.parent.relative_to(abs_root_path).parts
595
+ subresult_dict = get_and_create_nested_dict(subdirs, result_dict)
596
+ if path.is_file():
597
+ subresult_dict[path.name] = self.store(path)
598
+ else:
599
+ # needed, otherwise empty directories are not created in _get_and_create_nested_dict
600
+ subresult_dict[path.name] = RecursiveDictionaryOfEntityHandles()
601
+
602
+ return result_dict