comcheck-api 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- comcheck_api/DISCLAIMER.md +24 -0
- comcheck_api/__init__.py +99 -0
- comcheck_api/ai/__init__.py +30 -0
- comcheck_api/ai/skill/SKILL.md +285 -0
- comcheck_api/ai/skill/__init__.py +5 -0
- comcheck_api/ai/skill/reference/operations.md +101 -0
- comcheck_api/ai/skill/reference/simulation.md +99 -0
- comcheck_api/ai/skill/reference/types.md +90 -0
- comcheck_api/ai/skill/scripts/__init__.py +1 -0
- comcheck_api/ai/skill/scripts/validate_code.py +210 -0
- comcheck_api/api/__init__.py +1 -0
- comcheck_api/api/api_services.py +273 -0
- comcheck_api/cli.py +136 -0
- comcheck_api/client/__init__.py +1 -0
- comcheck_api/client/comcheck_client.py +335 -0
- comcheck_api/constants/__init__.py +0 -0
- comcheck_api/constants/building_area_constants.py +35 -0
- comcheck_api/constants/common_constants.py +116 -0
- comcheck_api/constants/envelope_constants.py +250 -0
- comcheck_api/defaults.py +150 -0
- comcheck_api/exceptions.py +54 -0
- comcheck_api/introspection.py +188 -0
- comcheck_api/managers/__init__.py +0 -0
- comcheck_api/managers/components/__init__.py +0 -0
- comcheck_api/managers/components/building_area.py +11 -0
- comcheck_api/managers/components/envelope/__init__.py +0 -0
- comcheck_api/managers/components/envelope/ag_wall.py +97 -0
- comcheck_api/managers/components/envelope/bg_wall.py +39 -0
- comcheck_api/managers/components/envelope/door.py +11 -0
- comcheck_api/managers/components/envelope/floor.py +11 -0
- comcheck_api/managers/components/envelope/roof.py +30 -0
- comcheck_api/managers/components/envelope/skylight.py +11 -0
- comcheck_api/managers/components/envelope/window.py +11 -0
- comcheck_api/managers/data_manager.py +369 -0
- comcheck_api/project_operations/__init__.py +8 -0
- comcheck_api/project_operations/project_building_area_operations.py +107 -0
- comcheck_api/project_operations/project_envelope_operations.py +899 -0
- comcheck_api/schemas/comCheck.schema.json +6463 -0
- comcheck_api/types/__init__.py +49 -0
- comcheck_api/types/api_types.py +127 -0
- comcheck_api/types/common_types.py +32 -0
- comcheck_api/types/core_types.py +4198 -0
- comcheck_api/types/custom_base_model.py +314 -0
- comcheck_api/utilities/__init__.py +5 -0
- comcheck_api/utilities/common.py +50 -0
- comcheck_api/utilities/envelope_utilities.py +46 -0
- comcheck_api/utilities/id_registry.py +79 -0
- comcheck_api/utilities/project_utilities.py +60 -0
- comcheck_api/validation.py +64 -0
- comcheck_api-1.0.0.dist-info/METADATA +244 -0
- comcheck_api-1.0.0.dist-info/RECORD +54 -0
- comcheck_api-1.0.0.dist-info/WHEEL +4 -0
- comcheck_api-1.0.0.dist-info/entry_points.txt +2 -0
- comcheck_api-1.0.0.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""AgWall (exterior wall) manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from comcheck_api.types.core_types import (
|
|
4
|
+
AgWall,
|
|
5
|
+
Door,
|
|
6
|
+
ThermalBridge,
|
|
7
|
+
ThermalBridgeCategoryOptions,
|
|
8
|
+
ThermalBridgeComplianceTypeOptions,
|
|
9
|
+
ThermalBridgeTypeOptions,
|
|
10
|
+
Window,
|
|
11
|
+
)
|
|
12
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ThermalBridgeListManager(DataManager[ThermalBridge]):
|
|
16
|
+
"""Manager for ThermalBridge assemblies."""
|
|
17
|
+
|
|
18
|
+
model_type = ThermalBridge
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgWallListManager(DataManager[AgWall]):
|
|
22
|
+
"""Manager for AgWall assemblies with support for nested components.
|
|
23
|
+
|
|
24
|
+
This manager handles AgWall assemblies and their nested components
|
|
25
|
+
(thermal bridges, doors, windows).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
model_type = AgWall
|
|
29
|
+
|
|
30
|
+
def add_new_thermal_bridge(
|
|
31
|
+
self,
|
|
32
|
+
ag_wall: AgWall,
|
|
33
|
+
thermal_bridge_type: (
|
|
34
|
+
ThermalBridgeTypeOptions | None
|
|
35
|
+
) = ThermalBridgeTypeOptions.THERMAL_BRIDGE_OTHER,
|
|
36
|
+
thermal_bridge_category: (
|
|
37
|
+
ThermalBridgeCategoryOptions | None
|
|
38
|
+
) = ThermalBridgeCategoryOptions.THERMAL_BRIDGE_UNCATEGORIZED,
|
|
39
|
+
thermal_bridge_compliance_type: (
|
|
40
|
+
ThermalBridgeComplianceTypeOptions | None
|
|
41
|
+
) = ThermalBridgeComplianceTypeOptions.THERMAL_BRIDGE_NON_PRESCRIPTIVE,
|
|
42
|
+
psi_factor: float = 0.0,
|
|
43
|
+
chi_factor: float = 0.0,
|
|
44
|
+
thermal_bridge_length: float = 0.0,
|
|
45
|
+
number_of_points: int = 0,
|
|
46
|
+
) -> AgWall:
|
|
47
|
+
"""Add a new thermal bridge to an AgWall.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
ag_wall: The AgWall to add the thermal bridge to.
|
|
51
|
+
thermal_bridge_type: Type of thermal bridge.
|
|
52
|
+
thermal_bridge_category: Category of thermal bridge.
|
|
53
|
+
thermal_bridge_compliance_type: Compliance type.
|
|
54
|
+
psi_factor: Linear thermal transmittance (Psi factor).
|
|
55
|
+
chi_factor: Point thermal transmittance (Chi factor).
|
|
56
|
+
thermal_bridge_length: Length of the thermal bridge.
|
|
57
|
+
number_of_points: Number of points for the thermal bridge.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The updated AgWall.
|
|
61
|
+
"""
|
|
62
|
+
# Initialize new thermal bridge
|
|
63
|
+
initialize_thermal_bridge: ThermalBridge = ThermalBridge(
|
|
64
|
+
thermalBridgeType=thermal_bridge_type,
|
|
65
|
+
thermalBridgeCategory=thermal_bridge_category,
|
|
66
|
+
thermalBridgeComplianceType=thermal_bridge_compliance_type,
|
|
67
|
+
psiFactor=psi_factor,
|
|
68
|
+
chiFactor=chi_factor,
|
|
69
|
+
thermalBridgeLength=thermal_bridge_length,
|
|
70
|
+
numberOfPoints=number_of_points,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return self.add_subcomponent(ag_wall, initialize_thermal_bridge)
|
|
74
|
+
|
|
75
|
+
def add_new_door(self, ag_wall: AgWall, door: Door) -> AgWall:
|
|
76
|
+
"""Add a new door to an AgWall.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
ag_wall: The AgWall to add the door to.
|
|
80
|
+
door: The door configuration to add.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The updated AgWall.
|
|
84
|
+
"""
|
|
85
|
+
return self.add_subcomponent(ag_wall, door)
|
|
86
|
+
|
|
87
|
+
def add_new_window(self, ag_wall: AgWall, window: Window) -> AgWall:
|
|
88
|
+
"""Add a new window to an AgWall.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
ag_wall: The AgWall to add the window to.
|
|
92
|
+
window: The window configuration to add.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The updated AgWall.
|
|
96
|
+
"""
|
|
97
|
+
return self.add_subcomponent(ag_wall, window)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""BgWall (basement wall) manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from comcheck_api.types.core_types import BgWall, Door, Window
|
|
5
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BgWallListManager(DataManager[BgWall]):
|
|
9
|
+
"""Manager for BgWall assemblies with support for nested components.
|
|
10
|
+
|
|
11
|
+
This manager handles BgWall assemblies and their nested components
|
|
12
|
+
(doors, windows).
|
|
13
|
+
"""
|
|
14
|
+
model_type = BgWall
|
|
15
|
+
|
|
16
|
+
def add_new_door(self, bg_wall: BgWall, door: Door) -> BgWall:
|
|
17
|
+
"""Add a new door to a BgWall.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
bg_wall: The BgWall to add the door to.
|
|
21
|
+
door: The door configuration to add.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The updated BgWall.
|
|
25
|
+
"""
|
|
26
|
+
return self.add_subcomponent(bg_wall, door)
|
|
27
|
+
|
|
28
|
+
def add_new_window(self, bg_wall: BgWall, window: Window) -> BgWall:
|
|
29
|
+
"""Add a new window to a BgWall.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
bg_wall: The BgWall to add the window to.
|
|
33
|
+
window: The window configuration to add.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The updated BgWall.
|
|
37
|
+
"""
|
|
38
|
+
# Create window manager
|
|
39
|
+
return self.add_subcomponent(bg_wall, window)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Door list manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from comcheck_api.types.core_types import Door
|
|
6
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DoorListManager(DataManager[Door]):
|
|
10
|
+
"""Manager for Door assemblies."""
|
|
11
|
+
model_type = Door
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Floor list manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from comcheck_api.types.core_types import Floor
|
|
6
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FloorListManager(DataManager[Floor]):
|
|
10
|
+
"""Manager for Floor assemblies."""
|
|
11
|
+
model_type = Floor
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Roof manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from comcheck_api.types.core_types import Roof, Skylight
|
|
6
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
7
|
+
from comcheck_api.utilities.envelope_utilities import generate_assembly
|
|
8
|
+
|
|
9
|
+
from .skylight import SkylightListManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RoofListManager(DataManager[Roof]):
|
|
13
|
+
"""Manager for Roof assemblies with support for nested skylights.
|
|
14
|
+
|
|
15
|
+
This manager handles Roof assemblies and their nested skylights,
|
|
16
|
+
with automatic unique assemblyType generation.
|
|
17
|
+
"""
|
|
18
|
+
model_type = Roof
|
|
19
|
+
|
|
20
|
+
def add_new_skylight(self, roof: Roof, skylight: Skylight) -> Roof:
|
|
21
|
+
"""Add a new skylight to a Roof.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
roof: The Roof to add the skylight to.
|
|
25
|
+
skylight: The skylight configuration to add.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The updated Roof.
|
|
29
|
+
"""
|
|
30
|
+
return self.add_subcomponent(roof, skylight)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Skylight list manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from comcheck_api.types.core_types import Skylight
|
|
6
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SkylightListManager(DataManager[Skylight]):
|
|
10
|
+
"""Manager for Skylight assemblies."""
|
|
11
|
+
model_type = Skylight
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Window list manager for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from comcheck_api.types.core_types import Window
|
|
6
|
+
from comcheck_api.managers.data_manager import DataManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WindowListManager(DataManager[Window]):
|
|
10
|
+
"""Manager for Window assemblies."""
|
|
11
|
+
model_type = Window
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Generic data manager with JSON schema validation."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Generic,
|
|
9
|
+
List,
|
|
10
|
+
Type,
|
|
11
|
+
TypeVar,
|
|
12
|
+
)
|
|
13
|
+
from collections import namedtuple
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
from comcheck_api.utilities.id_registry import (
|
|
17
|
+
register_existing_id,
|
|
18
|
+
generate_id_with_prefix,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
S = TypeVar("S")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataManager(Generic[T]):
|
|
26
|
+
"""Generic data manager for storing and validating typed data items.
|
|
27
|
+
|
|
28
|
+
This class provides CRUD operations on a collection of items with:
|
|
29
|
+
- JSON schema validation against a schema reference
|
|
30
|
+
- Unique identifier enforcement
|
|
31
|
+
- Type-safe operations
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
initial_data: Initial list of items to populate the manager.
|
|
35
|
+
schema_path: Path to the JSON schema file (defaults to comCheck.schema.json).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
model_type: Type[BaseModel] | None = None
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
initial_data: list[T] | None = [],
|
|
43
|
+
model_type: Type[BaseModel] | None = None,
|
|
44
|
+
schema_path: str | Path = "../schemas/comCheck.schema.json",
|
|
45
|
+
):
|
|
46
|
+
"""Initialize the data manager.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
initial_data: Items to pre-populate the manager with.
|
|
50
|
+
model_type: Pydantic model class for the managed items.
|
|
51
|
+
Falls back to the class-level ``model_type`` attribute.
|
|
52
|
+
schema_path: Path to a JSON schema file for validation.
|
|
53
|
+
"""
|
|
54
|
+
self._data: list[T] = []
|
|
55
|
+
self._schema: dict[str, Any] | None = None
|
|
56
|
+
|
|
57
|
+
self._initialize_metadata(model_type)
|
|
58
|
+
self._initialize_schema(schema_path)
|
|
59
|
+
self._initialize_data(initial_data)
|
|
60
|
+
|
|
61
|
+
def _initialize_metadata(self, model_type: BaseModel | None = None) -> None:
|
|
62
|
+
"""Resolve and store the model type and its ID field information.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
model_type: Explicit model class; falls back to the class-level ``model_type``.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ValueError: If no model type is available or no ID info is found for it.
|
|
69
|
+
"""
|
|
70
|
+
self.model_type = model_type or self.__class__.model_type
|
|
71
|
+
if self.model_type is None:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"{self.__class__.__name__} requires a model_type "
|
|
74
|
+
"either as a class variable or via initialization."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
id_info = get_model_info(self.model_type)
|
|
78
|
+
if id_info:
|
|
79
|
+
self._identifier = id_info.identifier
|
|
80
|
+
self._id_prefix = id_info.id_prefix
|
|
81
|
+
else:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"No ID info found for model class {self.model_type.__name__}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _initialize_schema(self, schema_path: str | Path) -> None:
|
|
87
|
+
"""Load the JSON schema from disk if the file exists.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
schema_path: Path to the JSON schema file.
|
|
91
|
+
"""
|
|
92
|
+
if schema_path:
|
|
93
|
+
schema_path = Path(schema_path)
|
|
94
|
+
if schema_path.exists():
|
|
95
|
+
with open(schema_path, "r", encoding="utf-8") as f:
|
|
96
|
+
self._schema = json.load(f)
|
|
97
|
+
|
|
98
|
+
def _initialize_data(self, initial_data: list[T] = []) -> None:
|
|
99
|
+
"""Populate the manager with an initial list of items.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
initial_data: Items to add via ``add_new`` on startup.
|
|
103
|
+
"""
|
|
104
|
+
for item in initial_data:
|
|
105
|
+
self.add_new(item)
|
|
106
|
+
|
|
107
|
+
def _validate_item(self, item: T) -> T:
|
|
108
|
+
"""Validate an item against the JSON schema reference.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
item: The item to validate.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
A model instance.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValidationError: If validation fails.
|
|
118
|
+
"""
|
|
119
|
+
if isinstance(item, self.model_type):
|
|
120
|
+
model_instance = item
|
|
121
|
+
else:
|
|
122
|
+
model_instance = self.model_type.model_validate(item)
|
|
123
|
+
|
|
124
|
+
return model_instance
|
|
125
|
+
|
|
126
|
+
def _get_identifier_value(self, item: T) -> Any:
|
|
127
|
+
"""Extract the identifier value from an item.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
item: The item to extract the identifier from.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The identifier value.
|
|
134
|
+
"""
|
|
135
|
+
return getattr(item, self._identifier, None)
|
|
136
|
+
|
|
137
|
+
def add_new(self, item: T | dict[str, Any]) -> list[T]:
|
|
138
|
+
"""Add a new item to the data array.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
item: The item to add.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The updated data array.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
ValueError: If the identifier is missing or if an item with the
|
|
148
|
+
same identifier already exists.
|
|
149
|
+
ValidationError: If schema validation fails.
|
|
150
|
+
"""
|
|
151
|
+
model_instance = self._validate_item(item)
|
|
152
|
+
|
|
153
|
+
if item:
|
|
154
|
+
self.generate_identifier(model_instance)
|
|
155
|
+
|
|
156
|
+
self._data.append(copy.deepcopy(model_instance))
|
|
157
|
+
|
|
158
|
+
return self.get_all()
|
|
159
|
+
|
|
160
|
+
def generate_identifier(self, item: T) -> None:
|
|
161
|
+
"""Ensure the item has a valid and unique identifier.
|
|
162
|
+
|
|
163
|
+
If the identifier is missing, invalid, duplicated, or does not match
|
|
164
|
+
the expected prefix, a new unique one is generated via the ID registry.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
item: The item whose identifier should be validated or assigned.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
Exception: If the item is already present in the managed list.
|
|
171
|
+
"""
|
|
172
|
+
if any(existing is item for existing in self._data):
|
|
173
|
+
raise Exception("Item already exists in managed list")
|
|
174
|
+
|
|
175
|
+
if self._id_prefix is None:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
current = getattr(item, self._identifier, None)
|
|
179
|
+
|
|
180
|
+
needs_new_identifier = (
|
|
181
|
+
not current
|
|
182
|
+
or not isinstance(current, str)
|
|
183
|
+
or current
|
|
184
|
+
in {getattr(existing, self._identifier, None) for existing in self._data}
|
|
185
|
+
or (not current.startswith(self._id_prefix))
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if not needs_new_identifier:
|
|
189
|
+
register_existing_id(current)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
new_id = generate_id_with_prefix(self._id_prefix)
|
|
193
|
+
|
|
194
|
+
item.__dict__[self._identifier] = new_id
|
|
195
|
+
|
|
196
|
+
def add_subcomponent(
|
|
197
|
+
self,
|
|
198
|
+
parent: T,
|
|
199
|
+
subcomponent: S,
|
|
200
|
+
) -> T:
|
|
201
|
+
"""
|
|
202
|
+
Add a new subcomponent to a parent item.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
parent: The parent item (e.g., BgWall)
|
|
206
|
+
subcomponent: The subcomponent to add (e.g., Door)
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Updated parent with the new subcomponent added.
|
|
210
|
+
"""
|
|
211
|
+
subcomponent_type = type(subcomponent)
|
|
212
|
+
|
|
213
|
+
subcomponent_name = subcomponent.json_key()
|
|
214
|
+
|
|
215
|
+
current_subcomponents: List[S] = getattr(parent, subcomponent_name, [])
|
|
216
|
+
|
|
217
|
+
subcomponent_manager = DataManager[subcomponent_type](
|
|
218
|
+
initial_data=current_subcomponents, model_type=subcomponent_type
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
parent.__dict__[subcomponent_name] = subcomponent_manager.add_new(subcomponent)
|
|
222
|
+
updated = self.modify_one(self._get_identifier_value(parent), parent)
|
|
223
|
+
return updated
|
|
224
|
+
|
|
225
|
+
def get_all(self) -> list[T]:
|
|
226
|
+
"""Get all items.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
A deep copy of the data array.
|
|
230
|
+
"""
|
|
231
|
+
return copy.deepcopy(self._data)
|
|
232
|
+
|
|
233
|
+
def get_by_identifier(self, id_value: Any) -> T | None:
|
|
234
|
+
"""Find an item by its identifier.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
id_value: The value of the identifier to search for.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
The found item, or None if not found.
|
|
241
|
+
"""
|
|
242
|
+
for item in self._data:
|
|
243
|
+
if self._get_identifier_value(item) == id_value:
|
|
244
|
+
return copy.deepcopy(item)
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def delete_one(self, id_value: Any) -> bool:
|
|
248
|
+
"""Delete an item by its identifier.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
id_value: The value of the identifier to delete.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
True if an item was deleted, False otherwise.
|
|
255
|
+
"""
|
|
256
|
+
initial_length = len(self._data)
|
|
257
|
+
self._data = [
|
|
258
|
+
item for item in self._data if self._get_identifier_value(item) != id_value
|
|
259
|
+
]
|
|
260
|
+
return len(self._data) != initial_length
|
|
261
|
+
|
|
262
|
+
def modify_one(self, id_value: Any, updates: T | dict[str, Any]) -> T:
|
|
263
|
+
"""Modify an existing item by its identifier.
|
|
264
|
+
|
|
265
|
+
If the identifier changes, ensures no duplicates exist in the data array.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
id_value: The current identifier value.
|
|
269
|
+
updates: Partial updates (dict) or full model object to apply to the item.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The updated item.
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: If the item is not found or if the updated identifier
|
|
276
|
+
already exists.
|
|
277
|
+
"""
|
|
278
|
+
item_index = None
|
|
279
|
+
for i, item in enumerate(self._data):
|
|
280
|
+
if self._get_identifier_value(item) == id_value:
|
|
281
|
+
item_index = i
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
if item_index is None:
|
|
285
|
+
raise ValueError(f"Item with {self._identifier} '{id_value}' not found")
|
|
286
|
+
|
|
287
|
+
# Convert updates to dict if it's a model object
|
|
288
|
+
updates_dict = (
|
|
289
|
+
updates.model_dump(mode="json", exclude_unset=True)
|
|
290
|
+
if isinstance(updates, BaseModel)
|
|
291
|
+
else updates
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# If identifier is changing, validate its uniqueness
|
|
295
|
+
new_identifier = updates_dict.get(self._identifier)
|
|
296
|
+
if new_identifier and new_identifier != id_value:
|
|
297
|
+
if any(
|
|
298
|
+
self._get_identifier_value(existing) == new_identifier
|
|
299
|
+
for existing in self._data
|
|
300
|
+
):
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"Item with {self._identifier} '{new_identifier}' already exists"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
original = self._data[item_index]
|
|
306
|
+
|
|
307
|
+
# Validate that all keys in updates_dict are valid fields
|
|
308
|
+
model_fields = type(original).model_fields
|
|
309
|
+
invalid_fields = set(updates_dict.keys()) - set(model_fields.keys())
|
|
310
|
+
if invalid_fields:
|
|
311
|
+
raise ValueError(f"Invalid fields in updates: {invalid_fields}")
|
|
312
|
+
|
|
313
|
+
# Validate partial updates by attempting to construct with merged data
|
|
314
|
+
# This will raise Pydantic ValidationError if types don't align
|
|
315
|
+
# Get only the model fields to avoid serializing dynamically added methods
|
|
316
|
+
original_dict = original.model_dump(
|
|
317
|
+
mode="python", by_alias=False, exclude_unset=True
|
|
318
|
+
)
|
|
319
|
+
merged = {**original_dict, **updates_dict}
|
|
320
|
+
try:
|
|
321
|
+
updated_item = type(original).model_validate(merged)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
raise ValueError(
|
|
324
|
+
f"Validation failed for updates to {self._identifier} '{id_value}': {str(e)}"
|
|
325
|
+
) from e
|
|
326
|
+
|
|
327
|
+
self._data[item_index] = copy.deepcopy(updated_item) # Protect internal state
|
|
328
|
+
return copy.deepcopy(
|
|
329
|
+
updated_item
|
|
330
|
+
) # Protect returned object from external mutation
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
IdInfo = namedtuple("IdInfo", ["identifier", "id_prefix"])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def get_model_info(model_class: Type[BaseModel]) -> IdInfo | None:
|
|
337
|
+
"""Return the identifier field name and ID prefix for a known model class.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
model_class: The Pydantic model class to look up.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
An ``IdInfo`` namedtuple with ``identifier`` and ``id_prefix`` fields,
|
|
344
|
+
or ``None`` if the class is not registered.
|
|
345
|
+
"""
|
|
346
|
+
from comcheck_api.types.core_types import (
|
|
347
|
+
Door,
|
|
348
|
+
Roof,
|
|
349
|
+
Window,
|
|
350
|
+
BgWall,
|
|
351
|
+
AgWall,
|
|
352
|
+
Floor,
|
|
353
|
+
Skylight,
|
|
354
|
+
ThermalBridge,
|
|
355
|
+
WholeBldgUse,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
MODEL_TO_ID_INFO = {
|
|
359
|
+
Door: IdInfo(identifier="assemblyType", id_prefix="Door:Door"),
|
|
360
|
+
Roof: IdInfo(identifier="assemblyType", id_prefix="Roof:Roof"),
|
|
361
|
+
Window: IdInfo(identifier="assemblyType", id_prefix="Window:Window"),
|
|
362
|
+
BgWall: IdInfo(identifier="assemblyType", id_prefix="Basement:Basement"),
|
|
363
|
+
AgWall: IdInfo(identifier="assemblyType", id_prefix="AgWall:Ext Wall"),
|
|
364
|
+
Floor: IdInfo(identifier="assemblyType", id_prefix="Floor:Floor"),
|
|
365
|
+
Skylight: IdInfo(identifier="assemblyType", id_prefix="Skylight:Skylight"),
|
|
366
|
+
ThermalBridge: IdInfo(identifier="id", id_prefix=None),
|
|
367
|
+
WholeBldgUse: IdInfo(identifier="key", id_prefix=None),
|
|
368
|
+
}
|
|
369
|
+
return MODEL_TO_ID_INFO.get(model_class)
|