Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 15 additions & 1 deletion codecarbon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@
OfflineEmissionsTracker,
track_emissions,
)
from .core.telemetry import (
TelemetryConfig,
TelemetryTier,
init_telemetry,
set_telemetry,
)

__all__ = ["EmissionsTracker", "OfflineEmissionsTracker", "track_emissions"]
__all__ = [
"EmissionsTracker",
"OfflineEmissionsTracker",
"track_emissions",
"TelemetryConfig",
"TelemetryTier",
"init_telemetry",
"set_telemetry",
]
__app_name__ = "codecarbon"
88 changes: 88 additions & 0 deletions codecarbon/cli/cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import typer
from rich.prompt import Confirm

from codecarbon.external.logger import logger


def get_config(path: Optional[Path] = None):
p = path or Path.cwd().resolve() / ".codecarbon.config"
Expand Down Expand Up @@ -108,3 +110,89 @@ def create_new_config_file():
f.write("[codecarbon]\n")
typer.echo(f"Config file created at {file_path}")
return file_path


def save_telemetry_config_to_file(
tier: str = None,
project_token: str = None,
telemetry_api_endpoint: str = None,
telemetry_api_key: str = None,
path: Path = None,
) -> None:
"""
Save telemetry configuration as JSON in the existing config file.

Args:
tier: Telemetry tier (off, internal, public)
project_token: Telemetry auth token (stored as ``telemetry_api_key`` and legacy
``telemetry_project_token`` in JSON)
telemetry_api_endpoint: Base URL for telemetry HTTP (optional)
telemetry_api_key: Telemetry auth token (optional; overrides ``project_token`` if both set)
path: Path to config file (defaults to ~/.codecarbon.config)
"""
import json

p = path or Path.home() / ".codecarbon.config"

config = configparser.ConfigParser()
if p.exists():
config.read(str(p))

if "codecarbon" not in config.sections():
config.add_section("codecarbon")

existing = load_telemetry_config_from_file(p)
telemetry_config = dict(existing) if existing else {}
if tier:
telemetry_config["telemetry_tier"] = tier
token = telemetry_api_key or project_token
if token:
t = str(token).strip()
telemetry_config["telemetry_api_key"] = t
telemetry_config["telemetry_project_token"] = t
if telemetry_api_endpoint is not None:
te = str(telemetry_api_endpoint).strip().rstrip("/")
if te:
telemetry_config["telemetry_api_endpoint"] = te
else:
telemetry_config.pop("telemetry_api_endpoint", None)

if telemetry_config:
config["codecarbon"]["telemetry"] = json.dumps(telemetry_config)

with p.open("w") as f:
config.write(f)
logger.info(f"Telemetry config saved to {p}")


def load_telemetry_config_from_file(path: Path = None) -> dict:
"""
Load telemetry configuration from the existing config file.

Args:
path: Path to config file (defaults to ~/.codecarbon.config)

Returns:
Dictionary with telemetry configuration
"""
import json

p = path or Path.home() / ".codecarbon.config"

if not p.exists():
return {}

config = configparser.ConfigParser()
config.read(str(p))

if "codecarbon" not in config.sections():
return {}

telemetry_str = config["codecarbon"].get("telemetry")
if telemetry_str:
try:
return json.loads(telemetry_str)
except json.JSONDecodeError:
return {}

return {}
142 changes: 142 additions & 0 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
DEFAULT_ORGANIzATION_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"

codecarbon = typer.Typer(no_args_is_help=True)
telemetry_app = typer.Typer(no_args_is_help=True)


def main():
Expand Down Expand Up @@ -436,5 +437,146 @@ def questionary_prompt(prompt, list_options, default):
return value


def _get_project_token() -> Optional[str]:
"""Resolve telemetry auth token (env, JSON, preference, default constant)."""
from codecarbon.core.telemetry.config import get_telemetry_auth_token

return get_telemetry_auth_token()


@telemetry_app.command("setup", short_help="Interactive telemetry setup wizard")
def telemetry_setup(
token: Annotated[
Optional[str],
typer.Option(
"--token",
"-t",
help="Telemetry API key for public tier (same as CODECARBON_TELEMETRY_API_KEY)",
)
] = None,
tier: Annotated[
Optional[str],
typer.Option("--tier", help="Telemetry tier: off, internal, or public")
] = None,
):
"""
Interactive wizard to configure CodeCarbon telemetry.

Examples:
# Interactive mode
codecarbon telemetry setup

# Non-interactive mode with options
codecarbon telemetry setup --tier public --token YOUR_TOKEN

This command automatically saves configuration to ~/.codecarbon.config
and writes environment variables to your shell config.
"""
from codecarbon.core.telemetry.config import (
TELEMETRY_ENV_VAR,
TelemetryTier,
get_telemetry_config,
save_telemetry_project_token,
set_telemetry_tier,
)

print("\n=== CodeCarbon Telemetry Setup ===\n")

