Skip to content

Commit 136be28

Browse files
Merge branch 'improvements-basedon-plan' into 'main'
Make API base URL configurable via environment variable See merge request becp/checktool-libraries/comcheckweb-api-python!14
2 parents 2effb3d + c58c87b commit 136be28

10 files changed

Lines changed: 214 additions & 71 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
- Made BASE_URL configurable via COMCHECK_API_URL environment variable
12+
1013
### To Be Added
1114
- Logging implementation to replace print statements
1215
- Custom exception classes for better error handling
1316
- Comprehensive API documentation
1417
- Additional examples and tutorials
1518

1619
### To Be Fixed
17-
- Remove hardcoded BASE_URL, make configurable via environment
1820
- Fix import path documentation in __init__.py
1921
- Security: Remove .env from version control
2022

@@ -71,7 +73,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7173
### Known Issues
7274
- API key must be provided manually (no OAuth flow yet)
7375
- Some print() statements used instead of proper logging
74-
- Hardcoded API URL (should be configurable)
7576
- Limited error handling with custom exceptions
7677

7778
### Breaking Changes

comcheck_api/__init__.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,56 @@
1-
"""COMcheckWeb API package.
1+
"""COMcheckWeb API - Python client for programmatic building energy code compliance.
22
3-
Example:
4-
from comcheckweb.client import COMcheckClient
5-
from comcheckweb.project_operations import (
6-
project_building_area_operations,
7-
project_envelope_operations,
3+
This package provides a type-safe, automated interface to the U.S. Department of
4+
Energy's COMcheck Web API for building energy code compliance verification.
5+
6+
Quick Start:
7+
>>> from comcheck_api import COMcheckClient
8+
>>> client = COMcheckClient(api_key="your-api-key")
9+
>>> project = client.get_project("123")
10+
>>> session_id = client.start_run_simulation(project)
11+
12+
Basic Usage:
13+
from comcheck_api import (
14+
COMcheckClient,
15+
COMCheckHTTPError,
16+
export_to_json,
817
)
9-
from comcheckweb.utilities import export_to_json
10-
from comcheckweb.utilities.get_project_default import (
11-
get_default_ag_wall_template,
12-
get_default_building_area_template,
18+
19+
client = COMcheckClient(api_key="your-key")
20+
21+
try:
22+
projects = client.list_projects()
23+
export_to_json(projects, "projects.json")
24+
except COMCheckHTTPError as e:
25+
print(f"HTTP error: {e.status_code}")
26+
27+
Advanced Usage:
28+
from comcheck_api import (
29+
COMcheckClient,
30+
project_envelope_operations,
1331
)
14-
from comcheckweb.types import AssemblyType, AgWall
32+
from comcheck_api.types import AgWall, ComBuilding
33+
34+
# For type imports, use the types submodule
35+
# from comcheck_api.types import ...
36+
37+
# For advanced utilities, use the utilities submodule
38+
# from comcheck_api.utilities import ...
1539
"""
1640

17-
# COMcheck API Client
41+
# Client
1842
from .client import COMcheckClient
1943

44+
# Exceptions
45+
from .exceptions import (
46+
COMCheckAPIError,
47+
COMCheckHTTPError,
48+
COMCheckConnectionError,
49+
COMCheckValidationError,
50+
COMCheckSimulationError,
51+
COMCheckProjectNotFoundError,
52+
)
53+
2054
# Project Defaults
2155
from .utilities import get_project_default
2256

@@ -26,7 +60,7 @@
2660
project_envelope_operations,
2761
)
2862

29-
# Utility Functions
63+
# Utilities
3064
from . import utilities
3165

3266
# Types
@@ -35,7 +69,14 @@
3569
__all__ = [
3670
# Client
3771
"COMcheckClient",
38-
# Defaults
72+
# Exceptions
73+
"COMCheckAPIError",
74+
"COMCheckHTTPError",
75+
"COMCheckConnectionError",
76+
"COMCheckValidationError",
77+
"COMCheckSimulationError",
78+
"COMCheckProjectNotFoundError",
79+
# Project Defaults
3980
"get_project_default",
4081
# Project Operations
4182
"project_building_area_operations",

comcheck_api/api/api_services.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
"""COMCheck API service module for making HTTP requests to the COMCheck API."""
22

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
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
55
and catch API schema mismatches at the boundary."""
66

7-
from typing import Any, Dict, Optional
7+
import logging
8+
import os
9+
from typing import Any, Dict, NoReturn, Optional
810

911
import httpx
1012

13+
from comcheck_api.exceptions import (
14+
COMCheckHTTPError,
15+
COMCheckConnectionError,
16+
COMCheckValidationError,
17+
)
1118
from comcheck_api.types.api_types import (
1219
RunSimulationResponse,
1320
SimulationStatusResponse,
@@ -18,8 +25,6 @@
1825
class COMCheckApiService:
1926
"""COMCheck API service class for interacting with the COM API."""
2027

21-
BASE_URL: str = "https://becp-dev.pnl.gov/ahj/COM" # COM API base URL
22-
2328
def __init__(self, api_key: str) -> None:
2429
"""Initialize COMCheck API service.
2530
@@ -35,6 +40,9 @@ def __init__(self, api_key: str) -> None:
3540
"or set it in your environment variables."
3641
)
3742
self.api_key = api_key
43+
self.base_url: str = os.getenv(
44+
"COMCHECK_API_URL", "https://becp-dev.pnl.gov/ahj/COM"
45+
)
3846
self._client: Optional[httpx.Client] = None
3947

4048
def _get_client(self) -> httpx.Client:
@@ -45,7 +53,7 @@ def _get_client(self) -> httpx.Client:
4553
"""
4654
if self._client is None:
4755
self._client = httpx.Client(
48-
base_url=self.BASE_URL,
56+
base_url=self.base_url,
4957
headers=self._prepare_headers(),
5058
timeout=30.0,
5159
)
@@ -63,22 +71,48 @@ def _prepare_headers(self) -> Dict[str, str]:
6371
"Accept": "application/json",
6472
}
6573

66-
def _handle_api_error(self, error: Exception) -> None:
67-
"""Handle API errors with detailed logging.
74+
def _handle_api_error(self, error: Exception) -> NoReturn:
75+
"""Handle API errors with detailed logging and raise custom exceptions.
6876
6977
Args:
7078
error: The error object to handle
79+
80+
Raises:
81+
COMCheckHTTPError: For HTTP status errors
82+
COMCheckConnectionError: For connection/request errors
83+
COMCheckValidationError: For validation errors
7184
"""
85+
logger = logging.getLogger(__name__)
86+
7287
if isinstance(error, httpx.HTTPStatusError):
73-
print(f"HTTP error occurred: {error}")
74-
print(f"Status: {error.response.status_code}")
75-
print(f"Response data: {error.response.text}")
76-
print(f"Response headers: {error.response.headers}")
88+
logger.error(
89+
"HTTP error occurred: %s (Status: %s)",
90+
error,
91+
error.response.status_code,
92+
exc_info=True,
93+
extra={
94+
"response_data": error.response.text,
95+
"response_headers": dict(error.response.headers),
96+
},
97+
)
98+
raise COMCheckHTTPError(
99+
status_code=error.response.status_code,
100+
message=error.response.reason_phrase,
101+
response_data=error.response.text,
102+
) from error
77103
elif isinstance(error, httpx.RequestError):
78-
print(f"Request error occurred: {error}")
79-
print(f"Request: {error.request}")
104+
logger.error(
105+
"Request error occurred: %s",
106+
error,
107+
exc_info=True,
108+
extra={"request": str(error.request)},
109+
)
110+
raise COMCheckConnectionError(
111+
f"Failed to connect to COMcheck API: {str(error)}"
112+
) from error
80113
else:
81-
print(f"Unexpected error: {error}")
114+
logger.error("Unexpected error: %s", error, exc_info=True)
115+
raise
82116

83117
def get_project(self, project_id: str) -> Dict[str, Any]:
84118
"""Get a single project by ID.
@@ -90,8 +124,8 @@ def get_project(self, project_id: str) -> Dict[str, Any]:
90124
API response data as dictionary
91125
92126
Raises:
93-
httpx.HTTPStatusError: If the API returns an error status
94-
httpx.RequestError: If the request fails
127+
COMCheckHTTPError: If the API returns an error status
128+
COMCheckConnectionError: If the request fails
95129
"""
96130
try:
97131
client = self._get_client()
@@ -100,7 +134,6 @@ def get_project(self, project_id: str) -> Dict[str, Any]:
100134
return response.json()
101135
except Exception as error:
102136
self._handle_api_error(error)
103-
raise
104137

105138
def get_project_list(self) -> Dict[str, Any]:
106139
"""Get a list of all projects.
@@ -109,8 +142,8 @@ def get_project_list(self) -> Dict[str, Any]:
109142
API response data as dictionary
110143
111144
Raises:
112-
httpx.HTTPStatusError: If the API returns an error status
113-
httpx.RequestError: If the request fails
145+
COMCheckHTTPError: If the API returns an error status
146+
COMCheckConnectionError: If the request fails
114147
"""
115148
try:
116149
client = self._get_client()
@@ -119,7 +152,6 @@ def get_project_list(self) -> Dict[str, Any]:
119152
return response.json()
120153
except Exception as error:
121154
self._handle_api_error(error)
122-
raise
123155

124156
def update_project(
125157
self, project_id: str, project_data: Dict[str, Any]
@@ -134,8 +166,8 @@ def update_project(
134166
API response data as dictionary
135167
136168
Raises:
137-
httpx.HTTPStatusError: If the API returns an error status
138-
httpx.RequestError: If the request fails
169+
COMCheckHTTPError: If the API returns an error status
170+
COMCheckConnectionError: If the request fails
139171
"""
140172
try:
141173
client = self._get_client()
@@ -144,7 +176,6 @@ def update_project(
144176
return response.json()
145177
except Exception as error:
146178
self._handle_api_error(error)
147-
raise
148179

149180
def start_run_simulation(
150181
self, project_data: Dict[str, Any]
@@ -158,8 +189,8 @@ def start_run_simulation(
158189
RunSimulationResponse with session information
159190
160191
Raises:
161-
httpx.HTTPStatusError: If the API returns an error status
162-
httpx.RequestError: If the request fails
192+
COMCheckHTTPError: If the API returns an error status
193+
COMCheckConnectionError: If the request fails
163194
"""
164195
try:
165196
client = self._get_client()
@@ -170,7 +201,6 @@ def start_run_simulation(
170201
return RunSimulationResponse.model_construct(**response.json())
171202
except Exception as error:
172203
self._handle_api_error(error)
173-
raise
174204

175205
def get_simulation_status(self, session_id: str) -> SimulationStatusResponse:
176206
"""Get status of a simulation.
@@ -182,8 +212,8 @@ def get_simulation_status(self, session_id: str) -> SimulationStatusResponse:
182212
SimulationStatusResponse with status information
183213
184214
Raises:
185-
httpx.HTTPStatusError: If the API returns an error status
186-
httpx.RequestError: If the request fails
215+
COMCheckHTTPError: If the API returns an error status
216+
COMCheckConnectionError: If the request fails
187217
"""
188218
try:
189219
client = self._get_client()
@@ -194,7 +224,6 @@ def get_simulation_status(self, session_id: str) -> SimulationStatusResponse:
194224
return SimulationStatusResponse.model_construct(**response.json())
195225
except Exception as error:
196226
self._handle_api_error(error)
197-
raise
198227

199228
def get_simulation_result(self, session_id: str) -> SimulationResultResponse:
200229
"""Get result of a simulation.
@@ -206,8 +235,8 @@ def get_simulation_result(self, session_id: str) -> SimulationResultResponse:
206235
SimulationResultResponse with simulation results
207236
208237
Raises:
209-
httpx.HTTPStatusError: If the API returns an error status
210-
httpx.RequestError: If the request fails
238+
COMCheckHTTPError: If the API returns an error status
239+
COMCheckConnectionError: If the request fails
211240
"""
212241
try:
213242
client = self._get_client()
@@ -218,7 +247,6 @@ def get_simulation_result(self, session_id: str) -> SimulationResultResponse:
218247
return SimulationResultResponse.model_construct(**response.json())
219248
except Exception as error:
220249
self._handle_api_error(error)
221-
raise
222250

223251
def close(self) -> None:
224252
"""Close the HTTP client connection."""

0 commit comments

Comments
 (0)