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 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/index.md b/docs/index.md index 6ccaed89..945ba32a 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="ecologits.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") + ``` + +=== "ecologits.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 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/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/docs/tutorial/index.md b/docs/tutorial/index.md index d8b0cbf2..8b7277e3 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 ecologits.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="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 `ecologits.toml` located at the root of your project). +The expected formatting of the EcoLogits configuration is as follows. + +```toml title="ecologits.toml" +[ecologits] +region="FRA" +providers=[ + "openai", + "mistral" +] +``` 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/_ecologits.py b/ecologits/_ecologits.py index a264a35e..fec2e27d 100644 --- a/ecologits/_ecologits.py +++ b/ecologits/_ecologits.py @@ -1,12 +1,13 @@ import importlib.metadata import importlib.util +import os from dataclasses import dataclass, field -from typing import Optional, Union 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: @@ -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,17 +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: + 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") + return user_config @staticmethod def init( - providers: Optional[Union[str, list[str]]] = None, - electricity_mix_zone: str = "WOR", + config_path: str| None = None, + providers: str | list[str]|None = None, + electricity_mix_zone: str|None = None, ) -> None: """ Initialization static method. Will attempt to initialize all providers by default. @@ -132,15 +142,43 @@ 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). """ + 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 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("ecologits.toml")): + + config_path = "ecologits.toml" + + 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("File does not exist, falling back on defaults") + user_config = None + + 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) + if isinstance(providers, str): providers = [providers] - if providers is None: - providers = list(_INSTRUMENTS.keys()) + elif providers is None: + providers = default_providers + if electricity_mix_zone is None: + electricity_mix_zone = default_electricity_mix_zone 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/ecologits/tracers/huggingface_tracer.py b/ecologits/tracers/huggingface_tracer.py index 42878a8f..ac3c0ed0 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, start=1): request_latency = time.perf_counter() - timer_start impacts = llm_impacts( provider=PROVIDER, 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 ddc04123..1619cf77 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" @@ -3415,26 +3415,14 @@ 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" 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"}, @@ -3494,6 +3482,7 @@ telegram = ["requests"] [[package]] name = "types-requests" + version = "2.32.0.20241016" description = "Typing stubs for requests" optional = true @@ -3565,7 +3554,7 @@ files = [ markers = {dev = "platform_python_implementation != \"PyPy\" and python_version >= \"3.10\""} [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)"] @@ -3645,7 +3634,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" @@ -3903,11 +3892,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] @@ -3922,4 +3911,4 @@ openai = ["openai"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4" -content-hash = "3f1fc0bd6ede721d78f819f387be493ed78a96ce975731faedcdac188885b115" +content-hash = "001b017cb3417c02689b6a6ff8fcd5459ffcf98ce19389e0c15d9e4a02a7ad6b" diff --git a/pyproject.toml b/pyproject.toml index 70762e88..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] @@ -52,7 +53,6 @@ huggingface-hub = ["huggingface-hub (>=0.28.1,<0.29.0)", "tiktoken (>=0.8.0,<0.9 google-generativeai = ["google-generativeai (>=0.8.4,<0.9.0)"] litellm = ["litellm (>=1.60.6,<2.0.0)", "rapidfuzz (>=3.12.1,<4.0.0)"] - [tool.poetry] requires-poetry = ">=2.0" @@ -67,7 +67,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" @@ -104,7 +103,7 @@ ignore_errors = true [tool.ruff] -select = [ +lint.select = [ "A", "ANN", "ARG", @@ -136,10 +135,8 @@ select = [ "YTT" ] -ignore = [ +lint.ignore = [ "A003", - "ANN101", - "ANN102", "ANN401", "N805", "N818", @@ -151,7 +148,7 @@ ignore = [ "TRY003" ] -fixable = [ +lint.fixable = [ "A", "ANN", "ARG", @@ -181,7 +178,7 @@ fixable = [ "W", "YTT" ] -unfixable = [] +lint.unfixable = [] exclude = [ ".bzr", @@ -206,12 +203,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" 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..c87b2f98 --- /dev/null +++ b/tests/config/test_config.py @@ -0,0 +1,87 @@ + +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 = "FRA" +user_providers_list = ["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(config_path=default_path) + + 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}) + def test_working_config_single_provider(self, toml): + 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(config_path=default_path) + + assert EcoLogits.config.providers.sort() == user_providers_list.sort() + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + 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) + 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) + + 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") + + EcoLogits.config.providers.sort() == user_providers_list.sort() + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + 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) + + 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) + + 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) + + 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}) + def test_init_parameters_and_config_provided(self, patch, caplog): + EcoLogits.init(config_path=default_path, providers=["anthropic"]) + + assert EcoLogits.config.providers == ["anthropic"] + assert EcoLogits.config.electricity_mix_zone == default_electricity_mix + assert "Both config path and init arguments provided, init arguments 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 diff --git a/tests/test_version.py b/tests/test_version.py index e5f736f7..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 toml - 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), "r") as fd: - pyproject = toml.loads(fd.read()) - return pyproject['project']['version'] + pyproject = load_toml(str(path)) + return pyproject['project']['version']