Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7064490
fix: remove PUE double-counting in request_usage_wcf (#230)
vinisha231 May 13, 2026
233051b
test: add regression tests for WCF PUE double-counting bug (#230)
vinisha231 May 13, 2026
c7cfe8c
feat: add ProviderNotInstalledError, InvalidProviderError, Configurat…
vinisha231 May 13, 2026
59f059a
refactor: use InvalidProviderError and OpenTelemetryNotInstalledError…
vinisha231 May 13, 2026
5e10411
refactor: raise ConfigurationError with actionable message in EcoLogi…
vinisha231 May 13, 2026
1a1c34d
feat: add load_config_from_json and load_config_from_env helpers (#98)
vinisha231 May 13, 2026
84cb0f8
feat: add EcoLogits.init_from_config and init_from_env class methods …
vinisha231 May 13, 2026
40fc66b
test: add tests for load_config_from_json and load_config_from_env (#98)
vinisha231 May 13, 2026
1d3e654
test: add comprehensive exception tests for all new error classes (#100)
vinisha231 May 13, 2026
56c14e0
feat: add __repr__, __sub__, __neg__, __abs__, to_dict to RangeValue …
vinisha231 May 13, 2026
d7da973
feat: add to_dict and __repr__ to BaseImpact (#118)
vinisha231 May 13, 2026
6611aa1
feat: add to_dict, to_json, summary, __repr__ to Impacts model (#118)
vinisha231 May 13, 2026
c134274
test: add comprehensive tests for Impacts.to_dict, to_json, summary, …
vinisha231 May 13, 2026
eb04faf
test: add tests for RangeValue.__sub__, __neg__, __abs__, to_dict, __…
vinisha231 May 13, 2026
6dd6f96
feat: add ElectricityMixNotAvailableWarning, ModelArchDeprecatedWarni…
vinisha231 May 13, 2026
ef846ea
test: add tests for new warning codes and WarningMessage.from_code
vinisha231 May 13, 2026
22c2b82
feat: export config helpers and new exception classes from top-level …
vinisha231 May 13, 2026
f800f2f
feat: add impacts_to_csv export utility (#118)
vinisha231 May 13, 2026
ecc6129
test: add tests for impacts_to_csv export (#118)
vinisha231 May 13, 2026
5b637c7
style: add missing blank line before WCF class in modeling.py (PEP 8)
vinisha231 May 13, 2026
b0e3540
feat: export RangeValue, ValueOrRange, impacts_to_csv from ecologits.…
vinisha231 May 13, 2026
b721d88
fix: correct gpu_energy docstring β€” gamma was mislabelled as 'Beta co…
vinisha231 May 13, 2026
935196a
feat: add load_config_from_yaml helper (#98)
vinisha231 May 13, 2026
a764103
feat: add EcoLogits.init_from_yaml class method (#98)
vinisha231 May 13, 2026
84f4d15
feat: export load_config_from_yaml from top-level package
vinisha231 May 13, 2026
667b733
test: add YAML config loading tests (#98)
vinisha231 May 13, 2026
f2579a0
test: add unit tests for BaseImpact.to_dict, __repr__, and Impacts me…
vinisha231 May 13, 2026
5dc51d0
feat: add __repr__ to _StatusMessage; fix from_code return type annot…
vinisha231 May 13, 2026
53abc2a
test: add integration tests for EcoLogits.init_from_config and init_f…
vinisha231 May 13, 2026
579f898
feat: export all impact model classes from ecologits.impacts
vinisha231 May 13, 2026
d2cf0dc
fix: add __rsub__ to RangeValue so scalar - RangeValue is computed co…
vinisha231 May 13, 2026
5804008
test: add __rsub__ test for scalar minus RangeValue
vinisha231 May 13, 2026
1559f39
feat: add __repr__ to EcoLogits._Config and EcoLogits.reset() for tes…
vinisha231 May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion ecologits/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
from ._ecologits import EcoLogits
from .config import load_config_from_env, load_config_from_json, load_config_from_yaml
from .exceptions import (
ConfigurationError,
EcoLogitsError,
InvalidProviderError,
ModelingError,
OpenTelemetryNotInstalledError,
ProviderNotInstalledError,
TracerInitializationError,
)

__version__ = "0.10.1"
__all__ = [
"EcoLogits",
"__version__"
"load_config_from_json",
"load_config_from_yaml",
"load_config_from_env",
"EcoLogitsError",
"TracerInitializationError",
"ModelingError",
"ProviderNotInstalledError",
"InvalidProviderError",
"ConfigurationError",
"OpenTelemetryNotInstalledError",
"__version__",
]
96 changes: 88 additions & 8 deletions ecologits/_ecologits.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

from packaging.version import Version

from ecologits.exceptions import EcoLogitsError
from ecologits.exceptions import (
ConfigurationError,
EcoLogitsError,
InvalidProviderError,
OpenTelemetryNotInstalledError,
ProviderNotInstalledError,
_PROVIDER_INSTALL_HINTS, # noqa: F401 – re-exported for convenience
)
from ecologits.log import logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -117,8 +124,19 @@ class _Config:
electricity_mix_zone: str | None = None
opentelemetry: OpenTelemetry | None = None

def __repr__(self) -> str:
return (
f"EcoLogits.Config(providers={self.providers!r}, "
f"electricity_mix_zone={self.electricity_mix_zone!r})"
)

config = _Config()

@classmethod
def reset(cls) -> None:
"""Reset EcoLogits configuration to defaults. Useful in tests."""
cls.config = cls._Config()

@staticmethod
def init(
providers: str | list[str] | None = None,
Expand Down Expand Up @@ -153,14 +171,75 @@ def init(

if opentelemetry_endpoint is not None:
if not is_opentelemetry_installed():
logger.error("OpenTelemetry package is not installed. Install with "
"`pip install ecologits[opentelemetry]`.")
raise EcoLogitsError("OpenTelemetry package is not installed.")
raise OpenTelemetryNotInstalledError()

from ecologits.utils.opentelemetry import OpenTelemetry

EcoLogits.config.opentelemetry = OpenTelemetry(endpoint=opentelemetry_endpoint)

@staticmethod
def init_from_yaml(path: "str | Path") -> None:
"""
Initialize EcoLogits from a YAML configuration file.

Requires ``pyyaml`` (``pip install pyyaml``).

Args:
path: Path to a YAML file containing ``providers``,
``electricity_mix_zone``, and/or ``opentelemetry_endpoint`` keys.

Example::

EcoLogits.init_from_yaml("ecologits.yaml")
"""
from pathlib import Path as Path_
from ecologits.config import load_config_from_yaml
config = load_config_from_yaml(Path_(path))
EcoLogits.init(**config)

@staticmethod
def init_from_config(path: "str | Path") -> None:
"""
Initialize EcoLogits from a JSON configuration file.

Args:
path: Path to a JSON file containing ``providers``,
``electricity_mix_zone``, and/or ``opentelemetry_endpoint`` keys.

Example::

EcoLogits.init_from_config("ecologits.json")
"""
from pathlib import Path as Path_
from ecologits.config import load_config_from_json
config = load_config_from_json(Path_(path))
EcoLogits.init(**config)

@staticmethod
def init_from_env(prefix: str = "ECOLOGITS_") -> None:
"""
Initialize EcoLogits from environment variables.

Reads ``ECOLOGITS_PROVIDERS``, ``ECOLOGITS_ELECTRICITY_MIX_ZONE``,
and ``ECOLOGITS_OPENTELEMETRY_ENDPOINT`` (all overridable via *prefix*).

Args:
prefix: Environment variable prefix (default ``"ECOLOGITS_"``).

Example::

# export ECOLOGITS_PROVIDERS=openai,anthropic
EcoLogits.init_from_env()
"""
from ecologits.config import load_config_from_env
config = load_config_from_env(prefix=prefix)
if not config:
raise ConfigurationError(
"No EcoLogits configuration found in environment variables. "
f"Set at least {prefix}PROVIDERS to one or more provider names."
)
EcoLogits.init(**config)

@staticmethod
def label(**labels: str) -> OpenTelemetryLabels:
"""
Expand Down Expand Up @@ -198,9 +277,10 @@ async def text_summarization(text: str) -> str:
```
"""
if EcoLogits.config.opentelemetry is None:
logger.error("You must enable OpenTelemetry to use labels. Initialize with "
"opentelemetry_endpoint='http://localhost:4318/v1/metrics' for instance.")
raise EcoLogitsError("OpenTelemetry is not enabled.")
raise ConfigurationError(
"OpenTelemetry is not enabled. Initialize EcoLogits with "
"opentelemetry_endpoint='http://localhost:4318/v1/metrics' to use labels."
)

from ecologits.utils.opentelemetry import OpenTelemetryLabels

Expand All @@ -210,7 +290,7 @@ async def text_summarization(text: str) -> str:
def init_instruments(providers: list[str]) -> None:
for provider in providers:
if provider not in _INSTRUMENTS:
raise EcoLogitsError(f"Could not find tracer for the `{provider}` provider.")
raise InvalidProviderError(provider)
if provider not in EcoLogits.config.providers:
init_func = _INSTRUMENTS[provider]
init_func()
Expand Down
111 changes: 111 additions & 0 deletions ecologits/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Configuration loading utilities for EcoLogits (#98)."""
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any


def load_config_from_json(path: str | Path) -> dict[str, Any]:
"""
Load EcoLogits configuration from a JSON file.

Args:
path: Path to the JSON config file.

Returns:
Dictionary of configuration values suitable for passing to ``EcoLogits.init``.

Raises:
FileNotFoundError: If the file does not exist.
ValueError: If the file is not valid JSON.

Example::

config = load_config_from_json("ecologits.json")
EcoLogits.init(**config)
"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
try:
with path.open() as f:
return json.load(f)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON in config file {path}: {exc}") from exc


def load_config_from_yaml(path: str | Path) -> dict[str, Any]:
"""
Load EcoLogits configuration from a YAML file.

Requires ``pyyaml`` (``pip install pyyaml``).

Args:
path: Path to the YAML config file.

Returns:
Dictionary of configuration values suitable for passing to ``EcoLogits.init``.

Raises:
FileNotFoundError: If the file does not exist.
ImportError: If ``pyyaml`` is not installed.

Example::

config = load_config_from_yaml("ecologits.yaml")
EcoLogits.init(**config)
"""
try:
import yaml
except ImportError as exc:
raise ImportError(
"PyYAML is required to load YAML config files. "
"Install it with: pip install pyyaml"
) from exc

path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
with path.open() as f:
return yaml.safe_load(f) or {}


def load_config_from_env(prefix: str = "ECOLOGITS_") -> dict[str, Any]:
"""
Load EcoLogits configuration from environment variables.

Recognised variables (all prefixed with *prefix*):

- ``ECOLOGITS_PROVIDERS`` – comma-separated list of providers, e.g. ``openai,anthropic``
- ``ECOLOGITS_ELECTRICITY_MIX_ZONE`` – ISO 3166-1 alpha-3 zone code, e.g. ``FRA``
- ``ECOLOGITS_OPENTELEMETRY_ENDPOINT`` – OpenTelemetry collector URL

Args:
prefix: Environment variable prefix (default ``"ECOLOGITS_"``).

Returns:
Dictionary of configuration values with only the keys that were set.

Example::

# With ECOLOGITS_PROVIDERS=openai,anthropic in the environment:
config = load_config_from_env()
EcoLogits.init(**config)
"""
config: dict[str, Any] = {}

raw_providers = os.environ.get(f"{prefix}PROVIDERS")
if raw_providers:
config["providers"] = [p.strip() for p in raw_providers.split(",") if p.strip()]

zone = os.environ.get(f"{prefix}ELECTRICITY_MIX_ZONE")
if zone:
config["electricity_mix_zone"] = zone

otel = os.environ.get(f"{prefix}OPENTELEMETRY_ENDPOINT")
if otel:
config["opentelemetry_endpoint"] = otel

return config
58 changes: 56 additions & 2 deletions ecologits/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,66 @@
from __future__ import annotations

_PROVIDER_INSTALL_HINTS: dict[str, str] = {
"openai": "pip install ecologits[openai]",
"anthropic": "pip install ecologits[anthropic]",
"mistralai": "pip install ecologits[mistralai]",
"huggingface_hub": "pip install ecologits[huggingface-hub]",
"cohere": "pip install ecologits[cohere]",
"google_genai": "pip install ecologits[google-genai]",
"litellm": "pip install ecologits[litellm]",
}

_VALID_PROVIDERS = list(_PROVIDER_INSTALL_HINTS.keys())


class EcoLogitsError(Exception):
pass


class TracerInitializationError(EcoLogitsError):
"""Tracer is initialized twice"""
"""Tracer is initialized twice."""
pass


class ModelingError(EcoLogitsError):
"""Operation or computation not allowed"""
"""Operation or computation not allowed."""
pass


class ProviderNotInstalledError(EcoLogitsError):
"""Required provider package is not installed."""

def __init__(self, provider: str) -> None:
self.provider = provider
hint = _PROVIDER_INSTALL_HINTS.get(provider, f"pip install {provider}")
super().__init__(
f"Provider '{provider}' is not installed. "
f"Install it with: `{hint}`"
)


class InvalidProviderError(EcoLogitsError):
"""Provider name is not recognised by EcoLogits."""

def __init__(self, provider: str) -> None:
self.provider = provider
valid = ", ".join(f"'{p}'" for p in _VALID_PROVIDERS)
super().__init__(
f"Unknown provider '{provider}'. "
f"Valid providers are: {valid}."
)


class ConfigurationError(EcoLogitsError):
"""EcoLogits configuration is invalid or missing."""
pass


class OpenTelemetryNotInstalledError(EcoLogitsError):
"""OpenTelemetry packages are not installed."""

def __init__(self) -> None:
super().__init__(
"OpenTelemetry package is not installed. "
"Install it with: `pip install ecologits[opentelemetry]`"
)
11 changes: 9 additions & 2 deletions ecologits/impacts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from .llm import compute_llm_impacts
from .modeling import Impacts
from .modeling import ADPe, Embodied, Energy, GWP, Impacts, PE, Usage, WCF

__all__ = [
"Impacts",
"compute_llm_impacts"
"Energy",
"GWP",
"ADPe",
"PE",
"WCF",
"Usage",
"Embodied",
"compute_llm_impacts",
]
15 changes: 11 additions & 4 deletions ecologits/impacts/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def gpu_energy(
batch_size: Number of requests handled concurrently by the server.
gpu_energy_alpha: Alpha coefficient of the energy regression.
gpu_energy_beta: Beta coefficient of the energy regression.
gpu_energy_gamma: Beta coefficient of the energy regression.
gpu_energy_gamma: Gamma coefficient of the energy regression.

Returns:
The energy consumption of a single GPU in kWh.
Expand Down Expand Up @@ -250,14 +250,21 @@ def request_usage_wcf(
Compute the water usage impact of the request.

Args:
request_energy: Energy consumption of the request in kWh.
request_energy: Energy consumption of the request in kWh (already includes PUE).
if_electricity_mix_wue: WCF impact factor of electricity consumption in L / kWh.
datacenter_wue: Water Usage Effectiveness of the data center in L/kWh.
datacenter_wue: Water Usage Effectiveness of the data center in L/kWh (relative to IT energy).
datacenter_pue: Power Usage Effectiveness of the data center.

Returns:
The water usage impact of the request in liters.

Note:
`request_energy` already embeds PUE. WUE is defined relative to IT energy (before PUE),
so we divide by PUE to recover IT energy for the datacenter cooling term.
The electricity-mix term uses `request_energy` directly (grid sees the full PUE-inflated draw).
"""
return request_energy * (datacenter_wue + datacenter_pue * if_electricity_mix_wue)
it_energy = request_energy / datacenter_pue
return it_energy * datacenter_wue + request_energy * if_electricity_mix_wue


@dag.asset
Expand Down
Loading