Skip to content

Commit 1e71708

Browse files
committed
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.
1 parent ad6e005 commit 1e71708

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
lines changed

src/bcf/bcf/agnostic/extensions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import bcf.v2.model.extensions
2+
import bcf.v3.model.extensions
3+
from typing import NamedTuple, Union
4+
from dataclasses import fields
5+
6+
7+
class AttributeData(NamedTuple):
8+
attr_type: type
9+
subattr_name: str
10+
subattr_xsd_name: str
11+
12+
13+
Extensions = Union[bcf.v2.model.extensions.Extensions, bcf.v3.model.extensions.Extensions]
14+
15+
16+
def get_extensions_attributes(extensions: Extensions) -> dict[str, AttributeData]:
17+
"""Return mapping of xsd attribute name to a tuple that consists of:
18+
- Extensions attribute name
19+
- Extensions attribute type type
20+
- subattribute name"""
21+
possible_attributes = {}
22+
for field in fields(type(extensions)):
23+
field_type = field.type.__args__[0] # type: ignore [reportAttributeAccessIssue]
24+
subfield = next(iter(fields(field_type)))
25+
xsd_name = subfield.metadata["name"]
26+
possible_attributes[field.name] = AttributeData(field_type, subfield.name, xsd_name)
27+
28+
return possible_attributes

src/bcf/bcf/extensions.py

Whitespace-only changes.

src/bcf/bcf/v2/bcfxml.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from pathlib import Path
77
from typing import Any, NoReturn, Optional, TypeVar
88

