diff --git a/src/bonsai/bonsai/bim/operator.py b/src/bonsai/bonsai/bim/operator.py index 2d3c1e2643..1dd01d8563 100644 --- a/src/bonsai/bonsai/bim/operator.py +++ b/src/bonsai/bonsai/bim/operator.py @@ -171,7 +171,7 @@ class BIM_OT_multiple_file_selector(bpy.types.Operator): filepath: bpy.props.StringProperty(subtype="FILE_PATH") @classmethod - def poll(self, context): + def poll(cls, context): return getattr(context, "file_props", None) is not None def execute(self, context): diff --git a/src/bonsai/bonsai/bim/ui.py b/src/bonsai/bonsai/bim/ui.py index 3e8bdbba2f..7461c4aa07 100644 --- a/src/bonsai/bonsai/bim/ui.py +++ b/src/bonsai/bonsai/bim/ui.py @@ -434,6 +434,7 @@ class BIM_PT_tabs(Panel): "QUALITY", "SWITCH", ]: + # Draw a little underscore below the active tab icon. if aprops.tab == tab: row.prop(aprops, "active_tab", text="", icon="BLANK1") else: diff --git a/src/ifcfm/ifcfm/__init__.py b/src/ifcfm/ifcfm/__init__.py index 149d8b71e5..d323f3c7a3 100644 --- a/src/ifcfm/ifcfm/__init__.py +++ b/src/ifcfm/ifcfm/__init__.py @@ -20,6 +20,9 @@ import os import re import csv import importlib +from pathlib import Path +from collections import defaultdict +from typing import Literal, Union, Any, Callable try: from openpyxl import Workbook @@ -44,12 +47,21 @@ except: __version__ = version = "0.0.0" +ParserPreset = Literal["basic", "cobie24", "cobie24legacy"] + + class Parser: - def __init__(self, preset="basic"): + config: dict[str, Any] + categories: defaultdict[str, dict[str, Any]] + get_custom_element_data: dict[ + str, Union[Callable[[ifcopenshell.file, ifcopenshell.entity_instance], dict[str, Any]], dict[str, Any]] + ] + duplicate_keys: list[tuple[dict[str, Any], dict[str, Any]]] + + def __init__(self, preset: Union[ParserPreset, dict[str, Any]] = "basic"): self.file = None self.preset = preset - self.categories = {} - self.config = None + self.categories = defaultdict(dict) self.get_custom_element_data = {} self.duplicate_keys = [] @@ -59,9 +71,9 @@ class Parser: else: self.config = preset - def parse(self, ifc_file, name=None): + # TODO: name is unused? + def parse(self, ifc_file: ifcopenshell.file, name=None): for category_name, category_config in self.config["categories"].items(): - self.categories.setdefault(category_name, {}) for element in category_config["get_category_elements"](ifc_file): get_element_data = category_config["get_element_data"] @@ -69,7 +81,7 @@ class Parser: data = {} for key, query in get_element_data.items(): data[key] = ifcopenshell.util.selector.get_element_value(element, query) - else: + elif isinstance(get_element_data, Callable): data = get_element_data(ifc_file, element) or {} get_custom_element_data = self.get_custom_element_data.get(category_name, lambda x, y: None) @@ -77,28 +89,26 @@ class Parser: custom_data = {} for key, query in get_custom_element_data.items(): custom_data[key] = ifcopenshell.util.selector.get_element_value(element, query) - else: + elif isinstance(get_custom_element_data, Callable): custom_data = get_custom_element_data(ifc_file, element) or {} data.update(custom_data) if data: key = "-".join([str(data[k]) for k in category_config["keys"]]) + # TODO: duplicate_keys are never used? if key in self.categories[category_name]: self.duplicate_keys.append((self.categories[category_name][key], data)) self.categories[category_name][key] = data - def federate(self, paths): + def federate(self, paths: list[str]) -> None: for path in paths: spreadsheet = pd.ExcelFile(path) sheet_names = spreadsheet.sheet_names for category_name, category_config in self.config["categories"].items(): - self.categories.setdefault(category_name, {}) if category_name not in sheet_names: continue - - self.categories.setdefault(category_name, {}) df = pd.read_excel(spreadsheet, sheet_name=category_name, keep_default_na=False) for _, row in df.iterrows(): key = "-".join([str(row[k]) for k in category_config["keys"]]) @@ -106,18 +116,21 @@ class Parser: continue self.categories[category_name][key] = row.to_dict() - def exclude_categories(self, names): + def exclude_categories(self, names: list[str]) -> None: for name in names: if name in self.config["categories"]: del self.config["categories"][name] - def exclude_element_data(self, category, names): + def exclude_element_data(self, category: str, names: list[str]) -> None: headers = self.config["categories"][category]["headers"] self.config["categories"][category]["headers"] = [h for h in headers if h not in names] class Writer: - def __init__(self, parser): + config: dict[str, Any] + categories: dict[str, dict[str, Any]] + + def __init__(self, parser: Parser): self.parser = parser if isinstance(self.parser.preset, str): module = importlib.import_module(f"ifcfm.{self.parser.preset}") @@ -127,7 +140,14 @@ class Writer: else: self.config = getattr(self.parser.preset, "config") - def write(self, null="N/A", empty="-", bool_true="YES", bool_false="NO", list_separator=", "): + def write( + self, + null: str = "N/A", + empty: str = "-", + bool_true: str = "YES", + bool_false: str = "NO", + list_separator: str = ", ", + ) -> None: self.categories = {} null = self.config.get("null", null) empty = self.config.get("empty", empty) @@ -191,7 +211,7 @@ class Writer: for row in data["rows"]: writer.writerow(row) - def write_ods(self, output): + def write_ods(self, output: str) -> None: doc = OpenDocumentSpreadsheet() for key, value in self.config.get("colours", {}).items(): @@ -229,7 +249,7 @@ class Writer: doc.save(output, True) - def write_xlsx(self, output): + def write_xlsx(self, output: str) -> None: workbook = Workbook() cell_formats = {} @@ -270,7 +290,7 @@ class Writer: workbook.save(output) - def write_pd(self): + def write_pd(self) -> dict[str, pd.DataFrame]: results = {} for category, data in self.categories.items(): results[category] = pd.DataFrame(data["rows"], columns=data["headers"]) diff --git a/src/ifcfm/ifcfm/__main__.py b/src/ifcfm/ifcfm/__main__.py index 3a34dc245f..9133611716 100644 --- a/src/ifcfm/ifcfm/__main__.py +++ b/src/ifcfm/ifcfm/__main__.py @@ -1,3 +1,21 @@ +# IfcFM - IFC for facility management +# Copyright (C) 2023 Dion Moult +# +# This file is part of IfcFM. +# +# IfcFM is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IfcFM is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with IfcFM. If not, see . + import ifcfm import argparse import ifcopenshell diff --git a/src/ifcfm/ifcfm/basic.py b/src/ifcfm/ifcfm/basic.py index 527fedd98c..62fbdd47e0 100644 --- a/src/ifcfm/ifcfm/basic.py +++ b/src/ifcfm/ifcfm/basic.py @@ -17,26 +17,28 @@ # along with IfcFM. If not, see . import ifcopenshell -import ifcopenshell.util.fm -import ifcopenshell.util.date -import ifcopenshell.util.system -import ifcopenshell.util.placement import ifcopenshell.util.classification +import ifcopenshell.util.date +import ifcopenshell.util.element +import ifcopenshell.util.fm +import ifcopenshell.util.placement +import ifcopenshell.util.system +from typing import Any, Union, Optional -def get_facilities(ifc_file): +def get_facilities(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcBuilding") -def get_storeys(ifc_file): +def get_storeys(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcBuildingStorey") -def get_spaces(ifc_file): +def get_spaces(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcSpace") -def get_zones(ifc_file): +def get_zones(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: zones = [] for zone in ifc_file.by_type("IfcZone"): for rel in zone.IsGroupedBy: @@ -44,22 +46,22 @@ def get_zones(ifc_file): return zones -def get_element_types(ifc_file): +def get_element_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifcopenshell.util.fm.get_fmhem_types(ifc_file) -def get_elements(ifc_file): +def get_elements(ifc_file: ifcopenshell.file) -> set[ifcopenshell.entity_instance]: elements = set() for element_type in get_element_types(ifc_file): elements.update(ifcopenshell.util.element.get_types(element_type)) return elements -def get_systems(ifc_file): +def get_systems(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcSystem") -def get_facility_data(ifc_file, element): +def get_facility_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: return { "Name": element.Name, "ProjectName": ifc_file.by_type("IfcProject")[0].Name, @@ -78,7 +80,7 @@ def get_facility_data(ifc_file, element): } -def get_storey_data(ifc_file, element): +def get_storey_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: return { "Name": element.Name, "ClassificationIdentification": "Level", @@ -92,7 +94,7 @@ def get_storey_data(ifc_file, element): } -def get_space_data(ifc_file, element): +def get_space_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: psets = ifcopenshell.util.element.get_psets(element) return { "Name": element.Name, @@ -110,7 +112,7 @@ def get_space_data(ifc_file, element): } -def get_zone_data(ifc_file, element): +def get_zone_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: zone, space = element return { "Name": zone.Name, @@ -122,7 +124,7 @@ def get_zone_data(ifc_file, element): } -def get_element_type_data(ifc_file, element): +def get_element_type_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: psets = ifcopenshell.util.element.get_psets(element) return { "Name": element.Name, @@ -143,7 +145,7 @@ def get_element_type_data(ifc_file, element): } -def get_element_data(ifc_file, element): +def get_element_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: space = ifcopenshell.util.element.get_container(element) space_name = space.Name if space.is_a("IfcSpace") else None systems = ifcopenshell.util.system.get_element_systems(element) @@ -170,7 +172,7 @@ def get_element_data(ifc_file, element): } -def get_system_data(ifc_file, element): +def get_system_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: return { "Name": element.Name, "Description": element.Description, @@ -183,25 +185,27 @@ def get_system_data(ifc_file, element): } -def get_owner_name(element): +def get_owner_name(element: ifcopenshell.entity_instance) -> Union[str, None]: if not getattr(element, "OwnerHistory", None): return return element.OwnerHistory.OwningUser.TheOrganization.Name -def get_owner_creation_date(element): +def get_owner_creation_date(element: ifcopenshell.entity_instance) -> Union[str, None]: if not getattr(element, "OwnerHistory", None): return return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() -def get_owner_application(element): +def get_owner_application(element: ifcopenshell.entity_instance) -> Union[str, None]: if not getattr(element, "OwnerHistory", None): return return element.OwnerHistory.OwningApplication.ApplicationFullName -def get_facility_parent(element, ifc_class): +def get_facility_parent( + element: ifcopenshell.entity_instance, ifc_class: str +) -> Union[ifcopenshell.entity_instance, None]: parent = ifcopenshell.util.element.get_aggregate(element) while parent: if parent.is_a(ifc_class): @@ -211,7 +215,7 @@ def get_facility_parent(element, ifc_class): parent = ifcopenshell.util.element.get_aggregate(parent) -def get_classification_identification(element): +def get_classification_identification(element: ifcopenshell.entity_instance) -> Union[str, None]: references = list(ifcopenshell.util.classification.get_references(element)) if references: if hasattr(references[0], "Identification"): @@ -219,13 +223,15 @@ def get_classification_identification(element): return references[0].ItemReference -def get_classification_name(element): +def get_classification_name(element: ifcopenshell.entity_instance) -> Union[str, None]: references = list(ifcopenshell.util.classification.get_references(element)) if references: return references[0].Name -def get_property(psets, pset_name, prop_name, decimals=None): +def get_property( + psets: dict[str, Any], pset_name: str, prop_name: str, decimals: Optional[int] = None +) -> Union[Any, None, float]: if pset_name in psets: result = psets[pset_name].get(prop_name, None) if decimals is None or result is None: diff --git a/src/ifcfm/ifcfm/cobie24.py b/src/ifcfm/ifcfm/cobie24.py index f1f4f74dbe..6038161b57 100644 --- a/src/ifcfm/ifcfm/cobie24.py +++ b/src/ifcfm/ifcfm/cobie24.py @@ -17,11 +17,13 @@ # along with IfcFM. If not, see . import ifcopenshell -import ifcopenshell.util.fm -import ifcopenshell.util.date -import ifcopenshell.util.system -import ifcopenshell.util.placement import ifcopenshell.util.classification +import ifcopenshell.util.date +import ifcopenshell.util.element +import ifcopenshell.util.fm +import ifcopenshell.util.placement +import ifcopenshell.util.system +from typing import Any, Union, Optional # The original BIMServer plugin has a function called ifcToCOBie: @@ -34,15 +36,15 @@ import ifcopenshell.util.classification # Impact, Coordinate, Issue, Picklist -def get_contacts(ifc_file): +def get_contacts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcActor") -def get_facilities(ifc_file): +def get_facilities(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcBuilding") -def get_floors(ifc_file): +def get_floors(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return [ e for e in ifc_file.by_type("IfcBuildingStorey") @@ -50,11 +52,11 @@ def get_floors(ifc_file): ] -def get_spaces(ifc_file): +def get_spaces(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcSpace") -def get_zones(ifc_file): +def get_zones(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: results = [] zones = ifc_file.by_type("IfcZone") for zone in zones or []: @@ -69,18 +71,18 @@ def get_zones(ifc_file): return results -def get_types(ifc_file): +def get_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifcopenshell.util.fm.get_cobie_types(ifc_file) -def get_components(ifc_file): +def get_components(ifc_file: ifcopenshell.file) -> set[ifcopenshell.entity_instance]: elements = set() for element_type in get_types(ifc_file): elements.update(ifcopenshell.util.element.get_types(element_type)) return elements -def get_systems(ifc_file): +def get_systems(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: results = [] components = get_components(ifc_file) if ifc_file.schema == "IFC2X3": @@ -94,7 +96,7 @@ def get_systems(ifc_file): return results -def get_assemblies(ifc_file): +def get_assemblies(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: results = [] layer_sets = ifc_file.by_type("IfcMaterialLayerSet") layer_sets = [] # This is temporarily overridden because it is unclear exactly how this is stored in Type. @@ -128,23 +130,25 @@ def get_assemblies(ifc_file): return results -def get_connections(ifc_file): +def get_connections(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcRelConnectsPorts") -def get_spares(ifc_file): +def get_spares(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcConstructionProductResource") -def get_resources(ifc_file): +def get_resources(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcConstructionEquipmentResource") -def get_jobs(ifc_file): +def get_jobs(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcTask") -def get_documents(ifc_file): +def get_documents( + ifc_file: ifcopenshell.file, +) -> list[tuple[ifcopenshell.entity_instance, ifcopenshell.entity_instance, ifcopenshell.entity_instance]]: # The original COBie-Plugins assumes a single related object per rel. I think this was wrong. results = [] for rel in ifc_file.by_type("IfcRelAssociatesDocument"): @@ -158,7 +162,7 @@ def get_documents(ifc_file): return results -def get_attributes(ifc_file): +def get_attributes(ifc_file: ifcopenshell.file) -> list[dict[str, Any]]: results = [] history = get_history(ifc_file) created_by = get_email_from_history(history) if history else None @@ -247,7 +251,7 @@ def get_attributes(ifc_file): return results -def get_contact_data(ifc_file, element): +def get_contact_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: the_actor = element.TheActor if the_actor.is_a("IfcPerson"): @@ -299,7 +303,7 @@ def get_contact_data(ifc_file, element): } -def get_facility_data(ifc_file, element): +def get_facility_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: site = get_facility_parent(element, "IfcSite") site_name = None site_description = None @@ -343,7 +347,7 @@ def get_facility_data(ifc_file, element): } -def get_floor_data(ifc_file, element): +def get_floor_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: height_names = { "Height", "NetHeight", @@ -382,7 +386,7 @@ def get_floor_data(ifc_file, element): } -def get_space_data(ifc_file, element): +def get_space_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: floor_name = None floor = ifcopenshell.util.element.get_aggregate(element) if floor and floor.is_a("IfcBuildingStorey"): @@ -425,7 +429,7 @@ def get_space_data(ifc_file, element): } -def get_zone_data(ifc_file, element): +def get_zone_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: zone, space = element name = zone.Name @@ -449,7 +453,7 @@ def get_zone_data(ifc_file, element): } -def get_type_data(ifc_file, element): +def get_type_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: asset_type = None manufacturer = None model_number = None @@ -555,7 +559,7 @@ def get_type_data(ifc_file, element): } -def get_component_data(ifc_file, element): +def get_component_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: space = ifcopenshell.util.element.get_container(element) space_name = space.Name if space.is_a("IfcSpace") else None systems = ifcopenshell.util.system.get_element_systems(element) @@ -610,7 +614,7 @@ def get_component_data(ifc_file, element): } -def get_system_data(ifc_file, element): +def get_system_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: system, component = element category = get_category(system) component_name = val(component.Name) @@ -627,7 +631,7 @@ def get_system_data(ifc_file, element): } -def get_assembly_data(ifc_file, element): +def get_assembly_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: rel, relating_object, related_object = element if relating_object.is_a("IfcMaterialLayerSet"): @@ -663,7 +667,7 @@ def get_assembly_data(ifc_file, element): } -def get_connection_data(ifc_file, element): +def get_connection_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: connection_type = ( val(element.RelatingPort.ObjectType) or val(element.RelatedPort.ObjectType) @@ -691,7 +695,7 @@ def get_connection_data(ifc_file, element): } -def get_spare_data(ifc_file, element): +def get_spare_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: type_name = None for rel in element.ResourceOf or []: for related_object in rel.RelatedObjects or []: @@ -726,7 +730,7 @@ def get_spare_data(ifc_file, element): } -def get_resource_data(ifc_file, element): +def get_resource_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: return { "Name": val(element.Name), "CreatedBy": get_created_by(element), @@ -739,7 +743,7 @@ def get_resource_data(ifc_file, element): } -def get_job_data(ifc_file, element): +def get_job_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: type_names = [] resource_names = [] for rel in element.OperatesOn or []: @@ -810,7 +814,7 @@ def get_job_data(ifc_file, element): } -def get_document_data(ifc_file, element): +def get_document_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: rel, doc, related_object = element directory = getattr(doc, "Location", None) file = None @@ -847,11 +851,13 @@ def get_document_data(ifc_file, element): } -def get_attribute_data(ifc_file, element): +def get_attribute_data( + ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance +) -> ifcopenshell.entity_instance: return element -def get_unit_type_name(ifc_file, unit_type): +def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str, None]: for unit in ifc_file.by_type("IfcUnitAssignment")[0].Units: if unit.is_a("IfcNamedUnit") and unit.UnitType == unit_type: if unit.is_a("IfcSIUnit"): @@ -868,17 +874,17 @@ def get_unit_type_name(ifc_file, unit_type): return val(unit.Currency) -def get_unit_name(ifc_file, unit): +def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]: if unit.is_a("IfcNamedUnit"): return val(unit.Name) -def get_created_by(element): +def get_created_by(element: ifcopenshell.entity_instance) -> Union[str, None]: if getattr(element, "OwnerHistory", None): return get_email_from_history(element.OwnerHistory) -def get_email_from_history(element): +def get_email_from_history(element: ifcopenshell.entity_instance) -> Union[str, None]: pao = element.OwningUser if pao.is_a("IfcPersonAndOrganization"): return get_email_from_pao(pao.ThePerson, pao.TheOrganization) @@ -888,7 +894,9 @@ def get_email_from_history(element): return get_email_from_pao(None, pao) -def get_email_from_pao(person, organization): +def get_email_from_pao( + person: ifcopenshell.entity_instance, organization: ifcopenshell.entity_instance +) -> Union[str, None]: if organization: for address in organization.Addresses or []: if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: @@ -900,23 +908,25 @@ def get_email_from_pao(person, organization): return address.ElectronicMailAddresses[0] -def get_owner_name(element): +def get_owner_name(element: ifcopenshell.entity_instance) -> Union[str, None]: if getattr(element, "OwnerHistory", None): return element.OwnerHistory.OwningUser.TheOrganization.Name -def get_created_on(element): +def get_created_on(element: ifcopenshell.entity_instance) -> str: if getattr(element, "OwnerHistory", None): return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() return "1900-12-31T23:59:59" # Yes, really -def get_external_system(element): +def get_external_system(element: ifcopenshell.entity_instance) -> Union[str, None]: if getattr(element, "OwnerHistory", None): return val(element.OwnerHistory.OwningApplication.ApplicationFullName) -def get_facility_parent(element, ifc_class): +def get_facility_parent( + element: ifcopenshell.entity_instance, ifc_class: str +) -> Union[ifcopenshell.entity_instance, None]: parent = ifcopenshell.util.element.get_aggregate(element) while parent: if parent.is_a(ifc_class): @@ -926,11 +936,11 @@ def get_facility_parent(element, ifc_class): parent = ifcopenshell.util.element.get_aggregate(parent) -def val(x): +def val(x: Any) -> Any: return x if x not in ("", "n/a") else None -def get_area_measurement(element): +def get_area_measurement(element: ifcopenshell.entity_instance) -> Union[str, None]: for relationship in getattr(element, "IsDefinedBy", []) or []: if relationship.is_a("IfcRelDefinesByProperties"): definition = relationship.RelatingPropertyDefinition @@ -950,7 +960,7 @@ def get_area_measurement(element): return value -def get_category(element): +def get_category(element: ifcopenshell.entity_instance) -> Union[str, None]: references = list(ifcopenshell.util.classification.get_references(element)) results = [] for reference in references: @@ -968,7 +978,9 @@ def get_category(element): return ",".join(results) -def get_pao_address(person, organization, name): +def get_pao_address( + person: ifcopenshell.entity_instance, organization: ifcopenshell.entity_instance, name: str +) -> Union[str, None]: for actor in [organization, person]: if not actor: continue @@ -982,7 +994,9 @@ def get_pao_address(person, organization, name): return result -def get_property(psets, pset_name, prop_name, decimals=None): +def get_property( + psets: dict[str, dict[str, Any]], pset_name: str, prop_name: str, decimals: Optional[int] = None +) -> Union[Any, None, float]: if pset_name in psets: result = psets[pset_name].get(prop_name, None) if decimals is None or result is None: @@ -990,13 +1004,13 @@ def get_property(psets, pset_name, prop_name, decimals=None): return round(result, decimals) -def get_history(ifc_file): +def get_history(ifc_file: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: histories = ifc_file.by_type("IfcOwnerHistory") if histories: return sorted(histories, key=lambda x: x.id())[-1] -def get_property_unit(pset, prop_name): +def get_property_unit(pset: ifcopenshell.entity_instance, prop_name: str) -> Union[str, None]: for prop in getattr(pset, "HasProperties", []) or []: if prop.Name == prop_name: unit = getattr(prop, "Unit", None) @@ -1004,7 +1018,8 @@ def get_property_unit(pset, prop_name): return get_unit_name(unit) -def get_allowed_values(pset_id, prop_name): +# TODO: never used? +def get_allowed_values(ifc_file: ifcopenshell.file, pset_id: int, prop_name: str) -> Union[str, None]: pset = ifc_file.by_id(pset_id) for prop in getattr(pset, "HasProperties", []) or []: if prop.Name == prop_name: @@ -1012,7 +1027,7 @@ def get_allowed_values(pset_id, prop_name): return ",".join([v.wrappedValue for v in prop.EnumerationValues]) -def get_sheet_name(element): +def get_sheet_name(element: ifcopenshell.entity_instance) -> Union[str, None]: if element.is_a("IfcBuilding"): return "Facility" elif element.is_a("IfcBuildingStorey"): diff --git a/src/ifcfm/ifcfm/cobie24legacy.py b/src/ifcfm/ifcfm/cobie24legacy.py index 1442e1f43e..ea8a077b1f 100644 --- a/src/ifcfm/ifcfm/cobie24legacy.py +++ b/src/ifcfm/ifcfm/cobie24legacy.py @@ -18,11 +18,13 @@ import ifcopenshell import ifcopenshell.guid -import ifcopenshell.util.fm -import ifcopenshell.util.date -import ifcopenshell.util.system -import ifcopenshell.util.placement import ifcopenshell.util.classification +import ifcopenshell.util.date +import ifcopenshell.util.element +import ifcopenshell.util.fm +import ifcopenshell.util.placement +import ifcopenshell.util.system +from typing import Any, Union, Optional # The original BIMServer plugin has a function called ifcToCOBie: @@ -35,15 +37,15 @@ import ifcopenshell.util.classification # Impact, Coordinate, Issue, Picklist -def get_contacts(ifc_file): +def get_contacts(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcPersonAndOrganization") -def get_facilities(ifc_file): +def get_facilities(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcBuilding") -def get_floors(ifc_file): +def get_floors(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return [ e for e in ifc_file.by_type("IfcBuildingStorey") @@ -51,11 +53,11 @@ def get_floors(ifc_file): ] -def get_spaces(ifc_file): +def get_spaces(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcSpace") -def get_zones(ifc_file): +def get_zones(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: results = [] zones = ifc_file.by_type("IfcZone") for zone in zones or []: @@ -86,18 +88,18 @@ def get_zones(ifc_file): return results -def get_types(ifc_file): +def get_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifcopenshell.util.fm.get_cobie_types(ifc_file) -def get_components(ifc_file): +def get_components(ifc_file: ifcopenshell.file) -> set[ifcopenshell.entity_instance]: elements = set() for element_type in get_types(ifc_file): elements.update(ifcopenshell.util.element.get_types(element_type)) return elements -def get_systems(ifc_file): +def get_systems(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: results = [] components = get_components(ifc_file) if ifc_file.schema == "IFC2X3": @@ -111,7 +113,7 @@ def get_systems(ifc_file): return results -def get_assemblies(ifc_file): +def get_assemblies(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: results = [] layer_sets = ifc_file.by_type("IfcMaterialLayerSet") layer_sets = [] # This is temporarily overridden because it is unclear exactly how this is stored in Type. @@ -145,23 +147,25 @@ def get_assemblies(ifc_file): return results -def get_connections(ifc_file): +def get_connections(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcRelConnectsPorts") -def get_spares(ifc_file): +def get_spares(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcConstructionProductResource") -def get_resources(ifc_file): +def get_resources(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcConstructionEquipmentResource") -def get_jobs(ifc_file): +def get_jobs(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: return ifc_file.by_type("IfcTask") -def get_documents(ifc_file): +def get_documents( + ifc_file: ifcopenshell.file, +) -> list[tuple[ifcopenshell.entity_instance, ifcopenshell.entity_instance, ifcopenshell.entity_instance]]: # The original COBie-Plugins assumes a single related object per rel. I think this was wrong. results = [] for rel in ifc_file.by_type("IfcRelAssociatesDocument"): @@ -175,7 +179,7 @@ def get_documents(ifc_file): return results -def get_attributes(ifc_file): +def get_attributes(ifc_file: ifcopenshell.file) -> list[dict[str, Any]]: results = [] history = get_history(ifc_file) created_by = get_email_from_history(history) if history else None @@ -265,7 +269,7 @@ def get_attributes(ifc_file): return results -def get_contact_data(ifc_file, element): +def get_contact_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: email = get_email_from_pao(element) history = get_history(ifc_file) @@ -314,7 +318,7 @@ def get_contact_data(ifc_file, element): } -def get_facility_data(ifc_file, element): +def get_facility_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: site = get_facility_parent(element, "IfcSite") site_name = None site_description = None @@ -365,7 +369,7 @@ def get_facility_data(ifc_file, element): } -def get_floor_data(ifc_file, element): +def get_floor_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: external_object = element.is_a() if element.ObjectType and element.ObjectType.lower() in ("site", "ifcsite"): external_object = "IfcSite" @@ -409,7 +413,7 @@ def get_floor_data(ifc_file, element): } -def get_space_data(ifc_file, element): +def get_space_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: floor_name = None floor = ifcopenshell.util.element.get_aggregate(element) if floor and floor.is_a("IfcBuildingStorey"): @@ -452,7 +456,7 @@ def get_space_data(ifc_file, element): } -def get_zone_data(ifc_file, element): +def get_zone_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: zone, space = element if isinstance(zone, tuple): @@ -493,7 +497,7 @@ def get_zone_data(ifc_file, element): } -def get_type_data(ifc_file, element): +def get_type_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: pset_metadata = {} pset_mapping = { "manufacturer": {"Manufacturer"}, @@ -628,7 +632,7 @@ def get_type_data(ifc_file, element): } -def get_component_data(ifc_file, element): +def get_component_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: space = ifcopenshell.util.element.get_container(element) space_name = space.Name if space.is_a("IfcSpace") else None systems = ifcopenshell.util.system.get_element_systems(element) @@ -684,7 +688,7 @@ def get_component_data(ifc_file, element): } -def get_system_data(ifc_file, element): +def get_system_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: system, component = element category = get_category(system) component_name = val(component.Name) @@ -702,7 +706,7 @@ def get_system_data(ifc_file, element): } -def get_assembly_data(ifc_file, element): +def get_assembly_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: rel, relating_object, related_object = element if relating_object.is_a("IfcMaterialLayerSet"): @@ -739,7 +743,7 @@ def get_assembly_data(ifc_file, element): } -def get_connection_data(ifc_file, element): +def get_connection_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: connection_type = ( val(element.RelatingPort.ObjectType) or val(element.RelatedPort.ObjectType) @@ -768,7 +772,7 @@ def get_connection_data(ifc_file, element): } -def get_spare_data(ifc_file, element): +def get_spare_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: type_name = None for rel in element.ResourceOf or []: for related_object in rel.RelatedObjects or []: @@ -804,7 +808,7 @@ def get_spare_data(ifc_file, element): } -def get_resource_data(ifc_file, element): +def get_resource_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: return { "key": val(element.Name), "Name": val(element.Name), @@ -818,7 +822,7 @@ def get_resource_data(ifc_file, element): } -def get_job_data(ifc_file, element): +def get_job_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: type_names = [] resource_names = [] for rel in element.OperatesOn or []: @@ -890,7 +894,7 @@ def get_job_data(ifc_file, element): } -def get_document_data(ifc_file, element): +def get_document_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]: rel, doc, related_object = element directory = getattr(doc, "Location", None) file = None @@ -928,11 +932,13 @@ def get_document_data(ifc_file, element): } -def get_attribute_data(ifc_file, element): +def get_attribute_data( + ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance +) -> ifcopenshell.entity_instance: return element -def get_unit_type_name(ifc_file, unit_type): +def get_unit_type_name(ifc_file: ifcopenshell.file, unit_type: str) -> Union[str, None]: for unit in ifc_file.by_type("IfcUnitAssignment")[0].Units: if unit.is_a("IfcNamedUnit") and unit.UnitType == unit_type: if unit.is_a("IfcSIUnit"): @@ -949,23 +955,23 @@ def get_unit_type_name(ifc_file, unit_type): return val(unit.Currency) -def get_unit_name(ifc_file, unit): +def get_unit_name(ifc_file: ifcopenshell.entity_instance, unit: ifcopenshell.entity_instance) -> Union[str, None]: if unit.is_a("IfcNamedUnit"): return val(unit.Name) -def get_created_by(element): +def get_created_by(element: ifcopenshell.entity_instance) -> Union[str, None]: if getattr(element, "OwnerHistory", None): return get_email_from_history(element.OwnerHistory) -def get_email_from_history(element): +def get_email_from_history(element: ifcopenshell.entity_instance) -> Union[str, None]: pao = element.OwningUser if pao.is_a("IfcPersonAndOrganization"): return get_email_from_pao(pao) -def get_email_from_pao(pao): +def get_email_from_pao(pao: ifcopenshell.entity_instance) -> Union[str, None]: for address in pao.ThePerson.Addresses or []: if address.is_a("IfcTelecomAddress") and address.ElectronicMailAddresses: return address.ElectronicMailAddresses[0] @@ -986,23 +992,25 @@ def get_email_from_pao(pao): return pao.ThePerson.GivenName + pao.ThePerson.FamilyName + "@" + pao.TheOrganization.Name + ".com" -def get_owner_name(element): +def get_owner_name(element: ifcopenshell.entity_instance) -> Union[str, None]: if getattr(element, "OwnerHistory", None): return element.OwnerHistory.OwningUser.TheOrganization.Name -def get_created_on(element): +def get_created_on(element: ifcopenshell.entity_instance) -> str: if getattr(element, "OwnerHistory", None): return ifcopenshell.util.date.ifc2datetime(element.OwnerHistory.CreationDate).isoformat() return "1900-12-31T23:59:59" # Yes, really -def get_external_system(element): +def get_external_system(element: ifcopenshell.entity_instance) -> Union[str, None]: if getattr(element, "OwnerHistory", None): return val(element.OwnerHistory.OwningApplication.ApplicationFullName) -def get_facility_parent(element, ifc_class): +def get_facility_parent( + element: ifcopenshell.entity_instance, ifc_class: str +) -> Union[ifcopenshell.entity_instance, None]: parent = ifcopenshell.util.element.get_aggregate(element) while parent: if parent.is_a(ifc_class): @@ -1012,11 +1020,11 @@ def get_facility_parent(element, ifc_class): parent = ifcopenshell.util.element.get_aggregate(parent) -def val(x): +def val(x: Any) -> Any: return x if x not in ("", "n/a") else None -def get_area_measurement(element): +def get_area_measurement(element: ifcopenshell.entity_instance) -> Union[str, None]: for relationship in getattr(element, "IsDefinedBy", []) or []: if relationship.is_a("IfcRelDefinesByProperties"): definition = relationship.RelatingPropertyDefinition @@ -1029,7 +1037,7 @@ def get_area_measurement(element): return result -def get_category(element): +def get_category(element: ifcopenshell.entity_instance) -> Union[str, None]: references = list(ifcopenshell.util.classification.get_references(element)) results = [] for reference in references: @@ -1078,7 +1086,7 @@ def get_category(element): return val(getattr(element, "ObjectType", None)) -def get_pao_address(element, name): +def get_pao_address(element: ifcopenshell.entity_instance, name: str) -> Union[str, None]: for actor in [element.ThePerson, element.TheOrganization]: for address in actor.Addresses or []: if hasattr(address, name) and getattr(address, name, None): @@ -1090,7 +1098,9 @@ def get_pao_address(element, name): return result -def get_property(psets, pset_name, prop_name, decimals=None): +def get_property( + psets: dict[str, dict[str, Any]], pset_name: str, prop_name: str, decimals: Optional[int] = None +) -> Union[Any, None, float]: if pset_name in psets: result = psets[pset_name].get(prop_name, None) if decimals is None or result is None: @@ -1098,13 +1108,13 @@ def get_property(psets, pset_name, prop_name, decimals=None): return round(result, decimals) -def get_history(ifc_file): +def get_history(ifc_file: ifcopenshell.file) -> Union[ifcopenshell.entity_instance, None]: histories = ifc_file.by_type("IfcOwnerHistory") if histories: return sorted(histories, key=lambda x: x.id())[-1] -def get_property_unit(pset, prop_name): +def get_property_unit(pset: ifcopenshell.entity_instance, prop_name: str) -> Union[str, None]: for prop in getattr(pset, "HasProperties", []) or []: if prop.Name == prop_name: unit = getattr(prop, "Unit", None) @@ -1112,7 +1122,8 @@ def get_property_unit(pset, prop_name): return get_unit_name(unit) -def get_allowed_values(pset_id, prop_name): +# TODO: never used? +def get_allowed_values(ifc_file: ifcopenshell.file, pset_id: int, prop_name: str) -> Union[str, None]: pset = ifc_file.by_id(pset_id) for prop in getattr(pset, "HasProperties", []) or []: if prop.Name == prop_name: @@ -1120,7 +1131,7 @@ def get_allowed_values(pset_id, prop_name): return ",".join([v.wrappedValue for v in prop.EnumerationValues]) -def get_sheet_name(element): +def get_sheet_name(element: ifcopenshell.entity_instance) -> Union[str, None]: if element.is_a("IfcBuilding"): return "Facility" elif element.is_a("IfcBuildingStorey"): diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 95c1031ae1..f36ce8456c 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -19,6 +19,7 @@ import ifcopenshell import ifcopenshell.guid import ifcopenshell.util.element +import ifcopenshell.util.representation from typing import Any, Callable, Optional, Union, Literal, overload from collections import namedtuple diff --git a/src/ifcopenshell-python/ifcopenshell/util/fm.py b/src/ifcopenshell-python/ifcopenshell/util/fm.py index b58d688c99..c9b1977e4e 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/fm.py +++ b/src/ifcopenshell-python/ifcopenshell/util/fm.py @@ -17,7 +17,10 @@ # along with IfcOpenShell. If not, see . import ifcopenshell +import ifcopenshell.ifcopenshell_wrapper as W import ifcopenshell.util.attribute +from typing import Literal +from typing_extensions import assert_never # COBie actually uses an exclusion list, but this inclusion list is equivalent. cobie_type_classes = [ @@ -114,7 +117,7 @@ fmhem_excluded_classes = [ ] -def get_cobie_types(ifc_file): +def get_cobie_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: elements = [] for ifc_class in cobie_type_classes: try: @@ -124,7 +127,7 @@ def get_cobie_types(ifc_file): return elements -def get_cobie_components(ifc_file): +def get_cobie_components(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: elements = [] for ifc_class in cobie_component_classes: try: @@ -134,7 +137,7 @@ def get_cobie_components(ifc_file): return elements -def get_fmhem_types(ifc_file): +def get_fmhem_types(ifc_file: ifcopenshell.file) -> list[ifcopenshell.entity_instance]: elements = [] if ifc_file.schema == "IFC2X3": fmhem_classes = fmhem_classes_ifc2x3 @@ -148,10 +151,10 @@ def get_fmhem_types(ifc_file): return elements -def get_fmhem_classes(schema="IFC4"): +def get_fmhem_classes(schema: Literal["IFC4", "IFC2X3"] = "IFC4") -> dict[str, list[str]]: results = {} - def get_fmhem_class(declaration): + def get_fmhem_class(declaration: W.entity) -> None: if declaration.name() in fmhem_excluded_classes: pass elif declaration.is_abstract(): @@ -172,8 +175,10 @@ def get_fmhem_classes(schema="IFC4"): classes = fmhem_classes_ifc4 elif schema == "IFC2X3": classes = fmhem_classes_ifc2x3 - schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(schema) + else: + assert_never(schema) + schema_ = ifcopenshell.schema_by_name(schema) for ifc_class in classes: - declaration = schema.declaration_by_name(ifc_class) + declaration = schema_.declaration_by_name(ifc_class) get_fmhem_class(declaration) return results