site-calc-investment 1.2.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.
- site_calc_investment/__init__.py +100 -0
- site_calc_investment/analysis/__init__.py +17 -0
- site_calc_investment/analysis/comparison.py +121 -0
- site_calc_investment/analysis/financial.py +202 -0
- site_calc_investment/api/__init__.py +5 -0
- site_calc_investment/api/client.py +442 -0
- site_calc_investment/exceptions.py +91 -0
- site_calc_investment/models/__init__.py +84 -0
- site_calc_investment/models/common.py +174 -0
- site_calc_investment/models/devices.py +263 -0
- site_calc_investment/models/requests.py +133 -0
- site_calc_investment/models/responses.py +105 -0
- site_calc_investment-1.2.0.dist-info/METADATA +256 -0
- site_calc_investment-1.2.0.dist-info/RECORD +16 -0
- site_calc_investment-1.2.0.dist-info/WHEEL +4 -0
- site_calc_investment-1.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Investment Client for Site-Calc API."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from site_calc_investment import __version__
|
|
10
|
+
from site_calc_investment.exceptions import (
|
|
11
|
+
ApiError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ForbiddenFeatureError,
|
|
14
|
+
JobNotFoundError,
|
|
15
|
+
LimitExceededError,
|
|
16
|
+
OptimizationError,
|
|
17
|
+
SiteCalcError,
|
|
18
|
+
TimeoutError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
from site_calc_investment.models.requests import InvestmentPlanningRequest
|
|
22
|
+
from site_calc_investment.models.responses import InvestmentPlanningResponse, Job
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InvestmentClient:
|
|
26
|
+
"""Client for Site-Calc investment planning API.
|
|
27
|
+
|
|
28
|
+
This client is specifically for long-term capacity planning and
|
|
29
|
+
investment ROI analysis. It:
|
|
30
|
+
- Only supports 1-hour resolution
|
|
31
|
+
- Maximum 100,000 intervals (~11 years)
|
|
32
|
+
- Does NOT support ancillary services
|
|
33
|
+
- Only has access to /device-planning endpoint
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> client = InvestmentClient(
|
|
37
|
+
... base_url="https://api.site-calc.example.com",
|
|
38
|
+
... api_key="inv_your_key_here"
|
|
39
|
+
... )
|
|
40
|
+
>>> job = client.create_planning_job(request)
|
|
41
|
+
>>> result = client.wait_for_completion(job.job_id, timeout=7200)
|
|
42
|
+
>>> print(f"NPV: €{result.summary.investment_metrics.npv:,.0f}")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
base_url: str,
|
|
48
|
+
api_key: str,
|
|
49
|
+
timeout: float = 900.0,
|
|
50
|
+
max_retries: int = 3,
|
|
51
|
+
):
|
|
52
|
+
"""Initialize the investment client.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
base_url: Base URL of the API (e.g., "https://api.site-calc.example.com")
|
|
56
|
+
api_key: API key with 'inv_' prefix (investment client)
|
|
57
|
+
timeout: Default request timeout in seconds (default: 1 hour)
|
|
58
|
+
max_retries: Maximum number of retry attempts for failed requests
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If API key doesn't start with 'inv_'
|
|
62
|
+
"""
|
|
63
|
+
if not api_key.startswith("inv_"):
|
|
64
|
+
raise ValueError("API key must start with 'inv_' for investment client")
|
|
65
|
+
|
|
66
|
+
self.base_url = base_url.rstrip("/")
|
|
67
|
+
self.api_key = api_key
|
|
68
|
+
self.timeout = timeout
|
|
69
|
+
self.max_retries = max_retries
|
|
70
|
+
self.max_intervals = 100_000
|
|
71
|
+
|
|
72
|
+
self._client = httpx.Client(
|
|
73
|
+
base_url=self.base_url,
|
|
74
|
+
headers={
|
|
75
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
"Accept": "application/json",
|
|
78
|
+
},
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
self._version_checked = False
|
|
82
|
+
|
|
83
|
+
def __enter__(self) -> "InvestmentClient":
|
|
84
|
+
"""Context manager entry."""
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def __exit__(
|
|
88
|
+
self,
|
|
89
|
+
exc_type: type[BaseException] | None,
|
|
90
|
+
exc_val: BaseException | None,
|
|
91
|
+
exc_tb: object,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Context manager exit."""
|
|
94
|
+
self.close()
|
|
95
|
+
|
|
96
|
+
def close(self) -> None:
|
|
97
|
+
"""Close the HTTP client."""
|
|
98
|
+
self._client.close()
|
|
99
|
+
|
|
100
|
+
def _validate_server_version(self) -> None:
|
|
101
|
+
"""Check server API version compatibility and warn if mismatched.
|
|
102
|
+
|
|
103
|
+
Compares client MAJOR.MINOR with server api_version.
|
|
104
|
+
Only runs once per client instance.
|
|
105
|
+
"""
|
|
106
|
+
if self._version_checked:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
self._version_checked = True
|
|
110
|
+
client_api_version = ".".join(__version__.split(".")[:2])
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
response = self._client.get("/health")
|
|
114
|
+
if response.status_code == 200:
|
|
115
|
+
health = response.json()
|
|
116
|
+
server_api_version = health.get("api_version")
|
|
117
|
+
if server_api_version and client_api_version != server_api_version:
|
|
118
|
+
warnings.warn(
|
|
119
|
+
f"Client version {__version__} (API {client_api_version}) may not be compatible "
|
|
120
|
+
f"with server API {server_api_version}. Consider upgrading.",
|
|
121
|
+
UserWarning,
|
|
122
|
+
stacklevel=3,
|
|
123
|
+
)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def _handle_error(self, response: httpx.Response) -> None:
|
|
128
|
+
"""Handle API error responses.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
response: HTTP response with error status
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
Appropriate exception based on status code and error details
|
|
135
|
+
"""
|
|
136
|
+
details: dict[str, Any] | None = None
|
|
137
|
+
try:
|
|
138
|
+
error_data = response.json()
|
|
139
|
+
# Handle FastAPI error format: {"detail": ...}
|
|
140
|
+
if "detail" in error_data:
|
|
141
|
+
detail = error_data["detail"]
|
|
142
|
+
if isinstance(detail, list):
|
|
143
|
+
# Pydantic validation error: [{"msg": ..., "loc": [...], ...}]
|
|
144
|
+
messages = [item.get("msg", str(item)) for item in detail]
|
|
145
|
+
message = "; ".join(messages)
|
|
146
|
+
details = {"validation_errors": detail}
|
|
147
|
+
else:
|
|
148
|
+
# Simple detail string
|
|
149
|
+
message = str(detail)
|
|
150
|
+
details = None
|
|
151
|
+
code = None
|
|
152
|
+
else:
|
|
153
|
+
# Handle custom error format: {"error": {"code": ..., "message": ...}}
|
|
154
|
+
error = error_data.get("error", {})
|
|
155
|
+
code = error.get("code")
|
|
156
|
+
message = error.get("message", "Unknown error")
|
|
157
|
+
details = error.get("details")
|
|
158
|
+
except Exception:
|
|
159
|
+
message = response.text or f"HTTP {response.status_code}"
|
|
160
|
+
code = None
|
|
161
|
+
details = None
|
|
162
|
+
|
|
163
|
+
if response.status_code == 400:
|
|
164
|
+
raise ValidationError(message, code, details)
|
|
165
|
+
elif response.status_code == 401:
|
|
166
|
+
raise AuthenticationError(message, code, details)
|
|
167
|
+
elif response.status_code == 403:
|
|
168
|
+
if code == "forbidden_feature":
|
|
169
|
+
raise ForbiddenFeatureError(message, code, details)
|
|
170
|
+
elif code == "limit_exceeded" or code == "invalid_resolution":
|
|
171
|
+
requested = details.get("requested") if details and isinstance(details, dict) else None
|
|
172
|
+
max_allowed = details.get("max_allowed") if details and isinstance(details, dict) else None
|
|
173
|
+
raise LimitExceededError(message, requested, max_allowed, code, details)
|
|
174
|
+
else:
|
|
175
|
+
raise ApiError(message, code, details)
|
|
176
|
+
elif response.status_code == 404:
|
|
177
|
+
if "job" in message.lower() or "not found" in message.lower():
|
|
178
|
+
raise JobNotFoundError(message, code, details)
|
|
179
|
+
raise ApiError(message, code, details)
|
|
180
|
+
elif response.status_code == 408 or response.status_code == 504:
|
|
181
|
+
raise TimeoutError(message, code=code)
|
|
182
|
+
elif response.status_code == 422:
|
|
183
|
+
raise ValidationError(message, code, details)
|
|
184
|
+
elif response.status_code >= 500:
|
|
185
|
+
raise ApiError(f"Server error: {message}", code, details)
|
|
186
|
+
else:
|
|
187
|
+
raise ApiError(message, code, details)
|
|
188
|
+
|
|
189
|
+
def _request_with_retry(
|
|
190
|
+
self,
|
|
191
|
+
method: str,
|
|
192
|
+
path: str,
|
|
193
|
+
**kwargs: Any,
|
|
194
|
+
) -> httpx.Response:
|
|
195
|
+
"""Make HTTP request with retry logic.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
method: HTTP method (GET, POST, DELETE)
|
|
199
|
+
path: API path
|
|
200
|
+
**kwargs: Additional arguments for httpx
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
HTTP response
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
Various exceptions based on response status
|
|
207
|
+
"""
|
|
208
|
+
self._validate_server_version()
|
|
209
|
+
last_exception: SiteCalcError | None = None
|
|
210
|
+
|
|
211
|
+
for attempt in range(self.max_retries):
|
|
212
|
+
try:
|
|
213
|
+
response = self._client.request(method, path, **kwargs)
|
|
214
|
+
|
|
215
|
+
if response.status_code < 400:
|
|
216
|
+
return response
|
|
217
|
+
|
|
218
|
+
# Don't retry client errors (4xx) except timeouts
|
|
219
|
+
if 400 <= response.status_code < 500 and response.status_code not in [408, 429]:
|
|
220
|
+
self._handle_error(response)
|
|
221
|
+
|
|
222
|
+
# Retry server errors (5xx) and specific client errors
|
|
223
|
+
if attempt < self.max_retries - 1:
|
|
224
|
+
wait_time = 2**attempt # Exponential backoff
|
|
225
|
+
time.sleep(wait_time)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
self._handle_error(response)
|
|
229
|
+
|
|
230
|
+
except httpx.TimeoutException:
|
|
231
|
+
last_exception = TimeoutError(f"Request timeout after {self.timeout}s", timeout=self.timeout)
|
|
232
|
+
if attempt < self.max_retries - 1:
|
|
233
|
+
time.sleep(2**attempt)
|
|
234
|
+
continue
|
|
235
|
+
raise last_exception
|
|
236
|
+
except httpx.RequestError as e:
|
|
237
|
+
last_exception = ApiError(f"Request failed: {str(e)}")
|
|
238
|
+
if attempt < self.max_retries - 1:
|
|
239
|
+
time.sleep(2**attempt)
|
|
240
|
+
continue
|
|
241
|
+
raise last_exception
|
|
242
|
+
|
|
243
|
+
if last_exception:
|
|
244
|
+
raise last_exception
|
|
245
|
+
raise ApiError("Request failed after retries")
|
|
246
|
+
|
|
247
|
+
def create_planning_job(self, request: InvestmentPlanningRequest) -> Job:
|
|
248
|
+
"""Create a long-term investment planning job.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
request: Investment planning request
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Job object with job_id and initial status
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValidationError: If request is invalid
|
|
258
|
+
ForbiddenFeatureError: If using forbidden features (ANS)
|
|
259
|
+
LimitExceededError: If exceeding client limits
|
|
260
|
+
AuthenticationError: If API key is invalid
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
>>> request = InvestmentPlanningRequest(
|
|
264
|
+
... sites=[site],
|
|
265
|
+
... timespan=TimeSpan.for_years(2025, 10),
|
|
266
|
+
... investment_parameters=inv_params
|
|
267
|
+
... )
|
|
268
|
+
>>> job = client.create_planning_job(request)
|
|
269
|
+
>>> print(f"Job ID: {job.job_id}")
|
|
270
|
+
"""
|
|
271
|
+
payload = request.model_dump_for_api()
|
|
272
|
+
|
|
273
|
+
response = self._request_with_retry(
|
|
274
|
+
"POST",
|
|
275
|
+
"/api/v1/jobs/device-planning",
|
|
276
|
+
json=payload,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return Job(**response.json())
|
|
280
|
+
|
|
281
|
+
def get_job_status(self, job_id: str) -> Job:
|
|
282
|
+
"""Get current job status.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
job_id: Job identifier
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Job object with current status
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
JobNotFoundError: If job doesn't exist
|
|
292
|
+
|
|
293
|
+
Example:
|
|
294
|
+
>>> job = client.get_job_status(job_id)
|
|
295
|
+
>>> print(f"Status: {job.status}, Progress: {job.progress}%")
|
|
296
|
+
"""
|
|
297
|
+
response = self._request_with_retry(
|
|
298
|
+
"GET",
|
|
299
|
+
f"/api/v1/jobs/{job_id}",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return Job(**response.json())
|
|
303
|
+
|
|
304
|
+
def get_job_result(self, job_id: str) -> InvestmentPlanningResponse:
|
|
305
|
+
"""Get job result (must be completed).
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
job_id: Job identifier
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Complete optimization result
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
JobNotFoundError: If job doesn't exist
|
|
315
|
+
ApiError: If job is not completed
|
|
316
|
+
|
|
317
|
+
Example:
|
|
318
|
+
>>> result = client.get_job_result(job_id)
|
|
319
|
+
>>> print(f"NPV: €{result.investment_metrics.npv:,.0f}")
|
|
320
|
+
"""
|
|
321
|
+
response = self._request_with_retry(
|
|
322
|
+
"GET",
|
|
323
|
+
f"/api/v1/jobs/{job_id}/result",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
data = response.json()
|
|
327
|
+
|
|
328
|
+
# Extract result from wrapper and flatten
|
|
329
|
+
result_data = {
|
|
330
|
+
"job_id": str(data.get("job_id")),
|
|
331
|
+
"status": data.get("status"),
|
|
332
|
+
**data.get("result", {}),
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return InvestmentPlanningResponse(**result_data)
|
|
336
|
+
|
|
337
|
+
def cancel_job(self, job_id: str) -> Job:
|
|
338
|
+
"""Cancel a running job.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
job_id: Job identifier
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Job object with cancelled status
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
JobNotFoundError: If job doesn't exist
|
|
348
|
+
ApiError: If job cannot be cancelled (already completed)
|
|
349
|
+
|
|
350
|
+
Example:
|
|
351
|
+
>>> cancelled = client.cancel_job(job_id)
|
|
352
|
+
>>> print(f"Status: {cancelled.status}")
|
|
353
|
+
"""
|
|
354
|
+
response = self._request_with_retry(
|
|
355
|
+
"DELETE",
|
|
356
|
+
f"/api/v1/jobs/{job_id}",
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
return Job(**response.json())
|
|
360
|
+
|
|
361
|
+
def cancel_all_jobs(self) -> dict[str, object]:
|
|
362
|
+
"""Cancel all pending or running jobs.
|
|
363
|
+
|
|
364
|
+
Cancels all jobs that are currently pending or running for the
|
|
365
|
+
authenticated user. Useful for cleanup when shutting down or
|
|
366
|
+
when jobs are no longer needed.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Dictionary with:
|
|
370
|
+
- cancelled_count: Number of jobs cancelled
|
|
371
|
+
- cancelled_jobs: List of cancelled job IDs
|
|
372
|
+
- message: Status message
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
AuthenticationError: If API key is invalid
|
|
376
|
+
|
|
377
|
+
Example:
|
|
378
|
+
>>> result = client.cancel_all_jobs()
|
|
379
|
+
>>> print(f"Cancelled {result['cancelled_count']} jobs")
|
|
380
|
+
"""
|
|
381
|
+
response = self._request_with_retry(
|
|
382
|
+
"DELETE",
|
|
383
|
+
"/api/v1/jobs",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
result: dict[str, object] = response.json()
|
|
387
|
+
return result
|
|
388
|
+
|
|
389
|
+
def wait_for_completion(
|
|
390
|
+
self,
|
|
391
|
+
job_id: str,
|
|
392
|
+
poll_interval: float = 30,
|
|
393
|
+
timeout: Optional[float] = 7200,
|
|
394
|
+
) -> InvestmentPlanningResponse:
|
|
395
|
+
"""Wait for job to complete and return result.
|
|
396
|
+
|
|
397
|
+
Polls the job status at regular intervals until completion or timeout.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
job_id: Job identifier
|
|
401
|
+
poll_interval: Seconds between status checks (default: 30s)
|
|
402
|
+
timeout: Maximum wait time in seconds (default: 2 hours, None=unlimited)
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Complete optimization result
|
|
406
|
+
|
|
407
|
+
Raises:
|
|
408
|
+
TimeoutError: If timeout is exceeded
|
|
409
|
+
JobNotFoundError: If job doesn't exist
|
|
410
|
+
OptimizationError: If job fails
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
>>> result = client.wait_for_completion(
|
|
414
|
+
... job_id,
|
|
415
|
+
... poll_interval=30,
|
|
416
|
+
... timeout=7200
|
|
417
|
+
... )
|
|
418
|
+
>>> print(f"Solved in {result.summary.solve_time_seconds:.1f}s")
|
|
419
|
+
"""
|
|
420
|
+
start_time = time.time()
|
|
421
|
+
|
|
422
|
+
while True:
|
|
423
|
+
job = self.get_job_status(job_id)
|
|
424
|
+
|
|
425
|
+
if job.status == "completed":
|
|
426
|
+
return self.get_job_result(job_id)
|
|
427
|
+
elif job.status == "failed":
|
|
428
|
+
error_msg: str = str(job.error.get("message", "Unknown error")) if job.error else "Unknown error"
|
|
429
|
+
error_code = job.error.get("code") if job.error else None
|
|
430
|
+
error_details = job.error.get("details") if job.error else None
|
|
431
|
+
raise OptimizationError(error_msg, error_code, error_details)
|
|
432
|
+
elif job.status == "cancelled":
|
|
433
|
+
raise ApiError("Job was cancelled")
|
|
434
|
+
|
|
435
|
+
# Check timeout
|
|
436
|
+
if timeout is not None:
|
|
437
|
+
elapsed = time.time() - start_time
|
|
438
|
+
if elapsed > timeout:
|
|
439
|
+
raise TimeoutError(f"Job did not complete within {timeout}s", timeout=timeout)
|
|
440
|
+
|
|
441
|
+
# Wait before next poll
|
|
442
|
+
time.sleep(poll_interval)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# SYNC: This file may be synced between investment and operational clients
|
|
2
|
+
"""Custom exceptions for the investment client."""
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SiteCalcError(Exception):
|
|
8
|
+
"""Base exception for all Site-Calc client errors."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, code: Optional[str] = None, details: Optional[Dict[str, Any]] = None):
|
|
11
|
+
self.message = message
|
|
12
|
+
self.code = code
|
|
13
|
+
self.details = details or {}
|
|
14
|
+
super().__init__(self.message)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApiError(SiteCalcError):
|
|
18
|
+
"""General API error."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationError(SiteCalcError):
|
|
24
|
+
"""Request validation failed."""
|
|
25
|
+
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthenticationError(SiteCalcError):
|
|
30
|
+
"""Authentication failed (invalid API key)."""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ForbiddenFeatureError(SiteCalcError):
|
|
36
|
+
"""Attempted to use a feature not available for this client type.
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
- Investment client trying to use ancillary_services
|
|
40
|
+
- Investment client trying to access /optimal-bidding endpoint
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LimitExceededError(SiteCalcError):
|
|
47
|
+
"""Request exceeded client limits.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
- Too many intervals
|
|
51
|
+
- Wrong resolution
|
|
52
|
+
- Request too large
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
message: str,
|
|
58
|
+
requested: Optional[int] = None,
|
|
59
|
+
max_allowed: Optional[int] = None,
|
|
60
|
+
code: Optional[str] = None,
|
|
61
|
+
details: Optional[Dict[str, Any]] = None,
|
|
62
|
+
):
|
|
63
|
+
self.requested = requested
|
|
64
|
+
self.max_allowed = max_allowed
|
|
65
|
+
super().__init__(message, code, details)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TimeoutError(SiteCalcError):
|
|
69
|
+
"""Request or operation timed out."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, message: str, timeout: Optional[float] = None, code: Optional[str] = None):
|
|
72
|
+
self.timeout = timeout
|
|
73
|
+
super().__init__(message, code)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class OptimizationError(SiteCalcError):
|
|
77
|
+
"""Optimization solver error.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
- Infeasible problem
|
|
81
|
+
- Unbounded problem
|
|
82
|
+
- Solver timeout
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class JobNotFoundError(SiteCalcError):
|
|
89
|
+
"""Job ID not found."""
|
|
90
|
+
|
|
91
|
+
pass
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Data models for investment client."""
|
|
2
|
+
|
|
3
|
+
from site_calc_investment.models.common import Location, Resolution, TimeSpan
|
|
4
|
+
from site_calc_investment.models.devices import (
|
|
5
|
+
CHP,
|
|
6
|
+
# Devices
|
|
7
|
+
Battery,
|
|
8
|
+
# Properties
|
|
9
|
+
BatteryProperties,
|
|
10
|
+
CHPProperties,
|
|
11
|
+
DemandProperties,
|
|
12
|
+
Device,
|
|
13
|
+
ElectricityDemand,
|
|
14
|
+
ElectricityExport,
|
|
15
|
+
ElectricityImport,
|
|
16
|
+
GasImport,
|
|
17
|
+
HeatAccumulator,
|
|
18
|
+
HeatAccumulatorProperties,
|
|
19
|
+
HeatDemand,
|
|
20
|
+
HeatExport,
|
|
21
|
+
MarketExportProperties,
|
|
22
|
+
MarketImportProperties,
|
|
23
|
+
Photovoltaic,
|
|
24
|
+
PhotovoltaicProperties,
|
|
25
|
+
# Schedule
|
|
26
|
+
Schedule,
|
|
27
|
+
)
|
|
28
|
+
from site_calc_investment.models.requests import (
|
|
29
|
+
InvestmentParameters,
|
|
30
|
+
InvestmentPlanningRequest,
|
|
31
|
+
OptimizationConfig,
|
|
32
|
+
Site,
|
|
33
|
+
TimeSpanInvestment,
|
|
34
|
+
)
|
|
35
|
+
from site_calc_investment.models.responses import (
|
|
36
|
+
DeviceSchedule,
|
|
37
|
+
InvestmentMetrics,
|
|
38
|
+
InvestmentPlanningResponse,
|
|
39
|
+
Job,
|
|
40
|
+
SiteResult,
|
|
41
|
+
Summary,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Common
|
|
46
|
+
"TimeSpan",
|
|
47
|
+
"Resolution",
|
|
48
|
+
"Location",
|
|
49
|
+
# Device Properties
|
|
50
|
+
"BatteryProperties",
|
|
51
|
+
"CHPProperties",
|
|
52
|
+
"HeatAccumulatorProperties",
|
|
53
|
+
"PhotovoltaicProperties",
|
|
54
|
+
"DemandProperties",
|
|
55
|
+
"MarketImportProperties",
|
|
56
|
+
"MarketExportProperties",
|
|
57
|
+
# Schedule
|
|
58
|
+
"Schedule",
|
|
59
|
+
# Devices
|
|
60
|
+
"Battery",
|
|
61
|
+
"CHP",
|
|
62
|
+
"HeatAccumulator",
|
|
63
|
+
"Photovoltaic",
|
|
64
|
+
"HeatDemand",
|
|
65
|
+
"ElectricityDemand",
|
|
66
|
+
"ElectricityImport",
|
|
67
|
+
"ElectricityExport",
|
|
68
|
+
"GasImport",
|
|
69
|
+
"HeatExport",
|
|
70
|
+
"Device",
|
|
71
|
+
# Request models
|
|
72
|
+
"Site",
|
|
73
|
+
"InvestmentParameters",
|
|
74
|
+
"OptimizationConfig",
|
|
75
|
+
"TimeSpanInvestment",
|
|
76
|
+
"InvestmentPlanningRequest",
|
|
77
|
+
# Response models
|
|
78
|
+
"Job",
|
|
79
|
+
"DeviceSchedule",
|
|
80
|
+
"SiteResult",
|
|
81
|
+
"InvestmentMetrics",
|
|
82
|
+
"Summary",
|
|
83
|
+
"InvestmentPlanningResponse",
|
|
84
|
+
]
|