diff --git a/carbonserver/tests/api/test_schema_compatibility.py b/carbonserver/tests/api/test_schema_compatibility.py new file mode 100644 index 000000000..94522fc8c --- /dev/null +++ b/carbonserver/tests/api/test_schema_compatibility.py @@ -0,0 +1,120 @@ +import dataclasses +import importlib.util +from pathlib import Path + +import pytest + +from carbonserver.api import schemas as server_schemas + + +def load_client_schemas(): + repo_root = Path(__file__).resolve().parents[3] + client_schema_path = repo_root / "codecarbon" / "core" / "schemas.py" + spec = importlib.util.spec_from_file_location( + "codecarbon_client_schemas", client_schema_path + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +client_schemas = load_client_schemas() + + +CREATE_SCHEMA_PAIRS = [ + (client_schemas.EmissionCreate, server_schemas.EmissionCreate), + (client_schemas.RunCreate, server_schemas.RunCreate), + (client_schemas.ExperimentCreate, server_schemas.ExperimentCreate), + (client_schemas.ProjectCreate, server_schemas.ProjectCreate), + (client_schemas.OrganizationCreate, server_schemas.OrganizationCreate), +] + + +@pytest.mark.parametrize(("client_schema", "server_schema"), CREATE_SCHEMA_PAIRS) +def test_client_create_schemas_match_server_fields(client_schema, server_schema): + client_fields = {field.name for field in dataclasses.fields(client_schema)} + server_fields = set(server_schema.model_fields) + + assert client_fields == server_fields + + +@pytest.mark.parametrize( + ("client_payload", "server_schema"), + [ + ( + client_schemas.EmissionCreate( + timestamp="2021-04-04T08:43:00+02:00", + run_id="40088f1a-d28e-4980-8d80-bf5600056a14", + duration=98745, + emissions_sum=1544.54, + emissions_rate=1.548444, + cpu_power=0.3, + gpu_power=0.0, + ram_power=0.15, + cpu_energy=55.21874, + gpu_energy=0.0, + ram_energy=2.0, + energy_consumed=57.21874, + cpu_utilization_percent=12.5, + gpu_utilization_percent=34.5, + ram_utilization_percent=56.5, + wue=0.8, + ), + server_schemas.EmissionCreate, + ), + ( + client_schemas.RunCreate( + timestamp="2021-04-04T08:43:00+02:00", + experiment_id="8edb03e1-9a28-452a-9c93-a3b6560136d7", + os="macOS-10.15.7-x86_64-i386-64bit", + python_version="3.8.0", + codecarbon_version="2.1.3", + cpu_count=12, + cpu_model="Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz", + gpu_count=4, + gpu_model="NVIDIA", + longitude=-7.6174, + latitude=33.5822, + region="EUROPE", + provider="AWS", + ram_total_size=83948.22, + tracking_mode="Machine", + ), + server_schemas.RunCreate, + ), + ( + client_schemas.ExperimentCreate( + timestamp="2021-04-04T08:43:00+02:00", + name="Run on AWS", + description="AWS API for Code Carbon", + country_name="France", + country_iso_code="FRA", + region="france", + on_cloud=True, + cloud_provider="aws", + cloud_region="eu-west-1a", + project_id="8edb03e1-9a28-452a-9c93-a3b6560136d7", + ), + server_schemas.ExperimentCreate, + ), + ( + client_schemas.ProjectCreate( + name="API Code Carbon", + description="API for Code Carbon", + organization_id="8edb03e1-9a28-452a-9c93-a3b6560136d7", + ), + server_schemas.ProjectCreate, + ), + ( + client_schemas.OrganizationCreate( + name="Code Carbon", + description="Save the world, one run at a time.", + ), + server_schemas.OrganizationCreate, + ), + ], +) +def test_client_create_payloads_validate_against_server_schemas( + client_payload, server_schema +): + server_schema.model_validate(dataclasses.asdict(client_payload)) diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py index 34067c71c..9349b9dc1 100644 --- a/codecarbon/core/api_client.py +++ b/codecarbon/core/api_client.py @@ -227,6 +227,10 @@ def add_emission(self, carbon_emission: dict): gpu_energy=carbon_emission["gpu_energy"], ram_energy=carbon_emission["ram_energy"], energy_consumed=carbon_emission["energy_consumed"], + cpu_utilization_percent=carbon_emission.get("cpu_utilization_percent"), + gpu_utilization_percent=carbon_emission.get("gpu_utilization_percent"), + ram_utilization_percent=carbon_emission.get("ram_utilization_percent"), + wue=carbon_emission.get("wue", 0), ) try: payload = dataclasses.asdict(emission) diff --git a/codecarbon/core/schemas.py b/codecarbon/core/schemas.py index 225deb6a5..84d1e9c77 100644 --- a/codecarbon/core/schemas.py +++ b/codecarbon/core/schemas.py @@ -22,6 +22,10 @@ class EmissionBase: gpu_energy: float ram_energy: float energy_consumed: float + cpu_utilization_percent: Optional[float] = None + gpu_utilization_percent: Optional[float] = None + ram_utilization_percent: Optional[float] = None + wue: Optional[float] = 0 class EmissionCreate(EmissionBase): diff --git a/tests/test_api_call.py b/tests/test_api_call.py index a4bb4cd7f..39822ece7 100644 --- a/tests/test_api_call.py +++ b/tests/test_api_call.py @@ -125,8 +125,17 @@ def test_call_api(self): latitude=33.5822, ram_total_size=83948.22, tracking_mode="Machine", + cpu_utilization_percent=12.5, + gpu_utilization_percent=34.5, + ram_utilization_percent=56.5, + wue=0.8, ) assert api.add_emission(dataclasses.asdict(carbon_emission)) + payload = m.last_request.json() + assert payload["cpu_utilization_percent"] == 12.5 + assert payload["gpu_utilization_percent"] == 34.5 + assert payload["ram_utilization_percent"] == 56.5 + assert payload["wue"] == 0.8 def test_check_auth_returns_none_on_error(self): with requests_mock.Mocker() as m: