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.
Files changed (54) hide show
  1. comcheck_api/DISCLAIMER.md +24 -0
  2. comcheck_api/__init__.py +99 -0
  3. comcheck_api/ai/__init__.py +30 -0
  4. comcheck_api/ai/skill/SKILL.md +285 -0
  5. comcheck_api/ai/skill/__init__.py +5 -0
  6. comcheck_api/ai/skill/reference/operations.md +101 -0
  7. comcheck_api/ai/skill/reference/simulation.md +99 -0
  8. comcheck_api/ai/skill/reference/types.md +90 -0
  9. comcheck_api/ai/skill/scripts/__init__.py +1 -0
  10. comcheck_api/ai/skill/scripts/validate_code.py +210 -0
  11. comcheck_api/api/__init__.py +1 -0
  12. comcheck_api/api/api_services.py +273 -0
  13. comcheck_api/cli.py +136 -0
  14. comcheck_api/client/__init__.py +1 -0
  15. comcheck_api/client/comcheck_client.py +335 -0
  16. comcheck_api/constants/__init__.py +0 -0
  17. comcheck_api/constants/building_area_constants.py +35 -0
  18. comcheck_api/constants/common_constants.py +116 -0
  19. comcheck_api/constants/envelope_constants.py +250 -0
  20. comcheck_api/defaults.py +150 -0
  21. comcheck_api/exceptions.py +54 -0
  22. comcheck_api/introspection.py +188 -0
  23. comcheck_api/managers/__init__.py +0 -0
  24. comcheck_api/managers/components/__init__.py +0 -0
  25. comcheck_api/managers/components/building_area.py +11 -0
  26. comcheck_api/managers/components/envelope/__init__.py +0 -0
  27. comcheck_api/managers/components/envelope/ag_wall.py +97 -0
  28. comcheck_api/managers/components/envelope/bg_wall.py +39 -0
  29. comcheck_api/managers/components/envelope/door.py +11 -0
  30. comcheck_api/managers/components/envelope/floor.py +11 -0
  31. comcheck_api/managers/components/envelope/roof.py +30 -0
  32. comcheck_api/managers/components/envelope/skylight.py +11 -0
  33. comcheck_api/managers/components/envelope/window.py +11 -0
  34. comcheck_api/managers/data_manager.py +369 -0
  35. comcheck_api/project_operations/__init__.py +8 -0
  36. comcheck_api/project_operations/project_building_area_operations.py +107 -0
  37. comcheck_api/project_operations/project_envelope_operations.py +899 -0
  38. comcheck_api/schemas/comCheck.schema.json +6463 -0
  39. comcheck_api/types/__init__.py +49 -0
  40. comcheck_api/types/api_types.py +127 -0
  41. comcheck_api/types/common_types.py +32 -0
  42. comcheck_api/types/core_types.py +4198 -0
  43. comcheck_api/types/custom_base_model.py +314 -0
  44. comcheck_api/utilities/__init__.py +5 -0
  45. comcheck_api/utilities/common.py +50 -0
  46. comcheck_api/utilities/envelope_utilities.py +46 -0
  47. comcheck_api/utilities/id_registry.py +79 -0
  48. comcheck_api/utilities/project_utilities.py +60 -0
  49. comcheck_api/validation.py +64 -0
  50. comcheck_api-1.0.0.dist-info/METADATA +244 -0
  51. comcheck_api-1.0.0.dist-info/RECORD +54 -0
  52. comcheck_api-1.0.0.dist-info/WHEEL +4 -0
  53. comcheck_api-1.0.0.dist-info/entry_points.txt +2 -0
  54. 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)
@@ -0,0 +1,8 @@
1
+ """Project operations module."""
2
+
3
+ from . import project_building_area_operations, project_envelope_operations
4
+
5
+ __all__ = [
6
+ "project_building_area_operations",
7
+ "project_envelope_operations",
8
+ ]