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
comcheck_api/cli.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""``comcheck-api`` console-script entry point.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
* ``install-skill`` — install the bundled Skill so an AI coding agent
|
|
6
|
+
picks it up. The Skill follows the open agent-skills standard
|
|
7
|
+
(``SKILL.md`` + ``reference/`` + ``scripts/``), so the same folder
|
|
8
|
+
works for both Claude Code and OpenAI Codex; only the install
|
|
9
|
+
location differs:
|
|
10
|
+
|
|
11
|
+
- Claude Code → ``.claude/skills/comcheck-api/`` (or
|
|
12
|
+
``~/.claude/skills/`` with ``--global``).
|
|
13
|
+
- OpenAI Codex → ``.agents/skills/comcheck-api/`` (or
|
|
14
|
+
``~/.agents/skills/`` with ``--global``). Codex scans
|
|
15
|
+
``.agents/skills`` from the working directory up to the repo root.
|
|
16
|
+
|
|
17
|
+
By default both are installed. Pass ``--claude`` to install only the
|
|
18
|
+
Claude Code Skill, or ``--codex`` to install only the Codex Skill.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from comcheck_api import ai
|
|
29
|
+
|
|
30
|
+
# (agent label, skills dir) for each supported target.
|
|
31
|
+
_TARGETS = {
|
|
32
|
+
"claude": ("Claude", ".claude"),
|
|
33
|
+
"codex": ("Codex", ".agents"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _install_one(
|
|
38
|
+
src: Path, base_dir: Path, agent_dir: str, force: bool, dry_run: bool
|
|
39
|
+
) -> tuple[bool, Path]:
|
|
40
|
+
"""Install the Skill for one agent. Returns (installed, target)."""
|
|
41
|
+
target_root = base_dir / agent_dir / "skills" / "comcheck-api"
|
|
42
|
+
|
|
43
|
+
if dry_run:
|
|
44
|
+
print(f"[dry-run] would copy {src} → {target_root}")
|
|
45
|
+
return True, target_root
|
|
46
|
+
|
|
47
|
+
if target_root.exists() and not force:
|
|
48
|
+
print(f"{target_root} already exists. Re-run with --force to overwrite.")
|
|
49
|
+
return False, target_root
|
|
50
|
+
|
|
51
|
+
if target_root.exists():
|
|
52
|
+
shutil.rmtree(target_root)
|
|
53
|
+
target_root.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
shutil.copytree(src, target_root)
|
|
55
|
+
print(f"Installed Skill into {target_root}")
|
|
56
|
+
return True, target_root
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _cmd_install_skill(args: argparse.Namespace) -> int:
|
|
60
|
+
if args.global_install:
|
|
61
|
+
base_dir = Path.home()
|
|
62
|
+
else:
|
|
63
|
+
base_dir = Path(args.path).resolve()
|
|
64
|
+
if not base_dir.is_dir():
|
|
65
|
+
print(f"ERROR: {base_dir} is not a directory.")
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
# No agent flag → install both. Otherwise install only the
|
|
69
|
+
# selected agents.
|
|
70
|
+
if args.claude or args.codex:
|
|
71
|
+
agents = [k for k in _TARGETS if getattr(args, k)]
|
|
72
|
+
else:
|
|
73
|
+
agents = list(_TARGETS)
|
|
74
|
+
|
|
75
|
+
src = ai.skill_root()
|
|
76
|
+
any_installed = False
|
|
77
|
+
for key in agents:
|
|
78
|
+
label, agent_dir = _TARGETS[key]
|
|
79
|
+
installed, _ = _install_one(src, base_dir, agent_dir, args.force, args.dry_run)
|
|
80
|
+
if installed and not args.dry_run:
|
|
81
|
+
print(f"Restart {label} to pick up the new Skill.")
|
|
82
|
+
any_installed = any_installed or installed
|
|
83
|
+
|
|
84
|
+
return 0 if any_installed else 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
88
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
89
|
+
common.add_argument(
|
|
90
|
+
"--dry-run", action="store_true", help="Show actions without applying"
|
|
91
|
+
)
|
|
92
|
+
common.add_argument(
|
|
93
|
+
"--force", action="store_true", help="Overwrite existing files/configs"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
parser = argparse.ArgumentParser(prog="comcheck-api")
|
|
97
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
98
|
+
|
|
99
|
+
p_skill = sub.add_parser(
|
|
100
|
+
"install-skill",
|
|
101
|
+
parents=[common],
|
|
102
|
+
help="Install the Skill for Claude Code and/or Codex (both by default)",
|
|
103
|
+
)
|
|
104
|
+
p_skill.add_argument(
|
|
105
|
+
"path", nargs="?", default=".", help="Project directory (default: cwd)"
|
|
106
|
+
)
|
|
107
|
+
p_skill.add_argument(
|
|
108
|
+
"--global",
|
|
109
|
+
dest="global_install",
|
|
110
|
+
action="store_true",
|
|
111
|
+
help="Install into the user-global skills dir (~/.claude/skills/ "
|
|
112
|
+
"and/or ~/.agents/skills/) instead of project-level",
|
|
113
|
+
)
|
|
114
|
+
p_skill.add_argument(
|
|
115
|
+
"--claude",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="Install only the Claude Code Skill (.claude/skills/)",
|
|
118
|
+
)
|
|
119
|
+
p_skill.add_argument(
|
|
120
|
+
"--codex",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="Install only the Codex Skill (.agents/skills/)",
|
|
123
|
+
)
|
|
124
|
+
p_skill.set_defaults(func=_cmd_install_skill)
|
|
125
|
+
|
|
126
|
+
return parser
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main(argv: list[str] | None = None) -> int:
|
|
130
|
+
parser = build_parser()
|
|
131
|
+
args = parser.parse_args(argv)
|
|
132
|
+
return args.func(args)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__": # pragma: no cover
|
|
136
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from comcheck_api.client.comcheck_client import COMcheckClient
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""COMcheck Client module for simplified API interactions."""
|
|
2
|
+
|
|
3
|
+
"""Note: Client layer provides user-friendly methods that accept Pydantic models as inputs
|
|
4
|
+
and return either Pydantic models, primitives, or raw dicts depending on the operation."""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List, Literal, Optional, Union, overload
|
|
8
|
+
|
|
9
|
+
from comcheck_api.api import COMCheckApiService
|
|
10
|
+
from comcheck_api.constants.building_area_constants import DEFAULT_BUILDING_AREA
|
|
11
|
+
from comcheck_api.exceptions import (
|
|
12
|
+
COMCheckProjectNotFoundError,
|
|
13
|
+
COMCheckSimulationError,
|
|
14
|
+
)
|
|
15
|
+
from comcheck_api.types.core_types import ComBuilding, InteriorLightingSpace
|
|
16
|
+
|
|
17
|
+
Mode = Literal["python", "json"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class COMcheckClient:
|
|
21
|
+
"""High-level client for the COMcheck Web API.
|
|
22
|
+
|
|
23
|
+
Provides user-friendly methods that accept Pydantic models as inputs
|
|
24
|
+
and return either :class:`~comcheck_api.types.core_types.ComBuilding`
|
|
25
|
+
models, primitives, or raw dicts depending on the operation.
|
|
26
|
+
|
|
27
|
+
Can be used as a context manager to ensure the underlying HTTP
|
|
28
|
+
connection is closed::
|
|
29
|
+
|
|
30
|
+
with COMcheckClient(api_key="your-key") as client:
|
|
31
|
+
project = client.get_project("123")
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
client = COMcheckClient(api_key="your-key")
|
|
36
|
+
projects = client.list_projects()
|
|
37
|
+
project = client.get_project(projects[0]["projectId"])
|
|
38
|
+
client.close()
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, api_key: Optional[str] = None) -> None:
|
|
43
|
+
"""Initialize COMcheck client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
api_key: Optional API key for authentication. Can be set later with set_api_key()
|
|
47
|
+
"""
|
|
48
|
+
self._api_service: Optional[COMCheckApiService] = None
|
|
49
|
+
if api_key:
|
|
50
|
+
self.set_api_key(api_key)
|
|
51
|
+
|
|
52
|
+
def set_api_key(self, api_key: str) -> None:
|
|
53
|
+
"""Set the API key and initialize the API service.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
api_key: API key for authentication
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If API key is not provided
|
|
60
|
+
"""
|
|
61
|
+
if not api_key:
|
|
62
|
+
raise ValueError("API key is required.")
|
|
63
|
+
self._api_service = COMCheckApiService(api_key)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def _service(self) -> COMCheckApiService:
|
|
67
|
+
"""Get the API service instance.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
COMCheckApiService instance
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
RuntimeError: If API key has not been set
|
|
74
|
+
"""
|
|
75
|
+
if self._api_service is None:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"API key not set. Call set_api_key() before using the client."
|
|
78
|
+
)
|
|
79
|
+
return self._api_service
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def get_project(
|
|
83
|
+
self,
|
|
84
|
+
project_id: str,
|
|
85
|
+
mode: Literal["python"] = "python",
|
|
86
|
+
) -> Optional["ComBuilding"]: ...
|
|
87
|
+
|
|
88
|
+
@overload
|
|
89
|
+
def get_project(
|
|
90
|
+
self,
|
|
91
|
+
project_id: str,
|
|
92
|
+
mode: Literal["json"],
|
|
93
|
+
) -> Optional[Dict[str, Any]]: ...
|
|
94
|
+
|
|
95
|
+
def get_project(
|
|
96
|
+
self,
|
|
97
|
+
project_id: str,
|
|
98
|
+
mode: Literal["python", "json"] = "python",
|
|
99
|
+
) -> Optional[Union["ComBuilding", Dict[str, Any]]]:
|
|
100
|
+
"""Fetch a single project by ID.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
project_id: The project ID to retrieve.
|
|
104
|
+
mode: ``"python"`` returns a ``ComBuilding`` model; ``"json"`` returns a raw dict.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The project as a ``ComBuilding`` or dict, or ``None`` if not found.
|
|
108
|
+
"""
|
|
109
|
+
resp = self._service.get_project(project_id)
|
|
110
|
+
data = resp.get("data")
|
|
111
|
+
if data is not None:
|
|
112
|
+
for building_area in data["lighting"]["wholeBldgUse"]:
|
|
113
|
+
building_area["interiorLightingSpace"] = {
|
|
114
|
+
**DEFAULT_BUILDING_AREA.interiorLightingSpace.model_dump(
|
|
115
|
+
mode="json", exclude_unset=True
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return self._parse_data(data, mode)
|
|
120
|
+
|
|
121
|
+
def list_projects(self) -> List[Dict[str, Any]]:
|
|
122
|
+
"""Get a list of all projects.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
API response data as list of project dictionaries
|
|
126
|
+
"""
|
|
127
|
+
return self._service.get_project_list().get("data", [])
|
|
128
|
+
|
|
129
|
+
# TODO: return of update_project should be ComBuilding
|
|
130
|
+
def update_project(
|
|
131
|
+
self,
|
|
132
|
+
project_id: str,
|
|
133
|
+
project_data: ComBuilding,
|
|
134
|
+
mode: Literal["python", "json"] = "python",
|
|
135
|
+
) -> ComBuilding | dict[str, Any] | None:
|
|
136
|
+
"""Update a project by ID.
|
|
137
|
+
|
|
138
|
+
The existing project is fetched first to preserve server-assigned IDs
|
|
139
|
+
and the ``userProject`` reference. Envelope component IDs that are
|
|
140
|
+
``None`` (auto-generated by Pydantic) are stripped before submission.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
project_id: The project ID to update.
|
|
144
|
+
project_data: A ``ComBuilding`` model with the desired state.
|
|
145
|
+
mode: ``"python"`` returns a ``ComBuilding`` model;
|
|
146
|
+
``"json"`` returns a raw dict.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The refreshed project after the update, in the requested format.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
COMCheckProjectNotFoundError: If the project does not exist.
|
|
153
|
+
"""
|
|
154
|
+
# Get existing project
|
|
155
|
+
old_project = self.get_project(project_id, mode="json")
|
|
156
|
+
|
|
157
|
+
if not old_project:
|
|
158
|
+
raise COMCheckProjectNotFoundError(project_id)
|
|
159
|
+
|
|
160
|
+
project_data_json = project_data.model_dump(mode="json", exclude_unset=True)
|
|
161
|
+
|
|
162
|
+
# Preserve user project reference
|
|
163
|
+
user_project = old_project["userProject"]
|
|
164
|
+
project_data_json["userProject"] = user_project
|
|
165
|
+
|
|
166
|
+
# Preserve IDs from existing project
|
|
167
|
+
for section in [
|
|
168
|
+
"envelope",
|
|
169
|
+
"lighting",
|
|
170
|
+
"hvac",
|
|
171
|
+
"control",
|
|
172
|
+
"project",
|
|
173
|
+
"location",
|
|
174
|
+
"renewable",
|
|
175
|
+
]:
|
|
176
|
+
project_data_json[section]["id"] = old_project[section]["id"]
|
|
177
|
+
project_data_json["id"] = old_project["id"]
|
|
178
|
+
|
|
179
|
+
# Ensure each building area has interiorLightingSpace initialized
|
|
180
|
+
for building_area in project_data_json["lighting"]["wholeBldgUse"]:
|
|
181
|
+
building_area["interiorLightingSpace"] = {
|
|
182
|
+
**DEFAULT_BUILDING_AREA.interiorLightingSpace.model_dump(
|
|
183
|
+
mode="json", exclude_unset=True
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# TODO: need to verify if other componets also need to remove None IDs (auto-generated through pydantic )
|
|
188
|
+
# Remove None IDs from envelope components
|
|
189
|
+
if "envelope" in project_data_json:
|
|
190
|
+
envelope = project_data_json["envelope"]
|
|
191
|
+
for component_type in [
|
|
192
|
+
"roof",
|
|
193
|
+
"agWall",
|
|
194
|
+
"bgWall",
|
|
195
|
+
"floor",
|
|
196
|
+
"skylight",
|
|
197
|
+
"window",
|
|
198
|
+
"door",
|
|
199
|
+
]:
|
|
200
|
+
if component_type in envelope and isinstance(
|
|
201
|
+
envelope[component_type], list
|
|
202
|
+
):
|
|
203
|
+
for component in envelope[component_type]:
|
|
204
|
+
if isinstance(component, dict) and component.get("id") is None:
|
|
205
|
+
component.pop("id", None)
|
|
206
|
+
# Also remove None IDs from nested components (skylights in roofs, windows/doors in walls)
|
|
207
|
+
if component_type == "roof" and "skylight" in component:
|
|
208
|
+
for skylight in component.get("skylight", []):
|
|
209
|
+
if (
|
|
210
|
+
isinstance(skylight, dict)
|
|
211
|
+
and skylight.get("id") is None
|
|
212
|
+
):
|
|
213
|
+
skylight.pop("id", None)
|
|
214
|
+
elif component_type in ["agWall", "bgWall"]:
|
|
215
|
+
for nested_type in ["window", "door", "thermalBridge"]:
|
|
216
|
+
if nested_type in component:
|
|
217
|
+
for nested in component.get(nested_type, []):
|
|
218
|
+
if (
|
|
219
|
+
isinstance(nested, dict)
|
|
220
|
+
and nested.get("id") is None
|
|
221
|
+
):
|
|
222
|
+
nested.pop("id", None)
|
|
223
|
+
|
|
224
|
+
self._service.update_project(project_id, project_data_json).get("data")
|
|
225
|
+
|
|
226
|
+
# It's necessary that we call return get_project instead of what's returned from
|
|
227
|
+
# the service's update project due to interiorLightingSpace not being updated correctly
|
|
228
|
+
# when returned from the update call
|
|
229
|
+
return self.get_project(project_id=project_id, mode=mode)
|
|
230
|
+
|
|
231
|
+
def _parse_data(self, data, mode):
|
|
232
|
+
"""Convert raw API data to the requested output format.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
data: Raw dict from the API response, or ``None``.
|
|
236
|
+
mode: ``"python"`` wraps data in a ``ComBuilding``; ``"json"`` returns it as-is.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
A ``ComBuilding``, a raw dict, or ``None`` if ``data`` is ``None``.
|
|
240
|
+
"""
|
|
241
|
+
if data is None:
|
|
242
|
+
return None
|
|
243
|
+
if mode == "python":
|
|
244
|
+
return ComBuilding(**data)
|
|
245
|
+
return data
|
|
246
|
+
|
|
247
|
+
def start_run_simulation(
|
|
248
|
+
self, project: ComBuilding, project_id: Optional[int] = None
|
|
249
|
+
) -> str:
|
|
250
|
+
"""Start a compliance simulation run for a project.
|
|
251
|
+
|
|
252
|
+
If *project_id* is supplied the project is persisted via
|
|
253
|
+
:meth:`update_project` before the simulation is launched.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
project: The project data to simulate.
|
|
257
|
+
project_id: Optional server-side project ID. When given, the
|
|
258
|
+
project is saved before the simulation starts.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
The simulation session ID that can be passed to
|
|
262
|
+
:meth:`get_simulation_status` and :meth:`get_simulation_result`.
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
COMCheckSimulationError: If the API returns no session data.
|
|
266
|
+
"""
|
|
267
|
+
logger = logging.getLogger(__name__)
|
|
268
|
+
|
|
269
|
+
if project_id:
|
|
270
|
+
logger.info("Updating project: %s", project_id)
|
|
271
|
+
self.update_project(str(project_id), project)
|
|
272
|
+
|
|
273
|
+
project_data = project.model_dump(mode="json", exclude_unset=True)
|
|
274
|
+
run_result = self._service.start_run_simulation(project_data)
|
|
275
|
+
if run_result.data is None:
|
|
276
|
+
raise COMCheckSimulationError(
|
|
277
|
+
"Simulation start failed: no session data returned"
|
|
278
|
+
)
|
|
279
|
+
return run_result.data.sessionId
|
|
280
|
+
|
|
281
|
+
def get_simulation_status(self, session_id: str) -> Dict[str, Any]:
|
|
282
|
+
"""Get the status of a running or completed simulation.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
session_id: The session ID returned by :meth:`start_run_simulation`.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
A dict containing ``sessionId``, ``status``, and optional
|
|
289
|
+
``message``. ``status`` is one of the values defined in
|
|
290
|
+
:class:`comcheck_api.types.SimulationStatus`. Terminal
|
|
291
|
+
values are ``"SUCCESS"`` and ``"FAILED"``.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
COMCheckSimulationError: If the status cannot be retrieved.
|
|
295
|
+
"""
|
|
296
|
+
status_response = self._service.get_simulation_status(session_id)
|
|
297
|
+
if status_response.data is None:
|
|
298
|
+
raise COMCheckSimulationError(
|
|
299
|
+
f"Failed to get simulation status for session {session_id}"
|
|
300
|
+
)
|
|
301
|
+
return status_response.data.model_dump(mode="json")
|
|
302
|
+
|
|
303
|
+
def get_simulation_result(self, session_id: str) -> Dict[str, Any]:
|
|
304
|
+
"""Get the result metrics of a completed simulation.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
session_id: The session ID returned by :meth:`start_run_simulation`.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
A dict containing ``sessionId``, ``performanceRating``,
|
|
311
|
+
``energyCreditPerformanceRating``, ``proposedBpf``, and
|
|
312
|
+
``baselineBpf``.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
COMCheckSimulationError: If the result cannot be retrieved.
|
|
316
|
+
"""
|
|
317
|
+
result_response = self._service.get_simulation_result(session_id)
|
|
318
|
+
if result_response.data is None:
|
|
319
|
+
raise COMCheckSimulationError(
|
|
320
|
+
f"Failed to get simulation result for session {session_id}"
|
|
321
|
+
)
|
|
322
|
+
return result_response.data.model_dump(mode="python")
|
|
323
|
+
|
|
324
|
+
def close(self) -> None:
|
|
325
|
+
"""Close the API service connection."""
|
|
326
|
+
if self._api_service is not None:
|
|
327
|
+
self._api_service.close()
|
|
328
|
+
|
|
329
|
+
def __enter__(self) -> "COMcheckClient":
|
|
330
|
+
"""Context manager entry."""
|
|
331
|
+
return self
|
|
332
|
+
|
|
333
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
334
|
+
"""Context manager exit."""
|
|
335
|
+
self.close()
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Building area constants for COMcheck projects."""
|
|
2
|
+
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from comcheck_api.types.core_types import WholeBldgUse
|
|
6
|
+
|
|
7
|
+
# Default building area structure with interior lighting space
|
|
8
|
+
DEFAULT_BUILDING_AREA: WholeBldgUse = WholeBldgUse.model_validate(
|
|
9
|
+
{
|
|
10
|
+
"key": str(uuid4()),
|
|
11
|
+
"wholeBldgType": "WHOLE_BUILDING_AUTOMOTIVE", # TODO: wholeBldgType is a list of enum, need to define the enum type
|
|
12
|
+
"areaDescription": "Automotive Facility",
|
|
13
|
+
"constructionType": "NON_RESIDENTIAL",
|
|
14
|
+
"floorArea": 1000,
|
|
15
|
+
"internalLoad": 0,
|
|
16
|
+
"powerDensity": 0.71, # TODO: power density is automatically calculated based on wholeBldgType
|
|
17
|
+
"isTenantSpace": False, # TODO: space conditioning is a list of enum, need to define the enum type
|
|
18
|
+
"activityUse": [],
|
|
19
|
+
"interiorLightingSpace": {
|
|
20
|
+
"altExemptType": "EXEMPT_NOT_SET",
|
|
21
|
+
"description": "",
|
|
22
|
+
"numFixturesAlteredOrAdded": 0,
|
|
23
|
+
"postAltTotalWattage": 0,
|
|
24
|
+
"preAltNumberFixtures": 0,
|
|
25
|
+
"preAltTotalWattage": 0,
|
|
26
|
+
"allowanceType": None,
|
|
27
|
+
"exemptionType": None,
|
|
28
|
+
"allowanceFloorArea": 0,
|
|
29
|
+
"rcrFloorToWorkplaneHeight": 0,
|
|
30
|
+
"rcrPerimeter": 0,
|
|
31
|
+
"rcrWorkplaneToLuminaireHeight": 0,
|
|
32
|
+
"fixture": [],
|
|
33
|
+
}, # redundant data, but need it for backend call
|
|
34
|
+
}
|
|
35
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Default project template used to bootstrap new COMcheck projects.
|
|
2
|
+
|
|
3
|
+
The :data:`PROJECT_TEMPLATE` constant is a fully validated :class:`ComBuilding`
|
|
4
|
+
instance pre-populated with sensible defaults for all required sections
|
|
5
|
+
(envelope, lighting, HVAC, location, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from comcheck_api.types.core_types import ComBuilding
|
|
9
|
+
|
|
10
|
+
PROJECT_TEMPLATE = ComBuilding.model_validate(
|
|
11
|
+
{
|
|
12
|
+
"control": {
|
|
13
|
+
"code": "CEZ_IECC2018",
|
|
14
|
+
"complianceMode": "UA",
|
|
15
|
+
"version": "",
|
|
16
|
+
},
|
|
17
|
+
"envelope": {
|
|
18
|
+
"useVltDetails": False,
|
|
19
|
+
"useCoolRoofPerformanceDetails": False,
|
|
20
|
+
"postAltWindowWallPct": None,
|
|
21
|
+
"postAltSkylightRoofPct": None,
|
|
22
|
+
"altPctGlazingAreaReplaced": None,
|
|
23
|
+
"altPctSkylightAreaReplaced": None,
|
|
24
|
+
"applyWindowPctAllowanceForDaylighting": False,
|
|
25
|
+
"applySkylightPctAllowanceForDaylighting": False,
|
|
26
|
+
"agWall": [],
|
|
27
|
+
"bgWall": [],
|
|
28
|
+
"roof": [],
|
|
29
|
+
"floor": [],
|
|
30
|
+
"door": [],
|
|
31
|
+
"window": [],
|
|
32
|
+
"skylight": [],
|
|
33
|
+
"useOrientDetails": True,
|
|
34
|
+
"airBarrierComplianceType": "AIR_BARRIER_OPTION_UNKNOWN",
|
|
35
|
+
},
|
|
36
|
+
"project": {
|
|
37
|
+
"projectTitle": "Default Project",
|
|
38
|
+
"projectPermitNumber": "",
|
|
39
|
+
"projectPermitDate": "",
|
|
40
|
+
"projectTaxMap": None,
|
|
41
|
+
"projectLotNumber": None,
|
|
42
|
+
"projectAddress": "",
|
|
43
|
+
"projectAddress2": None,
|
|
44
|
+
"projectCity": "",
|
|
45
|
+
"projectState": "",
|
|
46
|
+
"projectZipCode": "",
|
|
47
|
+
"projectComplete": None,
|
|
48
|
+
"ownerFirstName": "",
|
|
49
|
+
"ownerLastName": "",
|
|
50
|
+
"ownerCompany": "",
|
|
51
|
+
"ownerAddress": "",
|
|
52
|
+
"ownerAddress2": None,
|
|
53
|
+
"ownerCity": "",
|
|
54
|
+
"ownerState": "",
|
|
55
|
+
"ownerZipCode": "",
|
|
56
|
+
"ownerPhone": "",
|
|
57
|
+
"ownerEmail": "",
|
|
58
|
+
"developerFirstName": "",
|
|
59
|
+
"developerLastName": "",
|
|
60
|
+
"developerCompany": "",
|
|
61
|
+
"developerAddress": "",
|
|
62
|
+
"developerAddress2": None,
|
|
63
|
+
"developerCity": "",
|
|
64
|
+
"developerState": "",
|
|
65
|
+
"developerZipCode": "",
|
|
66
|
+
"developerPhone": "",
|
|
67
|
+
"developerEmail": "",
|
|
68
|
+
"notes": "",
|
|
69
|
+
},
|
|
70
|
+
"lighting": {
|
|
71
|
+
"exteriorLightingZoneType": "EXT_ZONE_UNSPECIFIED",
|
|
72
|
+
"exteriorUse": [],
|
|
73
|
+
"wholeBldgUse": [],
|
|
74
|
+
"fixtureSchedule": [],
|
|
75
|
+
},
|
|
76
|
+
"renewable": {
|
|
77
|
+
"renewableException": "RENEWABLE_ONSITE_EXCEPTION_NONE",
|
|
78
|
+
"numberOfFloors": 1,
|
|
79
|
+
"largestThreeFloorArea": 0,
|
|
80
|
+
"requiredCapacity": 0,
|
|
81
|
+
"proposedCapacity": 0,
|
|
82
|
+
"roofAreaForRenewable": 0,
|
|
83
|
+
"requiredOffsiteRenewableEnergy": 0,
|
|
84
|
+
"proposedOffsiteRenewableEnergy": 0,
|
|
85
|
+
"renewableSystem": [],
|
|
86
|
+
"offsiteRenewableProcurement": [],
|
|
87
|
+
},
|
|
88
|
+
"location": {"state": "Colorado", "city": "Boulder", "climateZone": 5},
|
|
89
|
+
"semiheated": None,
|
|
90
|
+
"hvac": {"hvacPlant": [], "hvacSystem": [], "fanSystem": []},
|
|
91
|
+
"swhSystem": [],
|
|
92
|
+
"requirements": [],
|
|
93
|
+
"isNonresidentialConditioning": True,
|
|
94
|
+
"isResidentialConditioning": False,
|
|
95
|
+
"isSemiheatedConditioning": False,
|
|
96
|
+
"isHistoricBuilding": False,
|
|
97
|
+
"feetBldgHeight": None,
|
|
98
|
+
"constructionType": "NONE",
|
|
99
|
+
"projectType": "NEW_CONSTRUCTION",
|
|
100
|
+
"projectSubType": "CONSTRUCTION_COMPLETE",
|
|
101
|
+
"projectMechanicalType": "PROJECT_HVAC_WITH_CENTRAL",
|
|
102
|
+
"projectCoreAndShellCredit": 0,
|
|
103
|
+
"efficiencyPackages": [],
|
|
104
|
+
"performanceRating": 0,
|
|
105
|
+
"energyCreditPerformanceRating": 0,
|
|
106
|
+
"buildingUseType": "ACTIVITY",
|
|
107
|
+
"conditioningType": "HEATING_AND_COOLING",
|
|
108
|
+
"heatingFuelType": "UNKNOWN_FUEL",
|
|
109
|
+
"userProject": None,
|
|
110
|
+
"allElectric": None,
|
|
111
|
+
"isRenewable": None,
|
|
112
|
+
"hasBattery": None,
|
|
113
|
+
"hasCharger": None,
|
|
114
|
+
"hasHeatPump": None,
|
|
115
|
+
}
|
|
116
|
+
)
|