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