# Show current config
config = get_telemetry_config()
print(f"Current tier: {config.tier.value}")
print(
f"Current telemetry API key: {'set' if config.project_token else 'not set'}"
)
print(f"Current API endpoint: {config.api_endpoint or 'default'}")

# Determine tier (use provided value or prompt)
if tier is not None:
try:
tier_choice = TelemetryTier(tier).value
except ValueError:
print(f"[red]Invalid tier: {tier}. Valid values: off, internal, public[/red]")
raise typer.Exit(1)
else:
print("\nChoose telemetry tier:")
tier_choice = questionary.select(
"Telemetry tier:",
["off", "internal", "public"],
default=config.tier.value,
).ask()

# Save tier preference to file
set_telemetry_tier(TelemetryTier(tier_choice), dont_ask_again=True)
print(f"\nTelemetry tier set to: {tier_choice}")

# Get project token (priority: CLI option > env var > config file)
project_token = token or _get_project_token()
if tier_choice == "public" and not project_token:
project_token = typer.prompt(
"Telemetry API key (CODECARBON_TELEMETRY_API_KEY; not your dashboard api_key)",
default="",
)

# Save project token to JSON config file (so it persists without env vars)
if tier_choice == "public" and project_token:
save_telemetry_project_token(project_token)
print("[green]Telemetry API key saved to config file[/green]")

# Write to shell rc file automatically
shell_rc_path = Path.home() / ".zshrc"
if not shell_rc_path.exists():
shell_rc_path = Path.home() / ".bashrc"

# Read existing content
existing_content = ""
if shell_rc_path.exists():
existing_content = shell_rc_path.read_text()

env_vars = {
TELEMETRY_ENV_VAR: tier_choice,
}
purge_markers = (*env_vars.keys(), "CODECARBON_TELEMETRY_PROJECT_TOKEN")
new_lines = []
for line in existing_content.split("\n"):
if not any(marker in line for marker in purge_markers):
new_lines.append(line)

# Add new environment variables
for var_name, var_value in env_vars.items():
new_lines.append(f'export {var_name}="{var_value}"')

# Write back
shell_rc_path.write_text("\n".join(new_lines) + "\n")
print(f"\n[green]Environment variables written to {shell_rc_path}[/green]")
print(f"[yellow]Run 'source {shell_rc_path}' or restart your terminal to apply[/yellow]")
print("\n[green]Setup complete! Configuration saved.[/green]")


@telemetry_app.command("config", short_help="Show current telemetry configuration")
def telemetry_config():
"""
Display current telemetry configuration.
"""
from codecarbon.core.telemetry.config import get_telemetry_config

config = get_telemetry_config()

print("\n=== Current Telemetry Configuration ===\n")
print(f"Tier: {config.tier.value}")
print(f"Enabled: {config.is_enabled}")
print(
f"Telemetry API key: {'configured' if config.project_token else 'not configured'}"
)
print(
f"Telemetry base URL: {config.api_endpoint or 'default (https://api.codecarbon.io)'}"
)
print(f"First Run: {config.first_run}")
print(f"Has Consent: {config.has_consent}")


# Register telemetry as a subcommand of codecarbon
codecarbon.add_typer(telemetry_app, name="telemetry")


if __name__ == "__main__":
main()
57 changes: 57 additions & 0 deletions codecarbon/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,63 @@ def close_experiment(self):
Tell the API that the experiment has ended.
"""

def add_public_emissions(self, payload: dict, project_token: str) -> bool:
"""
Send public-tier emissions payload to POST /emissions (flat JSON, project token).

Args:
payload: JSON-serializable body (e.g. utilization and energy fields).
project_token: Project token sent as x-api-token.

Returns:
True if the server accepted the request (HTTP 200 or 201).
"""
if not project_token:
logger.warning("add_public_emissions: missing project_token")
return False
try:
url = self.url + "/emissions"
headers = self._get_headers()
headers["x-api-token"] = project_token
r = requests.post(url=url, json=payload, timeout=5, headers=headers)
if r.status_code not in (200, 201):
self._log_error(url, payload, r)
return False
logger.debug(f"Public emissions telemetry sent successfully to {url}")
return True
except Exception as e:
logger.error(f"Failed to send public emissions telemetry: {e}")
return False

def add_telemetry(self, telemetry_data: dict, api_key: str = None) -> bool:
"""
Send telemetry data to the /telemetry endpoint (Tier 1).

Args:
telemetry_data: Dictionary containing telemetry payload
api_key: Optional API key for authentication

Returns:
True if successful, False otherwise
"""
try:
url = self.url + "/telemetry"
headers = self._get_headers()

# Use provided api_key or fall back to instance api_key
if api_key:
headers["x-api-token"] = api_key

r = requests.post(url=url, json=telemetry_data, timeout=5, headers=headers)
if r.status_code not in (200, 201):
self._log_error(url, telemetry_data, r)
return False
logger.debug(f"Telemetry data sent successfully to {url}")
return True
except Exception as e:
logger.error(f"Failed to send telemetry data: {e}")
return False


class simple_utc(tzinfo):
def tzname(self, **kwargs):
Expand Down
Loading