bcf v2 - extensions support #3220

The api is the same as for bcf v3 though for bcf v2 they are currently available only in read-only mode.
This commit is contained in:
Andrej730
2024-08-13 16:03:31 +05:00
parent ad6e00571e
commit 1e71708aba
5 changed files with 290 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import bcf.v2.model.extensions
import bcf.v3.model.extensions
from typing import NamedTuple, Union
from dataclasses import fields
class AttributeData(NamedTuple):
attr_type: type
subattr_name: str
subattr_xsd_name: str
Extensions = Union[bcf.v2.model.extensions.Extensions, bcf.v3.model.extensions.Extensions]
def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData]:
"""Return mapping of xsd attribute name to a tuple that consists of:
- Extensions attribute name
- Extensions attribute type type
- subattribute name"""
possible_attributes = {}
for field in fields(type(extensions)):
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
subfield = next(iter(fields(field_type)))
xsd_name = subfield.metadata["name"]
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
return possible_attributes
View File
+42
View File
@@ -6,7 +6,9 @@ import zipfile
from pathlib import Path
from typing import Any, NoReturn, Optional, TypeVar
import bcf.agnostic.extensions
import bcf.v2.model as mdl
import bcf.v2.model.extensions as mdl_extensions
from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface
from bcf.v2.topic import TopicHandler
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
@@ -24,6 +26,7 @@ class BcfXml:
self._xml_handler = xml_handler or XmlParserSerializer()
self._version: Optional[mdl.Version] = None
self._project_info: Optional[mdl.ProjectExtension] = None
self._extensions: Optional[mdl_extensions.Extensions] = None
self._topics: Optional[dict[str, TopicHandler]] = None
self._extension_schema: Optional[bytes] = None
self._zip_file = self._load_zip_file()
@@ -85,6 +88,45 @@ class BcfXml:
def extension_schema(self, value: bytes) -> None:
self._extension_schema = value
@property
def extensions(self) -> Optional[mdl_extensions.Extensions]:
"""BCF extensions."""
if not self._extensions and self.extension_schema:
import io
from xml.etree import ElementTree as etree
extensions = mdl_extensions.Extensions()
xs = "{http://www.w3.org/2001/XMLSchema}"
root = etree.parse(io.BytesIO((self.extension_schema)))
attrs = bcf.agnostic.extensions.get_extensions_attributes(extensions)
xsd_to_attrs = {v.subattr_xsd_name: k for k, v in attrs.items()}
for node in root.findall(f".//{xs}restriction"):
attr_type = node.attrib.get("base")
if not attr_type:
continue
attr_name = xsd_to_attrs.get(attr_type)
if attr_name is None:
continue
values = []
for enum in node.findall(f".//{xs}enumeration"):
values.append(enum.attrib["value"])
if not values:
continue
attr_data = attrs[attr_name]
attr = attr_data.attr_type()
setattr(attr, attr_data.subattr_name, values)
setattr(extensions, attr_name, attr)
self._extensions = extensions
return self._extensions
@extensions.setter
def extensions(self, value: Optional[mdl_extensions.Extensions]) -> None:
self._extensions = value
@property
def topics(self) -> dict[str, TopicHandler]:
"""BCF topics."""
+197
View File
@@ -0,0 +1,197 @@
# NOTE: This file is not generated from
# https://github.com/buildingSMART/BCF-XML/blob/release_2_1/Extension%20Schemas/extensions.xsd
# because in bcf 2.1 there is no extensions.xml - I guess, the schema assumes that each .bcf
# will use their own schema patched by it's own extensions.xsd.
#
# To make things simpler we just mimic extensions structures from bcf 3, so they'll have common API,
# and parse extensions.xsd inside .bcf as .xml and fill our structures.
#
# Preferably if we could generate some kind of extensions.xml from extensions.xsd
# and leave all the handling to xsdata.
#
# We also don't add it to __init__.py not to mess with generator.
#
# Currently extensions support for v2 is only read-only.
from dataclasses import dataclass, field, fields
from typing import List, NamedTuple, Optional
@dataclass(slots=True, kw_only=True)
class ExtensionsPriorities:
class Meta:
global_type = False
priority: List[str] = field(
default_factory=list,
metadata={
"name": "Priority",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class ExtensionsSnippetTypes:
class Meta:
global_type = False
snippet_type: List[str] = field(
default_factory=list,
metadata={
"name": "SnippetType",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class ExtensionsStages:
class Meta:
global_type = False
stage: List[str] = field(
default_factory=list,
metadata={
"name": "Stage",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicLabels:
class Meta:
global_type = False
topic_label: List[str] = field(
default_factory=list,
metadata={
"name": "TopicLabel",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicStatuses:
class Meta:
global_type = False
topic_status: List[str] = field(
default_factory=list,
metadata={
"name": "TopicStatus",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class ExtensionsTopicTypes:
class Meta:
global_type = False
topic_type: List[str] = field(
default_factory=list,
metadata={
"name": "TopicType",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class ExtensionsUsers:
class Meta:
global_type = False
user: List[str] = field(
default_factory=list,
metadata={
"name": "UserIdType",
"type": "Element",
"namespace": "",
"min_length": 1,
"white_space": "collapse",
},
)
@dataclass(slots=True, kw_only=True)
class Extensions:
topic_types: Optional[ExtensionsTopicTypes] = field(
default=None,
metadata={
"name": "TopicTypes",
"type": "Element",
"namespace": "",
},
)
topic_statuses: Optional[ExtensionsTopicStatuses] = field(
default=None,
metadata={
"name": "TopicStatuses",
"type": "Element",
"namespace": "",
},
)
priorities: Optional[ExtensionsPriorities] = field(
default=None,
metadata={
"name": "Priorities",
"type": "Element",
"namespace": "",
},
)
topic_labels: Optional[ExtensionsTopicLabels] = field(
default=None,
metadata={
"name": "TopicLabels",
"type": "Element",
"namespace": "",
},
)
users: Optional[ExtensionsUsers] = field(
default=None,
metadata={
"name": "Users",
"type": "Element",
"namespace": "",
},
)
snippet_types: Optional[ExtensionsSnippetTypes] = field(
default=None,
metadata={
"name": "SnippetTypes",
"type": "Element",
"namespace": "",
},
)
stages: Optional[ExtensionsStages] = field(
default=None,
metadata={
"name": "Stages",
"type": "Element",
"namespace": "",
},
)
+23
View File
@@ -55,9 +55,32 @@ def test_save_maximum_information() -> None:
def assert_everything_in_place(bcf: BcfXml):
assert bcf.version.version_id == "2.1"
assert bcf.project
assert bcf.project.name == "BCF API Implementation"
assert bcf.project_info
assert bcf.project_info.extension_schema == "extensions.xsd"
assert bcf.extensions
assert bcf.extensions.topic_types
assert bcf.extensions.topic_types.topic_type == ["Architecture", "Hidden Type", "Structural"]
assert bcf.extensions.topic_statuses
assert bcf.extensions.topic_statuses.topic_status == ["Finished status", "Open", "Closed"]
assert bcf.extensions.priorities
assert bcf.extensions.priorities.priority == ["Low", "High", "Medium"]
assert bcf.extensions.topic_labels
assert bcf.extensions.topic_labels.topic_label == [
"Architecture",
"IT Development",
"Management",
"Mechanical",
"Structural",
]
assert bcf.extensions.users
assert bcf.extensions.users.user == ["dangl@iabi.eu", "linhard@iabi.eu"]
assert bcf.extensions.snippet_types
assert bcf.extensions.snippet_types.snippet_type == ["IFC2X3", "PDF", "XLSX"]
assert bcf.extensions.stages is None
assert len(bcf.topics) == 2
assert_first_topic_handler(bcf.topics["7ddc3ef0-0ab7-43f1-918a-45e38b42369c"])
second_th = bcf.topics["d1068c81-af04-4546-b63c-348810f6c716"]