Skip to content

Commit 6e483c9

Browse files
authored
Merge pull request #786 from simvue-io/dev
Create v2.1.1 Release
2 parents eb38356 + 5ce8092 commit 6e483c9

36 files changed

+432
-331
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
args: [--branch, main, --branch, dev]
2424
- id: check-added-large-files
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: v0.11.2
26+
rev: v0.11.4
2727
hooks:
2828
- id: ruff
2929
args: [ --fix, --exit-non-zero-on-fix, "--ignore=C901" ]

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# Change log
2+
## [v2.1.1](https://github.com/simvue-io/client/releases/tag/v2.1.1) - 2025-04-25
3+
* Changed from CO2 Signal to ElectricityMaps
4+
* Fixed a number of bugs in how offline mode is handled with emissions
5+
* Streamlined EmissionsMonitor class and handling
6+
* Fixed bugs in client getting results from Simvue server arising from pagination
7+
* Fixed bug in setting visibility in `run.init` method
8+
* Default setting in `Client.get_runs` is now `show_shared=True`
29
## [v2.1.0](https://github.com/simvue-io/client/releases/tag/v2.1.0) - 2025-03-28
310
* Removed CodeCarbon dependence in favour of a slimmer solution using the CO2 Signal API.
411
* Added sorting to server queries, users can now specify to sort by columns during data retrieval from the database.

