From d8d19601e8bd29c6034ffb131989939e3dfd9a80 Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Sun, 29 Sep 2024 04:17:49 -0400 Subject: [PATCH 01/10] implemented abstract class for carbon intensity provider as well as ElectrictyMaps carbon intensity provider --- ...ctricity_maps_carbon_intensity_provider.py | 121 ++++++++++++++++++ zeus/carbon/__init__.py | 21 +++ zeus/carbon/carbon_intensity_provider.py | 19 +++ ...ctricity_maps_carbon_intensity_provider.py | 30 +++++ 4 files changed, 191 insertions(+) create mode 100644 tests/carbon/test_electricity_maps_carbon_intensity_provider.py create mode 100644 zeus/carbon/__init__.py create mode 100644 zeus/carbon/carbon_intensity_provider.py create mode 100644 zeus/carbon/electricity_maps_carbon_intensity_provider.py diff --git a/tests/carbon/test_electricity_maps_carbon_intensity_provider.py b/tests/carbon/test_electricity_maps_carbon_intensity_provider.py new file mode 100644 index 00000000..3d47af5a --- /dev/null +++ b/tests/carbon/test_electricity_maps_carbon_intensity_provider.py @@ -0,0 +1,121 @@ +import requests +import pytest +import json + +from unittest.mock import patch + +from zeus.carbon import get_ip_lat_long +from zeus.carbon.electricity_maps_carbon_intensity_provider import ( + ElectricityMapsCarbonIntensityProvider, +) + + +class MockHttpResponse: + def __init__(self, text): + self.text = text + self.json_obj = json.loads(text) + + def json(self): + return self.json_obj + + +@pytest.fixture +def mock_requests(): + IP_INFO_RESPONSE = """{ + "ip": "35.3.237.23", + "hostname": "0587459863.wireless.umich.net", + "city": "Ann Arbor", + "region": "Michigan", + "country": "US", + "loc": "42.2776,-83.7409", + "org": "AS36375 University of Michigan", + "postal": "48109", + "timezone": "America/Detroit", + "readme": "https://ipinfo.io/missingauth" + }""" + + NO_MEASUREMENT_RESPONSE = '{"error":"No recent data for zone "US-MIDW-MISO""}' + + ELECTRICITY_MAPS_RESPONSE_LIFECYCLE = ( + '{"zone":"US-MIDW-MISO","carbonIntensity":466,"datetime":"2024-09-24T03:00:00.000Z",' + '"updatedAt":"2024-09-24T02:47:02.408Z","createdAt":"2024-09-21T03:45:20.860Z",' + '"emissionFactorType":"lifecycle","isEstimated":true,"estimationMethod":"TIME_SLICER_AVERAGE"}' + ) + + ELECTRICITY_MAPS_RESPONSE_DIRECT = ( + '{"zone":"US-MIDW-MISO","carbonIntensity":506,"datetime":"2024-09-27T00:00:00.000Z",' + '"updatedAt":"2024-09-27T00:43:50.277Z","createdAt":"2024-09-24T00:46:38.741Z",' + '"emissionFactorType":"direct","isEstimated":true,"estimationMethod":"TIME_SLICER_AVERAGE"}' + ) + + real_requests_get = requests.get + + def mock_requests_get(url, *args, **kwargs): + if url == "http://ipinfo.io/json": + return MockHttpResponse(IP_INFO_RESPONSE) + elif ( + url + == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=True&emissionFactorType=direct" + ): + return MockHttpResponse(NO_MEASUREMENT_RESPONSE) + elif ( + url + == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=False&emissionFactorType=direct" + ): + return MockHttpResponse(ELECTRICITY_MAPS_RESPONSE_DIRECT) + elif ( + url + == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=False&emissionFactorType=lifecycle" + ): + return MockHttpResponse(ELECTRICITY_MAPS_RESPONSE_LIFECYCLE) + else: + return real_requests_get(url, *args, **kwargs) + + patch_request_get = patch("requests.get", side_effect=mock_requests_get) + + patch_request_get.start() + + yield + + patch_request_get.stop() + + +@pytest.fixture +def mock_exception_ip(): + def mock_requests_get(url): + raise ConnectionError + + patch_request_get = patch("requests.get", side_effect=mock_requests_get) + + patch_request_get.start() + + yield + + patch_request_get.stop() + + +def test_get_current_carbon_intensity(mock_requests): + latlong = get_ip_lat_long() + assert latlong == (42.2776, -83.7409) + provider = ElectricityMapsCarbonIntensityProvider(latlong) + assert ( + provider.get_current_carbon_intensity( + estimate=True, emission_factor_type="lifecycle" + ) + == 466 + ) + assert provider.get_current_carbon_intensity(estimate=True) == 506 + + +def test_get_current_carbon_intensity_no_response(mock_requests): + latlong = get_ip_lat_long() + assert latlong == (42.2776, -83.7409) + provider = ElectricityMapsCarbonIntensityProvider(latlong) + + with pytest.raises(Exception): + provider.get_current_carbon_intensity() + + +def test_get_lat_long_excpetion(mock_exception_ip): + with pytest.raises(ConnectionError): + get_ip_lat_long() diff --git a/zeus/carbon/__init__.py b/zeus/carbon/__init__.py new file mode 100644 index 00000000..dfb15f09 --- /dev/null +++ b/zeus/carbon/__init__.py @@ -0,0 +1,21 @@ +"""Carbon intensity providers used for carbon-aware optimizers.""" + +from zeus.carbon.electricity_maps_carbon_intensity_provider import ( + ElectricityMapsCarbonIntensityProvider, +) + +import requests + + +def get_ip_lat_long() -> tuple[float, float]: + """Retrieve the latitude and longitude of the current IP position.""" + try: + ip_url = "http://ipinfo.io/json" + resp = requests.get(ip_url) + loc = resp.json()["loc"] + lat, long = map(float, loc.split(",")) + print(f"Retrieve latitude and longitude: {lat}, {long}") + return lat, long + except Exception as e: + print(f"Failed to Retrieve Current IP's Latitude and Longitude: {e}") + raise (e) diff --git a/zeus/carbon/carbon_intensity_provider.py b/zeus/carbon/carbon_intensity_provider.py new file mode 100644 index 00000000..8c39207c --- /dev/null +++ b/zeus/carbon/carbon_intensity_provider.py @@ -0,0 +1,19 @@ +"""Abstract Carbon Intensity Provider Class.""" +import abc + + +class CarbonIntensityProvider(abc.ABC): + """Abstract class for implementing ways to fetch carbon intensity.""" + + def __init__(self, location: tuple[float, float]) -> None: + """Initializes carbon intensity provider location to the latitude and longitude of the input `location`. + + Location is a tuple of floats where latitude is the first float and longitude is the second float. + """ + self.lat = location[0] + self.long = location[1] + + @abc.abstractmethod + def get_current_carbon_intensity(self) -> float: + """Abstract method for fetching the current carbon intensity of the set location of the class.""" + pass diff --git a/zeus/carbon/electricity_maps_carbon_intensity_provider.py b/zeus/carbon/electricity_maps_carbon_intensity_provider.py new file mode 100644 index 00000000..8890117c --- /dev/null +++ b/zeus/carbon/electricity_maps_carbon_intensity_provider.py @@ -0,0 +1,30 @@ +"""Carbon Intensity Provider using ElectrictyMaps API.""" +from zeus.carbon.carbon_intensity_provider import CarbonIntensityProvider +import requests + + +class ElectricityMapsCarbonIntensityProvider(CarbonIntensityProvider): + """Carbon Intensity Provider with ElectricityMaps API.""" + + def get_current_carbon_intensity( + self, estimate: bool = False, emission_factor_type: str = "direct" + ) -> float: + """Fetches current carbon intensity of the location of the class. + + Args: + estimate: bool to toggle whether carbon intensity is estimated or not + emission_factor_type: emission factor to be measured (`direct` or `lifestyle`) + + !!! Note + In some locations, there is no recent carbon intensity data. `estimate` can be used to approximate the carbon intensity in such cases. + """ + try: + url = ( + f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}" + + f"&disableEstimations={not estimate}&emissionFactorType={emission_factor_type}" + ) + resp = requests.get(url) + return resp.json()["carbonIntensity"] + except Exception as e: + print(f"Failed to retrieve live carbon intensity data: {e}") + raise (e) From 066103017199396f974c32fd67ff10b217cf97b5 Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Sun, 29 Sep 2024 04:17:49 -0400 Subject: [PATCH 02/10] CarbonIntensityProvider and ElectricityMaps implementation --- ...ctricity_maps_carbon_intensity_provider.py | 104 ++++++++++++++++++ zeus/carbon/carbon.py | 95 ++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 tests/carbon/test_electricity_maps_carbon_intensity_provider.py create mode 100644 zeus/carbon/carbon.py diff --git a/tests/carbon/test_electricity_maps_carbon_intensity_provider.py b/tests/carbon/test_electricity_maps_carbon_intensity_provider.py new file mode 100644 index 00000000..4362e5c4 --- /dev/null +++ b/tests/carbon/test_electricity_maps_carbon_intensity_provider.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import requests +import pytest +import json + +from unittest.mock import patch + +from zeus.carbon.carbon import ( + ElectrictyMapsClient, + get_ip_lat_long, + CarbonIntensityNotFoundException, +) + + +class MockHttpResponse: + def __init__(self, text): + self.text = text + self.json_obj = json.loads(text) + + def json(self): + return self.json_obj + + +@pytest.fixture +def mock_requests(): + IP_INFO_RESPONSE = """{ + "ip": "35.3.237.23", + "hostname": "0587459863.wireless.umich.net", + "city": "Ann Arbor", + "region": "Michigan", + "country": "US", + "loc": "42.2776,-83.7409", + "org": "AS36375 University of Michigan", + "postal": "48109", + "timezone": "America/Detroit", + "readme": "https://ipinfo.io/missingauth" + }""" + + NO_MEASUREMENT_RESPONSE = '{"error":"No recent data for zone "US-MIDW-MISO""}' + + ELECTRICITY_MAPS_RESPONSE_LIFECYCLE = ( + '{"zone":"US-MIDW-MISO","carbonIntensity":466,"datetime":"2024-09-24T03:00:00.000Z",' + '"updatedAt":"2024-09-24T02:47:02.408Z","createdAt":"2024-09-21T03:45:20.860Z",' + '"emissionFactorType":"lifecycle","isEstimated":true,"estimationMethod":"TIME_SLICER_AVERAGE"}' + ) + + ELECTRICITY_MAPS_RESPONSE_DIRECT = ( + '{"zone":"US-MIDW-MISO","carbonIntensity":506,"datetime":"2024-09-27T00:00:00.000Z",' + '"updatedAt":"2024-09-27T00:43:50.277Z","createdAt":"2024-09-24T00:46:38.741Z",' + '"emissionFactorType":"direct","isEstimated":true,"estimationMethod":"TIME_SLICER_AVERAGE"}' + ) + + real_requests_get = requests.get + + def mock_requests_get(url, *args, **kwargs): + if url == "http://ipinfo.io/json": + return MockHttpResponse(IP_INFO_RESPONSE) + elif ( + url + == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=True&emissionFactorType=direct" + ): + return MockHttpResponse(NO_MEASUREMENT_RESPONSE) + elif ( + url + == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=False&emissionFactorType=direct" + ): + return MockHttpResponse(ELECTRICITY_MAPS_RESPONSE_DIRECT) + elif ( + url + == "https://api.electricitymap.org/v3/carbon-intensity/latest?lat=42.2776&lon=-83.7409&disableEstimations=False&emissionFactorType=lifecycle" + ): + return MockHttpResponse(ELECTRICITY_MAPS_RESPONSE_LIFECYCLE) + else: + return real_requests_get(url, *args, **kwargs) + + patch_request_get = patch("requests.get", side_effect=mock_requests_get) + + patch_request_get.start() + + yield + + patch_request_get.stop() + + +def test_get_current_carbon_intensity(mock_requests): + latlong = get_ip_lat_long() + assert latlong == (pytest.approx(42.2776), pytest.approx(-83.7409)) + provider = ElectrictyMapsClient( + latlong, estimate=True, emission_factor_type="lifecycle" + ) + assert provider.get_current_carbon_intensity() == 466 + + provider.emission_factor_type = "direct" + assert provider.get_current_carbon_intensity() == 506 + + +def test_get_current_carbon_intensity_no_response(mock_requests): + latlong = get_ip_lat_long() + assert latlong == (pytest.approx(42.2776), pytest.approx(-83.7409)) + provider = ElectrictyMapsClient(latlong) + + with pytest.raises(CarbonIntensityNotFoundException): + provider.get_current_carbon_intensity() diff --git a/zeus/carbon/carbon.py b/zeus/carbon/carbon.py new file mode 100644 index 00000000..091afe9b --- /dev/null +++ b/zeus/carbon/carbon.py @@ -0,0 +1,95 @@ +"""Carbon intensity providers used for carbon-aware optimizers.""" + +from __future__ import annotations + +import requests +import logging +import abc +import json + +logger = logging.getLogger(__name__) + + +def get_ip_lat_long() -> tuple[float, float]: + """Retrieve the latitude and longitude of the current IP position.""" + try: + ip_url = "http://ipinfo.io/json" + resp = requests.get(ip_url) + loc = resp.json()["loc"] + lat, long = map(float, loc.split(",")) + logger.info("Retrieved latitude and longitude: %s, %s", lat, long) + return lat, long + except requests.exceptions.RequestException as e: + logger.exception( + "Failed to retrieve current latitude and longitude of IP: %s", e + ) + raise + + +class CarbonIntensityNotFoundError(Exception): + """Exception when carbon intensity measurement could not be retrieved.""" + + def __init__(self, message: str) -> None: + """Initialize carbon not found exception.""" + super().__init__(message) + + +class CarbonIntensityProvider(abc.ABC): + """Abstract class for implementing ways to fetch carbon intensity.""" + + def __init__( + self, + location: tuple[float, float], + estimate: bool = False, + emission_factor_type: str = "direct", + ) -> None: + """Initializes carbon intensity provider location to the latitude and longitude of the input `location`. + + Args: + location: tuple of latitude and longitude (latitude, longitude) + estimate: bool to toggle whether carbon intensity is estimated or not + emission_factor_type: emission factor to be measured (`direct` or `lifestyle`) + """ + self.lat, self.long = location + self.estimate = estimate + self.emission_factor_type = emission_factor_type + + @abc.abstractmethod + def get_current_carbon_intensity(self) -> float: + """Abstract method for fetching the current carbon intensity of the set location of the class.""" + pass + + +class ElectrictyMapsClient(CarbonIntensityProvider): + """Carbon Intensity Provider with ElectricityMaps API. + + ElectricityMaps: https://www.electricitymaps.com/ + ElectricityMaps API: https://static.electricitymaps.com/api/docs/index.html + ElectricityMaps GitHub: https://github.com/electricitymaps/electricitymaps-contrib + """ + + def get_current_carbon_intensity(self) -> float: + """Fetches current carbon intensity of the location of the class. + + !!! Note + In some locations, there is no recent carbon intensity data. `self.estimate` can be used to approximate the carbon intensity in such cases. + """ + try: + url = ( + f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}" + + f"&disableEstimations={not self.estimate}&emissionFactorType={self.emission_factor_type}" + ) + resp = requests.get(url) + + return resp.json()["carbonIntensity"] + except json.decoder.JSONDecodeError as e: + # ElectricityMaps returns an invalid JSON response that cannot be decoded when no carbon intensity measurement found + raise CarbonIntensityNotFoundError( + f"Recent carbon intensity measurement not found at ({self.lat}, {self.long}) " + f"with estimate set to {self.estimate} and emission_factor_type set to {self.emission_factor_type}" + ) from e + except requests.exceptions.RequestException as e: + logger.exception( + "Failed to retrieve recent carbon intensnity measurement: %s", e + ) + raise From 1b10e7db5e8e5b8ff2f8c66189134f9e558a3e97 Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Mon, 30 Sep 2024 02:53:25 -0400 Subject: [PATCH 03/10] CarbonIntensityProvider and ElectricityMaps implementation --- ...ctricity_maps_carbon_intensity_provider.py | 8 ++--- zeus/carbon/__init__.py | 21 ------------- zeus/carbon/carbon.py | 4 +-- zeus/carbon/carbon_intensity_provider.py | 19 ------------ ...ctricity_maps_carbon_intensity_provider.py | 30 ------------------- 5 files changed, 6 insertions(+), 76 deletions(-) delete mode 100644 zeus/carbon/__init__.py delete mode 100644 zeus/carbon/carbon_intensity_provider.py delete mode 100644 zeus/carbon/electricity_maps_carbon_intensity_provider.py diff --git a/tests/carbon/test_electricity_maps_carbon_intensity_provider.py b/tests/carbon/test_electricity_maps_carbon_intensity_provider.py index 4362e5c4..46636e57 100644 --- a/tests/carbon/test_electricity_maps_carbon_intensity_provider.py +++ b/tests/carbon/test_electricity_maps_carbon_intensity_provider.py @@ -1,15 +1,15 @@ from __future__ import annotations -import requests -import pytest import json +import pytest +import requests from unittest.mock import patch from zeus.carbon.carbon import ( ElectrictyMapsClient, get_ip_lat_long, - CarbonIntensityNotFoundException, + CarbonIntensityNotFoundError, ) @@ -100,5 +100,5 @@ def test_get_current_carbon_intensity_no_response(mock_requests): assert latlong == (pytest.approx(42.2776), pytest.approx(-83.7409)) provider = ElectrictyMapsClient(latlong) - with pytest.raises(CarbonIntensityNotFoundException): + with pytest.raises(CarbonIntensityNotFoundError): provider.get_current_carbon_intensity() diff --git a/zeus/carbon/__init__.py b/zeus/carbon/__init__.py deleted file mode 100644 index dfb15f09..00000000 --- a/zeus/carbon/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Carbon intensity providers used for carbon-aware optimizers.""" - -from zeus.carbon.electricity_maps_carbon_intensity_provider import ( - ElectricityMapsCarbonIntensityProvider, -) - -import requests - - -def get_ip_lat_long() -> tuple[float, float]: - """Retrieve the latitude and longitude of the current IP position.""" - try: - ip_url = "http://ipinfo.io/json" - resp = requests.get(ip_url) - loc = resp.json()["loc"] - lat, long = map(float, loc.split(",")) - print(f"Retrieve latitude and longitude: {lat}, {long}") - return lat, long - except Exception as e: - print(f"Failed to Retrieve Current IP's Latitude and Longitude: {e}") - raise (e) diff --git a/zeus/carbon/carbon.py b/zeus/carbon/carbon.py index 091afe9b..fdac2223 100644 --- a/zeus/carbon/carbon.py +++ b/zeus/carbon/carbon.py @@ -2,10 +2,10 @@ from __future__ import annotations -import requests -import logging import abc import json +import logging +import requests logger = logging.getLogger(__name__) diff --git a/zeus/carbon/carbon_intensity_provider.py b/zeus/carbon/carbon_intensity_provider.py deleted file mode 100644 index 8c39207c..00000000 --- a/zeus/carbon/carbon_intensity_provider.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Abstract Carbon Intensity Provider Class.""" -import abc - - -class CarbonIntensityProvider(abc.ABC): - """Abstract class for implementing ways to fetch carbon intensity.""" - - def __init__(self, location: tuple[float, float]) -> None: - """Initializes carbon intensity provider location to the latitude and longitude of the input `location`. - - Location is a tuple of floats where latitude is the first float and longitude is the second float. - """ - self.lat = location[0] - self.long = location[1] - - @abc.abstractmethod - def get_current_carbon_intensity(self) -> float: - """Abstract method for fetching the current carbon intensity of the set location of the class.""" - pass diff --git a/zeus/carbon/electricity_maps_carbon_intensity_provider.py b/zeus/carbon/electricity_maps_carbon_intensity_provider.py deleted file mode 100644 index 8890117c..00000000 --- a/zeus/carbon/electricity_maps_carbon_intensity_provider.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Carbon Intensity Provider using ElectrictyMaps API.""" -from zeus.carbon.carbon_intensity_provider import CarbonIntensityProvider -import requests - - -class ElectricityMapsCarbonIntensityProvider(CarbonIntensityProvider): - """Carbon Intensity Provider with ElectricityMaps API.""" - - def get_current_carbon_intensity( - self, estimate: bool = False, emission_factor_type: str = "direct" - ) -> float: - """Fetches current carbon intensity of the location of the class. - - Args: - estimate: bool to toggle whether carbon intensity is estimated or not - emission_factor_type: emission factor to be measured (`direct` or `lifestyle`) - - !!! Note - In some locations, there is no recent carbon intensity data. `estimate` can be used to approximate the carbon intensity in such cases. - """ - try: - url = ( - f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}" - + f"&disableEstimations={not estimate}&emissionFactorType={emission_factor_type}" - ) - resp = requests.get(url) - return resp.json()["carbonIntensity"] - except Exception as e: - print(f"Failed to retrieve live carbon intensity data: {e}") - raise (e) From 28acd21a33ebad2015d935bf2f9536087567a837 Mon Sep 17 00:00:00 2001 From: Jae-Won Chung Date: Mon, 23 Sep 2024 03:15:18 -0400 Subject: [PATCH 04/10] [Docs] Update Perseus paper page --- docs/research_overview/perseus.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/research_overview/perseus.md b/docs/research_overview/perseus.md index aff3429c..dbdd6d27 100644 --- a/docs/research_overview/perseus.md +++ b/docs/research_overview/perseus.md @@ -6,9 +6,9 @@ description: Reducing Energy Bloat in Large Model Training

Perseus:
Reducing Energy Bloat in Large Model Training

-SOSP '24 (To appear) +SOSP '24 -[**Preprint**](https://arxiv.org/abs/2312.06902) +[**Paper**](https://arxiv.org/abs/2312.06902)
From 66648b5dc1f746aec0fad68bae882a91912046f1 Mon Sep 17 00:00:00 2001 From: Jae-Won Chung Date: Mon, 30 Sep 2024 16:20:18 -0400 Subject: [PATCH 05/10] [Docs] Add `BUILD_SOCIAL_CARD` env, skip social card build by default (#130) --- .github/workflows/check_homepage_build.yaml | 2 ++ .github/workflows/deploy_homepage.yaml | 2 ++ mkdocs.yml | 1 + scripts/preview_docs.sh | 4 ++++ 4 files changed, 9 insertions(+) diff --git a/.github/workflows/check_homepage_build.yaml b/.github/workflows/check_homepage_build.yaml index 2adcef31..30204a70 100644 --- a/.github/workflows/check_homepage_build.yaml +++ b/.github/workflows/check_homepage_build.yaml @@ -38,3 +38,5 @@ jobs: run: pip install '.[docs]' - name: Build homepage run: mkdocs build --verbose --strict + env: + BUILD_SOCIAL_CARD: true diff --git a/.github/workflows/deploy_homepage.yaml b/.github/workflows/deploy_homepage.yaml index e39ea516..c6d409ab 100644 --- a/.github/workflows/deploy_homepage.yaml +++ b/.github/workflows/deploy_homepage.yaml @@ -37,3 +37,5 @@ jobs: run: pip install '.[docs]' - name: Build homepage run: mkdocs gh-deploy --force + env: + BUILD_SOCIAL_CARD: true diff --git a/mkdocs.yml b/mkdocs.yml index ca0b25d4..72da629c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,7 @@ plugins: - search - autorefs - social: + enabled: !ENV [BUILD_SOCIAL_CARD, false] cards_dir: assets/img/social cards_layout_options: background_color: "#f7e96d" diff --git a/scripts/preview_docs.sh b/scripts/preview_docs.sh index 74fb7f5a..24c02faf 100644 --- a/scripts/preview_docs.sh +++ b/scripts/preview_docs.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +# This script builds a local version of the documentation and makes it available at localhost:7777. +# By default it does not build social preview cards. If you want to debug social cards, +# set the environment variable `BUILD_SOCIAL_CARD=true` to this script. + pip list | grep mkdocs-material 2>&1 >/dev/null if [[ ! $? -eq 0 ]]; then From 600bf08928eba1858d23a46e38f5dee8eb767669 Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Mon, 30 Sep 2024 18:16:55 -0400 Subject: [PATCH 06/10] CarbonIntensityProvider and ElectricityMaps implementation --- ...n_intensity_provider.py => test_carbon.py} | 4 +- zeus/{carbon => }/carbon.py | 50 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) rename tests/{carbon/test_electricity_maps_carbon_intensity_provider.py => test_carbon.py} (96%) rename zeus/{carbon => }/carbon.py (83%) diff --git a/tests/carbon/test_electricity_maps_carbon_intensity_provider.py b/tests/test_carbon.py similarity index 96% rename from tests/carbon/test_electricity_maps_carbon_intensity_provider.py rename to tests/test_carbon.py index 46636e57..adfad10c 100644 --- a/tests/carbon/test_electricity_maps_carbon_intensity_provider.py +++ b/tests/test_carbon.py @@ -6,7 +6,7 @@ from unittest.mock import patch -from zeus.carbon.carbon import ( +from zeus.carbon import ( ElectrictyMapsClient, get_ip_lat_long, CarbonIntensityNotFoundError, @@ -37,7 +37,7 @@ def mock_requests(): "readme": "https://ipinfo.io/missingauth" }""" - NO_MEASUREMENT_RESPONSE = '{"error":"No recent data for zone "US-MIDW-MISO""}' + NO_MEASUREMENT_RESPONSE = r'{"error":"No recent data for zone \"US-MIDW-MISO\""}' ELECTRICITY_MAPS_RESPONSE_LIFECYCLE = ( '{"zone":"US-MIDW-MISO","carbonIntensity":466,"datetime":"2024-09-24T03:00:00.000Z",' diff --git a/zeus/carbon/carbon.py b/zeus/carbon.py similarity index 83% rename from zeus/carbon/carbon.py rename to zeus/carbon.py index fdac2223..102da789 100644 --- a/zeus/carbon/carbon.py +++ b/zeus/carbon.py @@ -3,11 +3,13 @@ from __future__ import annotations import abc -import json -import logging import requests +from typing import Literal -logger = logging.getLogger(__name__) +from zeus.exception import ZeusBaseError +from zeus.utils.logging import get_logger + +logger = get_logger(__name__) def get_ip_lat_long() -> tuple[float, float]: @@ -26,7 +28,7 @@ def get_ip_lat_long() -> tuple[float, float]: raise -class CarbonIntensityNotFoundError(Exception): +class CarbonIntensityNotFoundError(ZeusBaseError): """Exception when carbon intensity measurement could not be retrieved.""" def __init__(self, message: str) -> None: @@ -37,13 +39,27 @@ def __init__(self, message: str) -> None: class CarbonIntensityProvider(abc.ABC): """Abstract class for implementing ways to fetch carbon intensity.""" + @abc.abstractmethod + def get_current_carbon_intensity(self) -> float: + """Abstract method for fetching the current carbon intensity of the set location of the class.""" + pass + + +class ElectrictyMapsClient(CarbonIntensityProvider): + """Carbon Intensity Provider with ElectricityMaps API. + + ElectricityMaps: https://www.electricitymaps.com/ + ElectricityMaps API: https://static.electricitymaps.com/api/docs/index.html + ElectricityMaps GitHub: https://github.com/electricitymaps/electricitymaps-contrib + """ + def __init__( self, location: tuple[float, float], estimate: bool = False, - emission_factor_type: str = "direct", + emission_factor_type: Literal["direct", "lifecycle"] = "direct", ) -> None: - """Initializes carbon intensity provider location to the latitude and longitude of the input `location`. + """Iniitializes ElectricityMaps Carbon Provider. Args: location: tuple of latitude and longitude (latitude, longitude) @@ -54,20 +70,6 @@ def __init__( self.estimate = estimate self.emission_factor_type = emission_factor_type - @abc.abstractmethod - def get_current_carbon_intensity(self) -> float: - """Abstract method for fetching the current carbon intensity of the set location of the class.""" - pass - - -class ElectrictyMapsClient(CarbonIntensityProvider): - """Carbon Intensity Provider with ElectricityMaps API. - - ElectricityMaps: https://www.electricitymaps.com/ - ElectricityMaps API: https://static.electricitymaps.com/api/docs/index.html - ElectricityMaps GitHub: https://github.com/electricitymaps/electricitymaps-contrib - """ - def get_current_carbon_intensity(self) -> float: """Fetches current carbon intensity of the location of the class. @@ -82,11 +84,11 @@ def get_current_carbon_intensity(self) -> float: resp = requests.get(url) return resp.json()["carbonIntensity"] - except json.decoder.JSONDecodeError as e: - # ElectricityMaps returns an invalid JSON response that cannot be decoded when no carbon intensity measurement found + except KeyError as e: + # Raise exception when carbonIntensity does not exist in response raise CarbonIntensityNotFoundError( - f"Recent carbon intensity measurement not found at ({self.lat}, {self.long}) " - f"with estimate set to {self.estimate} and emission_factor_type set to {self.emission_factor_type}" + f"Recent carbon intensity measurement not found at `({self.lat}, {self.long})` " + f"with estimate set to `{self.estimate}` and emission_factor_type set to `{self.emission_factor_type}`" ) from e except requests.exceptions.RequestException as e: logger.exception( From 85f85eaf11cc6238049bc5e276a0633c8c2898ae Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Mon, 30 Sep 2024 22:04:23 -0400 Subject: [PATCH 07/10] CarbonIntensityProvider and ElectricityMaps implementation --- zeus/carbon.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/zeus/carbon.py b/zeus/carbon.py index 102da789..000d9bc9 100644 --- a/zeus/carbon.py +++ b/zeus/carbon.py @@ -48,9 +48,13 @@ def get_current_carbon_intensity(self) -> float: class ElectrictyMapsClient(CarbonIntensityProvider): """Carbon Intensity Provider with ElectricityMaps API. - ElectricityMaps: https://www.electricitymaps.com/ - ElectricityMaps API: https://static.electricitymaps.com/api/docs/index.html - ElectricityMaps GitHub: https://github.com/electricitymaps/electricitymaps-contrib + Reference: + + 1. [ElectricityMaps](https://www.electricitymaps.com/) + + 2. [ElectricityMaps API](https://static.electricitymaps.com/api/docs/index.html) + + 3. [ElectricityMaps GitHub](https://github.com/electricitymaps/electricitymaps-contrib) """ def __init__( From 5066f68e2d8c792045d69c10b7778773da2c13c7 Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Fri, 4 Oct 2024 15:27:42 -0400 Subject: [PATCH 08/10] Hi Mom --- tests/test_carbon.py | 4 ++-- zeus/carbon.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_carbon.py b/tests/test_carbon.py index adfad10c..b5d0bda7 100644 --- a/tests/test_carbon.py +++ b/tests/test_carbon.py @@ -9,7 +9,7 @@ from zeus.carbon import ( ElectrictyMapsClient, get_ip_lat_long, - CarbonIntensityNotFoundError, + ZeusCarbonIntensityNotFoundError, ) @@ -100,5 +100,5 @@ def test_get_current_carbon_intensity_no_response(mock_requests): assert latlong == (pytest.approx(42.2776), pytest.approx(-83.7409)) provider = ElectrictyMapsClient(latlong) - with pytest.raises(CarbonIntensityNotFoundError): + with pytest.raises(ZeusCarbonIntensityNotFoundError): provider.get_current_carbon_intensity() diff --git a/zeus/carbon.py b/zeus/carbon.py index 000d9bc9..7b5cbeff 100644 --- a/zeus/carbon.py +++ b/zeus/carbon.py @@ -28,7 +28,7 @@ def get_ip_lat_long() -> tuple[float, float]: raise -class CarbonIntensityNotFoundError(ZeusBaseError): +class ZeusCarbonIntensityNotFoundError(ZeusBaseError): """Exception when carbon intensity measurement could not be retrieved.""" def __init__(self, message: str) -> None: @@ -51,9 +51,7 @@ class ElectrictyMapsClient(CarbonIntensityProvider): Reference: 1. [ElectricityMaps](https://www.electricitymaps.com/) - 2. [ElectricityMaps API](https://static.electricitymaps.com/api/docs/index.html) - 3. [ElectricityMaps GitHub](https://github.com/electricitymaps/electricitymaps-contrib) """ @@ -80,6 +78,7 @@ def get_current_carbon_intensity(self) -> float: !!! Note In some locations, there is no recent carbon intensity data. `self.estimate` can be used to approximate the carbon intensity in such cases. """ + resp = None try: url = ( f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}" @@ -90,9 +89,10 @@ def get_current_carbon_intensity(self) -> float: return resp.json()["carbonIntensity"] except KeyError as e: # Raise exception when carbonIntensity does not exist in response - raise CarbonIntensityNotFoundError( + raise ZeusCarbonIntensityNotFoundError( f"Recent carbon intensity measurement not found at `({self.lat}, {self.long})` " - f"with estimate set to `{self.estimate}` and emission_factor_type set to `{self.emission_factor_type}`" + f"with estimate set to `{self.estimate}` and emission_factor_type set to `{self.emission_factor_type}`\n" + f"JSON Response: {resp.text}" ) from e except requests.exceptions.RequestException as e: logger.exception( From 1f901c6b41cdb942fd1b386da41fdb9ae9032aeb Mon Sep 17 00:00:00 2001 From: Daniel Hou Date: Fri, 4 Oct 2024 15:30:33 -0400 Subject: [PATCH 09/10] Hi Mom --- zeus/carbon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zeus/carbon.py b/zeus/carbon.py index 7b5cbeff..dc5c6966 100644 --- a/zeus/carbon.py +++ b/zeus/carbon.py @@ -78,13 +78,14 @@ def get_current_carbon_intensity(self) -> float: !!! Note In some locations, there is no recent carbon intensity data. `self.estimate` can be used to approximate the carbon intensity in such cases. """ - resp = None + resp_str = None try: url = ( f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}" + f"&disableEstimations={not self.estimate}&emissionFactorType={self.emission_factor_type}" ) resp = requests.get(url) + resp_str = resp.text return resp.json()["carbonIntensity"] except KeyError as e: @@ -92,7 +93,7 @@ def get_current_carbon_intensity(self) -> float: raise ZeusCarbonIntensityNotFoundError( f"Recent carbon intensity measurement not found at `({self.lat}, {self.long})` " f"with estimate set to `{self.estimate}` and emission_factor_type set to `{self.emission_factor_type}`\n" - f"JSON Response: {resp.text}" + f"JSON Response: {resp_str}" ) from e except requests.exceptions.RequestException as e: logger.exception( From cee2bd207a2c472142cd3367ab7363628b72ebc4 Mon Sep 17 00:00:00 2001 From: Jae-Won Chung Date: Fri, 4 Oct 2024 22:27:13 -0400 Subject: [PATCH 10/10] Hi dad --- zeus/carbon.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/zeus/carbon.py b/zeus/carbon.py index dc5c6966..6098f047 100644 --- a/zeus/carbon.py +++ b/zeus/carbon.py @@ -78,25 +78,24 @@ def get_current_carbon_intensity(self) -> float: !!! Note In some locations, there is no recent carbon intensity data. `self.estimate` can be used to approximate the carbon intensity in such cases. """ - resp_str = None try: url = ( f"https://api.electricitymap.org/v3/carbon-intensity/latest?lat={self.lat}&lon={self.long}" + f"&disableEstimations={not self.estimate}&emissionFactorType={self.emission_factor_type}" ) resp = requests.get(url) - resp_str = resp.text + except requests.exceptions.RequestException as e: + logger.exception( + "Failed to retrieve recent carbon intensnity measurement: %s", e + ) + raise + try: return resp.json()["carbonIntensity"] except KeyError as e: # Raise exception when carbonIntensity does not exist in response raise ZeusCarbonIntensityNotFoundError( f"Recent carbon intensity measurement not found at `({self.lat}, {self.long})` " f"with estimate set to `{self.estimate}` and emission_factor_type set to `{self.emission_factor_type}`\n" - f"JSON Response: {resp_str}" + f"JSON Response: {resp.text}" ) from e - except requests.exceptions.RequestException as e: - logger.exception( - "Failed to retrieve recent carbon intensnity measurement: %s", e - ) - raise