|
| 1 | +import tempfile |
| 2 | +import bcf.v2.bcfxml |
| 3 | +import bcf.v2.model |
1 | 4 | import bcf.v2.topic |
| 5 | +import bcf.v3.bcfxml |
| 6 | +import bcf.v3.model |
2 | 7 | import bcf.v3.topic |
3 | | -from typing import Union |
| 8 | +import bcf.agnostic.model as mdl |
| 9 | +from pathlib import Path |
| 10 | +from typing import Union, Optional |
| 11 | +from typing_extensions import assert_never |
4 | 12 |
|
5 | 13 | TopicHandler = Union[bcf.v2.topic.TopicHandler, bcf.v3.topic.TopicHandler] |
| 14 | + |
| 15 | + |
| 16 | +def extract_file( |
| 17 | + topic: TopicHandler, |
| 18 | + entity: Union[mdl.HeaderFile, mdl.BimSnippet, mdl.DocumentReference], |
| 19 | + bcfxml: Optional[Union[bcf.v2.bcfxml.BcfXml, bcf.v3.bcfxml.BcfXml]] = None, |
| 20 | + outfile: Optional[Path] = None, |
| 21 | +) -> Union[Path, str, None]: |
| 22 | + """Extracts an element with a file into a temporary directory |
| 23 | +
|
| 24 | + These include header files, bim snippets, document references, and |
| 25 | + viewpoint bitmaps. External reference are not downloaded. Instead, the |
| 26 | + URI reference is returned. |
| 27 | +
|
| 28 | + :param entity: The entity with a file reference to extract |
| 29 | + :param outfile: If provided, save the header file to that location. |
| 30 | + Otherwise, a temporary directory is created and the filename is |
| 31 | + derived from the header's original filename. |
| 32 | + :param bcfxml: The BCF XML file to use for resolving document references files. |
| 33 | + Required only for BCF v3 document references (in BCF v3 internal documents |
| 34 | + are stored at BCF root, not in the topic). |
| 35 | + :return: The filepath of the extracted file. It may be a URL if the |
| 36 | + header file is external. |
| 37 | + """ |
| 38 | + if isinstance(entity, mdl.DocumentReference): |
| 39 | + if isinstance(entity, bcf.v2.model.TopicDocumentReference): |
| 40 | + reference = entity.referenced_document |
| 41 | + else: |
| 42 | + reference = entity.document_guid |
| 43 | + else: |
| 44 | + reference = entity.reference |
| 45 | + |
| 46 | + if not reference: |
| 47 | + return None |
| 48 | + |
| 49 | + # For v3 document references external documents are detected by empty document_guid. |
| 50 | + if not isinstance(entity, bcf.v3.model.DocumentReference) and entity.is_external: |
| 51 | + return reference |
| 52 | + |
| 53 | + if isinstance(entity, bcf.v3.model.DocumentReference): |
| 54 | + # Extract document reference filename and contents. |
| 55 | + if not bcfxml: |
| 56 | + raise TypeError("bcfxml is required for BCF v3 document references.") |
| 57 | + assert isinstance(bcfxml, bcf.v3.bcfxml.BcfXml) |
| 58 | + error_msg = f"BCF XML is missing document with guid '{reference}'." |
| 59 | + if not bcfxml.documents: |
| 60 | + raise Exception(error_msg) |
| 61 | + definition_docs = bcfxml.documents.definition.documents |
| 62 | + if not definition_docs: |
| 63 | + raise Exception(error_msg) |
| 64 | + docs = next((doc for doc in definition_docs.document if doc.guid == reference), None) |
| 65 | + if not docs: |
| 66 | + raise Exception(error_msg) |
| 67 | + filename = docs.filename |
| 68 | + bytes_data = bcfxml.documents.documents[filename] |
| 69 | + else: |
| 70 | + if isinstance(entity, mdl.BimSnippet): |
| 71 | + bytes_data = topic.bim_snippet |
| 72 | + assert isinstance(bytes_data, bytes) |
| 73 | + elif isinstance(entity, mdl.HeaderFile): |
| 74 | + bytes_data = topic.reference_files[reference] |
| 75 | + elif isinstance(entity, bcf.v2.model.TopicDocumentReference): |
| 76 | + assert isinstance(topic, bcf.v2.topic.TopicHandler) |
| 77 | + bytes_data = topic.document_references[reference] |
| 78 | + else: |
| 79 | + assert_never(entity) |
| 80 | + |
| 81 | + # We don't really need it if 'outfile' is None, just keeping type checker happy. |
| 82 | + if isinstance(entity, mdl.HeaderFile) and entity.filename: |
| 83 | + filename = entity.filename |
| 84 | + else: |
| 85 | + filename = Path(reference).name |
| 86 | + |
| 87 | + if not outfile: |
| 88 | + outfile = Path(tempfile.mkdtemp()) / filename |
| 89 | + |
| 90 | + with open(outfile, "wb") as f: |
| 91 | + f.write(bytes_data) |
| 92 | + |
| 93 | + return outfile |
0 commit comments