pyetm 2.0.2__tar.gz → 2.0.4__tar.gz
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.
- {pyetm-2.0.2 → pyetm-2.0.4}/PKG-INFO +2 -1
- {pyetm-2.0.2 → pyetm-2.0.4}/pyproject.toml +1 -1
- pyetm-2.0.4/src/pyetm/config/api_compat.py +55 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/scenario.py +27 -4
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/scenario_loader.py +46 -55
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/scenarios.py +93 -124
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/create_saved_scenario.py +12 -12
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_saved_scenario.py +13 -14
- {pyetm-2.0.2 → pyetm-2.0.4}/LICENSE +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/README.md +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/__main__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/clients/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/clients/base_client.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/clients/session.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/config/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/config/curve_registry.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/config/settings.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/exporters/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/exporters/excel_exporter.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/annual_exports.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/base.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/collection.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/couplings.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/custom_curves.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/error_policy.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/export_config.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/export_data_collection.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/gqueries.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/hourly_output_curves.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/inputs.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/annual_exports_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/custom_curves_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/hourly_output_curves_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/inputs_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/packable.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/query_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/sortable_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/packables/users_pack.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/scenario_packer.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/session.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/sessions.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/sortables.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/models/warnings.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/py.typed +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/base_runner.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/break_preset_link.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/copy_scenario.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/create_collection.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/create_scenario.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/delete_collection.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/delete_custom_curves.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/destroy_saved_scenario.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/destroy_session.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/discard_saved_scenario.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_annual_exports.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_collection.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_couplings.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_curves_generic.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_custom_curves.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_hourly_output_curves.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_inputs.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_metadata.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_saved_scenario.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_sortables.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_user_collections.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_user_saved_scenarios.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/fetch_user_scenarios.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/get_query_results.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/interpolate_scenarios.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/saved_scenario_users_create.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/saved_scenario_users_destroy.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/saved_scenario_users_index.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/saved_scenario_users_update.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/scenario_users_create.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/scenario_users_destroy.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/scenario_users_index.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/scenario_users_update.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_collection.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_couplings.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_custom_curves.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_inputs.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_metadata.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/scenario_runners/update_sortables.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/service_result.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/services/validators/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/templates/.env.template +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/templates/input_template.xlsx +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/types.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/utils/__init__.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/utils/excel_utils.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/utils/safe_cast.py +0 -0
- {pyetm-2.0.2 → pyetm-2.0.4}/src/pyetm/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyetm
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.4
|
|
4
4
|
Summary: Python-ETM Connector
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
17
|
Requires-Dist: aiohttp (>=3.14.1,<4.0.0)
|
|
17
18
|
Requires-Dist: click (>=8.0.0)
|
|
18
19
|
Requires-Dist: openpyxl (>=3.0.0)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Request shapes that work across every engine version pyetm targets.
|
|
2
|
+
|
|
3
|
+
Sibling of :mod:`pyetm.config.curve_registry`, which does the same for curve names.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
STABLE_2025_01_LABEL = "2025-01"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _targets_stable_2025_01(base_url: str | None) -> bool:
|
|
15
|
+
"""Is this client pointed at the 2025-01 stable engine?"""
|
|
16
|
+
if not base_url:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
host = urlparse(base_url).hostname or ""
|
|
20
|
+
return host.split(".")[0] == STABLE_2025_01_LABEL
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _environment_label(base_url: str | None) -> str | None:
|
|
24
|
+
"""Environment label used to build this host, or None for production/local."""
|
|
25
|
+
if not base_url:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
host = urlparse(base_url).hostname or ""
|
|
29
|
+
label = host.split(".")[0]
|
|
30
|
+
|
|
31
|
+
if label in ("", "engine", "localhost"):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
return label
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def saved_scenario_payload(
|
|
38
|
+
attributes: dict[str, Any], base_url: str | None = None
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Build a saved scenario body the target engine can read.
|
|
41
|
+
|
|
42
|
+
2025-01 reads attributes from the top level; later engines from a
|
|
43
|
+
``saved_scenario`` root key. Also stamps ``version`` with the environment
|
|
44
|
+
label, since MyETM defaults an unlabelled scenario to production.
|
|
45
|
+
"""
|
|
46
|
+
body = dict(attributes)
|
|
47
|
+
|
|
48
|
+
label = _environment_label(base_url)
|
|
49
|
+
if label is not None:
|
|
50
|
+
body.setdefault("version", label)
|
|
51
|
+
|
|
52
|
+
if _targets_stable_2025_01(base_url):
|
|
53
|
+
return body
|
|
54
|
+
|
|
55
|
+
return {"saved_scenario": body}
|
|
@@ -388,14 +388,28 @@ class Scenario(Base):
|
|
|
388
388
|
|
|
389
389
|
def update(self, client: Optional[BaseClient] = None, **kwargs: Any) -> None:
|
|
390
390
|
"""
|
|
391
|
-
Update this SavedScenario
|
|
391
|
+
Update this SavedScenario.
|
|
392
|
+
|
|
393
|
+
Passing a new ``scenario_id`` saves a version: MyETM moves the current
|
|
394
|
+
session into the scenario's history and points the SavedScenario at the
|
|
395
|
+
new session. Passing an ID already in the history restores that version.
|
|
392
396
|
|
|
393
397
|
Args:
|
|
394
398
|
client: Optional BaseClient instance
|
|
395
|
-
**kwargs: Fields to update (title, private, discarded)
|
|
399
|
+
**kwargs: Fields to update (title, private, discarded, scenario_id)
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
SavedScenarioError: If the update fails
|
|
403
|
+
|
|
404
|
+
Example:
|
|
405
|
+
```python
|
|
406
|
+
snapshot = scenario.session.copy_with_preset()
|
|
407
|
+
scenario.update(scenario_id=snapshot.id)
|
|
408
|
+
```
|
|
396
409
|
"""
|
|
397
410
|
if client is None:
|
|
398
411
|
client = get_client()
|
|
412
|
+
previous_scenario_id = self.scenario_id
|
|
399
413
|
result = UpdateSavedScenarioRunner.run(client, self.id, kwargs)
|
|
400
414
|
|
|
401
415
|
if not result.success:
|
|
@@ -408,13 +422,22 @@ class Scenario(Base):
|
|
|
408
422
|
|
|
409
423
|
if result.data:
|
|
410
424
|
for field, value in result.data.items():
|
|
411
|
-
if
|
|
425
|
+
if field in Scenario.model_fields:
|
|
412
426
|
setattr(self, field, value)
|
|
413
427
|
|
|
414
428
|
for field, value in kwargs.items():
|
|
415
|
-
if
|
|
429
|
+
if field in Scenario.model_fields and (not result.data or field not in result.data):
|
|
416
430
|
setattr(self, field, value)
|
|
417
431
|
|
|
432
|
+
if self.scenario_id != previous_scenario_id:
|
|
433
|
+
self._forget_session(result.data)
|
|
434
|
+
|
|
435
|
+
def _forget_session(self, response: Optional[Dict[str, Any]]) -> None:
|
|
436
|
+
"""Drop the cached session so the next access loads the new scenario_id."""
|
|
437
|
+
self._scenario_session = None
|
|
438
|
+
if not response or "scenario" not in response:
|
|
439
|
+
self.scenario = None
|
|
440
|
+
|
|
418
441
|
def discard(self, client: Optional[BaseClient] = None) -> None:
|
|
419
442
|
"""
|
|
420
443
|
Discard this SavedScenario from MyETM (soft-delete, recoverable).
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
"""Utilities for loading scenarios from various sources."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any, Protocol, cast
|
|
5
|
+
|
|
5
6
|
from pyetm.models.session import Session
|
|
6
7
|
|
|
7
8
|
logger = logging.getLogger(__name__)
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
def _warn_save_failed(session: Session, row_label: str, error: Exception) -> Session:
|
|
12
|
+
"""Record a failed MyETM save on the session itself."""
|
|
13
|
+
message = f"Row '{row_label}' was not saved to MyETM: {error}"
|
|
14
|
+
logger.warning("%s Returning session instead.", message)
|
|
15
|
+
session.add_warning("save", message)
|
|
16
|
+
return session
|
|
17
|
+
|
|
18
|
+
|
|
10
19
|
class ScenarioLoader(Protocol):
|
|
11
|
-
"""
|
|
12
|
-
Protocol for loading, copying, and creating scenarios.
|
|
20
|
+
"""Protocol for loading, copying, and creating scenarios.
|
|
13
21
|
|
|
14
22
|
Different implementations interpret scenario IDs differently:
|
|
15
23
|
- SessionLoader: IDs refer to ETEngine Sessions
|
|
@@ -20,10 +28,10 @@ class ScenarioLoader(Protocol):
|
|
|
20
28
|
self,
|
|
21
29
|
scenario_id: int,
|
|
22
30
|
area_code: Any,
|
|
23
|
-
end_year:
|
|
31
|
+
end_year: int | None,
|
|
24
32
|
row_label: str,
|
|
25
|
-
metadata_updates:
|
|
26
|
-
) ->
|
|
33
|
+
metadata_updates: dict[str, Any],
|
|
34
|
+
) -> Session | None:
|
|
27
35
|
"""Load an existing scenario by ID and apply metadata updates."""
|
|
28
36
|
...
|
|
29
37
|
|
|
@@ -31,33 +39,31 @@ class ScenarioLoader(Protocol):
|
|
|
31
39
|
self,
|
|
32
40
|
scenario_id: int,
|
|
33
41
|
row_label: str,
|
|
34
|
-
metadata_updates:
|
|
35
|
-
) ->
|
|
42
|
+
metadata_updates: dict[str, Any],
|
|
43
|
+
) -> Session | None:
|
|
36
44
|
"""Create a deep copy of a scenario (no template link)."""
|
|
37
45
|
...
|
|
38
46
|
|
|
39
47
|
def create_new(
|
|
40
48
|
self,
|
|
41
49
|
area_code: Any,
|
|
42
|
-
end_year:
|
|
50
|
+
end_year: int | None,
|
|
43
51
|
row_label: str,
|
|
44
|
-
metadata_updates:
|
|
45
|
-
) ->
|
|
52
|
+
metadata_updates: dict[str, Any],
|
|
53
|
+
) -> Session | None:
|
|
46
54
|
"""Create a brand new scenario."""
|
|
47
55
|
...
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
class SessionLoader:
|
|
51
|
-
"""
|
|
52
|
-
Loader for ETEngine Sessions.
|
|
59
|
+
"""Loader for ETEngine Sessions.
|
|
53
60
|
|
|
54
61
|
Interprets IDs as ETEngine scenario/session IDs.
|
|
55
62
|
"""
|
|
56
63
|
|
|
57
64
|
def __init__(self, packer_helper: Any) -> None:
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
packer_helper: Reference to ScenarioPacker instance for helper methods
|
|
65
|
+
"""Args:
|
|
66
|
+
packer_helper: Reference to ScenarioPacker instance for helper methods
|
|
61
67
|
"""
|
|
62
68
|
self._helper = packer_helper
|
|
63
69
|
|
|
@@ -65,10 +71,10 @@ class SessionLoader:
|
|
|
65
71
|
self,
|
|
66
72
|
scenario_id: int,
|
|
67
73
|
area_code: Any,
|
|
68
|
-
end_year:
|
|
74
|
+
end_year: int | None,
|
|
69
75
|
row_label: str,
|
|
70
|
-
metadata_updates:
|
|
71
|
-
) ->
|
|
76
|
+
metadata_updates: dict[str, Any],
|
|
77
|
+
) -> Session | None:
|
|
72
78
|
"""Load an ETEngine Session by ID."""
|
|
73
79
|
scenario = self._helper._load_or_create_scenario(
|
|
74
80
|
scenario_id, area_code, end_year, row_label, **metadata_updates
|
|
@@ -82,8 +88,8 @@ class SessionLoader:
|
|
|
82
88
|
self,
|
|
83
89
|
scenario_id: int,
|
|
84
90
|
row_label: str,
|
|
85
|
-
metadata_updates:
|
|
86
|
-
) ->
|
|
91
|
+
metadata_updates: dict[str, Any],
|
|
92
|
+
) -> Session | None:
|
|
87
93
|
"""Deep copy an ETEngine Session."""
|
|
88
94
|
try:
|
|
89
95
|
source_scenario = Session.load(scenario_id)
|
|
@@ -100,10 +106,10 @@ class SessionLoader:
|
|
|
100
106
|
def create_new(
|
|
101
107
|
self,
|
|
102
108
|
area_code: Any,
|
|
103
|
-
end_year:
|
|
109
|
+
end_year: int | None,
|
|
104
110
|
row_label: str,
|
|
105
|
-
metadata_updates:
|
|
106
|
-
) ->
|
|
111
|
+
metadata_updates: dict[str, Any],
|
|
112
|
+
) -> Session | None:
|
|
107
113
|
"""Create a new ETEngine Session."""
|
|
108
114
|
scenario = self._helper._load_or_create_scenario(
|
|
109
115
|
None, area_code, end_year, row_label, **metadata_updates
|
|
@@ -115,22 +121,19 @@ class SessionLoader:
|
|
|
115
121
|
|
|
116
122
|
|
|
117
123
|
class SavedScenarioLoader:
|
|
118
|
-
"""
|
|
119
|
-
Loader for MyETM SavedScenarios.
|
|
124
|
+
"""Loader for MyETM SavedScenarios.
|
|
120
125
|
|
|
121
126
|
Interprets IDs as MyETM SavedScenario IDs and automatically saves new scenarios.
|
|
122
127
|
"""
|
|
123
128
|
|
|
124
129
|
def __init__(self, packer_helper: Any) -> None:
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
packer_helper: Reference to ScenarioPacker instance for helper methods
|
|
130
|
+
"""Args:
|
|
131
|
+
packer_helper: Reference to ScenarioPacker instance for helper methods
|
|
128
132
|
"""
|
|
129
133
|
self._helper = packer_helper
|
|
130
134
|
|
|
131
135
|
def _require_authentication(self) -> None:
|
|
132
|
-
"""
|
|
133
|
-
Validate that authentication token is available for SavedScenario operations.
|
|
136
|
+
"""Validate that authentication token is available for SavedScenario operations.
|
|
134
137
|
|
|
135
138
|
Raises:
|
|
136
139
|
PermissionError: If no ETM_API_TOKEN is configured
|
|
@@ -150,10 +153,10 @@ class SavedScenarioLoader:
|
|
|
150
153
|
self,
|
|
151
154
|
scenario_id: int,
|
|
152
155
|
area_code: Any,
|
|
153
|
-
end_year:
|
|
156
|
+
end_year: int | None,
|
|
154
157
|
row_label: str,
|
|
155
|
-
metadata_updates:
|
|
156
|
-
) ->
|
|
158
|
+
metadata_updates: dict[str, Any],
|
|
159
|
+
) -> Session | None:
|
|
157
160
|
"""Load a SavedScenario from MyETM."""
|
|
158
161
|
from pyetm.models.scenario import Scenario
|
|
159
162
|
|
|
@@ -181,8 +184,8 @@ class SavedScenarioLoader:
|
|
|
181
184
|
self,
|
|
182
185
|
scenario_id: int,
|
|
183
186
|
row_label: str,
|
|
184
|
-
metadata_updates:
|
|
185
|
-
) ->
|
|
187
|
+
metadata_updates: dict[str, Any],
|
|
188
|
+
) -> Session | None:
|
|
186
189
|
"""Copy a SavedScenario and save the copy to MyETM."""
|
|
187
190
|
from pyetm.models.scenario import Scenario
|
|
188
191
|
|
|
@@ -197,7 +200,7 @@ class SavedScenarioLoader:
|
|
|
197
200
|
try:
|
|
198
201
|
saved_copy = copied_session.save(title=title)
|
|
199
202
|
# Validate that we got a proper SavedScenario back with required attributes
|
|
200
|
-
if hasattr(saved_copy,
|
|
203
|
+
if hasattr(saved_copy, "id") and hasattr(saved_copy, "scenario_id"):
|
|
201
204
|
logger.info(
|
|
202
205
|
"Automatically saved copy to MyETM with ID %s (session ID: %s)",
|
|
203
206
|
saved_copy.id,
|
|
@@ -211,12 +214,7 @@ class SavedScenarioLoader:
|
|
|
211
214
|
)
|
|
212
215
|
return copied_session
|
|
213
216
|
except Exception as save_error:
|
|
214
|
-
|
|
215
|
-
"Failed to save copy to MyETM for row '%s': %s. Returning session instead.",
|
|
216
|
-
row_label,
|
|
217
|
-
save_error,
|
|
218
|
-
)
|
|
219
|
-
return copied_session
|
|
217
|
+
return _warn_save_failed(copied_session, row_label, save_error)
|
|
220
218
|
except Exception as e:
|
|
221
219
|
error_msg = str(e)
|
|
222
220
|
if "does not exist" in error_msg or "not found" in error_msg.lower():
|
|
@@ -237,13 +235,11 @@ class SavedScenarioLoader:
|
|
|
237
235
|
def create_new(
|
|
238
236
|
self,
|
|
239
237
|
area_code: Any,
|
|
240
|
-
end_year:
|
|
238
|
+
end_year: int | None,
|
|
241
239
|
row_label: str,
|
|
242
|
-
metadata_updates:
|
|
243
|
-
) ->
|
|
240
|
+
metadata_updates: dict[str, Any],
|
|
241
|
+
) -> Session | None:
|
|
244
242
|
"""Create a new scenario and save it to MyETM."""
|
|
245
|
-
from pyetm.models.scenario import Scenario
|
|
246
|
-
|
|
247
243
|
# Validate authentication before attempting to save
|
|
248
244
|
self._require_authentication()
|
|
249
245
|
|
|
@@ -259,7 +255,7 @@ class SavedScenarioLoader:
|
|
|
259
255
|
try:
|
|
260
256
|
saved_scenario = scenario.save(title=title)
|
|
261
257
|
# Validate that we got a proper SavedScenario back with required attributes
|
|
262
|
-
if hasattr(saved_scenario,
|
|
258
|
+
if hasattr(saved_scenario, "id") and hasattr(saved_scenario, "scenario_id"):
|
|
263
259
|
logger.info(
|
|
264
260
|
"Saved new scenario to MyETM with ID %s (session ID: %s)",
|
|
265
261
|
saved_scenario.id,
|
|
@@ -273,9 +269,4 @@ class SavedScenarioLoader:
|
|
|
273
269
|
)
|
|
274
270
|
return cast(Session, scenario)
|
|
275
271
|
except Exception as e:
|
|
276
|
-
|
|
277
|
-
"Failed to save new scenario to MyETM for row '%s': %s. Returning session instead.",
|
|
278
|
-
row_label,
|
|
279
|
-
e,
|
|
280
|
-
)
|
|
281
|
-
return cast(Session, scenario)
|
|
272
|
+
return _warn_save_failed(cast(Session, scenario), row_label, e)
|