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,90 @@
|
|
|
1
|
+
# Types Reference
|
|
2
|
+
|
|
3
|
+
All types are Pydantic models. Import from `comcheck_api.types`.
|
|
4
|
+
|
|
5
|
+
## Top-level project model
|
|
6
|
+
|
|
7
|
+
`ComBuilding` is the root project model. **All top-level fields are
|
|
8
|
+
lowercase camelCase** (no `Project`/`Control`/`Envelope` PascalCase
|
|
9
|
+
aliases exist).
|
|
10
|
+
|
|
11
|
+
| Field | Type | Purpose |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `project` | `Project` | Project metadata. Title is `project.project.projectTitle`; address fields use `projectAddress` / `projectCity` / etc. |
|
|
14
|
+
| `location` | `Location` | State, city, climate zone. |
|
|
15
|
+
| `envelope` | `Envelope` | Roofs (`roof[]`), AG walls (`agWall[]`), BG walls (`bgWall[]`), floors, windows, doors, skylights. |
|
|
16
|
+
| `lighting` | `Lighting` | Holds `wholeBldgUse[]` — the **building areas / zones**. `wholeBldgUse[]` (including each area's `interiorLightingSpace` singleton) is operable; `activityUse[]`, `exteriorUse[]`, and `fixtureSchedule[]` have no operations. |
|
|
17
|
+
| `hvac` | `HVAC` | Mechanical systems — no operations; not editable via this SDK. |
|
|
18
|
+
| `renewable` | `Renewable` | Renewable energy systems — no operations; not editable via this SDK. |
|
|
19
|
+
| `control` | `Control` | Energy code (`control.code`, e.g. `CEZ_IECC2018`, `CEZ_90_1_2022`). |
|
|
20
|
+
|
|
21
|
+
Building areas (`WholeBldgUse`) are **not top-level** — they live
|
|
22
|
+
under `lighting.wholeBldgUse[]`. Use
|
|
23
|
+
`ba_ops.get_building_area_keys_from_project(project)` to enumerate
|
|
24
|
+
them.
|
|
25
|
+
|
|
26
|
+
## Envelope component models
|
|
27
|
+
|
|
28
|
+
| Model | Notes |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `Envelope` | Container for all envelope components. |
|
|
31
|
+
| `Roof` | Skylights nest inside. |
|
|
32
|
+
| `AgWall` (above-grade) | Windows, doors, thermal bridges nest inside. |
|
|
33
|
+
| `BgWall` (below-grade) | Same nesting as AgWall. |
|
|
34
|
+
| `Floor` | Top-level. |
|
|
35
|
+
| `Skylight` | Standalone or nested under a `Roof`. |
|
|
36
|
+
| `Window` | Standalone or nested under a wall. |
|
|
37
|
+
| `Door` | Standalone or nested under a wall. |
|
|
38
|
+
| `ThermalBridge` | Nested under a wall. |
|
|
39
|
+
|
|
40
|
+
## Building area / lighting
|
|
41
|
+
|
|
42
|
+
| Model | Purpose |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `WholeBldgUse` | One building area / zone. Lives in `project.lighting.wholeBldgUse[]`. Has `key`, `areaDescription`, `floorArea`, `ceilingHeight`, `interiorLightingSpace`, etc. |
|
|
45
|
+
| `InteriorLightingSpace` | Lighting configuration for one area. The singleton attached directly to a `WholeBldgUse` is editable via `update_building_area_in_project`; the same model nested under `activityUse[]` is **not** operable (interior-lighting fixtures live there). |
|
|
46
|
+
|
|
47
|
+
Every envelope component has a `bldgUseKey` field that ties it to
|
|
48
|
+
one of these area keys.
|
|
49
|
+
|
|
50
|
+
## HVAC
|
|
51
|
+
|
|
52
|
+
⚠️ Documented for shape only — no add/update/remove operations exist
|
|
53
|
+
for any of these models. Leave `project.hvac` at its template default.
|
|
54
|
+
|
|
55
|
+
| Model | Purpose |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `HVAC` | Container. |
|
|
58
|
+
| `HVACSystem` | One HVAC system definition. |
|
|
59
|
+
| `HVACPlant` | Central plant equipment. |
|
|
60
|
+
| `Fan` / `FanSystem` | Fan and fan-system models. |
|
|
61
|
+
| `ServiceWaterHeatingSystem` (SWH) | Service water heating. |
|
|
62
|
+
|
|
63
|
+
## Enums (StrEnum)
|
|
64
|
+
|
|
65
|
+
Always set typed fields with members from the matching `*Options`
|
|
66
|
+
enum imported from `comcheck_api.types`. Setting them as raw strings
|
|
67
|
+
"works" but emits `PydanticSerializationUnexpectedValue` warnings
|
|
68
|
+
on every serialize.
|
|
69
|
+
|
|
70
|
+
Common ones:
|
|
71
|
+
|
|
72
|
+
| Enum | Examples |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `EnergyCodeOptions` | `CEZ_IECC2018`, `CEZ_IECC2021`, `CEZ_90_1_2019`, `CEZ_90_1_2022` |
|
|
75
|
+
| `OrientationOptions` | `NORTH`, `EAST`, `SOUTH`, `WEST`, plus diagonals and `UNSPECIFIED_ORIENTATION` |
|
|
76
|
+
| `WallTypeOptions` | (see source for full set) |
|
|
77
|
+
| `SimulationStatus` | Known: `INITIALIZING`, `GENERATING_BASELINE`, `GENERATING_PROPOSED`, `RUNNING_SIMULATIONS`, `CALCULATING_RESULTS`, `EVALUATING`, `SUCCESS`, `FAILED`. Catalog is **not exhaustive** — only `SUCCESS`/`FAILED` are guaranteed terminal. |
|
|
78
|
+
|
|
79
|
+
For exhaustive enum values, use
|
|
80
|
+
`comcheck_api.lookup_type("WallTypeOptions")` (or any other enum
|
|
81
|
+
name) — that's the live source of truth and stays in sync with the
|
|
82
|
+
installed SDK.
|
|
83
|
+
|
|
84
|
+
## Tips
|
|
85
|
+
|
|
86
|
+
- Pydantic models accept dicts in their constructors and produce
|
|
87
|
+
validated instances — useful when interop with JSON.
|
|
88
|
+
- Models export to JSON with `.model_dump_json()`.
|
|
89
|
+
- The SDK accepts dicts for inputs but always returns validated
|
|
90
|
+
Pydantic models — that asymmetry is intentional.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Helper scripts bundled with the Skill (importable as a Python module)."""
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Validate generated Python that uses comcheck_api against a mocked client.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m comcheck_api.ai.skill.scripts.validate_code <path-to-script>
|
|
5
|
+
cat script.py | python -m comcheck_api.ai.skill.scripts.validate_code -
|
|
6
|
+
|
|
7
|
+
Runs the user's code in a subprocess with the network layer mocked.
|
|
8
|
+
Prints structured errors to stdout and exits non-zero if validation
|
|
9
|
+
fails.
|
|
10
|
+
|
|
11
|
+
This script is invoked by the Skill so Claude can run it via shell
|
|
12
|
+
tools to self-check generated code.
|
|
13
|
+
|
|
14
|
+
Security: the calling layer is responsible for sandboxing — running
|
|
15
|
+
this script from an untrusted source still executes arbitrary Python.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_input(arg: str) -> str:
|
|
26
|
+
if arg == "-":
|
|
27
|
+
return sys.stdin.read()
|
|
28
|
+
return Path(arg).read_text()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
UNSUPPORTED_PROJECT_ATTRS = {"hvac", "renewable"}
|
|
32
|
+
UNSUPPORTED_LIGHTING_ATTRS = {
|
|
33
|
+
"activityUse",
|
|
34
|
+
"exteriorUse",
|
|
35
|
+
"fixtureSchedule",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _supported_operation_names() -> set[str]:
|
|
40
|
+
"""Live set of supported operation function names from the SDK."""
|
|
41
|
+
try:
|
|
42
|
+
from comcheck_api import list_operations
|
|
43
|
+
except Exception: # noqa: BLE001
|
|
44
|
+
return set()
|
|
45
|
+
return {op.name for op in list_operations()}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_attr_chain(node, head: str, tail: str) -> bool:
|
|
49
|
+
"""True if `node` is `<head>.<tail>` — e.g. `project.hvac`."""
|
|
50
|
+
import ast
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
isinstance(node, ast.Attribute)
|
|
54
|
+
and node.attr == tail
|
|
55
|
+
and isinstance(node.value, ast.Name)
|
|
56
|
+
and node.value.id == head
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_lighting_chain(node, attr: str) -> bool:
|
|
61
|
+
"""True if `node` is `project.lighting.<attr>`."""
|
|
62
|
+
import ast
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
isinstance(node, ast.Attribute)
|
|
66
|
+
and node.attr == attr
|
|
67
|
+
and isinstance(node.value, ast.Attribute)
|
|
68
|
+
and node.value.attr == "lighting"
|
|
69
|
+
and isinstance(node.value.value, ast.Name)
|
|
70
|
+
and node.value.value.id == "project"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate(code: str) -> dict:
|
|
75
|
+
"""Compile-check the provided code and report errors as a dict.
|
|
76
|
+
|
|
77
|
+
Runs three passes:
|
|
78
|
+
1. Syntax check via :func:`compile`.
|
|
79
|
+
2. Import check on every imported module name.
|
|
80
|
+
3. Scope check that the code only uses operations actually exposed
|
|
81
|
+
by the SDK and does not mutate the unsupported `hvac`,
|
|
82
|
+
`renewable`, or non-`wholeBldgUse` lighting subtrees.
|
|
83
|
+
"""
|
|
84
|
+
errors: list[dict] = []
|
|
85
|
+
|
|
86
|
+
# 1. Syntax check via compile().
|
|
87
|
+
try:
|
|
88
|
+
compile(code, "<generated>", "exec")
|
|
89
|
+
except SyntaxError as e:
|
|
90
|
+
errors.append(
|
|
91
|
+
{
|
|
92
|
+
"kind": "syntax",
|
|
93
|
+
"line": e.lineno,
|
|
94
|
+
"col": e.offset,
|
|
95
|
+
"message": e.msg,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
return {"ok": False, "errors": errors}
|
|
99
|
+
|
|
100
|
+
# 2. Import check: parse import statements and try importing each
|
|
101
|
+
# module name without executing the user's code.
|
|
102
|
+
import ast
|
|
103
|
+
|
|
104
|
+
tree = ast.parse(code)
|
|
105
|
+
for node in ast.walk(tree):
|
|
106
|
+
modules: list[str] = []
|
|
107
|
+
if isinstance(node, ast.Import):
|
|
108
|
+
modules = [alias.name for alias in node.names]
|
|
109
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
110
|
+
modules = [node.module]
|
|
111
|
+
for mod in modules:
|
|
112
|
+
try:
|
|
113
|
+
__import__(mod)
|
|
114
|
+
except ImportError as e:
|
|
115
|
+
errors.append({"kind": "import", "module": mod, "message": str(e)})
|
|
116
|
+
|
|
117
|
+
# 3. Scope check: cross-reference symbols against list_operations()
|
|
118
|
+
# so the guard auto-tracks SDK growth, and forbid direct mutation
|
|
119
|
+
# of the unsupported subtrees.
|
|
120
|
+
supported_ops = _supported_operation_names()
|
|
121
|
+
|
|
122
|
+
# Collect the local names that resolve into the operation modules.
|
|
123
|
+
op_module_aliases: set[str] = set()
|
|
124
|
+
for node in ast.walk(tree):
|
|
125
|
+
if isinstance(node, ast.ImportFrom) and node.module == "comcheck_api":
|
|
126
|
+
for alias in node.names:
|
|
127
|
+
if alias.name in {
|
|
128
|
+
"project_envelope_operations",
|
|
129
|
+
"project_building_area_operations",
|
|
130
|
+
}:
|
|
131
|
+
op_module_aliases.add(alias.asname or alias.name)
|
|
132
|
+
elif isinstance(node, ast.ImportFrom) and node.module == (
|
|
133
|
+
"comcheck_api.project_operations"
|
|
134
|
+
):
|
|
135
|
+
for alias in node.names:
|
|
136
|
+
if alias.name in {
|
|
137
|
+
"project_envelope_operations",
|
|
138
|
+
"project_building_area_operations",
|
|
139
|
+
}:
|
|
140
|
+
op_module_aliases.add(alias.asname or alias.name)
|
|
141
|
+
|
|
142
|
+
for node in ast.walk(tree):
|
|
143
|
+
# Forbid `project.hvac` / `project.renewable` access in any context.
|
|
144
|
+
if isinstance(node, ast.Attribute) and any(
|
|
145
|
+
_is_attr_chain(node, "project", attr) for attr in UNSUPPORTED_PROJECT_ATTRS
|
|
146
|
+
):
|
|
147
|
+
errors.append(
|
|
148
|
+
{
|
|
149
|
+
"kind": "unsupported-scope",
|
|
150
|
+
"line": node.lineno,
|
|
151
|
+
"message": (
|
|
152
|
+
f"`project.{node.attr}` has no operations in this SDK; "
|
|
153
|
+
"leave it at template defaults."
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Forbid `project.lighting.<unsupported>`.
|
|
159
|
+
if isinstance(node, ast.Attribute) and any(
|
|
160
|
+
_is_lighting_chain(node, attr) for attr in UNSUPPORTED_LIGHTING_ATTRS
|
|
161
|
+
):
|
|
162
|
+
errors.append(
|
|
163
|
+
{
|
|
164
|
+
"kind": "unsupported-scope",
|
|
165
|
+
"line": node.lineno,
|
|
166
|
+
"message": (
|
|
167
|
+
f"`project.lighting.{node.attr}` has no operations; "
|
|
168
|
+
"only `lighting.wholeBldgUse[]` is editable."
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Calls of the form `<op_module>.<name>(...)` must hit a real op.
|
|
174
|
+
if (
|
|
175
|
+
supported_ops
|
|
176
|
+
and isinstance(node, ast.Call)
|
|
177
|
+
and isinstance(node.func, ast.Attribute)
|
|
178
|
+
and isinstance(node.func.value, ast.Name)
|
|
179
|
+
and node.func.value.id in op_module_aliases
|
|
180
|
+
):
|
|
181
|
+
fn_name = node.func.attr
|
|
182
|
+
if fn_name not in supported_ops and not fn_name.startswith("_"):
|
|
183
|
+
errors.append(
|
|
184
|
+
{
|
|
185
|
+
"kind": "unknown-operation",
|
|
186
|
+
"line": node.lineno,
|
|
187
|
+
"operation": fn_name,
|
|
188
|
+
"message": (
|
|
189
|
+
f"`{node.func.value.id}.{fn_name}` is not a supported "
|
|
190
|
+
"operation. Run comcheck_api.list_operations() for the "
|
|
191
|
+
"current set."
|
|
192
|
+
),
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return {"ok": not errors, "errors": errors}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main() -> int:
|
|
200
|
+
if len(sys.argv) != 2:
|
|
201
|
+
print("usage: validate_code.py <path|->", file=sys.stderr)
|
|
202
|
+
return 2
|
|
203
|
+
code = _read_input(sys.argv[1])
|
|
204
|
+
result = validate(code)
|
|
205
|
+
print(json.dumps(result, indent=2))
|
|
206
|
+
return 0 if result["ok"] else 1
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from comcheck_api.api.api_services import COMCheckApiService
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""COMCheck API service module for making HTTP requests to the COMCheck API."""
|
|
2
|
+
|
|
3
|
+
"""Note: Service layer accepts raw data types (dicts) as inputs to stay simple and
|
|
4
|
+
HTTP-library-friendly, but returns validated Pydantic models to provide type safety
|
|
5
|
+
and catch API schema mismatches at the boundary."""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Dict, NoReturn, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from comcheck_api.exceptions import (
|
|
14
|
+
COMCheckHTTPError,
|
|
15
|
+
COMCheckConnectionError,
|
|
16
|
+
COMCheckValidationError,
|
|
17
|
+
)
|
|
18
|
+
from comcheck_api.types.api_types import (
|
|
19
|
+
RunSimulationResponse,
|
|
20
|
+
SimulationStatusResponse,
|
|
21
|
+
SimulationResultResponse,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class COMCheckApiService:
|
|
26
|
+
"""Low-level HTTP service for the COMcheck Web API.
|
|
27
|
+
|
|
28
|
+
Handles authentication, request construction, response parsing, and
|
|
29
|
+
error mapping. Methods accept raw dicts as inputs and return either
|
|
30
|
+
raw dicts or validated Pydantic response models.
|
|
31
|
+
|
|
32
|
+
Can be used as a context manager::
|
|
33
|
+
|
|
34
|
+
with COMCheckApiService(api_key="key") as svc:
|
|
35
|
+
data = svc.get_project("123")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, api_key: str) -> None:
|
|
39
|
+
"""Initialize COMCheck API service.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_key: API key for authentication
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If API key is not provided
|
|
46
|
+
"""
|
|
47
|
+
if not api_key:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"COM_API_KEY is not set. Please provide it as a constructor parameter "
|
|
50
|
+
"or set it in your environment variables."
|
|
51
|
+
)
|
|
52
|
+
self.api_key = api_key
|
|
53
|
+
self.base_url: str = os.getenv(
|
|
54
|
+
"COMCHECK_API_URL", "https://comcheck.energycode.pnl.gov/checkweb-api/COM"
|
|
55
|
+
)
|
|
56
|
+
self._client: Optional[httpx.Client] = None
|
|
57
|
+
|
|
58
|
+
def _get_client(self) -> httpx.Client:
|
|
59
|
+
"""Get or create HTTP client instance.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Configured httpx.Client instance
|
|
63
|
+
"""
|
|
64
|
+
if self._client is None:
|
|
65
|
+
self._client = httpx.Client(
|
|
66
|
+
base_url=self.base_url,
|
|
67
|
+
headers=self._prepare_headers(),
|
|
68
|
+
timeout=30.0,
|
|
69
|
+
)
|
|
70
|
+
return self._client
|
|
71
|
+
|
|
72
|
+
def _prepare_headers(self) -> Dict[str, str]:
|
|
73
|
+
"""Prepare headers for API requests.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
HTTP headers dictionary
|
|
77
|
+
"""
|
|
78
|
+
return {
|
|
79
|
+
"x-api-key": self.api_key,
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"Accept": "application/json",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def _handle_api_error(self, error: Exception) -> NoReturn:
|
|
85
|
+
"""Handle API errors with detailed logging and raise custom exceptions.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
error: The error object to handle
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
COMCheckHTTPError: For HTTP status errors
|
|
92
|
+
COMCheckConnectionError: For connection/request errors
|
|
93
|
+
COMCheckValidationError: For validation errors
|
|
94
|
+
"""
|
|
95
|
+
logger = logging.getLogger(__name__)
|
|
96
|
+
|
|
97
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
98
|
+
logger.error(
|
|
99
|
+
"HTTP error occurred: %s (Status: %s)",
|
|
100
|
+
error,
|
|
101
|
+
error.response.status_code,
|
|
102
|
+
exc_info=True,
|
|
103
|
+
extra={
|
|
104
|
+
"response_data": error.response.text,
|
|
105
|
+
"response_headers": dict(error.response.headers),
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
raise COMCheckHTTPError(
|
|
109
|
+
status_code=error.response.status_code,
|
|
110
|
+
message=error.response.reason_phrase,
|
|
111
|
+
response_data=error.response.text,
|
|
112
|
+
) from error
|
|
113
|
+
elif isinstance(error, httpx.RequestError):
|
|
114
|
+
logger.error(
|
|
115
|
+
"Request error occurred: %s",
|
|
116
|
+
error,
|
|
117
|
+
exc_info=True,
|
|
118
|
+
extra={"request": str(error.request)},
|
|
119
|
+
)
|
|
120
|
+
raise COMCheckConnectionError(
|
|
121
|
+
f"Failed to connect to COMcheck API: {str(error)}"
|
|
122
|
+
) from error
|
|
123
|
+
else:
|
|
124
|
+
logger.error("Unexpected error: %s", error, exc_info=True)
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
def get_project(self, project_id: str) -> Dict[str, Any]:
|
|
128
|
+
"""Get a single project by ID.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
project_id: The project ID to retrieve
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
API response data as dictionary
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
COMCheckHTTPError: If the API returns an error status
|
|
138
|
+
COMCheckConnectionError: If the request fails
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
client = self._get_client()
|
|
142
|
+
response = client.get(f"/project/{project_id}")
|
|
143
|
+
response.raise_for_status()
|
|
144
|
+
return response.json()
|
|
145
|
+
except Exception as error:
|
|
146
|
+
self._handle_api_error(error)
|
|
147
|
+
|
|
148
|
+
def get_project_list(self) -> Dict[str, Any]:
|
|
149
|
+
"""Get a list of all projects.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
API response data as dictionary
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
COMCheckHTTPError: If the API returns an error status
|
|
156
|
+
COMCheckConnectionError: If the request fails
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
client = self._get_client()
|
|
160
|
+
response = client.get("/projects")
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
return response.json()
|
|
163
|
+
except Exception as error:
|
|
164
|
+
self._handle_api_error(error)
|
|
165
|
+
|
|
166
|
+
def update_project(
|
|
167
|
+
self, project_id: str, project_data: Dict[str, Any]
|
|
168
|
+
) -> Dict[str, Any]:
|
|
169
|
+
"""Update a project by ID.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
project_id: The project ID to update
|
|
173
|
+
project_data: The project data to send in the request body
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
API response data as dictionary
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
COMCheckHTTPError: If the API returns an error status
|
|
180
|
+
COMCheckConnectionError: If the request fails
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
client = self._get_client()
|
|
184
|
+
response = client.put(f"/project/{project_id}", json=project_data)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
return response.json()
|
|
187
|
+
except Exception as error:
|
|
188
|
+
self._handle_api_error(error)
|
|
189
|
+
|
|
190
|
+
def start_run_simulation(
|
|
191
|
+
self, project_data: Dict[str, Any]
|
|
192
|
+
) -> RunSimulationResponse:
|
|
193
|
+
"""Start a simulation.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
project_data: The project data to send in the request body
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
RunSimulationResponse with session information
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
COMCheckHTTPError: If the API returns an error status
|
|
203
|
+
COMCheckConnectionError: If the request fails
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
client = self._get_client()
|
|
207
|
+
response = client.post(
|
|
208
|
+
f"/compliance/start-run-simulation", json=project_data
|
|
209
|
+
)
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
return RunSimulationResponse.model_validate(response.json())
|
|
212
|
+
except Exception as error:
|
|
213
|
+
self._handle_api_error(error)
|
|
214
|
+
|
|
215
|
+
def get_simulation_status(self, session_id: str) -> SimulationStatusResponse:
|
|
216
|
+
"""Get status of a simulation.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
session_id: The session ID of the simulation
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
SimulationStatusResponse with status information
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
COMCheckHTTPError: If the API returns an error status
|
|
226
|
+
COMCheckConnectionError: If the request fails
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
client = self._get_client()
|
|
230
|
+
response = client.get(
|
|
231
|
+
f"/compliance/get-status-simulation?sessionId={session_id}"
|
|
232
|
+
)
|
|
233
|
+
response.raise_for_status()
|
|
234
|
+
return SimulationStatusResponse.model_validate(response.json())
|
|
235
|
+
except Exception as error:
|
|
236
|
+
self._handle_api_error(error)
|
|
237
|
+
|
|
238
|
+
def get_simulation_result(self, session_id: str) -> SimulationResultResponse:
|
|
239
|
+
"""Get result of a simulation.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
session_id: The session ID of the simulation
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
SimulationResultResponse with simulation results
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
COMCheckHTTPError: If the API returns an error status
|
|
249
|
+
COMCheckConnectionError: If the request fails
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
client = self._get_client()
|
|
253
|
+
response = client.get(
|
|
254
|
+
f"/compliance/get-result-simulation?sessionId={session_id}"
|
|
255
|
+
)
|
|
256
|
+
response.raise_for_status()
|
|
257
|
+
return SimulationResultResponse.model_validate(response.json())
|
|
258
|
+
except Exception as error:
|
|
259
|
+
self._handle_api_error(error)
|
|
260
|
+
|
|
261
|
+
def close(self) -> None:
|
|
262
|
+
"""Close the HTTP client connection."""
|
|
263
|
+
if self._client is not None:
|
|
264
|
+
self._client.close()
|
|
265
|
+
self._client = None
|
|
266
|
+
|
|
267
|
+
def __enter__(self) -> "COMCheckApiService":
|
|
268
|
+
"""Context manager entry."""
|
|
269
|
+
return self
|
|
270
|
+
|
|
271
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
272
|
+
"""Context manager exit."""
|
|
273
|
+
self.close()
|