mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-14 11:24:19 +00:00
321 lines
12 KiB
Python
321 lines
12 KiB
Python
"""BCF XML V2 handler."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
import warnings
|
|
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
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class BcfXml:
|
|
"""BCF XML handler."""
|
|
|
|
def __init__(
|
|
self, filename: Optional[Path] = None, xml_handler: Optional[AbstractXmlParserSerializer] = None
|
|
) -> None:
|
|
self._filename = filename
|
|
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()
|
|
|
|
def __enter__(self) -> BcfXml:
|
|
return self
|
|
|
|
def __exit__(self, *args: Any) -> None:
|
|
self.close()
|
|
|
|
def __del__(self) -> None:
|
|
self.close()
|
|
|
|
def close(self) -> None:
|
|
if self._zip_file:
|
|
self._zip_file.close()
|
|
|
|
def _load_zip_file(self) -> Optional[zipfile.ZipFile]:
|
|
return zipfile.ZipFile(self._filename) if self._filename else None
|
|
|
|
@property
|
|
def version(self) -> mdl.Version:
|
|
"""Bcf Version."""
|
|
if not self._version:
|
|
self._version = (
|
|
self._xml_handler.parse(self._zip_file.read("bcf.version"), mdl.Version)
|
|
if self._zip_file
|
|
else mdl.Version(version_id="2.1")
|
|
)
|
|
return self._version
|
|
|
|
@version.setter
|
|
def version(self, value: mdl.Version) -> None:
|
|
self._version = value
|
|
|
|
@property
|
|
def project_info(self) -> Optional[mdl.ProjectExtension]:
|
|
"""BCF project information."""
|
|
if not self._project_info and self._zip_file and zipfile.Path(self._zip_file, "project.bcfp").exists():
|
|
self._project_info = self._xml_handler.parse(self._zip_file.read("project.bcfp"), mdl.ProjectExtension)
|
|
return self._project_info
|
|
|
|
@project_info.setter
|
|
def project_info(self, value: Optional[mdl.ProjectExtension]) -> None:
|
|
self._project_info = value
|
|
|
|
@property
|
|
def project(self) -> Optional[mdl.Project]:
|
|
"""BCF project."""
|
|
return self.project_info.project if self.project_info else None
|
|
|
|
@property
|
|
def extension_schema(self) -> Optional[bytes]:
|
|
if not self._extension_schema and self._zip_file and self.project_info and self.project_info.extension_schema:
|
|
self._extension_schema = self._zip_file.read(self.project_info.extension_schema)
|
|
return self._extension_schema
|
|
|
|
@extension_schema.setter
|
|
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."""
|
|
if self._topics is None:
|
|
self._topics = self._load_topics()
|
|
return self._topics
|
|
|
|
def _load_topics(self) -> dict[str, TopicHandler]:
|
|
topics = {}
|
|
if self._zip_file is None:
|
|
return topics
|
|
for topic_dir in zipfile.Path(self._zip_file).iterdir():
|
|
if not topic_dir.is_dir():
|
|
continue
|
|
markup_path = topic_dir.joinpath("markup.bcf")
|
|
if not markup_path.exists():
|
|
continue
|
|
topics[topic_dir.name] = TopicHandler(topic_dir, self._xml_handler)
|
|
return topics
|
|
|
|
@classmethod
|
|
def load(cls, filename: Path, xml_handler: Optional[AbstractXmlParserSerializer] = None) -> Optional[BcfXml]:
|
|
"""
|
|
Create a BcfXml object from a file.
|
|
|
|
Args:
|
|
filename: Path to the file.
|
|
xml_handler: XML parser and serializer.
|
|
|
|
Returns:
|
|
A BcfXml object with the file contents.
|
|
|
|
Raises:
|
|
ValueError: If the file name is null or empty
|
|
"""
|
|
if not filename:
|
|
raise ValueError("filename is required")
|
|
xml_handler = xml_handler or XmlParserSerializer()
|
|
return cls(xml_handler=xml_handler, filename=filename)
|
|
|
|
@classmethod
|
|
def create_new(
|
|
cls,
|
|
project_name: Optional[str] = None,
|
|
xml_handler: Optional[AbstractXmlParserSerializer] = None,
|
|
) -> BcfXml:
|
|
"""
|
|
Create a new BcfXml object.
|
|
|
|
Args:
|
|
project_name: The name of the project.
|
|
xml_handler: XML parser and serializer.
|
|
|
|
Returns:
|
|
A new BcfXml object.
|
|
"""
|
|
instance = cls(xml_handler=xml_handler or XmlParserSerializer())
|
|
instance.project_info = mdl.ProjectExtension(
|
|
project=mdl.Project(name=project_name, project_id=str(uuid.uuid4())), extension_schema=""
|
|
)
|
|
return instance
|
|
|
|
def save(self, filename: Optional[Path] = None, keep_open: bool = False) -> None:
|
|
"""Save the BCF file to the given filename."""
|
|
if not filename and not self._filename:
|
|
raise ValueError("No file name specified, cannot save BCF file.")
|
|
if filename:
|
|
self._filename = filename
|
|
with InMemoryZipFile(self._filename) as bcf_zip:
|
|
self._save_project(bcf_zip)
|
|
self._save_version(bcf_zip)
|
|
self._save_topics(bcf_zip)
|
|
if keep_open:
|
|
self._zip_file = self._load_zip_file()
|
|
|
|
def _save_project(self, destination_zip: ZipFileInterface) -> None:
|
|
self._smart_save_xml(destination_zip, self._project_info, "project.bcfp")
|
|
if self.extension_schema and self.project_info:
|
|
destination_zip.writestr(self.project_info.extension_schema, self.extension_schema)
|
|
|
|
def _save_version(self, destination_zip: ZipFileInterface) -> None:
|
|
if not self._version and self._zip_file:
|
|
destination_zip.writestr("bcf.version", self._zip_file.read("bcf.version"))
|
|
else:
|
|
self._save_xml(destination_zip, "bcf.version", self.version)
|
|
|
|
def _smart_save_xml(self, destination_zip: ZipFileInterface, item: Any, target: str) -> None:
|
|
if item:
|
|
self._save_xml(destination_zip, target, item)
|
|
elif self._zip_file and zipfile.Path(self._zip_file, target).exists():
|
|
destination_zip.writestr(target, self._zip_file.read(target))
|
|
|
|
def _save_xml(self, destination_zip: ZipFileInterface, inner_file: str, xml_obj: Any) -> None:
|
|
destination_zip.writestr(inner_file, self._xml_handler.serialize(xml_obj))
|
|
|
|
def _save_topics(self, destination_zip: ZipFileInterface) -> None:
|
|
for topic_handler in self.topics.values():
|
|
topic_handler.save(destination_zip)
|
|
|
|
def add_topic(
|
|
self, title: str, description: str, author: str, topic_type: str = "", topic_status: str = ""
|
|
) -> TopicHandler:
|
|
"""
|
|
Add a new topic to the BCF.
|
|
|
|
Args:
|
|
title: The title of the topic.
|
|
description: The description of the topic.
|
|
author: The author of the topic.
|
|
topic_type: The type of the topic.
|
|
topic_status: The status of the topic.
|
|
|
|
Returns:
|
|
The newly created topic wrapped inside a TopicHandler object.
|
|
"""
|
|
topic_handler = TopicHandler.create_new(
|
|
title,
|
|
description,
|
|
author,
|
|
topic_type=topic_type,
|
|
topic_status=topic_status,
|
|
xml_handler=self._xml_handler,
|
|
)
|
|
self.topics[topic_handler.guid] = topic_handler
|
|
return topic_handler
|
|
|
|
def __eq__(self, other: object) -> bool | NoReturn:
|
|
return (
|
|
self.version == other.version and self.project_info == other.project_info
|
|
if isinstance(other, BcfXml)
|
|
else NotImplemented
|
|
)
|
|
|
|
# region Deprecated methods
|
|
def new_project(self) -> BcfXml:
|
|
"""Deprecated method."""
|
|
warnings.warn("new_project is deprecated, use create_new instead.", DeprecationWarning)
|
|
return self.create_new()
|
|
|
|
def get_project(self, _filepath: Optional[str] = None) -> Optional[mdl.Project]:
|
|
"""Deprecated method."""
|
|
warnings.warn("get_project is deprecated, use project_info.project instead.", DeprecationWarning)
|
|
return self.project
|
|
|
|
def edit_project(self) -> None:
|
|
"""Deprecated method."""
|
|
warnings.warn("edit_project is deprecated, there's no need to use it.", DeprecationWarning)
|
|
|
|
def save_project(self, filepath: Path) -> None:
|
|
"""Deprecated method."""
|
|
warnings.warn("save_project is deprecated, use save instead.", DeprecationWarning)
|
|
self.save(filepath)
|
|
|
|
def get_version(self) -> Optional[str]:
|
|
warnings.warn("get_version is deprecated, use version.version_id instead.", DeprecationWarning)
|
|
return self.version.version_id
|
|
|
|
def edit_version(self) -> None:
|
|
"""Deprecated method."""
|
|
warnings.warn("edit_version is deprecated, there's no need to use it.", DeprecationWarning)
|
|
|
|
def get_topics(self) -> dict[str, TopicHandler]:
|
|
"""Deprecated method."""
|
|
warnings.warn("get_topics is deprecated, use topics instead.", DeprecationWarning)
|
|
return self.topics
|
|
|
|
def get_topic(self, guid: str) -> TopicHandler:
|
|
"""Return a topic by its GUID."""
|
|
warnings.warn("get_topic is deprecated, use topics[guid] instead", DeprecationWarning)
|
|
return self.topics[guid]
|
|
|
|
def get_header(self, guid: str) -> Optional[mdl.Header]:
|
|
"""Return the header of a Topic by its GUID."""
|
|
return self.topics[guid].header
|
|
|
|
def edit_topic(self) -> None:
|
|
"""Deprecated method."""
|
|
warnings.warn("edit_topic is deprecated, there's no need to use it.", DeprecationWarning)
|
|
|
|
def add_comment(self, _topic: mdl.Topic, _comment: Optional[mdl.Comment] = None) -> None:
|
|
"""Deprecated method."""
|
|
warnings.warn("add_comment is deprecated, use topics methods instead.", DeprecationWarning)
|
|
|
|
def edit_comment(self) -> None:
|
|
"""Deprecated method."""
|
|
warnings.warn("edit_comment is deprecated, there's no need to use it.", DeprecationWarning)
|
|
|
|
# TODO: deprecate other methods
|
|
# endregion
|