From e4d14f27ef062c979f555cc58454f216d5b9c0c5 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 19 Jan 2025 18:14:37 +0000 Subject: [PATCH 01/14] feat: added config and tests. Todo: update docs --- ecologits/_ecologits.py | 51 +++++++++++--- pyproject.toml | 1 + tests/config/__init__.py | 0 tests/config/test_config.py | 88 ++++++++++++++++++++++++ tests/config/toml_with_no_ecologits.toml | 2 + 5 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 tests/config/__init__.py create mode 100644 tests/config/test_config.py create mode 100644 tests/config/toml_with_no_ecologits.toml diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index a264a35e..7b699d2b 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -1,8 +1,9 @@ import importlib.metadata import importlib.util +import os from dataclasses import dataclass, field -from typing import Optional, Union +import toml from packaging.version import Version from ecologits.exceptions import EcoLogitsError @@ -83,6 +84,11 @@ def init_litellm_instrumentor() -> None: "litellm": init_litellm_instrumentor } +@dataclass +class _Config: + electricity_mix_zone: str = field(default="WOR") + providers: list[str] = field(default_factory=list) + class EcoLogits: """ @@ -113,16 +119,21 @@ class EcoLogits: ``` """ - @dataclass - class _Config: - electricity_mix_zone: str = field(default="WOR") - providers: list[str] = field(default_factory=list) + config= _Config() - config = _Config() + @staticmethod + def _read_ecologits_config(config_path: str)-> dict[str, str]|None: + + with open(config_path, 'r') as config_file: + config = toml.load(config_file).get("ecologits", None) + if config is None: + logger.warning("The provided file did not contain the ecologits key, will fall back on default configuration") + return config @staticmethod def init( - providers: Optional[Union[str, list[str]]] = None, + config_path: str| None = None, + providers: str | list[str]|None = None, electricity_mix_zone: str = "WOR", ) -> None: """ @@ -132,15 +143,33 @@ def init( providers: list of providers to initialize (all providers by default). electricity_mix_zone: ISO 3166-1 alpha-3 code of the electricity mix zone (WOR by default). """ + if config_path is None and providers is None and electricity_mix_zone is None and os.path.isfile("ecologits.toml"): + config_path = "ecologits.toml" + + if config_path is not None and (providers is not None or electricity_mix_zone is not None): + logger.warning("Both config path and parameters provided, configuration will be prioritized") + + if providers is None: + providers=list(_INSTRUMENTS.keys()) + + if config_path: + try: + user_config: dict[str, str]|None = EcoLogits._read_ecologits_config(config_path) + logger.info("Ecologits configuration found in file and loaded") + except FileNotFoundError: + logger.warning("Provided file does not exist, will fall back on default values") + user_config = None + + if user_config is not None: + providers = user_config.get("providers", providers) + electricity_mix_zone = user_config.get("electricity_mix_zone", electricity_mix_zone) + if isinstance(providers, str): providers = [providers] - if providers is None: - providers = list(_INSTRUMENTS.keys()) init_instruments(providers) - EcoLogits.config.electricity_mix_zone = electricity_mix_zone - EcoLogits.config.providers += providers + EcoLogits.config=_Config(electricity_mix_zone=electricity_mix_zone, providers=providers) EcoLogits.config.providers = list(set(EcoLogits.config.providers)) diff --git a/pyproject.toml b/pyproject.toml index 89b2269d..aa1e7832 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ minijinja = { version = "^1.0.16", optional = true } google-generativeai = { version = "^0.8.3", optional = true } litellm = {version="^1.41.23", optional = true} rapidfuzz = {version = "^3.9.6", optional = true} +toml = "0.10.2" [tool.poetry.extras] openai = ["openai"] diff --git a/tests/config/__init__.py b/tests/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/config/test_config.py b/tests/config/test_config.py new file mode 100644 index 00000000..b87f6f04 --- /dev/null +++ b/tests/config/test_config.py @@ -0,0 +1,88 @@ + +import logging +from unittest.mock import Mock, patch + +from ecologits._ecologits import _INSTRUMENTS, EcoLogits + +default_path = "this_is_patched.toml" +default_providers = providers=list(set(_INSTRUMENTS.keys())) +default_electricity_mix = "WOR" + +user_electricity_mix = "FR" +user_providers_list = list(set(["openai", "mistral"])) +user_single_provider = ["openai"] + + +@patch("ecologits._ecologits.init_instruments", Mock) +class TestEcoLogitsConfig: + + @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix, "providers":user_providers_list}) + def test_working_config_provider_list(self, toml): + EcoLogits.init(default_path) + + assert EcoLogits.config.providers == user_providers_list + assert EcoLogits.config.electricity_mix_zone == user_electricity_mix + + @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix, "providers":user_single_provider}) + def test_working_config_single_provider(self, toml): + EcoLogits.init(default_path) + + assert EcoLogits.config.providers == user_single_provider + assert EcoLogits.config.electricity_mix_zone == user_electricity_mix + + def test_non_existing_file(self, caplog): + with caplog.at_level(logging.WARNING): + EcoLogits.init(default_path) + + assert EcoLogits.config. providers == default_providers + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + assert "Provided file does not exist, will fall back on default values" in caplog.text + + @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix}) + def test_only_elec_mix_provided(self, patch): + + EcoLogits.init(default_path) + + assert EcoLogits.config. providers == default_providers + assert EcoLogits.config.electricity_mix_zone == user_electricity_mix + + @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) + def test_only_provider_in_config(self, patch): + EcoLogits.init(default_path) + + assert EcoLogits.config. providers == user_providers_list + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + + def test_no_ecologits_key_in_toml(self, caplog): + with caplog.at_level(logging.INFO): + EcoLogits.init("./tests/config/toml_with_no_ecologits.toml") + + assert EcoLogits.config.providers == default_providers + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + assert "The provided file did not contain the ecologits key, will fall back on default configuration" in caplog.text + + def test_init_parameters_both_provided(self): + EcoLogits.init(providers = user_providers_list, electricity_mix_zone=user_electricity_mix) + + assert EcoLogits.config.providers == user_providers_list + assert EcoLogits.config.electricity_mix_zone == user_electricity_mix + + def test_init_parameters_elec_only_provided(self): + EcoLogits.init(electricity_mix_zone=user_electricity_mix) + + assert EcoLogits.config.providers == default_providers + assert EcoLogits.config.electricity_mix_zone == user_electricity_mix + + def test_init_parameters_providers_only_provided(self): + EcoLogits.init(providers=user_providers_list) + + assert EcoLogits.config.providers == user_providers_list + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + + @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) + def test_init_parameters_and_config_provided(self, patch, caplog): + EcoLogits.init(config_path=default_path, providers=["anthropic"]) + + assert EcoLogits.config.providers == user_providers_list + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + assert "Both config path and parameters provided, configuration will be prioritized" in caplog.text \ No newline at end of file diff --git a/tests/config/toml_with_no_ecologits.toml b/tests/config/toml_with_no_ecologits.toml new file mode 100644 index 00000000..cb7e070f --- /dev/null +++ b/tests/config/toml_with_no_ecologits.toml @@ -0,0 +1,2 @@ +[other_tool] +other_key = "othervalue" \ No newline at end of file From a48e3c4e61b3096fe61fa162e1e2277515f78f52 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:45:23 +0000 Subject: [PATCH 02/14] feat: added docs and updated conf handling --- docs/index.md | 96 ++++++++++++++++++++++++++++++------- docs/tutorial/index.md | 28 ++++++++++- ecologits/_ecologits.py | 22 +++++---- tests/config/test_config.py | 18 +++---- 4 files changed, 128 insertions(+), 36 deletions(-) diff --git a/docs/index.md b/docs/index.md index 6ccaed89..8a37f5b9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,26 +63,88 @@ For detailed instructions on each provider, refer to the complete list of [suppo Below is a simple example demonstrating how to use the GPT-3.5-Turbo model from OpenAI with EcoLogits to track environmental impacts. -```python -from ecologits import EcoLogits -from openai import OpenAI -# Initialize EcoLogits -EcoLogits.init() - -client = OpenAI(api_key="") - -response = client.chat.completions.create( - model="gpt-3.5-turbo", - messages=[ - {"role": "user", "content": "Tell me a funny joke!"} +=== "Default init with python" + ```python + from ecologits import EcoLogits + from openai import OpenAI + + # Initialize EcoLogits + EcoLogits.init() + + client = OpenAI(api_key="") + + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "user", "content": "Tell me a funny joke!"} + ] + ) + + # Get estimated environmental impacts of the inference + print(f"Energy consumption: {response.impacts.energy.value} kWh") + print(f"GHG emissions: {response.impacts.gwp.value} kgCO2eq") + ``` + +=== "Parametrized init" + + ```python + from ecologits import EcoLogits + from openai import OpenAI + + # Initialize EcoLogits + EcoLogits.init(providers=["openai", "mistral"], electricity_mix_zone="WOR") + + client = OpenAI(api_key="") + + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "user", "content": "Tell me a funny joke!"} + ] + ) + + # Get estimated environmental impacts of the inference + print(f"Energy consumption: {response.impacts.energy.value} kWh") + print(f"GHG emissions: {response.impacts.gwp.value} kgCO2eq") + ``` + +You can also provide the ecologits configuration through a toml file. + +=== "main.py" + ```python + from ecologits import EcoLogits + from openai import OpenAI + + # Initialize EcoLogits + EcoLogits.init(config_path="pyproject.toml") + + client = OpenAI(api_key="") + + response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[ + {"role": "user", "content": "Tell me a funny joke!"} + ] + ) + + # Get estimated environmental impacts of the inference + print(f"Energy consumption: {response.impacts.energy.value} kWh") + print(f"GHG emissions: {response.impacts.gwp.value} kgCO2eq") + ``` + +=== "pyproject.toml" + ```toml + [ecologits] + region="FRA" + providers=[ + "openai" ] -) + ``` -# Get estimated environmental impacts of the inference -print(f"Energy consumption: {response.impacts.energy.value} kWh") -print(f"GHG emissions: {response.impacts.gwp.value} kgCO2eq") -``` +!!! info Internal priorizations + - If no init parameters are provided, EcoLogits will check for a pyproject.toml file with an ecologits config an rely on it to initialize. + - If both a toml file and init parameter are provided, the parameters will prevail. Environmental impacts are quantified based on four criteria and across two phases: diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index d8b0cbf2..813e7052 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -51,9 +51,10 @@ To use EcoLogits in your projects, you will need to initialize the client tracer from ecologits import EcoLogits # Default initialization method -EcoLogits.init() +EcoLogits.init() # (1)! ``` +1. If you have a pyproject.toml containing an EcoLogits configuration, it will now be loaded by an empty init. ### Configure providers @@ -90,3 +91,28 @@ from ecologits import EcoLogits # Select the electricity mix of France EcoLogits.init(electricity_mix_zone="FRA") ``` + +### Configure both through a toml configuration file + +You can also set the providers and electricity mix zone through a .toml file (for example your project's .toml). + +```python title="Load an EcoLogits configuration file " +from ecologits import EcoLogits + +# Initialize EcoLogits with a config file +EcoLogits.init(config_path="pyproject.toml") +EcoLogits.init(config_path="ecologits.toml") + +``` + +To do so, just provide the path to your file (or provide nothing if it is a `pyproject.toml` located at the root of your project). +The expected formatting of the EcoLogits configuration is as follows. + +```toml title="pyproject.toml" +[ecologits] +region="FRA" +providers=[ + "openai", + "mistral" +] +``` diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index 7b699d2b..efbfebd0 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -124,7 +124,7 @@ class EcoLogits: @staticmethod def _read_ecologits_config(config_path: str)-> dict[str, str]|None: - with open(config_path, 'r') as config_file: + with open(config_path) as config_file: config = toml.load(config_file).get("ecologits", None) if config is None: logger.warning("The provided file did not contain the ecologits key, will fall back on default configuration") @@ -134,7 +134,7 @@ def _read_ecologits_config(config_path: str)-> dict[str, str]|None: def init( config_path: str| None = None, providers: str | list[str]|None = None, - electricity_mix_zone: str = "WOR", + electricity_mix_zone: str|None = None, ) -> None: """ Initialization static method. Will attempt to initialize all providers by default. @@ -143,14 +143,14 @@ def init( providers: list of providers to initialize (all providers by default). electricity_mix_zone: ISO 3166-1 alpha-3 code of the electricity mix zone (WOR by default). """ - if config_path is None and providers is None and electricity_mix_zone is None and os.path.isfile("ecologits.toml"): - config_path = "ecologits.toml" + default_providers = list(set(_INSTRUMENTS.keys())) + default_electricity_mix_zone = "WOR" if config_path is not None and (providers is not None or electricity_mix_zone is not None): - logger.warning("Both config path and parameters provided, configuration will be prioritized") + logger.warning("Both config path and init arguments provided, init arguments will be prioritized") - if providers is None: - providers=list(_INSTRUMENTS.keys()) + if config_path is None and providers is None and electricity_mix_zone is None and os.path.isfile("pyproject.toml"): + config_path = "pyproject.toml" if config_path: try: @@ -161,11 +161,15 @@ def init( user_config = None if user_config is not None: - providers = user_config.get("providers", providers) - electricity_mix_zone = user_config.get("electricity_mix_zone", electricity_mix_zone) + providers = user_config.get("providers", default_providers) if providers is None else providers + electricity_mix_zone = user_config.get("electricity_mix_zone", electricity_mix_zone) if electricity_mix_zone is None else electricity_mix_zone if isinstance(providers, str): providers = [providers] + elif providers is None: + providers = default_providers + if electricity_mix_zone is None: + electricity_mix_zone = default_electricity_mix_zone init_instruments(providers) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index b87f6f04..a5d31817 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -8,7 +8,7 @@ default_providers = providers=list(set(_INSTRUMENTS.keys())) default_electricity_mix = "WOR" -user_electricity_mix = "FR" +user_electricity_mix = "FRA" user_providers_list = list(set(["openai", "mistral"])) user_single_provider = ["openai"] @@ -18,21 +18,21 @@ class TestEcoLogitsConfig: @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix, "providers":user_providers_list}) def test_working_config_provider_list(self, toml): - EcoLogits.init(default_path) + EcoLogits.init(config_path=default_path) assert EcoLogits.config.providers == user_providers_list assert EcoLogits.config.electricity_mix_zone == user_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix, "providers":user_single_provider}) def test_working_config_single_provider(self, toml): - EcoLogits.init(default_path) + EcoLogits.init(config_path=default_path) assert EcoLogits.config.providers == user_single_provider assert EcoLogits.config.electricity_mix_zone == user_electricity_mix def test_non_existing_file(self, caplog): with caplog.at_level(logging.WARNING): - EcoLogits.init(default_path) + EcoLogits.init(config_path=default_path) assert EcoLogits.config. providers == default_providers assert EcoLogits.config.electricity_mix_zone == default_electricity_mix @@ -41,21 +41,21 @@ def test_non_existing_file(self, caplog): @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix}) def test_only_elec_mix_provided(self, patch): - EcoLogits.init(default_path) + EcoLogits.init(config_path=default_path) assert EcoLogits.config. providers == default_providers assert EcoLogits.config.electricity_mix_zone == user_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) def test_only_provider_in_config(self, patch): - EcoLogits.init(default_path) + EcoLogits.init(config_path=default_path) assert EcoLogits.config. providers == user_providers_list assert EcoLogits.config.electricity_mix_zone == default_electricity_mix def test_no_ecologits_key_in_toml(self, caplog): with caplog.at_level(logging.INFO): - EcoLogits.init("./tests/config/toml_with_no_ecologits.toml") + EcoLogits.init(config_path="./tests/config/toml_with_no_ecologits.toml") assert EcoLogits.config.providers == default_providers assert EcoLogits.config.electricity_mix_zone == default_electricity_mix @@ -83,6 +83,6 @@ def test_init_parameters_providers_only_provided(self): def test_init_parameters_and_config_provided(self, patch, caplog): EcoLogits.init(config_path=default_path, providers=["anthropic"]) - assert EcoLogits.config.providers == user_providers_list + assert EcoLogits.config.providers == ["anthropic"] assert EcoLogits.config.electricity_mix_zone == default_electricity_mix - assert "Both config path and parameters provided, configuration will be prioritized" in caplog.text \ No newline at end of file + assert "Both config path and init arguments provided, init arguments will be prioritized" in caplog.text \ No newline at end of file From 310e59bc549795bbf56232e35dc689d33a8993d7 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:52:49 +0000 Subject: [PATCH 03/14] chore: better with precommit --- ecologits/_ecologits.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index efbfebd0..5ec8ff85 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass, field -import toml +import toml # type: ignore from packaging.version import Version from ecologits.exceptions import EcoLogitsError @@ -123,11 +123,11 @@ class EcoLogits: @staticmethod def _read_ecologits_config(config_path: str)-> dict[str, str]|None: - + with open(config_path) as config_file: config = toml.load(config_file).get("ecologits", None) if config is None: - logger.warning("The provided file did not contain the ecologits key, will fall back on default configuration") + logger.warning("Provided file did not contain the ecologits key. Falling back on default configuration") return config @staticmethod @@ -149,7 +149,11 @@ def init( if config_path is not None and (providers is not None or electricity_mix_zone is not None): logger.warning("Both config path and init arguments provided, init arguments will be prioritized") - if config_path is None and providers is None and electricity_mix_zone is None and os.path.isfile("pyproject.toml"): + if (config_path is None + and providers is None + and electricity_mix_zone is None + and os.path.isfile("pyproject.toml")): + config_path = "pyproject.toml" if config_path: @@ -162,7 +166,9 @@ def init( if user_config is not None: providers = user_config.get("providers", default_providers) if providers is None else providers - electricity_mix_zone = user_config.get("electricity_mix_zone", electricity_mix_zone) if electricity_mix_zone is None else electricity_mix_zone + electricity_mix_zone = (user_config.get("electricity_mix_zone", electricity_mix_zone) + if electricity_mix_zone is None + else electricity_mix_zone) if isinstance(providers, str): providers = [providers] From eecb5b10586bb774943c15898e125b50052d076c Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:09:14 +0000 Subject: [PATCH 04/14] feat: fix mypy --- ecologits/_ecologits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index 5ec8ff85..e5368b70 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass, field -import toml # type: ignore +import toml # type: ignore [import-untyped] from packaging.version import Version from ecologits.exceptions import EcoLogitsError From 05e0a792b3424bf33ae2bf3219d928cc6af4427b Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:24:41 +0000 Subject: [PATCH 05/14] feat: fix tests --- tests/config/test_config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/config/test_config.py b/tests/config/test_config.py index a5d31817..d58e0eaa 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -34,7 +34,7 @@ def test_non_existing_file(self, caplog): with caplog.at_level(logging.WARNING): EcoLogits.init(config_path=default_path) - assert EcoLogits.config. providers == default_providers + assert EcoLogits.config.providers == default_providers assert EcoLogits.config.electricity_mix_zone == default_electricity_mix assert "Provided file does not exist, will fall back on default values" in caplog.text @@ -43,23 +43,23 @@ def test_only_elec_mix_provided(self, patch): EcoLogits.init(config_path=default_path) - assert EcoLogits.config. providers == default_providers + assert EcoLogits.config.providers == default_providers assert EcoLogits.config.electricity_mix_zone == user_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) def test_only_provider_in_config(self, patch): EcoLogits.init(config_path=default_path) - assert EcoLogits.config. providers == user_providers_list + assert EcoLogits.config.providers == user_providers_list assert EcoLogits.config.electricity_mix_zone == default_electricity_mix def test_no_ecologits_key_in_toml(self, caplog): - with caplog.at_level(logging.INFO): + with caplog.at_level(logging.WARNING): EcoLogits.init(config_path="./tests/config/toml_with_no_ecologits.toml") assert EcoLogits.config.providers == default_providers assert EcoLogits.config.electricity_mix_zone == default_electricity_mix - assert "The provided file did not contain the ecologits key, will fall back on default configuration" in caplog.text + assert "Provided file did not contain the ecologits key. Falling back on default configuration" in caplog.text def test_init_parameters_both_provided(self): EcoLogits.init(providers = user_providers_list, electricity_mix_zone=user_electricity_mix) From 3fec0abf2d98ea1ec371f66cf3581b783a0c2f95 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:31:10 +0000 Subject: [PATCH 06/14] feat: fix mypy --- ecologits/_ecologits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index e5368b70..746dc8fa 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass, field -import toml # type: ignore [import-untyped] +import toml # type: ignore [import] from packaging.version import Version from ecologits.exceptions import EcoLogitsError From 2154f83c7686fae994186c39fefa4c370770021e Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:55:04 +0000 Subject: [PATCH 07/14] feat: update pre-commit --- .pre-commit-config.yaml | 8 ++++---- docs/scripts/gen_references.py | 2 +- ecologits/__init__.py | 4 ++-- ecologits/tracers/huggingface_tracer.py | 4 +--- pyproject.toml | 17 ++++++++--------- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e291fc0..af1c322f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.254' + rev: 'v0.9.5' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: mixed-line-ending @@ -16,10 +16,10 @@ repos: # - id: bandit # exclude: tests/ - repo: https://github.com/Lucas-C/pre-commit-hooks-safety - rev: v1.3.1 + rev: v1.4.0 hooks: - id: python-safety-dependencies-check - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.13.0' + rev: 'v1.15.0' hooks: - id: mypy \ No newline at end of file diff --git a/docs/scripts/gen_references.py b/docs/scripts/gen_references.py index 99b7f0cf..b7c2a510 100644 --- a/docs/scripts/gen_references.py +++ b/docs/scripts/gen_references.py @@ -26,5 +26,5 @@ mkdocs_gen_files.set_edit_path(full_doc_path, Path("../") / path) -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: # +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/ecologits/__init__.py b/ecologits/__init__.py index 31d4b2b0..4efe3e51 100644 --- a/ecologits/__init__.py +++ b/ecologits/__init__.py @@ -2,6 +2,6 @@ __version__ = "0.5.2" __all__ = [ - "__version__", - "EcoLogits" + "EcoLogits", + "__version__" ] diff --git a/ecologits/tracers/huggingface_tracer.py b/ecologits/tracers/huggingface_tracer.py index 42878a8f..53597b69 100644 --- a/ecologits/tracers/huggingface_tracer.py +++ b/ecologits/tracers/huggingface_tracer.py @@ -69,9 +69,7 @@ def huggingface_chat_wrapper_stream( ) -> Iterable[ChatCompletionStreamOutput]: timer_start = time.perf_counter() stream = wrapped(*args, **kwargs) - token_count = 0 - for chunk in stream: - token_count += 1 + for token_count, chunk in enumerate(stream): request_latency = time.perf_counter() - timer_start impacts = llm_impacts( provider=PROVIDER, diff --git a/pyproject.toml b/pyproject.toml index 1b3ac20c..94be294a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,7 @@ ignore_errors = true [tool.ruff] -select = [ +lint.select = [ "A", "ANN", "ARG", @@ -135,10 +135,9 @@ select = [ "YTT" ] -ignore = [ +lint.ignore = [ "A003", - "ANN101", - "ANN102", + "ANN401", "N805", "N818", @@ -150,7 +149,7 @@ ignore = [ "TRY003" ] -fixable = [ +lint.fixable = [ "A", "ANN", "ARG", @@ -180,7 +179,7 @@ fixable = [ "W", "YTT" ] -unfixable = [] +lint.unfixable = [] exclude = [ ".bzr", @@ -205,12 +204,12 @@ exclude = [ line-length = 120 -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" target-version = "py39" -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 10 -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "google" From 1c6785c35ad8f2fe6f0d0a7aa0fa483253e97a25 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sun, 9 Feb 2025 13:24:27 +0000 Subject: [PATCH 08/14] feat: fix tests --- ecologits/tracers/huggingface_tracer.py | 2 +- pyproject.toml | 1 - tests/config/test_config.py | 11 +++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ecologits/tracers/huggingface_tracer.py b/ecologits/tracers/huggingface_tracer.py index 53597b69..ac3c0ed0 100644 --- a/ecologits/tracers/huggingface_tracer.py +++ b/ecologits/tracers/huggingface_tracer.py @@ -69,7 +69,7 @@ def huggingface_chat_wrapper_stream( ) -> Iterable[ChatCompletionStreamOutput]: timer_start = time.perf_counter() stream = wrapped(*args, **kwargs) - for token_count, chunk in enumerate(stream): + for token_count, chunk in enumerate(stream, start=1): request_latency = time.perf_counter() - timer_start impacts = llm_impacts( provider=PROVIDER, diff --git a/pyproject.toml b/pyproject.toml index 94be294a..676cbbce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,7 +137,6 @@ lint.select = [ lint.ignore = [ "A003", - "ANN401", "N805", "N818", diff --git a/tests/config/test_config.py b/tests/config/test_config.py index d58e0eaa..e764f659 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -33,8 +33,8 @@ def test_working_config_single_provider(self, toml): def test_non_existing_file(self, caplog): with caplog.at_level(logging.WARNING): EcoLogits.init(config_path=default_path) - - assert EcoLogits.config.providers == default_providers + + assert not (set(EcoLogits.config.providers) ^ set(default_providers)) assert EcoLogits.config.electricity_mix_zone == default_electricity_mix assert "Provided file does not exist, will fall back on default values" in caplog.text @@ -42,8 +42,7 @@ def test_non_existing_file(self, caplog): def test_only_elec_mix_provided(self, patch): EcoLogits.init(config_path=default_path) - - assert EcoLogits.config.providers == default_providers + assert not (set(EcoLogits.config.providers) ^ set(default_providers)) assert EcoLogits.config.electricity_mix_zone == user_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) @@ -57,7 +56,7 @@ def test_no_ecologits_key_in_toml(self, caplog): with caplog.at_level(logging.WARNING): EcoLogits.init(config_path="./tests/config/toml_with_no_ecologits.toml") - assert EcoLogits.config.providers == default_providers + assert not (set(EcoLogits.config.providers) ^ set(default_providers)) assert EcoLogits.config.electricity_mix_zone == default_electricity_mix assert "Provided file did not contain the ecologits key. Falling back on default configuration" in caplog.text @@ -70,7 +69,7 @@ def test_init_parameters_both_provided(self): def test_init_parameters_elec_only_provided(self): EcoLogits.init(electricity_mix_zone=user_electricity_mix) - assert EcoLogits.config.providers == default_providers + assert not (set(EcoLogits.config.providers) ^ set(default_providers)) assert EcoLogits.config.electricity_mix_zone == user_electricity_mix def test_init_parameters_providers_only_provided(self): From 0dc6c39b927af4c633600437ee15381b21d2f05c Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Wed, 12 Feb 2025 19:24:59 +0000 Subject: [PATCH 09/14] feat: review changes --- docs/index.md | 6 +++--- docs/tutorial/index.md | 8 ++++---- ecologits/_ecologits.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8a37f5b9..945ba32a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -117,7 +117,7 @@ You can also provide the ecologits configuration through a toml file. from openai import OpenAI # Initialize EcoLogits - EcoLogits.init(config_path="pyproject.toml") + EcoLogits.init(config_path="ecologits.toml") client = OpenAI(api_key="") @@ -133,7 +133,7 @@ You can also provide the ecologits configuration through a toml file. print(f"GHG emissions: {response.impacts.gwp.value} kgCO2eq") ``` -=== "pyproject.toml" +=== "ecologits.toml" ```toml [ecologits] region="FRA" @@ -143,7 +143,7 @@ You can also provide the ecologits configuration through a toml file. ``` !!! info Internal priorizations - - If no init parameters are provided, EcoLogits will check for a pyproject.toml file with an ecologits config an rely on it to initialize. + - If no init parameters are provided, EcoLogits will check for a ecologits.toml file with an EcoLogits config an rely on it to initialize. - If both a toml file and init parameter are provided, the parameters will prevail. Environmental impacts are quantified based on four criteria and across two phases: diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 813e7052..8b7277e3 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -54,7 +54,7 @@ from ecologits import EcoLogits EcoLogits.init() # (1)! ``` -1. If you have a pyproject.toml containing an EcoLogits configuration, it will now be loaded by an empty init. +1. If you have a ecologits.toml containing an EcoLogits configuration, it will now be loaded by an empty init. ### Configure providers @@ -100,15 +100,15 @@ You can also set the providers and electricity mix zone through a .toml file (fo from ecologits import EcoLogits # Initialize EcoLogits with a config file -EcoLogits.init(config_path="pyproject.toml") +EcoLogits.init(config_path="my_config_file.toml") EcoLogits.init(config_path="ecologits.toml") ``` -To do so, just provide the path to your file (or provide nothing if it is a `pyproject.toml` located at the root of your project). +To do so, just provide the path to your file (or provide nothing if it is a `ecologits.toml` located at the root of your project). The expected formatting of the EcoLogits configuration is as follows. -```toml title="pyproject.toml" +```toml title="ecologits.toml" [ecologits] region="FRA" providers=[ diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index 746dc8fa..04f738c4 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -152,9 +152,9 @@ def init( if (config_path is None and providers is None and electricity_mix_zone is None - and os.path.isfile("pyproject.toml")): + and os.path.isfile("ecologits.toml")): - config_path = "pyproject.toml" + config_path = "ecologits.toml" if config_path: try: From 7d82da17ca72048cf3b88a62e9dbfbae499ab863 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:17:04 +0000 Subject: [PATCH 10/14] feat: switched to tomllib --- ecologits/_ecologits.py | 15 ++++++++------- pyproject.toml | 1 - tests/config/test_config.py | 22 +++++++++++----------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index 04f738c4..e3d1b2e3 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass, field -import toml # type: ignore [import] +import tomllib from packaging.version import Version from ecologits.exceptions import EcoLogitsError @@ -124,11 +124,12 @@ class EcoLogits: @staticmethod def _read_ecologits_config(config_path: str)-> dict[str, str]|None: - with open(config_path) as config_file: - config = toml.load(config_file).get("ecologits", None) - if config is None: - logger.warning("Provided file did not contain the ecologits key. Falling back on default configuration") - return config + with open(config_path, "rb") as f: + config = tomllib.load(f) + user_config = config.get("ecologits",None) + if user_config is None: + logger.warning("File does not have the 'ecologits' key, falling back on defaults") + return user_config @staticmethod def init( @@ -161,7 +162,7 @@ def init( user_config: dict[str, str]|None = EcoLogits._read_ecologits_config(config_path) logger.info("Ecologits configuration found in file and loaded") except FileNotFoundError: - logger.warning("Provided file does not exist, will fall back on default values") + logger.warning("File does not exist, falling back on defaults") user_config = None if user_config is not None: diff --git a/pyproject.toml b/pyproject.toml index 676cbbce..c913ab33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,6 @@ pytest-recording = "^0.13.1" pytest-dotenv = "^0.5.2" pytest-asyncio = "^0.23.6" numpy = "^2.0.0" -toml = "^0.10.2" mypy = "^1.13.0" diff --git a/tests/config/test_config.py b/tests/config/test_config.py index e764f659..c87b2f98 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -9,7 +9,7 @@ default_electricity_mix = "WOR" user_electricity_mix = "FRA" -user_providers_list = list(set(["openai", "mistral"])) +user_providers_list = ["openai", "mistral"] user_single_provider = ["openai"] @@ -20,7 +20,7 @@ class TestEcoLogitsConfig: def test_working_config_provider_list(self, toml): EcoLogits.init(config_path=default_path) - assert EcoLogits.config.providers == user_providers_list + assert EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == user_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix, "providers":user_single_provider}) @@ -34,48 +34,48 @@ def test_non_existing_file(self, caplog): with caplog.at_level(logging.WARNING): EcoLogits.init(config_path=default_path) - assert not (set(EcoLogits.config.providers) ^ set(default_providers)) + assert EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == default_electricity_mix - assert "Provided file does not exist, will fall back on default values" in caplog.text + assert "File does not exist, falling back on defaults" in caplog.text @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"electricity_mix_zone":user_electricity_mix}) def test_only_elec_mix_provided(self, patch): EcoLogits.init(config_path=default_path) - assert not (set(EcoLogits.config.providers) ^ set(default_providers)) + EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == user_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) def test_only_provider_in_config(self, patch): EcoLogits.init(config_path=default_path) - assert EcoLogits.config.providers == user_providers_list + EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == default_electricity_mix def test_no_ecologits_key_in_toml(self, caplog): with caplog.at_level(logging.WARNING): EcoLogits.init(config_path="./tests/config/toml_with_no_ecologits.toml") - assert not (set(EcoLogits.config.providers) ^ set(default_providers)) + EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == default_electricity_mix - assert "Provided file did not contain the ecologits key. Falling back on default configuration" in caplog.text + assert "File does not have the 'ecologits' key, falling back on defaults" in caplog.text def test_init_parameters_both_provided(self): EcoLogits.init(providers = user_providers_list, electricity_mix_zone=user_electricity_mix) - assert EcoLogits.config.providers == user_providers_list + EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == user_electricity_mix def test_init_parameters_elec_only_provided(self): EcoLogits.init(electricity_mix_zone=user_electricity_mix) - assert not (set(EcoLogits.config.providers) ^ set(default_providers)) + EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == user_electricity_mix def test_init_parameters_providers_only_provided(self): EcoLogits.init(providers=user_providers_list) - assert EcoLogits.config.providers == user_providers_list + EcoLogits.config.providers.sort() == user_providers_list.sort() assert EcoLogits.config.electricity_mix_zone == default_electricity_mix @patch(target="ecologits._ecologits.EcoLogits._read_ecologits_config", return_value = {"providers":user_providers_list}) From 73f7a900ff5c2408c478fb1ea02475b6c4442fa1 Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:25:12 +0000 Subject: [PATCH 11/14] fix: poetry lock --- poetry.lock | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/poetry.lock b/poetry.lock index b328291f..5ba8413d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3415,18 +3415,6 @@ dev = ["tokenizers[testing]"] docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["dev"] -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - [[package]] name = "tomli" version = "2.2.1" @@ -3952,4 +3940,4 @@ openai = ["openai"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4" -content-hash = "3f1fc0bd6ede721d78f819f387be493ed78a96ce975731faedcdac188885b115" +content-hash = "2eb792162ca257c928c47e269e6ee5df9a19338ba8df1bf17e5ca3af7f6f9a47" From f1a08e55a57d7c7a4dedac848bbefd39cb7c0f9e Mon Sep 17 00:00:00 2001 From: "Nina P." <23741953+NP4567-dev@users.noreply.github.com> Date: Sat, 15 Feb 2025 12:32:28 +0000 Subject: [PATCH 12/14] feat: switched to tomllib bis --- tests/test_version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_version.py b/tests/test_version.py index e5f736f7..0c15c41a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Optional -import toml +import tomllib from ecologits import __version__ @@ -13,6 +13,6 @@ def test_version_alignment(): def get_poetry_version() -> Optional[str]: path = Path(__file__).resolve().parents[1] / 'pyproject.toml' - with open(str(path), "r") as fd: - pyproject = toml.loads(fd.read()) + with open(str(path), "rb") as fd: + pyproject = tomllib.load(fd) return pyproject['project']['version'] From 906a6696e6120025a108efbe975ea441ed8c84a7 Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Sat, 15 Feb 2025 21:53:53 +0100 Subject: [PATCH 13/14] fix: add tomli dependency for python version < 3.11 --- ecologits/_ecologits.py | 6 ++-- ecologits/utils/toml.py | 11 +++++++ poetry.lock | 66 ++++++++++++++++++++--------------------- pyproject.toml | 1 + tests/test_version.py | 8 ++--- 5 files changed, 50 insertions(+), 42 deletions(-) create mode 100644 ecologits/utils/toml.py diff --git a/ecologits/_ecologits.py b/ecologits/_ecologits.py index e3d1b2e3..fec2e27d 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -3,11 +3,11 @@ import os from dataclasses import dataclass, field -import tomllib from packaging.version import Version from ecologits.exceptions import EcoLogitsError from ecologits.log import logger +from ecologits.utils.toml import load_toml def init_openai_instrumentor() -> None: @@ -123,9 +123,7 @@ class EcoLogits: @staticmethod def _read_ecologits_config(config_path: str)-> dict[str, str]|None: - - with open(config_path, "rb") as f: - config = tomllib.load(f) + config = load_toml(config_path) user_config = config.get("ecologits",None) if user_config is None: logger.warning("File does not have the 'ecologits' key, falling back on defaults") diff --git a/ecologits/utils/toml.py b/ecologits/utils/toml.py new file mode 100644 index 00000000..ad542118 --- /dev/null +++ b/ecologits/utils/toml.py @@ -0,0 +1,11 @@ +import sys + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + + +def load_toml(file_path): + with open(file_path, "rb") as f: + return tomllib.load(f) diff --git a/poetry.lock b/poetry.lock index 5ba8413d..0502df3a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.0 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -116,7 +116,7 @@ propcache = ">=0.2.0" yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""] [[package]] name = "aiosignal" @@ -192,7 +192,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -202,7 +202,7 @@ description = "Timeout context manager for asyncio programs" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "python_version < \"3.11\" and (extra == \"huggingface-hub\" or extra == \"litellm\")" +markers = "(python_version <= \"3.10\" or platform_python_implementation == \"PyPy\") and (extra == \"huggingface-hub\" or extra == \"litellm\") and python_version < \"3.11\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -222,12 +222,12 @@ files = [ ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "babel" @@ -242,7 +242,7 @@ files = [ ] [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "cachetools" @@ -639,7 +639,7 @@ description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["main", "dev"] -markers = "python_version < \"3.11\"" +markers = "python_version <= \"3.10\" or platform_python_implementation == \"PyPy\" and python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -712,7 +712,7 @@ markers = {main = "extra == \"cohere\" or extra == \"litellm\" or extra == \"hug [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "frozenlist" @@ -894,7 +894,7 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extr google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" proto-plus = [ {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -920,7 +920,7 @@ grpcio = [ ] grpcio-status = [ {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, + {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, ] proto-plus = [ {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, @@ -931,7 +931,7 @@ requests = ">=2.18.0,<3.0.0.dev0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -1213,7 +1213,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1315,12 +1315,12 @@ markers = {main = "extra == \"litellm\""} zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -1339,7 +1339,7 @@ files = [ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] @@ -1748,7 +1748,7 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -2308,7 +2308,7 @@ docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -2557,7 +2557,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -3421,8 +3421,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" +groups = ["main", "dev"] +markers = "python_version <= \"3.10\" or platform_python_implementation == \"PyPy\" and python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -3487,7 +3487,7 @@ description = "Typing stubs for requests" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.10\" and extra == \"cohere\" or platform_python_implementation == \"PyPy\" and extra == \"cohere\"" +markers = "(python_version < \"3.10\" or platform_python_implementation == \"PyPy\") and extra == \"cohere\"" files = [ {file = "types-requests-2.31.0.6.tar.gz", hash = "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0"}, {file = "types_requests-2.31.0.6-py3-none-any.whl", hash = "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9"}, @@ -3519,7 +3519,7 @@ description = "Typing stubs for urllib3" optional = true python-versions = "*" groups = ["main"] -markers = "python_version < \"3.10\" and extra == \"cohere\" or platform_python_implementation == \"PyPy\" and extra == \"cohere\"" +markers = "(python_version < \"3.10\" or platform_python_implementation == \"PyPy\") and extra == \"cohere\"" files = [ {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, @@ -3582,8 +3582,8 @@ files = [ ] [package.extras] -brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -3600,7 +3600,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3663,7 +3663,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "watchdog" @@ -3921,11 +3921,11 @@ files = [ markers = {main = "extra == \"litellm\""} [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [extras] @@ -3940,4 +3940,4 @@ openai = ["openai"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4" -content-hash = "2eb792162ca257c928c47e269e6ee5df9a19338ba8df1bf17e5ca3af7f6f9a47" +content-hash = "001b017cb3417c02689b6a6ff8fcd5459ffcf98ce19389e0c15d9e4a02a7ad6b" diff --git a/pyproject.toml b/pyproject.toml index c913ab33..52de15d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "requests >=2.26.0", # only needed for `poetry lock` to work with `poetry >2.0` "httpx >=0.23.0,<1", # only needed for `poetry lock` to work with `poetry >2.0` "tqdm >4", # only needed for `poetry lock` to work with `poetry >2.0` + "tomli (>=2.0.1,<3.0) ; python_version < '3.11'" ] [project.optional-dependencies] diff --git a/tests/test_version.py b/tests/test_version.py index 0c15c41a..39540e20 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,9 +1,8 @@ from pathlib import Path from typing import Optional -import tomllib - from ecologits import __version__ +from ecologits.utils.toml import load_toml def test_version_alignment(): @@ -13,6 +12,5 @@ def test_version_alignment(): def get_poetry_version() -> Optional[str]: path = Path(__file__).resolve().parents[1] / 'pyproject.toml' - with open(str(path), "rb") as fd: - pyproject = tomllib.load(fd) - return pyproject['project']['version'] + pyproject = load_toml(str(path)) + return pyproject['project']['version'] From 9bbd6e596635a96aea5cdbaa1a07c8a5f39047d9 Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Sat, 15 Feb 2025 22:00:03 +0100 Subject: [PATCH 14/14] ci: add python version matrix from 3.9 to 3.13 --- .github/workflows/pytest.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 609d9055..85a105fe 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -8,6 +8,9 @@ on: jobs: pytest: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: #---------------------------------------------- # check-out repo and set-up python