CITATION.cff

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ keywords:
4242
- alerting
4343
- simulation
4444
license: Apache-2.0
45-
commit: 8f13a7adb2ad0ec53f0a4949e44e1c5676ae342d
46-
version: 2.1.0
47-
date-released: '2025-03-28'
45+
commit: f1bde5646b33f01ec15ef72a0c5843c1fe181ac1
46+
version: 2.1.1
47+
date-released: '2025-04-25'

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "simvue"
3-
version = "2.1.0"
3+
version = "2.1.1"
44
description = "Simulation tracking and monitoring"
55
authors = [
66
{name = "Simvue Development Team", email = "[email protected]"}

simvue/api/objects/base.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def ids(
365365
"""
366366
_class_instance = cls(_read_only=True, _local=True)
367367
_count: int = 0
368-
for response in cls._get_all_objects(offset):
368+
for response in cls._get_all_objects(offset, count=count):
369369
if (_data := response.get("data")) is None:
370370
raise RuntimeError(
371371
f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s"
@@ -404,7 +404,7 @@ def get(
404404
"""
405405
_class_instance = cls(_read_only=True, _local=True)
406406
_count: int = 0
407-
for _response in cls._get_all_objects(offset, **kwargs):
407+
for _response in cls._get_all_objects(offset, count=count, **kwargs):
408408
if count and _count > count:
409409
return
410410
if (_data := _response.get("data")) is None:
@@ -438,7 +438,7 @@ def count(cls, **kwargs) -> int:
438438

439439
@classmethod
440440
def _get_all_objects(
441-
cls, offset: int | None, **kwargs
441+
cls, offset: int | None, count: int | None, **kwargs
442442
) -> typing.Generator[dict, None, None]:
443443
_class_instance = cls(_read_only=True)
444444
_url = f"{_class_instance._base_url}"
@@ -448,7 +448,7 @@ def _get_all_objects(
448448
_label = _label[:-1]
449449

450450
for response in get_paginated(
451-
_url, headers=_class_instance._headers, offset=offset, **kwargs
451+
_url, headers=_class_instance._headers, offset=offset, count=count, **kwargs
452452
):
453453
yield get_json_from_response(
454454
response=response,

simvue/api/objects/events.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def get(
5151
_class_instance = cls(_read_only=True, _local=True)
5252
_count: int = 0
5353

54-
for response in cls._get_all_objects(offset, run=run_id, **kwargs):
54+
for response in cls._get_all_objects(offset, count=count, run=run_id, **kwargs):
5555
if (_data := response.get("data")) is None:
5656
raise RuntimeError(
5757
f"Expected key 'data' for retrieval of {_class_instance.__class__.__name__.lower()}s"

simvue/api/request.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ def get_paginated(
281281
timeout: int = DEFAULT_API_TIMEOUT,
282282
json: dict[str, typing.Any] | None = None,
283283
offset: int | None = None,
284+
count: int | None = None,
284285
**params,
285286
) -> typing.Generator[requests.Response, None, None]:
286287
"""Paginate results of a server query.
@@ -302,22 +303,18 @@ def get_paginated(
302303
server response
303304
"""
304305
_offset: int = offset or 0
305-
306306
while (
307-
(
308-
_response := get(
309-
url=url,
310-
headers=headers,
311-
params=(params or {})
312-
| {"count": MAX_ENTRIES_PER_PAGE, "start": _offset},
313-
timeout=timeout,
314-
json=json,
315-
)
307+
_response := get(
308+
url=url,
309+
headers=headers,
310+
params=(params or {})
311+
| {"count": count or MAX_ENTRIES_PER_PAGE, "start": _offset},
312+
timeout=timeout,
313+
json=json,
316314
)
317-
.json()
318-
.get("data")
319-
):
315+
).json():
320316
yield _response
321317
_offset += MAX_ENTRIES_PER_PAGE
322318

323-
yield _response
319+
if (count and _offset > count) or (_response.json().get("count", 0) < _offset):
320+
break

simvue/client.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def get_runs(
181181
output_format: typing.Literal["dict", "objects", "dataframe"] = "objects",
182182
count_limit: pydantic.PositiveInt | None = 100,
183183
start_index: pydantic.NonNegativeInt = 0,
184-
show_shared: bool = False,
184+
show_shared: bool = True,
185185
sort_by_columns: list[tuple[str, bool]] | None = None,
186186
) -> DataFrame | typing.Generator[tuple[str, Run], None, None] | None:
187187
"""Retrieve all runs matching filters.
@@ -210,7 +210,7 @@ def get_runs(
210210
start_index : int, optional
211211
the index from which to count entries. Default is 0.
212212
show_shared : bool, optional
213-
whether to include runs shared with the current user. Default is False.
213+
whether to include runs shared with the current user. Default is True.
214214
sort_by_columns : list[tuple[str, bool]], optional
215215
sort by columns in the order given,
216216
list of tuples in the form (column_name: str, sort_descending: bool),
@@ -234,8 +234,9 @@ def get_runs(
234234
RuntimeError
235235
if there was a failure in data retrieval from the server
236236
"""
237+
filters = filters or []
237238
if not show_shared:
238-
filters = (filters or []) + ["user == self"]
239+
filters += ["user == self"]
239240

240241
_runs = Run.get(
241242
count=count_limit,
@@ -835,7 +836,8 @@ def get_metric_values(
835836

836837
_args = {"filters": json.dumps(run_filters)} if run_filters else {}
837838

838-
_run_data = dict(Run.get(**_args))
839+
if not run_ids:
840+
_run_data = dict(Run.get(**_args))
839841

840842
if not (
841843
_run_metrics := self._get_run_metrics_from_server(
@@ -853,7 +855,8 @@ def get_metric_values(
853855
)
854856
if use_run_names:
855857
_run_metrics = {
856-
_run_data[key].name: _run_metrics[key] for key in _run_metrics.keys()
858+
Run(identifier=key).name: _run_metrics[key]
859+
for key in _run_metrics.keys()
857860
}
858861
return parse_run_set_metrics(
859862
_run_metrics,

simvue/config/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def fetch(
200200
_default_dir = _config_dict["offline"].get(
201201
"cache", DEFAULT_OFFLINE_DIRECTORY
202202
)
203+
pathlib.Path(_default_dir).mkdir(parents=True, exist_ok=True)
203204

204205
_config_dict["offline"]["cache"] = _default_dir
205206

simvue/eco/api_client.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,33 @@
1919
import geocoder.location
2020
import typing
2121

22-
CO2_SIGNAL_API_ENDPOINT: str = "https://api.co2signal.com/v1/latest"
22+
CO2_SIGNAL_API_ENDPOINT: str = (
23+
"https://api.electricitymap.org/v3/carbon-intensity/latest"
24+
)
2325

2426

2527
class CO2SignalData(pydantic.BaseModel):
2628
datetime: datetime.datetime
2729
carbon_intensity: float
28-
fossil_fuel_percentage: float
2930

3031

3132
class CO2SignalResponse(pydantic.BaseModel):
32-
disclaimer: str
3333
country_code: str
34-
status: str
3534
data: CO2SignalData
3635
carbon_intensity_units: str
3736

3837
@classmethod
3938
def from_json_response(cls, json_response: dict) -> "CO2SignalResponse":
40-
_data: dict[str, typing.Any] = json_response["data"]
4139
_co2_signal_data = CO2SignalData(
4240
datetime=datetime.datetime.fromisoformat(
43-
_data["datetime"].replace("Z", "+00:00")
41+
json_response["datetime"].replace("Z", "+00:00")
4442
),
45-
carbon_intensity=_data["carbonIntensity"],
46-
fossil_fuel_percentage=_data["fossilFuelPercentage"],
43+
carbon_intensity=json_response["carbonIntensity"],
4744
)
4845
return cls(
49-
disclaimer=json_response["_disclaimer"],
50-
country_code=json_response["countryCode"],
51-
status=json_response["status"],
46+
country_code=json_response["zone"],
5247
data=_co2_signal_data,
53-
carbon_intensity_units=json_response["units"]["carbonIntensity"],
48+
carbon_intensity_units="gCO2e/kWh",
5449
)
5550

5651

@@ -82,18 +77,15 @@ def __init__(self, *args, **kwargs) -> None:
8277
co2_api_endpoint : str
8378
endpoint for CO2 signal API
8479
co2_api_token: str
85-
RECOMMENDED. The API token for the CO2 Signal API, default is None.
80+
The API token for the ElectricityMaps API, default is None.
8681
timeout : int
8782
timeout for API
8883
"""
8984
super().__init__(*args, **kwargs)
9085
self._logger = logging.getLogger(self.__class__.__name__)
9186

9287
if not self.co2_api_token:
93-
self._logger.warning(
94-
"⚠️ No API token provided for CO2 Signal, "
95-
"use of a token is strongly recommended."
96-
)
88+
raise ValueError("API token is required for ElectricityMaps API.")
9789

9890
self._get_user_location_info()
9991

@@ -109,16 +101,14 @@ def _get_user_location_info(self) -> None:
109101
def get(self) -> CO2SignalResponse:
110102
"""Get the current data"""
111103
_params: dict[str, float | str] = {
112-
"lat": self._latitude,
113-
"lon": self._longitude,
114-
"countryCode": self._two_letter_country_code,
104+
"zone": self._two_letter_country_code,
115105
}
116106

117107
if self.co2_api_token:
118108
_params["auth-token"] = self.co2_api_token.get_secret_value()
119109

120110
self._logger.debug(f"🍃 Retrieving carbon intensity data for: {_params}")
121-
_response = requests.get(f"{self.co2_api_endpoint}", params=_params)
111+
_response = requests.get(f"{self.co2_api_endpoint}", headers=_params)
122112

123113
if _response.status_code != http.HTTPStatus.OK:
124114
try:

simvue/eco/config.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@
88
__date__ = "2025-03-06"
99

1010
import pydantic
11-
import pathlib
12-
import os
13-
14-
from simvue.config.files import DEFAULT_OFFLINE_DIRECTORY
1511

1612

1713
class EcoConfig(pydantic.BaseModel):
@@ -25,30 +21,13 @@ class EcoConfig(pydantic.BaseModel):
2521
the TDP for the CPU
2622
gpu_thermal_design_power: int | None, optional
2723
the TDP for each GPU
28-
local_data_directory: str, optional
29-
the directory to store local data, default is Simvue offline directory
3024
"""
3125

3226
co2_signal_api_token: pydantic.SecretStr | None = None
3327
cpu_thermal_design_power: pydantic.PositiveInt | None = None
3428
cpu_n_cores: pydantic.PositiveInt | None = None
3529
gpu_thermal_design_power: pydantic.PositiveInt | None = None
36-
local_data_directory: pydantic.DirectoryPath | None = pydantic.Field(
37-
None, validate_default=True
38-
)
3930
intensity_refresh_interval: pydantic.PositiveInt | str | None = pydantic.Field(
40-
default="1 day", gt=2 * 60
31+
default="1 hour", gt=2 * 60
4132
)
4233
co2_intensity: float | None = None
43-
44-
@pydantic.field_validator("local_data_directory", mode="before", check_fields=True)
45-
@classmethod
46-
def check_local_data_env(
47-
cls, local_data_directory: pathlib.Path | None
48-
) -> pathlib.Path:
49-
if _data_directory := os.environ.get("SIMVUE_ECO_DATA_DIRECTORY"):
50-
return pathlib.Path(_data_directory)
51-
if not local_data_directory:
52-
local_data_directory = pathlib.Path(DEFAULT_OFFLINE_DIRECTORY)
53-
local_data_directory.mkdir(exist_ok=True, parents=True)
54-
return local_data_directory

0 commit comments

Comments
 (0)