9+
import bcf.agnostic.extensions
910
import bcf.v2.model as mdl
11+
import bcf.v2.model.extensions as mdl_extensions
1012
from bcf.inmemory_zipfile import InMemoryZipFile, ZipFileInterface
1113
from bcf.v2.topic import TopicHandler
1214
from bcf.xml_parser import AbstractXmlParserSerializer, XmlParserSerializer
@@ -24,6 +26,7 @@ def __init__(
2426
self._xml_handler = xml_handler or XmlParserSerializer()
2527
self._version: Optional[mdl.Version] = None
2628
self._project_info: Optional[mdl.ProjectExtension] = None
29+
self._extensions: Optional[mdl_extensions.Extensions] = None
2730
self._topics: Optional[dict[str, TopicHandler]] = None
2831
self._extension_schema: Optional[bytes] = None
2932
self._zip_file = self._load_zip_file()
@@ -85,6 +88,45 @@ def extension_schema(self) -> Optional[bytes]:
8588
def extension_schema(self, value: bytes) -> None:
8689
self._extension_schema = value
8790

91+
@property
92+
def extensions(self) -> Optional[mdl_extensions.Extensions]:
93+
"""BCF extensions."""
94+
95+
if not self._extensions and self.extension_schema:
96+
import io
97+
from xml.etree import ElementTree as etree
98+
99+
extensions = mdl_extensions.Extensions()
100+
101+
xs = "{http://www.w3.org/2001/XMLSchema}"
102+
root = etree.parse(io.BytesIO((self.extension_schema)))
103+
104+
attrs = bcf.agnostic.extensions.get_extensions_attributes(extensions)
105+
xsd_to_attrs = {v.subattr_xsd_name: k for k, v in attrs.items()}
106+
for node in root.findall(f".//{xs}restriction"):
107+
attr_type = node.attrib.get("base")
108+
if not attr_type:
109+
continue
110+
attr_name = xsd_to_attrs.get(attr_type)
111+
if attr_name is None:
112+
continue
113+
values = []
114+
for enum in node.findall(f".//{xs}enumeration"):
115+
values.append(enum.attrib["value"])
116+
if not values:
117+
continue
118+
attr_data = attrs[attr_name]
119+
attr = attr_data.attr_type()
120+
setattr(attr, attr_data.subattr_name, values)
121+
setattr(extensions, attr_name, attr)
122+
123+
self._extensions = extensions
124+
return self._extensions
125+
126+
@extensions.setter
127+
def extensions(self, value: Optional[mdl_extensions.Extensions]) -> None:
128+
self._extensions = value
129+
88130
@property
89131
def topics(self) -> dict[str, TopicHandler]:
90132
"""BCF topics."""

src/bcf/bcf/v2/model/extensions.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# NOTE: This file is not generated from
2+
# https://github.com/buildingSMART/BCF-XML/blob/release_2_1/Extension%20Schemas/extensions.xsd
3+
# because in bcf 2.1 there is no extensions.xml - I guess, the schema assumes that each .bcf
4+
# will use their own schema patched by it's own extensions.xsd.
5+
#
6+
# To make things simpler we just mimic extensions structures from bcf 3, so they'll have common API,
7+
# and parse extensions.xsd inside .bcf as .xml and fill our structures.
8+
#
9+
# Preferably if we could generate some kind of extensions.xml from extensions.xsd
10+
# and leave all the handling to xsdata.
11+
#
12+
# We also don't add it to __init__.py not to mess with generator.
13+
#
14+
# Currently extensions support for v2 is only read-only.
15+
16+
17+
from dataclasses import dataclass, field, fields
18+
from typing import List, NamedTuple, Optional
19+
20+
21+
@dataclass(slots=True, kw_only=True)
22+
class ExtensionsPriorities:
23+
class Meta:
24+
global_type = False
25+
26+
priority: List[str] = field(
27+
default_factory=list,
28+
metadata={
29+
"name": "Priority",
30+
"type": "Element",
31+
"namespace": "",
32+
"min_length": 1,
33+
"white_space": "collapse",
34+
},
35+
)
36+
37+
38+
@dataclass(slots=True, kw_only=True)
39+
class ExtensionsSnippetTypes:
40+
class Meta:
41+
global_type = False
42+
43+
snippet_type: List[str] = field(
44+
default_factory=list,
45+
metadata={
46+
"name": "SnippetType",
47+
"type": "Element",
48+
"namespace": "",
49+
"min_length": 1,
50+
"white_space": "collapse",
51+
},
52+
)
53+
54+
55+
@dataclass(slots=True, kw_only=True)
56+
class ExtensionsStages:
57+
class Meta:
58+
global_type = False
59+
60+
stage: List[str] = field(
61+
default_factory=list,
62+
metadata={
63+
"name": "Stage",
64+
"type": "Element",
65+
"namespace": "",
66+
"min_length": 1,
67+
"white_space": "collapse",
68+
},
69+
)
70+
71+
72+
@dataclass(slots=True, kw_only=True)
73+
class ExtensionsTopicLabels:
74+
class Meta:
75+
global_type = False
76+
77+
topic_label: List[str] = field(
78+
default_factory=list,
79+
metadata={
80+
"name": "TopicLabel",
81+
"type": "Element",
82+
"namespace": "",
83+
"min_length": 1,
84+
"white_space": "collapse",
85+
},
86+
)
87+
88+
89+
@dataclass(slots=True, kw_only=True)
90+
class ExtensionsTopicStatuses:
91+
class Meta:
92+
global_type = False
93+
94+
topic_status: List[str] = field(
95+
default_factory=list,
96+
metadata={
97+
"name": "TopicStatus",
98+
"type": "Element",
99+
"namespace": "",
100+
"min_length": 1,
101+
"white_space": "collapse",
102+
},
103+
)
104+
105+
106+
@dataclass(slots=True, kw_only=True)
107+
class ExtensionsTopicTypes:
108+
class Meta:
109+
global_type = False
110+
111+
topic_type: List[str] = field(
112+
default_factory=list,
113+
metadata={
114+
"name": "TopicType",
115+
"type": "Element",
116+
"namespace": "",
117+
"min_length": 1,
118+
"white_space": "collapse",
119+
},
120+
)
121+
122+
123+
@dataclass(slots=True, kw_only=True)
124+
class ExtensionsUsers:
125+
class Meta:
126+
global_type = False
127+
128+
user: List[str] = field(
129+
default_factory=list,
130+
metadata={
131+
"name": "UserIdType",
132+
"type": "Element",
133+
"namespace": "",
134+
"min_length": 1,
135+
"white_space": "collapse",
136+
},
137+
)
138+
139+
140+
@dataclass(slots=True, kw_only=True)
141+
class Extensions:
142+
topic_types: Optional[ExtensionsTopicTypes] = field(
143+
default=None,
144+
metadata={
145+
"name": "TopicTypes",
146+
"type": "Element",
147+
"namespace": "",
148+
},
149+
)
150+
topic_statuses: Optional[ExtensionsTopicStatuses] = field(
151+
default=None,
152+
metadata={
153+
"name": "TopicStatuses",
154+
"type": "Element",
155+
"namespace": "",
156+
},
157+
)
158+
priorities: Optional[ExtensionsPriorities] = field(
159+
default=None,
160+
metadata={
161+
"name": "Priorities",
162+
"type": "Element",
163+
"namespace": "",
164+
},
165+
)
166+
topic_labels: Optional[ExtensionsTopicLabels] = field(
167+
default=None,
168+
metadata={
169+
"name": "TopicLabels",
170+
"type": "Element",
171+
"namespace": "",
172+
},
173+
)
174+
users: Optional[ExtensionsUsers] = field(
175+
default=None,
176+
metadata={
177+
"name": "Users",
178+
"type": "Element",
179+
"namespace": "",
180+
},
181+
)
182+
snippet_types: Optional[ExtensionsSnippetTypes] = field(
183+
default=None,
184+
metadata={
185+
"name": "SnippetTypes",
186+
"type": "Element",
187+
"namespace": "",
188+
},
189+
)
190+
stages: Optional[ExtensionsStages] = field(
191+
default=None,
192+
metadata={
193+
"name": "Stages",
194+
"type": "Element",
195+
"namespace": "",
196+
},
197+
)

src/bcf/tests/v2/test_example_files.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,32 @@ def test_save_maximum_information() -> None:
5555

5656
def assert_everything_in_place(bcf: BcfXml):
5757
assert bcf.version.version_id == "2.1"
58+
assert bcf.project
5859
assert bcf.project.name == "BCF API Implementation"
60+
assert bcf.project_info
5961
assert bcf.project_info.extension_schema == "extensions.xsd"
6062

63+
assert bcf.extensions
64+
assert bcf.extensions.topic_types
65+
assert bcf.extensions.topic_types.topic_type == ["Architecture", "Hidden Type", "Structural"]
66+
assert bcf.extensions.topic_statuses
67+
assert bcf.extensions.topic_statuses.topic_status == ["Finished status", "Open", "Closed"]
68+
assert bcf.extensions.priorities
69+
assert bcf.extensions.priorities.priority == ["Low", "High", "Medium"]
70+
assert bcf.extensions.topic_labels
71+
assert bcf.extensions.topic_labels.topic_label == [
72+
"Architecture",
73+
"IT Development",
74+
"Management",
75+
"Mechanical",
76+
"Structural",
77+
]
78+
assert bcf.extensions.users
79+
assert bcf.extensions.users.user == ["dangl@iabi.eu", "linhard@iabi.eu"]
80+
assert bcf.extensions.snippet_types
81+
assert bcf.extensions.snippet_types.snippet_type == ["IFC2X3", "PDF", "XLSX"]
82+
assert bcf.extensions.stages is None
83+
6184
assert len(bcf.topics) == 2
6285
assert_first_topic_handler(bcf.topics["7ddc3ef0-0ab7-43f1-918a-45e38b42369c"])
6386
second_th = bcf.topics["d1068c81-af04-4546-b63c-348810f6c716"]

0 commit comments

Comments
 (0)