From 380255c6733d0a8382e6a9e8ec79ca982d8f5ce9 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Tue, 12 Dec 2023 09:10:27 +0100
Subject: [PATCH 01/57] feat(cli): :sparkles: Rework CLI
prompts for Organisation Teams Project and Experiment Creations , uses typer instead of click
---
codecarbon/cli/main.py | 242 ++++++++++++++++++++++++++++++++++-------
1 file changed, 204 insertions(+), 38 deletions(-)
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 9031703f6..910acab5c 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -1,47 +1,205 @@
import sys
import time
+from typing import Optional
import click
+import questionary
+import typer
+from rich.prompt import Confirm
+from typing_extensions import Annotated
-from codecarbon import EmissionsTracker
+from codecarbon import __app_name__, __version__
from codecarbon.cli.cli_utils import (
get_api_endpoint,
get_existing_local_exp_id,
write_local_exp_id,
)
from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone
-from codecarbon.core.schemas import ExperimentCreate
+from codecarbon.core.schemas import (
+ ExperimentCreate,
+ OrganizationCreate,
+ ProjectCreate,
+ TeamCreate,
+)
+from codecarbon.emissions_tracker import EmissionsTracker
DEFAULT_PROJECT_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"
+DEFAULT_ORGANIzATION_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"
+
+codecarbon = typer.Typer()
+
+def _version_callback(value: bool) -> None:
+ if value:
+ typer.echo(f"{__app_name__} v{__version__}")
+ raise typer.Exit()
-@click.group()
-def codecarbon():
- pass
+
+@codecarbon.callback()
+def main(
+ version: Optional[bool] = typer.Option(
+ None,
+ "--version",
+ "-v",
+ help="Show the application's version and exit.",
+ callback=_version_callback,
+ is_eager=True,
+ ),
+) -> None:
+ return
@codecarbon.command("init", short_help="Create an experiment id in a public project.")
def init():
- experiment_id = get_existing_local_exp_id()
+ """
+ Initialize CodeCarbon, this will prompt you for configuration of Organisation/Team/Project/Experiment.
+ """
+ typer.echo("Welcome to CodeCarbon configuration wizard")
+ use_config = Confirm.ask(
+ "Use existing /.codecarbonconfig to configure ?",
+ )
+ if use_config is True:
+ experiment_id = get_existing_local_exp_id()
+ else:
+ experiment_id = None
new_local = False
if experiment_id is None:
- api = ApiClient(endpoint_url=get_api_endpoint())
- experiment = ExperimentCreate(
- timestamp=get_datetime_with_timezone(),
- name="Code Carbon user test",
- description="Code Carbon user test with default project",
- on_cloud=False,
- project_id=DEFAULT_PROJECT_ID,
- country_name="France",
- country_iso_code="FRA",
- region="france",
+ api_endpoint = get_api_endpoint()
+ api_endpoint = typer.prompt(
+ f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
+ type=str,
+ default=api_endpoint,
)
- experiment_id = api.add_experiment(experiment)
+ api = ApiClient(endpoint_url=api_endpoint)
+ organizations = api.get_list_organizations()
+ org = questionary.select(
+ "Pick existing organization from list or Create new organization ?",
+ [org["name"] for org in organizations] + ["Create New Organization"],
+ ).ask()
+
+ if org == "Create New Organization":
+ org_name = typer.prompt(
+ "Organization name", default="Code Carbon user test"
+ )
+ org_description = typer.prompt(
+ "Organization description", default="Code Carbon user test"
+ )
+ if org_name in organizations:
+ typer.echo(
+ f"Organization {org_name} already exists, using it for this experiment."
+ )
+ organization = [orga for orga in organizations if orga["name"] == org][
+ 0
+ ]
+ else:
+ organization_create = OrganizationCreate(
+ name=org_name,
+ description=org_description,
+ )
+ organization = api.create_organization(organization=organization_create)
+ typer.echo(f"Created organization : {organization}")
+ else:
+ organization = [orga for orga in organizations if orga["name"] == org][0]
+ teams = api.list_teams_from_organization(organization["id"])
+
+ team = questionary.select(
+ "Pick existing team from list or create new team in organization ?",
+ [team["name"] for team in teams] + ["Create New Team"],
+ ).ask()
+ if team == "Create New Team":
+ team_name = typer.prompt("Team name", default="Code Carbon user test")
+ team_description = typer.prompt(
+ "Team description", default="Code Carbon user test"
+ )
+ team_create = TeamCreate(
+ name=team_name,
+ description=team_description,
+ organization_id=organization["id"],
+ )
+ team = api.create_team(
+ team=team_create,
+ )
+ typer.echo(f"Created team : {team}")
+ else:
+ team = [t for t in teams if t["name"] == team][0]
+ projects = api.list_projects_from_team(team["id"])
+ project = questionary.select(
+ "Pick existing project from list or Create new project ?",
+ [project["name"] for project in projects] + ["Create New Project"],
+ default="Create New Project",
+ ).ask()
+ if project == "Create New Project":
+ project_name = typer.prompt("Project name", default="Code Carbon user test")
+ project_description = typer.prompt(
+ "Project description", default="Code Carbon user test"
+ )
+ project_create = ProjectCreate(
+ name=project_name,
+ description=project_description,
+ team_id=team["id"],
+ )
+ project = api.create_project(project=project_create)
+ typer.echo(f"Created project : {project}")
+ else:
+ project = [p for p in projects if p["name"] == project][0]
+
+ experiments = api.list_experiments_from_project(project["id"])
+ experiment = questionary.select(
+ "Pick existing experiment from list or Create new experiment ?",
+ [experiment["name"] for experiment in experiments]
+ + ["Create New Experiment"],
+ default="Create New Experiment",
+ ).ask()
+ if experiment == "Create New Experiment":
+ typer.echo("Creating new experiment")
+ exp_name = typer.prompt(
+ "Experiment name :", default="Code Carbon user test"
+ )
+ exp_description = typer.prompt(
+ "Experiment description :",
+ default="Code Carbon user test",
+ )
+
+ exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
+ if exp_on_cloud is True:
+ cloud_provider = typer.prompt(
+ "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
+ )
+ cloud_region = typer.prompt(
+ "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
+ )
+ else:
+ cloud_provider = None
+ cloud_region = None
+ country_name = typer.prompt("Country name :", default="France")
+ country_iso_code = typer.prompt("Country ISO code :", default="FRA")
+ region = typer.prompt("Region :", default="france")
+ experiment_create = ExperimentCreate(
+ timestamp=get_datetime_with_timezone(),
+ name=exp_name,
+ description=exp_description,
+ on_cloud=exp_on_cloud,
+ project_id=project["id"],
+ country_name=country_name,
+ country_iso_code=country_iso_code,
+ region=region,
+ cloud_provider=cloud_provider,
+ cloud_region=cloud_region,
+ )
+ experiment_id = api.add_experiment(experiment=experiment_create)
+
+ else:
+ experiment_id = [e for e in experiments if e["name"] == experiment][0]["id"]
+
+ write_to_config = Confirm.ask(
+ "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
+ )
+
+ if write_to_config is True:
write_local_exp_id(experiment_id)
new_local = True
-
- click.echo(
- "\nWelcome to CodeCarbon, here is your experiment id:\n"
+ typer.echo(
+ "\nCodeCarbon Initialization achieved, here is your experiment id:\n"
+ click.style(f"{experiment_id}", fg="bright_green")
+ (
""
@@ -53,32 +211,36 @@ def init():
)
if new_local:
click.echo(
- "\nCodeCarbon automatically added this id to your local config: "
+ "\nCodeCarbon added this id to your local config: "
+ click.style("./.codecarbon.config", fg="bright_blue")
+ "\n"
)
-@codecarbon.command(
- "monitor", short_help="Run an infinite loop to monitor this machine."
-)
-@click.option(
- "--measure_power_secs", default=10, help="Interval between two measures. (10)"
-)
-@click.option(
- "--api_call_interval",
- default=30,
- help="Number of measures before calling API. (30).",
-)
-@click.option(
- "--api/--no-api", default=True, help="Choose to call Code Carbon API or not. (yes)"
-)
-def monitor(measure_power_secs, api_call_interval, api):
+@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.")
+def monitor(
+ measure_power_secs: Annotated[
+ int, typer.Argument(help="Interval between two measures.")
+ ] = 10,
+ api_call_interval: Annotated[
+ int, typer.Argument(help="Number of measures between API calls.")
+ ] = 30,
+ api: Annotated[
+ bool, typer.Option(help="Choose to call Code Carbon API or not")
+ ] = True,
+):
+ """Monitor your machine's carbon emissions.
+
+ Args:
+ measure_power_secs (Annotated[int, typer.Argument, optional): Interval between two measures. Defaults to 10.
+ api_call_interval (Annotated[int, typer.Argument, optional): Number of measures before calling API. Defaults to 30.
+ api (Annotated[bool, typer.Option, optional): Choose to call Code Carbon API or not. Defaults to True.
+ """
experiment_id = get_existing_local_exp_id()
if api and experiment_id is None:
- click.echo("ERROR: No experiment id, call 'codecarbon init' first.")
+ typer.echo("ERROR: No experiment id, call 'codecarbon init' first.")
sys.exit(1)
- click.echo("CodeCarbon is going in an infinite loop to monitor this machine.")
+ typer.echo("CodeCarbon is going in an infinite loop to monitor this machine.")
with EmissionsTracker(
measure_power_secs=measure_power_secs,
api_call_interval=api_call_interval,
@@ -87,3 +249,7 @@ def monitor(measure_power_secs, api_call_interval, api):
# Infinite loop
while True:
time.sleep(300)
+
+
+if __name__ == "__main__":
+ codecarbon()
From 9b3a88f688391195b257d89532d3d53731ba59d5 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Tue, 12 Dec 2023 09:12:04 +0100
Subject: [PATCH 02/57] feat(core): :sparkles: add Organisation Team and
Project to APi Client
allows to create and list organisations, projects and teams from the CLI
---
codecarbon/core/api_client.py | 117 ++++++++++++++++++++++++++++++++--
codecarbon/core/schemas.py | 44 +++++++++++++
2 files changed, 157 insertions(+), 4 deletions(-)
diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py
index a1ca2cca9..76ef82536 100644
--- a/codecarbon/core/api_client.py
+++ b/codecarbon/core/api_client.py
@@ -13,7 +13,14 @@
import arrow
import requests
-from codecarbon.core.schemas import EmissionCreate, ExperimentCreate, RunCreate
+from codecarbon.core.schemas import (
+ EmissionCreate,
+ ExperimentCreate,
+ OrganizationCreate,
+ ProjectCreate,
+ RunCreate,
+ TeamCreate,
+)
from codecarbon.external.logger import logger
# from codecarbon.output import EmissionsData
@@ -52,6 +59,88 @@ def __init__(
if self.experiment_id is not None:
self._create_run(self.experiment_id)
+ def get_list_organizations(self):
+ """
+ List all organizations
+ """
+ url = self.url + "/organizations"
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def create_organization(self, organization: OrganizationCreate):
+ """
+ Create an organization
+ """
+ payload = dataclasses.asdict(organization)
+ url = self.url + "/organization"
+ r = requests.post(url=url, json=payload, timeout=2)
+ if r.status_code != 201:
+ self._log_error(url, payload, r)
+ return None
+ return r.json()
+
+ def get_organization(self, organization_id):
+ """
+ Get an organization
+ """
+ url = self.url + "/organization/" + organization_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def list_teams_from_organization(self, organization_id):
+ """
+ List all teams
+ """
+ url = (
+ self.url + "/teams/organization/" + organization_id
+ ) # TODO : check if this is the right url
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def create_team(self, team: TeamCreate):
+ """
+ Create a team
+ """
+ payload = dataclasses.asdict(team)
+ url = self.url + "/team"
+ r = requests.post(url=url, json=payload, timeout=2)
+ if r.status_code != 201:
+ self._log_error(url, payload, r)
+ return None
+ return r.json()
+
+ def list_projects_from_team(self, team_id):
+ """
+ List all projects
+ """
+ url = self.url + "/projects/team/" + team_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def create_project(self, project: ProjectCreate):
+ """
+ Create a project
+ """
+ payload = dataclasses.asdict(project)
+ url = self.url + "/project"
+ r = requests.post(url=url, json=payload, timeout=2)
+ if r.status_code != 201:
+ self._log_error(url, payload, r)
+ return None
+ return r.json()
+
def add_emission(self, carbon_emission: dict):
assert self.experiment_id is not None
self._previous_call = time.time()
@@ -148,6 +237,23 @@ def _create_run(self, experiment_id):
except Exception as e:
logger.error(e, exc_info=True)
+ def list_experiments_from_project(self, project_id: str):
+ """
+ List all experiments for a project
+ """
+ url = self.url + "/experiments/project/" + project_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def set_experiment(self, experiment_id: str):
+ """
+ Set the experiment id
+ """
+ self.experiment_id = experiment_id
+
def add_experiment(self, experiment: ExperimentCreate):
"""
Create an experiment, used by the CLI, not the package.
@@ -163,9 +269,12 @@ def add_experiment(self, experiment: ExperimentCreate):
return self.experiment_id
def _log_error(self, url, payload, response):
- logger.error(
- f"ApiClient Error when calling the API on {url} with : {json.dumps(payload)}"
- )
+ if len(payload) > 0:
+ logger.error(
+ f"ApiClient Error when calling the API on {url} with : {json.dumps(payload)}"
+ )
+ else:
+ logger.error(f"ApiClient Error when calling the API on {url}")
logger.error(
f"ApiClient API return http code {response.status_code} and answer : {response.text}"
)
diff --git a/codecarbon/core/schemas.py b/codecarbon/core/schemas.py
index 5d62abfae..481199892 100644
--- a/codecarbon/core/schemas.py
+++ b/codecarbon/core/schemas.py
@@ -78,3 +78,47 @@ class ExperimentCreate(ExperimentBase):
class Experiment(ExperimentBase):
id: str
+
+
+@dataclass
+class OrganizationBase:
+ name: str
+ description: str
+
+
+class OrganizationCreate(OrganizationBase):
+ pass
+
+
+class Organization(OrganizationBase):
+ id: str
+
+
+@dataclass
+class TeamBase:
+ name: str
+ description: str
+ organization_id: str
+
+
+class TeamCreate(TeamBase):
+ pass
+
+
+class Team(TeamBase):
+ id: str
+
+
+@dataclass
+class ProjectBase:
+ name: str
+ description: str
+ team_id: str
+
+
+class ProjectCreate(ProjectBase):
+ pass
+
+
+class Project(ProjectBase):
+ id: str
From d913379479a8f46aed12cbd2d6c4b16c16966ae3 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Tue, 12 Dec 2023 09:12:41 +0100
Subject: [PATCH 03/57] build: :arrow_up: add new dependencies for cli
---
codecarbon/__init__.py | 1 +
setup.py | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/codecarbon/__init__.py b/codecarbon/__init__.py
index 23a5aff39..f602f2635 100644
--- a/codecarbon/__init__.py
+++ b/codecarbon/__init__.py
@@ -10,3 +10,4 @@
)
__all__ = ["EmissionsTracker", "OfflineEmissionsTracker", "track_emissions"]
+__app_name__ = "codecarbon"
diff --git a/setup.py b/setup.py
index 68c52a814..755157c4a 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,9 @@
"psutil",
"py-cpuinfo",
"fuzzywuzzy",
- "click",
+ "typer",
+ "questionary",
+ "rich",
"prometheus_client",
]
From 951ed7c199807490b054a3f227bb88a7c9dcc66c Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sat, 13 Jan 2024 17:34:47 +0100
Subject: [PATCH 04/57] fix(cli): :art: add a questionary prompt function
allows easier testing
---
codecarbon/cli/main.py | 41 ++++++++++++++++++++++++++---------------
1 file changed, 26 insertions(+), 15 deletions(-)
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 910acab5c..d7dc28f85 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -72,10 +72,11 @@ def init():
)
api = ApiClient(endpoint_url=api_endpoint)
organizations = api.get_list_organizations()
- org = questionary.select(
+ org = questionary_prompt(
"Pick existing organization from list or Create new organization ?",
[org["name"] for org in organizations] + ["Create New Organization"],
- ).ask()
+ default="Create New Organization",
+ )
if org == "Create New Organization":
org_name = typer.prompt(
@@ -102,10 +103,11 @@ def init():
organization = [orga for orga in organizations if orga["name"] == org][0]
teams = api.list_teams_from_organization(organization["id"])
- team = questionary.select(
+ team = questionary_prompt(
"Pick existing team from list or create new team in organization ?",
[team["name"] for team in teams] + ["Create New Team"],
- ).ask()
+ default="Create New Team",
+ )
if team == "Create New Team":
team_name = typer.prompt("Team name", default="Code Carbon user test")
team_description = typer.prompt(
@@ -123,11 +125,11 @@ def init():
else:
team = [t for t in teams if t["name"] == team][0]
projects = api.list_projects_from_team(team["id"])
- project = questionary.select(
+ project = questionary_prompt(
"Pick existing project from list or Create new project ?",
[project["name"] for project in projects] + ["Create New Project"],
default="Create New Project",
- ).ask()
+ )
if project == "Create New Project":
project_name = typer.prompt("Project name", default="Code Carbon user test")
project_description = typer.prompt(
@@ -144,12 +146,12 @@ def init():
project = [p for p in projects if p["name"] == project][0]
experiments = api.list_experiments_from_project(project["id"])
- experiment = questionary.select(
+ experiment = questionary_prompt(
"Pick existing experiment from list or Create new experiment ?",
[experiment["name"] for experiment in experiments]
+ ["Create New Experiment"],
default="Create New Experiment",
- ).ask()
+ )
if experiment == "Create New Experiment":
typer.echo("Creating new experiment")
exp_name = typer.prompt(
@@ -157,7 +159,7 @@ def init():
)
exp_description = typer.prompt(
"Experiment description :",
- default="Code Carbon user test",
+ default="Code Carbon user test ",
)
exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
@@ -191,13 +193,13 @@ def init():
else:
experiment_id = [e for e in experiments if e["name"] == experiment][0]["id"]
- write_to_config = Confirm.ask(
- "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
- )
+ write_to_config = Confirm.ask(
+ "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
+ )
- if write_to_config is True:
- write_local_exp_id(experiment_id)
- new_local = True
+ if write_to_config is True:
+ write_local_exp_id(experiment_id)
+ new_local = True
typer.echo(
"\nCodeCarbon Initialization achieved, here is your experiment id:\n"
+ click.style(f"{experiment_id}", fg="bright_green")
@@ -251,5 +253,14 @@ def monitor(
time.sleep(300)
+def questionary_prompt(prompt, list_options, default):
+ value = questionary.select(
+ prompt,
+ list_options,
+ default,
+ ).ask()
+ return value
+
+
if __name__ == "__main__":
codecarbon()
From 1dbc62b22826806117aaba8dadbbe9c9269de600 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sat, 13 Jan 2024 17:35:03 +0100
Subject: [PATCH 05/57] test(cli): :white_check_mark: pass tests CLI
---
tests/test_cli.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 95 insertions(+)
create mode 100644 tests/test_cli.py
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 000000000..a6e3d6db0
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,95 @@
+import unittest
+from unittest.mock import MagicMock, patch
+
+from typer.testing import CliRunner
+
+from codecarbon import __app_name__, __version__
+from codecarbon.cli.main import codecarbon
+
+# MOCK API CLIENT
+
+
+@patch("codecarbon.cli.main.ApiClient")
+class TestApp(unittest.TestCase):
+ def setUp(self):
+ self.runner = CliRunner()
+ self.mock_api_client = MagicMock()
+ self.mock_api_client.get_list_organizations.return_value = [
+ {"id": 1, "name": "test org Code Carbon"}
+ ]
+ self.mock_api_client.list_teams_from_organization.return_value = [
+ {"id": 1, "name": "test team Code Carbon"}
+ ]
+
+ self.mock_api_client.list_projects_from_team.return_value = [
+ {"id": 1, "name": "test project Code Carbon"}
+ ]
+ self.mock_api_client.list_experiments_from_project.return_value = [
+ {"id": 1, "name": "test experiment Code Carbon"}
+ ]
+ self.mock_api_client.create_organization.return_value = {
+ "id": 1,
+ "name": "test org Code Carbon",
+ }
+ self.mock_api_client.create_team.return_value = {
+ "id": 1,
+ "name": "test team Code Carbon",
+ }
+ self.mock_api_client.create_project.return_value = {
+ "id": 1,
+ "name": "test project Code Carbon",
+ }
+ self.mock_api_client.create_experiment.return_value = {
+ "id": 1,
+ "name": "test experiment Code Carbon",
+ }
+
+ def test_app(self, MockApiClient):
+ result = self.runner.invoke(codecarbon, ["--version"])
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(__app_name__, result.stdout)
+ self.assertIn(__version__, result.stdout)
+
+ def test_init_aborted(self, MockApiClient):
+ result = self.runner.invoke(codecarbon, ["init"])
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn("Welcome to CodeCarbon configuration wizard", result.stdout)
+
+ def test_init_use_local(self, MockApiClient):
+ result = self.runner.invoke(codecarbon, ["init"], input="y")
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(
+ "CodeCarbon Initialization achieved, here is your experiment id:",
+ result.stdout,
+ )
+ self.assertIn("(from ./.codecarbon.config)", result.stdout)
+
+ def custom_questionary_side_effect(*args, **kwargs):
+ default_value = kwargs.get("default")
+ return MagicMock(return_value=default_value)
+
+ @patch("codecarbon.cli.main.Confirm.ask")
+ @patch("codecarbon.cli.main.questionary_prompt")
+ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
+ MockApiClient.return_value = self.mock_api_client
+ mock_prompt.side_effect = [
+ "Create New Organization",
+ "Create New Team",
+ "Create New Project",
+ "Create New Experiment",
+ ]
+ mock_confirm.side_effect = [False, False, False]
+ result = self.runner.invoke(
+ codecarbon,
+ ["init"],
+ input="n",
+ )
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(
+ "CodeCarbon Initialization achieved, here is your experiment id:",
+ result.stdout,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
From 17dbaf725764564aa108c9165d6f282132ea45ac Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sun, 3 Mar 2024 16:30:34 +0100
Subject: [PATCH 06/57] feat(cli): :sparkles: add more functionalities to
python API client and CLI
---
codecarbon/cli/cli_utils.py | 38 +++-
codecarbon/cli/main.py | 326 +++++++++++++++++++---------------
codecarbon/core/api_client.py | 39 +++-
tests/test_cli.py | 34 ++--
4 files changed, 263 insertions(+), 174 deletions(-)
diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py
index 3539c12cf..015bc1379 100644
--- a/codecarbon/cli/cli_utils.py
+++ b/codecarbon/cli/cli_utils.py
@@ -1,9 +1,20 @@
import configparser
from pathlib import Path
+from typing import Optional
-def get_api_endpoint():
- p = Path.cwd().resolve() / ".codecarbon.config"
+def get_config(path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
+ if p.exists():
+ config = configparser.ConfigParser()
+ config.read(str(p))
+ if "codecarbon" in config.sections():
+ d = dict(config["codecarbon"])
+ return d
+
+
+def get_api_endpoint(path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
if p.exists():
config = configparser.ConfigParser()
config.read(str(p))
@@ -14,8 +25,8 @@ def get_api_endpoint():
return "https://api.codecarbon.io"
-def get_existing_local_exp_id():
- p = Path.cwd().resolve() / ".codecarbon.config"
+def get_existing_local_exp_id(path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
if p.exists():
config = configparser.ConfigParser()
config.read(str(p))
@@ -25,8 +36,9 @@ def get_existing_local_exp_id():
return d["experiment_id"]
-def write_local_exp_id(exp_id):
- p = Path.cwd().resolve() / ".codecarbon.config"
+def write_local_exp_id(exp_id, path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
+
config = configparser.ConfigParser()
if p.exists():
config.read(str(p))
@@ -37,3 +49,17 @@ def write_local_exp_id(exp_id):
with p.open("w") as f:
config.write(f)
+
+
+def overwrite_local_config(config_name, value, path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
+
+ config = configparser.ConfigParser()
+ if p.exists():
+ config.read(str(p))
+ if "codecarbon" not in config.sections():
+ config.add_section("codecarbon")
+
+ config["codecarbon"][config_name] = value
+ with p.open("w") as f:
+ config.write(f)
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index d7dc28f85..2c4a35127 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -2,17 +2,18 @@
import time
from typing import Optional
-import click
import questionary
import typer
+from rich import print
from rich.prompt import Confirm
from typing_extensions import Annotated
from codecarbon import __app_name__, __version__
from codecarbon.cli.cli_utils import (
get_api_endpoint,
+ get_config,
get_existing_local_exp_id,
- write_local_exp_id,
+ overwrite_local_config,
)
from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone
from codecarbon.core.schemas import (
@@ -49,174 +50,202 @@ def main(
return
-@codecarbon.command("init", short_help="Create an experiment id in a public project.")
-def init():
+def show_config():
+ d = get_config()
+ api_endpoint = get_api_endpoint()
+ api = ApiClient(endpoint_url=api_endpoint)
+ try:
+ org = api.get_organization(d["organization_id"])
+ team = api.get_team(d["team_id"])
+ project = api.get_project(d["project_id"])
+ experiment = api.get_experiment(d["experiment_id"])
+ print(
+ "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n "
+ )
+ print("Experiment: \n ")
+ print(experiment)
+ print("Project: \n")
+ print(project)
+ print("Team: \n")
+ print(team)
+ print("Organization: \n")
+ print(org)
+ except:
+ raise ValueError(
+ "Your configuration is invalid, please run `codecarbon config --init` first!"
+ )
+
+
+@codecarbon.command("config", short_help="Generate or show config")
+def config(
+ init: Annotated[
+ bool, typer.Option(help="Initialise or modify configuration")
+ ] = None,
+ show: Annotated[bool, typer.Option(help="Show configuration details")] = None,
+):
"""
Initialize CodeCarbon, this will prompt you for configuration of Organisation/Team/Project/Experiment.
"""
- typer.echo("Welcome to CodeCarbon configuration wizard")
- use_config = Confirm.ask(
- "Use existing /.codecarbonconfig to configure ?",
- )
- if use_config is True:
- experiment_id = get_existing_local_exp_id()
- else:
- experiment_id = None
- new_local = False
- if experiment_id is None:
- api_endpoint = get_api_endpoint()
- api_endpoint = typer.prompt(
- f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
- type=str,
- default=api_endpoint,
- )
- api = ApiClient(endpoint_url=api_endpoint)
- organizations = api.get_list_organizations()
- org = questionary_prompt(
- "Pick existing organization from list or Create new organization ?",
- [org["name"] for org in organizations] + ["Create New Organization"],
- default="Create New Organization",
+ if show:
+ show_config()
+ elif init:
+ typer.echo("Welcome to CodeCarbon configuration wizard")
+ use_config = questionary_prompt(
+ "Use existing /.codecarbonconfig to configure or overwrite ? ",
+ ["/.codecarbonconfig", "Create New Config"],
+ default="/.codecarbonconfig",
)
- if org == "Create New Organization":
- org_name = typer.prompt(
- "Organization name", default="Code Carbon user test"
+ if use_config == "/.codecarbonconfig":
+ typer.echo("Using existing config file :")
+ show_config()
+ pass
+
+ else:
+ typer.echo("Creating new config file")
+ api_endpoint = get_api_endpoint()
+ api_endpoint = typer.prompt(
+ f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
+ type=str,
+ default=api_endpoint,
)
- org_description = typer.prompt(
- "Organization description", default="Code Carbon user test"
+ api = ApiClient(endpoint_url=api_endpoint)
+ organizations = api.get_list_organizations()
+ org = questionary_prompt(
+ "Pick existing organization from list or Create new organization ?",
+ [org["name"] for org in organizations] + ["Create New Organization"],
+ default="Create New Organization",
)
- if org_name in organizations:
- typer.echo(
- f"Organization {org_name} already exists, using it for this experiment."
+
+ if org == "Create New Organization":
+ org_name = typer.prompt(
+ "Organization name", default="Code Carbon user test"
)
+ org_description = typer.prompt(
+ "Organization description", default="Code Carbon user test"
+ )
+ if org_name in organizations:
+ typer.echo(
+ f"Organization {org_name} already exists, using it for this experiment."
+ )
+ organization = [
+ orga for orga in organizations if orga["name"] == org
+ ][0]
+ else:
+ organization_create = OrganizationCreate(
+ name=org_name,
+ description=org_description,
+ )
+ organization = api.create_organization(
+ organization=organization_create
+ )
+ typer.echo(f"Created organization : {organization}")
+ else:
organization = [orga for orga in organizations if orga["name"] == org][
0
]
- else:
- organization_create = OrganizationCreate(
- name=org_name,
- description=org_description,
- )
- organization = api.create_organization(organization=organization_create)
- typer.echo(f"Created organization : {organization}")
- else:
- organization = [orga for orga in organizations if orga["name"] == org][0]
- teams = api.list_teams_from_organization(organization["id"])
+ overwrite_local_config("organization_id", organization["id"])
+ teams = api.list_teams_from_organization(organization["id"])
- team = questionary_prompt(
- "Pick existing team from list or create new team in organization ?",
- [team["name"] for team in teams] + ["Create New Team"],
- default="Create New Team",
- )
- if team == "Create New Team":
- team_name = typer.prompt("Team name", default="Code Carbon user test")
- team_description = typer.prompt(
- "Team description", default="Code Carbon user test"
- )
- team_create = TeamCreate(
- name=team_name,
- description=team_description,
- organization_id=organization["id"],
- )
- team = api.create_team(
- team=team_create,
- )
- typer.echo(f"Created team : {team}")
- else:
- team = [t for t in teams if t["name"] == team][0]
- projects = api.list_projects_from_team(team["id"])
- project = questionary_prompt(
- "Pick existing project from list or Create new project ?",
- [project["name"] for project in projects] + ["Create New Project"],
- default="Create New Project",
- )
- if project == "Create New Project":
- project_name = typer.prompt("Project name", default="Code Carbon user test")
- project_description = typer.prompt(
- "Project description", default="Code Carbon user test"
- )
- project_create = ProjectCreate(
- name=project_name,
- description=project_description,
- team_id=team["id"],
+ team = questionary_prompt(
+ "Pick existing team from list or create new team in organization ?",
+ [team["name"] for team in teams] + ["Create New Team"],
+ default="Create New Team",
)
- project = api.create_project(project=project_create)
- typer.echo(f"Created project : {project}")
- else:
- project = [p for p in projects if p["name"] == project][0]
+ if team == "Create New Team":
+ team_name = typer.prompt("Team name", default="Code Carbon user test")
+ team_description = typer.prompt(
+ "Team description", default="Code Carbon user test"
+ )
+ team_create = TeamCreate(
+ name=team_name,
+ description=team_description,
+ organization_id=organization["id"],
+ )
+ team = api.create_team(
+ team=team_create,
+ )
+ typer.echo(f"Created team : {team}")
+ else:
+ team = [t for t in teams if t["name"] == team][0]
+ overwrite_local_config("team_id", team["id"])
- experiments = api.list_experiments_from_project(project["id"])
- experiment = questionary_prompt(
- "Pick existing experiment from list or Create new experiment ?",
- [experiment["name"] for experiment in experiments]
- + ["Create New Experiment"],
- default="Create New Experiment",
- )
- if experiment == "Create New Experiment":
- typer.echo("Creating new experiment")
- exp_name = typer.prompt(
- "Experiment name :", default="Code Carbon user test"
- )
- exp_description = typer.prompt(
- "Experiment description :",
- default="Code Carbon user test ",
+ projects = api.list_projects_from_team(team["id"])
+ project = questionary_prompt(
+ "Pick existing project from list or Create new project ?",
+ [project["name"] for project in projects] + ["Create New Project"],
+ default="Create New Project",
)
-
- exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
- if exp_on_cloud is True:
- cloud_provider = typer.prompt(
- "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
+ if project == "Create New Project":
+ project_name = typer.prompt(
+ "Project name", default="Code Carbon user test"
)
- cloud_region = typer.prompt(
- "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
+ project_description = typer.prompt(
+ "Project description", default="Code Carbon user test"
)
+ project_create = ProjectCreate(
+ name=project_name,
+ description=project_description,
+ team_id=team["id"],
+ )
+ project = api.create_project(project=project_create)
+ typer.echo(f"Created project : {project}")
else:
- cloud_provider = None
- cloud_region = None
- country_name = typer.prompt("Country name :", default="France")
- country_iso_code = typer.prompt("Country ISO code :", default="FRA")
- region = typer.prompt("Region :", default="france")
- experiment_create = ExperimentCreate(
- timestamp=get_datetime_with_timezone(),
- name=exp_name,
- description=exp_description,
- on_cloud=exp_on_cloud,
- project_id=project["id"],
- country_name=country_name,
- country_iso_code=country_iso_code,
- region=region,
- cloud_provider=cloud_provider,
- cloud_region=cloud_region,
+ project = [p for p in projects if p["name"] == project][0]
+ overwrite_local_config("project_id", project["id"])
+
+ experiments = api.list_experiments_from_project(project["id"])
+ experiment = questionary_prompt(
+ "Pick existing experiment from list or Create new experiment ?",
+ [experiment["name"] for experiment in experiments]
+ + ["Create New Experiment"],
+ default="Create New Experiment",
)
- experiment_id = api.add_experiment(experiment=experiment_create)
+ if experiment == "Create New Experiment":
+ typer.echo("Creating new experiment")
+ exp_name = typer.prompt(
+ "Experiment name :", default="Code Carbon user test"
+ )
+ exp_description = typer.prompt(
+ "Experiment description :",
+ default="Code Carbon user test ",
+ )
- else:
- experiment_id = [e for e in experiments if e["name"] == experiment][0]["id"]
+ exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
+ if exp_on_cloud is True:
+ cloud_provider = typer.prompt(
+ "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
+ )
+ cloud_region = typer.prompt(
+ "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
+ )
+ else:
+ cloud_provider = None
+ cloud_region = None
+ country_name = typer.prompt("Country name :", default="France")
+ country_iso_code = typer.prompt("Country ISO code :", default="FRA")
+ region = typer.prompt("Region :", default="france")
+ experiment_create = ExperimentCreate(
+ timestamp=get_datetime_with_timezone(),
+ name=exp_name,
+ description=exp_description,
+ on_cloud=exp_on_cloud,
+ project_id=project["id"],
+ country_name=country_name,
+ country_iso_code=country_iso_code,
+ region=region,
+ cloud_provider=cloud_provider,
+ cloud_region=cloud_region,
+ )
+ experiment_id = api.create_experiment(experiment=experiment_create)
- write_to_config = Confirm.ask(
- "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
- )
+ else:
+ experiment_id = [e for e in experiments if e["name"] == experiment][0][
+ "id"
+ ]
- if write_to_config is True:
- write_local_exp_id(experiment_id)
- new_local = True
- typer.echo(
- "\nCodeCarbon Initialization achieved, here is your experiment id:\n"
- + click.style(f"{experiment_id}", fg="bright_green")
- + (
- ""
- if new_local
- else " (from "
- + click.style("./.codecarbon.config", fg="bright_blue")
- + ")\n"
- )
- )
- if new_local:
- click.echo(
- "\nCodeCarbon added this id to your local config: "
- + click.style("./.codecarbon.config", fg="bright_blue")
- + "\n"
- )
+ overwrite_local_config("experiment_id", experiment_id["id"])
+ show_config()
@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.")
@@ -264,3 +293,4 @@ def questionary_prompt(prompt, list_options, default):
if __name__ == "__main__":
codecarbon()
+ codecarbon()
diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py
index 76ef82536..41d40ebaa 100644
--- a/codecarbon/core/api_client.py
+++ b/codecarbon/core/api_client.py
@@ -97,9 +97,7 @@ def list_teams_from_organization(self, organization_id):
"""
List all teams
"""
- url = (
- self.url + "/teams/organization/" + organization_id
- ) # TODO : check if this is the right url
+ url = self.url + "/teams/organization/" + organization_id
r = requests.get(url=url, timeout=2)
if r.status_code != 200:
self._log_error(url, {}, r)
@@ -118,6 +116,17 @@ def create_team(self, team: TeamCreate):
return None
return r.json()
+ def get_team(self, team_id):
+ """
+ Get a team
+ """
+ url = self.url + "/team/" + team_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
def list_projects_from_team(self, team_id):
"""
List all projects
@@ -141,6 +150,17 @@ def create_project(self, project: ProjectCreate):
return None
return r.json()
+ def get_project(self, project_id):
+ """
+ Get a project
+ """
+ url = self.url + "/project/" + project_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
def add_emission(self, carbon_emission: dict):
assert self.experiment_id is not None
self._previous_call = time.time()
@@ -254,7 +274,7 @@ def set_experiment(self, experiment_id: str):
"""
self.experiment_id = experiment_id
- def add_experiment(self, experiment: ExperimentCreate):
+ def create_experiment(self, experiment: ExperimentCreate):
"""
Create an experiment, used by the CLI, not the package.
::experiment:: The experiment to create.
@@ -268,6 +288,17 @@ def add_experiment(self, experiment: ExperimentCreate):
self.experiment_id = r.json()["id"]
return self.experiment_id
+ def get_experiment(self, experiment_id):
+ """
+ Get an experiment by id
+ """
+ url = self.url + "/experiment/" + experiment_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
def _log_error(self, url, payload, response):
if len(payload) > 0:
logger.error(
diff --git a/tests/test_cli.py b/tests/test_cli.py
index a6e3d6db0..7d39b0b4b 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -15,32 +15,32 @@ def setUp(self):
self.runner = CliRunner()
self.mock_api_client = MagicMock()
self.mock_api_client.get_list_organizations.return_value = [
- {"id": 1, "name": "test org Code Carbon"}
+ {"id": "1", "name": "test org Code Carbon"}
]
self.mock_api_client.list_teams_from_organization.return_value = [
- {"id": 1, "name": "test team Code Carbon"}
+ {"id": "1", "name": "test team Code Carbon"}
]
self.mock_api_client.list_projects_from_team.return_value = [
- {"id": 1, "name": "test project Code Carbon"}
+ {"id": "1", "name": "test project Code Carbon"}
]
self.mock_api_client.list_experiments_from_project.return_value = [
- {"id": 1, "name": "test experiment Code Carbon"}
+ {"id": "1", "name": "test experiment Code Carbon"}
]
self.mock_api_client.create_organization.return_value = {
- "id": 1,
+ "id": "1",
"name": "test org Code Carbon",
}
self.mock_api_client.create_team.return_value = {
- "id": 1,
+ "id": "1",
"name": "test team Code Carbon",
}
self.mock_api_client.create_project.return_value = {
- "id": 1,
+ "id": "1",
"name": "test project Code Carbon",
}
self.mock_api_client.create_experiment.return_value = {
- "id": 1,
+ "id": "1",
"name": "test experiment Code Carbon",
}
@@ -51,18 +51,19 @@ def test_app(self, MockApiClient):
self.assertIn(__version__, result.stdout)
def test_init_aborted(self, MockApiClient):
- result = self.runner.invoke(codecarbon, ["init"])
+ result = self.runner.invoke(codecarbon, ["config", "--init"])
self.assertEqual(result.exit_code, 1)
self.assertIn("Welcome to CodeCarbon configuration wizard", result.stdout)
- def test_init_use_local(self, MockApiClient):
- result = self.runner.invoke(codecarbon, ["init"], input="y")
+ @patch("codecarbon.cli.main.questionary_prompt")
+ def test_init_use_local(self, mock_prompt, MockApiClient):
+ mock_prompt.return_value = "/.codecarbonconfig"
+ result = self.runner.invoke(codecarbon, ["config", "--init"], input="y")
self.assertEqual(result.exit_code, 0)
self.assertIn(
- "CodeCarbon Initialization achieved, here is your experiment id:",
+ "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n ",
result.stdout,
)
- self.assertIn("(from ./.codecarbon.config)", result.stdout)
def custom_questionary_side_effect(*args, **kwargs):
default_value = kwargs.get("default")
@@ -73,6 +74,7 @@ def custom_questionary_side_effect(*args, **kwargs):
def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
MockApiClient.return_value = self.mock_api_client
mock_prompt.side_effect = [
+ "Create New Config",
"Create New Organization",
"Create New Team",
"Create New Project",
@@ -81,12 +83,12 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
mock_confirm.side_effect = [False, False, False]
result = self.runner.invoke(
codecarbon,
- ["init"],
- input="n",
+ ["config", "--init"],
+ input="y",
)
self.assertEqual(result.exit_code, 0)
self.assertIn(
- "CodeCarbon Initialization achieved, here is your experiment id:",
+ "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n ",
result.stdout,
)
From bb1be2d523ecf4ed717fcadebd7c4a7926223ced Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sun, 3 Mar 2024 16:31:46 +0100
Subject: [PATCH 07/57] feat(core): :sparkles: allows picking up API endpoint
from conf file for dashboard
---
dashboard/data/data_loader.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/dashboard/data/data_loader.py b/dashboard/data/data_loader.py
index 8fa7fc3af..9f6e6d07f 100644
--- a/dashboard/data/data_loader.py
+++ b/dashboard/data/data_loader.py
@@ -8,10 +8,16 @@
import requests
+from codecarbon.core.config import get_hierarchical_config
+
API_PATH = os.getenv("CODECARBON_API_URL")
if API_PATH is None:
+ conf = get_hierarchical_config()
+ if "api_endpoint" in conf:
+ API_PATH = conf["api_endpoint"]
# API_PATH = "http://carbonserver.cleverapps.io"
- API_PATH = "https://api.codecarbon.io"
+ else:
+ API_PATH = "https://api.codecarbon.io"
# API_PATH = "http://localhost:8008" # export CODECARBON_API_URL=http://localhost:8008
# API_PATH = "http://carbonserver.cleverapps.io"
USER = "jessica"
@@ -218,3 +224,4 @@ def load_run_infos(run_id: str, **kwargs) -> tuple:
"""
path = f"{API_PATH}/run/{run_id}"
return path, kwargs
+ return path, kwargs
From 0994802e2fca0094df718b665f77f87dd63aa1f9 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 10:52:54 +0100
Subject: [PATCH 08/57] feat(cli): :sparkles: allow to create a new custom
configuration file anywhere
---
codecarbon/cli/cli_utils.py | 1 +
codecarbon/cli/main.py | 19 +++++++++++++++++--
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py
index 015bc1379..727356bc7 100644
--- a/codecarbon/cli/cli_utils.py
+++ b/codecarbon/cli/cli_utils.py
@@ -5,6 +5,7 @@
def get_config(path: Optional[Path] = None):
p = path or Path.cwd().resolve() / ".codecarbon.config"
+
if p.exists():
config = configparser.ConfigParser()
config.read(str(p))
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 2c4a35127..cdb727e1e 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -1,5 +1,6 @@
import sys
import time
+from pathlib import Path
from typing import Optional
import questionary
@@ -98,11 +99,23 @@ def config(
if use_config == "/.codecarbonconfig":
typer.echo("Using existing config file :")
+ file_path = Path("/.codecarbonconfig")
show_config()
pass
else:
typer.echo("Creating new config file")
+ file_path = typer.prompt(
+ "Where do you want to put your config file ?",
+ type=str,
+ default="/.codecarbonconfig",
+ )
+ file_path = Path(file_path)
+ if not file_path.parent.exists():
+ Confirm.ask(
+ "Parent folder does not exist do you want to create it (and parents) ?"
+ )
+
api_endpoint = get_api_endpoint()
api_endpoint = typer.prompt(
f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
@@ -144,7 +157,9 @@ def config(
organization = [orga for orga in organizations if orga["name"] == org][
0
]
- overwrite_local_config("organization_id", organization["id"])
+ overwrite_local_config(
+ "organization_id", organization["id"], file_path=file_path
+ )
teams = api.list_teams_from_organization(organization["id"])
team = questionary_prompt(
@@ -244,7 +259,7 @@ def config(
"id"
]
- overwrite_local_config("experiment_id", experiment_id["id"])
+ overwrite_local_config("experiment_id", experiment_id)
show_config()
From 8b2c39bff5d8ab9e4df05511b528e23b751e0d42 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Tue, 12 Dec 2023 09:10:27 +0100
Subject: [PATCH 09/57] feat(cli): :sparkles: Rework CLI
prompts for Organisation Teams Project and Experiment Creations , uses typer instead of click
---
codecarbon/cli/main.py | 242 ++++++++++++++++++++++++++++++++++-------
1 file changed, 204 insertions(+), 38 deletions(-)
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 9031703f6..910acab5c 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -1,47 +1,205 @@
import sys
import time
+from typing import Optional
import click
+import questionary
+import typer
+from rich.prompt import Confirm
+from typing_extensions import Annotated
-from codecarbon import EmissionsTracker
+from codecarbon import __app_name__, __version__
from codecarbon.cli.cli_utils import (
get_api_endpoint,
get_existing_local_exp_id,
write_local_exp_id,
)
from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone
-from codecarbon.core.schemas import ExperimentCreate
+from codecarbon.core.schemas import (
+ ExperimentCreate,
+ OrganizationCreate,
+ ProjectCreate,
+ TeamCreate,
+)
+from codecarbon.emissions_tracker import EmissionsTracker
DEFAULT_PROJECT_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"
+DEFAULT_ORGANIzATION_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"
+
+codecarbon = typer.Typer()
+
+def _version_callback(value: bool) -> None:
+ if value:
+ typer.echo(f"{__app_name__} v{__version__}")
+ raise typer.Exit()
-@click.group()
-def codecarbon():
- pass
+
+@codecarbon.callback()
+def main(
+ version: Optional[bool] = typer.Option(
+ None,
+ "--version",
+ "-v",
+ help="Show the application's version and exit.",
+ callback=_version_callback,
+ is_eager=True,
+ ),
+) -> None:
+ return
@codecarbon.command("init", short_help="Create an experiment id in a public project.")
def init():
- experiment_id = get_existing_local_exp_id()
+ """
+ Initialize CodeCarbon, this will prompt you for configuration of Organisation/Team/Project/Experiment.
+ """
+ typer.echo("Welcome to CodeCarbon configuration wizard")
+ use_config = Confirm.ask(
+ "Use existing /.codecarbonconfig to configure ?",
+ )
+ if use_config is True:
+ experiment_id = get_existing_local_exp_id()
+ else:
+ experiment_id = None
new_local = False
if experiment_id is None:
- api = ApiClient(endpoint_url=get_api_endpoint())
- experiment = ExperimentCreate(
- timestamp=get_datetime_with_timezone(),
- name="Code Carbon user test",
- description="Code Carbon user test with default project",
- on_cloud=False,
- project_id=DEFAULT_PROJECT_ID,
- country_name="France",
- country_iso_code="FRA",
- region="france",
+ api_endpoint = get_api_endpoint()
+ api_endpoint = typer.prompt(
+ f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
+ type=str,
+ default=api_endpoint,
)
- experiment_id = api.add_experiment(experiment)
+ api = ApiClient(endpoint_url=api_endpoint)
+ organizations = api.get_list_organizations()
+ org = questionary.select(
+ "Pick existing organization from list or Create new organization ?",
+ [org["name"] for org in organizations] + ["Create New Organization"],
+ ).ask()
+
+ if org == "Create New Organization":
+ org_name = typer.prompt(
+ "Organization name", default="Code Carbon user test"
+ )
+ org_description = typer.prompt(
+ "Organization description", default="Code Carbon user test"
+ )
+ if org_name in organizations:
+ typer.echo(
+ f"Organization {org_name} already exists, using it for this experiment."
+ )
+ organization = [orga for orga in organizations if orga["name"] == org][
+ 0
+ ]
+ else:
+ organization_create = OrganizationCreate(
+ name=org_name,
+ description=org_description,
+ )
+ organization = api.create_organization(organization=organization_create)
+ typer.echo(f"Created organization : {organization}")
+ else:
+ organization = [orga for orga in organizations if orga["name"] == org][0]
+ teams = api.list_teams_from_organization(organization["id"])
+
+ team = questionary.select(
+ "Pick existing team from list or create new team in organization ?",
+ [team["name"] for team in teams] + ["Create New Team"],
+ ).ask()
+ if team == "Create New Team":
+ team_name = typer.prompt("Team name", default="Code Carbon user test")
+ team_description = typer.prompt(
+ "Team description", default="Code Carbon user test"
+ )
+ team_create = TeamCreate(
+ name=team_name,
+ description=team_description,
+ organization_id=organization["id"],
+ )
+ team = api.create_team(
+ team=team_create,
+ )
+ typer.echo(f"Created team : {team}")
+ else:
+ team = [t for t in teams if t["name"] == team][0]
+ projects = api.list_projects_from_team(team["id"])
+ project = questionary.select(
+ "Pick existing project from list or Create new project ?",
+ [project["name"] for project in projects] + ["Create New Project"],
+ default="Create New Project",
+ ).ask()
+ if project == "Create New Project":
+ project_name = typer.prompt("Project name", default="Code Carbon user test")
+ project_description = typer.prompt(
+ "Project description", default="Code Carbon user test"
+ )
+ project_create = ProjectCreate(
+ name=project_name,
+ description=project_description,
+ team_id=team["id"],
+ )
+ project = api.create_project(project=project_create)
+ typer.echo(f"Created project : {project}")
+ else:
+ project = [p for p in projects if p["name"] == project][0]
+
+ experiments = api.list_experiments_from_project(project["id"])
+ experiment = questionary.select(
+ "Pick existing experiment from list or Create new experiment ?",
+ [experiment["name"] for experiment in experiments]
+ + ["Create New Experiment"],
+ default="Create New Experiment",
+ ).ask()
+ if experiment == "Create New Experiment":
+ typer.echo("Creating new experiment")
+ exp_name = typer.prompt(
+ "Experiment name :", default="Code Carbon user test"
+ )
+ exp_description = typer.prompt(
+ "Experiment description :",
+ default="Code Carbon user test",
+ )
+
+ exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
+ if exp_on_cloud is True:
+ cloud_provider = typer.prompt(
+ "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
+ )
+ cloud_region = typer.prompt(
+ "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
+ )
+ else:
+ cloud_provider = None
+ cloud_region = None
+ country_name = typer.prompt("Country name :", default="France")
+ country_iso_code = typer.prompt("Country ISO code :", default="FRA")
+ region = typer.prompt("Region :", default="france")
+ experiment_create = ExperimentCreate(
+ timestamp=get_datetime_with_timezone(),
+ name=exp_name,
+ description=exp_description,
+ on_cloud=exp_on_cloud,
+ project_id=project["id"],
+ country_name=country_name,
+ country_iso_code=country_iso_code,
+ region=region,
+ cloud_provider=cloud_provider,
+ cloud_region=cloud_region,
+ )
+ experiment_id = api.add_experiment(experiment=experiment_create)
+
+ else:
+ experiment_id = [e for e in experiments if e["name"] == experiment][0]["id"]
+
+ write_to_config = Confirm.ask(
+ "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
+ )
+
+ if write_to_config is True:
write_local_exp_id(experiment_id)
new_local = True
-
- click.echo(
- "\nWelcome to CodeCarbon, here is your experiment id:\n"
+ typer.echo(
+ "\nCodeCarbon Initialization achieved, here is your experiment id:\n"
+ click.style(f"{experiment_id}", fg="bright_green")
+ (
""
@@ -53,32 +211,36 @@ def init():
)
if new_local:
click.echo(
- "\nCodeCarbon automatically added this id to your local config: "
+ "\nCodeCarbon added this id to your local config: "
+ click.style("./.codecarbon.config", fg="bright_blue")
+ "\n"
)
-@codecarbon.command(
- "monitor", short_help="Run an infinite loop to monitor this machine."
-)
-@click.option(
- "--measure_power_secs", default=10, help="Interval between two measures. (10)"
-)
-@click.option(
- "--api_call_interval",
- default=30,
- help="Number of measures before calling API. (30).",
-)
-@click.option(
- "--api/--no-api", default=True, help="Choose to call Code Carbon API or not. (yes)"
-)
-def monitor(measure_power_secs, api_call_interval, api):
+@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.")
+def monitor(
+ measure_power_secs: Annotated[
+ int, typer.Argument(help="Interval between two measures.")
+ ] = 10,
+ api_call_interval: Annotated[
+ int, typer.Argument(help="Number of measures between API calls.")
+ ] = 30,
+ api: Annotated[
+ bool, typer.Option(help="Choose to call Code Carbon API or not")
+ ] = True,
+):
+ """Monitor your machine's carbon emissions.
+
+ Args:
+ measure_power_secs (Annotated[int, typer.Argument, optional): Interval between two measures. Defaults to 10.
+ api_call_interval (Annotated[int, typer.Argument, optional): Number of measures before calling API. Defaults to 30.
+ api (Annotated[bool, typer.Option, optional): Choose to call Code Carbon API or not. Defaults to True.
+ """
experiment_id = get_existing_local_exp_id()
if api and experiment_id is None:
- click.echo("ERROR: No experiment id, call 'codecarbon init' first.")
+ typer.echo("ERROR: No experiment id, call 'codecarbon init' first.")
sys.exit(1)
- click.echo("CodeCarbon is going in an infinite loop to monitor this machine.")
+ typer.echo("CodeCarbon is going in an infinite loop to monitor this machine.")
with EmissionsTracker(
measure_power_secs=measure_power_secs,
api_call_interval=api_call_interval,
@@ -87,3 +249,7 @@ def monitor(measure_power_secs, api_call_interval, api):
# Infinite loop
while True:
time.sleep(300)
+
+
+if __name__ == "__main__":
+ codecarbon()
From 13068790d7ae0e8edb4356aae5c644f9cefef322 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Tue, 12 Dec 2023 09:12:04 +0100
Subject: [PATCH 10/57] feat(core): :sparkles: add Organisation Team and
Project to APi Client
allows to create and list organisations, projects and teams from the CLI
---
codecarbon/core/api_client.py | 117 ++++++++++++++++++++++++++++++++--
codecarbon/core/schemas.py | 44 +++++++++++++
2 files changed, 157 insertions(+), 4 deletions(-)
diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py
index 15539d8b6..ca89f27aa 100644
--- a/codecarbon/core/api_client.py
+++ b/codecarbon/core/api_client.py
@@ -14,7 +14,14 @@
import arrow
import requests
-from codecarbon.core.schemas import EmissionCreate, ExperimentCreate, RunCreate
+from codecarbon.core.schemas import (
+ EmissionCreate,
+ ExperimentCreate,
+ OrganizationCreate,
+ ProjectCreate,
+ RunCreate,
+ TeamCreate,
+)
from codecarbon.external.logger import logger
# from codecarbon.output import EmissionsData
@@ -53,6 +60,88 @@ def __init__(
if self.experiment_id is not None:
self._create_run(self.experiment_id)
+ def get_list_organizations(self):
+ """
+ List all organizations
+ """
+ url = self.url + "/organizations"
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def create_organization(self, organization: OrganizationCreate):
+ """
+ Create an organization
+ """
+ payload = dataclasses.asdict(organization)
+ url = self.url + "/organization"
+ r = requests.post(url=url, json=payload, timeout=2)
+ if r.status_code != 201:
+ self._log_error(url, payload, r)
+ return None
+ return r.json()
+
+ def get_organization(self, organization_id):
+ """
+ Get an organization
+ """
+ url = self.url + "/organization/" + organization_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def list_teams_from_organization(self, organization_id):
+ """
+ List all teams
+ """
+ url = (
+ self.url + "/teams/organization/" + organization_id
+ ) # TODO : check if this is the right url
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def create_team(self, team: TeamCreate):
+ """
+ Create a team
+ """
+ payload = dataclasses.asdict(team)
+ url = self.url + "/team"
+ r = requests.post(url=url, json=payload, timeout=2)
+ if r.status_code != 201:
+ self._log_error(url, payload, r)
+ return None
+ return r.json()
+
+ def list_projects_from_team(self, team_id):
+ """
+ List all projects
+ """
+ url = self.url + "/projects/team/" + team_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def create_project(self, project: ProjectCreate):
+ """
+ Create a project
+ """
+ payload = dataclasses.asdict(project)
+ url = self.url + "/project"
+ r = requests.post(url=url, json=payload, timeout=2)
+ if r.status_code != 201:
+ self._log_error(url, payload, r)
+ return None
+ return r.json()
+
def add_emission(self, carbon_emission: dict):
assert self.experiment_id is not None
self._previous_call = time.time()
@@ -149,6 +238,23 @@ def _create_run(self, experiment_id):
except Exception as e:
logger.error(e, exc_info=True)
+ def list_experiments_from_project(self, project_id: str):
+ """
+ List all experiments for a project
+ """
+ url = self.url + "/experiments/project/" + project_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
+ def set_experiment(self, experiment_id: str):
+ """
+ Set the experiment id
+ """
+ self.experiment_id = experiment_id
+
def add_experiment(self, experiment: ExperimentCreate):
"""
Create an experiment, used by the CLI, not the package.
@@ -164,9 +270,12 @@ def add_experiment(self, experiment: ExperimentCreate):
return self.experiment_id
def _log_error(self, url, payload, response):
- logger.error(
- f"ApiClient Error when calling the API on {url} with : {json.dumps(payload)}"
- )
+ if len(payload) > 0:
+ logger.error(
+ f"ApiClient Error when calling the API on {url} with : {json.dumps(payload)}"
+ )
+ else:
+ logger.error(f"ApiClient Error when calling the API on {url}")
logger.error(
f"ApiClient API return http code {response.status_code} and answer : {response.text}"
)
diff --git a/codecarbon/core/schemas.py b/codecarbon/core/schemas.py
index 2c6043d8d..d8969dfa1 100644
--- a/codecarbon/core/schemas.py
+++ b/codecarbon/core/schemas.py
@@ -79,3 +79,47 @@ class ExperimentCreate(ExperimentBase):
class Experiment(ExperimentBase):
id: str
+
+
+@dataclass
+class OrganizationBase:
+ name: str
+ description: str
+
+
+class OrganizationCreate(OrganizationBase):
+ pass
+
+
+class Organization(OrganizationBase):
+ id: str
+
+
+@dataclass
+class TeamBase:
+ name: str
+ description: str
+ organization_id: str
+
+
+class TeamCreate(TeamBase):
+ pass
+
+
+class Team(TeamBase):
+ id: str
+
+
+@dataclass
+class ProjectBase:
+ name: str
+ description: str
+ team_id: str
+
+
+class ProjectCreate(ProjectBase):
+ pass
+
+
+class Project(ProjectBase):
+ id: str
From cd7afe8735ddbf651b697e4bbb420013eab276bf Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Tue, 12 Dec 2023 09:12:41 +0100
Subject: [PATCH 11/57] build: :arrow_up: add new dependencies for cli
---
codecarbon/__init__.py | 1 +
setup.py | 3 +++
2 files changed, 4 insertions(+)
diff --git a/codecarbon/__init__.py b/codecarbon/__init__.py
index 23a5aff39..f602f2635 100644
--- a/codecarbon/__init__.py
+++ b/codecarbon/__init__.py
@@ -10,3 +10,4 @@
)
__all__ = ["EmissionsTracker", "OfflineEmissionsTracker", "track_emissions"]
+__app_name__ = "codecarbon"
diff --git a/setup.py b/setup.py
index 37b607519..52308f51a 100644
--- a/setup.py
+++ b/setup.py
@@ -12,6 +12,9 @@
"py-cpuinfo",
"rapidfuzz",
"click",
+ "typer",
+ "questionary",
+ "rich",
"prometheus_client",
]
From b52edabdf9479098b26367eed0db654bc25e8fc8 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sat, 13 Jan 2024 17:34:47 +0100
Subject: [PATCH 12/57] fix(cli): :art: add a questionary prompt function
allows easier testing
---
codecarbon/cli/main.py | 41 ++++++++++++++++++++++++++---------------
1 file changed, 26 insertions(+), 15 deletions(-)
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 910acab5c..d7dc28f85 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -72,10 +72,11 @@ def init():
)
api = ApiClient(endpoint_url=api_endpoint)
organizations = api.get_list_organizations()
- org = questionary.select(
+ org = questionary_prompt(
"Pick existing organization from list or Create new organization ?",
[org["name"] for org in organizations] + ["Create New Organization"],
- ).ask()
+ default="Create New Organization",
+ )
if org == "Create New Organization":
org_name = typer.prompt(
@@ -102,10 +103,11 @@ def init():
organization = [orga for orga in organizations if orga["name"] == org][0]
teams = api.list_teams_from_organization(organization["id"])
- team = questionary.select(
+ team = questionary_prompt(
"Pick existing team from list or create new team in organization ?",
[team["name"] for team in teams] + ["Create New Team"],
- ).ask()
+ default="Create New Team",
+ )
if team == "Create New Team":
team_name = typer.prompt("Team name", default="Code Carbon user test")
team_description = typer.prompt(
@@ -123,11 +125,11 @@ def init():
else:
team = [t for t in teams if t["name"] == team][0]
projects = api.list_projects_from_team(team["id"])
- project = questionary.select(
+ project = questionary_prompt(
"Pick existing project from list or Create new project ?",
[project["name"] for project in projects] + ["Create New Project"],
default="Create New Project",
- ).ask()
+ )
if project == "Create New Project":
project_name = typer.prompt("Project name", default="Code Carbon user test")
project_description = typer.prompt(
@@ -144,12 +146,12 @@ def init():
project = [p for p in projects if p["name"] == project][0]
experiments = api.list_experiments_from_project(project["id"])
- experiment = questionary.select(
+ experiment = questionary_prompt(
"Pick existing experiment from list or Create new experiment ?",
[experiment["name"] for experiment in experiments]
+ ["Create New Experiment"],
default="Create New Experiment",
- ).ask()
+ )
if experiment == "Create New Experiment":
typer.echo("Creating new experiment")
exp_name = typer.prompt(
@@ -157,7 +159,7 @@ def init():
)
exp_description = typer.prompt(
"Experiment description :",
- default="Code Carbon user test",
+ default="Code Carbon user test ",
)
exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
@@ -191,13 +193,13 @@ def init():
else:
experiment_id = [e for e in experiments if e["name"] == experiment][0]["id"]
- write_to_config = Confirm.ask(
- "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
- )
+ write_to_config = Confirm.ask(
+ "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
+ )
- if write_to_config is True:
- write_local_exp_id(experiment_id)
- new_local = True
+ if write_to_config is True:
+ write_local_exp_id(experiment_id)
+ new_local = True
typer.echo(
"\nCodeCarbon Initialization achieved, here is your experiment id:\n"
+ click.style(f"{experiment_id}", fg="bright_green")
@@ -251,5 +253,14 @@ def monitor(
time.sleep(300)
+def questionary_prompt(prompt, list_options, default):
+ value = questionary.select(
+ prompt,
+ list_options,
+ default,
+ ).ask()
+ return value
+
+
if __name__ == "__main__":
codecarbon()
From 3be900514d8ee7bc1697aa23fa72fc814e04896d Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sat, 13 Jan 2024 17:35:03 +0100
Subject: [PATCH 13/57] test(cli): :white_check_mark: pass tests CLI
---
tests/test_cli.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 95 insertions(+)
create mode 100644 tests/test_cli.py
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 000000000..a6e3d6db0
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,95 @@
+import unittest
+from unittest.mock import MagicMock, patch
+
+from typer.testing import CliRunner
+
+from codecarbon import __app_name__, __version__
+from codecarbon.cli.main import codecarbon
+
+# MOCK API CLIENT
+
+
+@patch("codecarbon.cli.main.ApiClient")
+class TestApp(unittest.TestCase):
+ def setUp(self):
+ self.runner = CliRunner()
+ self.mock_api_client = MagicMock()
+ self.mock_api_client.get_list_organizations.return_value = [
+ {"id": 1, "name": "test org Code Carbon"}
+ ]
+ self.mock_api_client.list_teams_from_organization.return_value = [
+ {"id": 1, "name": "test team Code Carbon"}
+ ]
+
+ self.mock_api_client.list_projects_from_team.return_value = [
+ {"id": 1, "name": "test project Code Carbon"}
+ ]
+ self.mock_api_client.list_experiments_from_project.return_value = [
+ {"id": 1, "name": "test experiment Code Carbon"}
+ ]
+ self.mock_api_client.create_organization.return_value = {
+ "id": 1,
+ "name": "test org Code Carbon",
+ }
+ self.mock_api_client.create_team.return_value = {
+ "id": 1,
+ "name": "test team Code Carbon",
+ }
+ self.mock_api_client.create_project.return_value = {
+ "id": 1,
+ "name": "test project Code Carbon",
+ }
+ self.mock_api_client.create_experiment.return_value = {
+ "id": 1,
+ "name": "test experiment Code Carbon",
+ }
+
+ def test_app(self, MockApiClient):
+ result = self.runner.invoke(codecarbon, ["--version"])
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(__app_name__, result.stdout)
+ self.assertIn(__version__, result.stdout)
+
+ def test_init_aborted(self, MockApiClient):
+ result = self.runner.invoke(codecarbon, ["init"])
+ self.assertEqual(result.exit_code, 1)
+ self.assertIn("Welcome to CodeCarbon configuration wizard", result.stdout)
+
+ def test_init_use_local(self, MockApiClient):
+ result = self.runner.invoke(codecarbon, ["init"], input="y")
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(
+ "CodeCarbon Initialization achieved, here is your experiment id:",
+ result.stdout,
+ )
+ self.assertIn("(from ./.codecarbon.config)", result.stdout)
+
+ def custom_questionary_side_effect(*args, **kwargs):
+ default_value = kwargs.get("default")
+ return MagicMock(return_value=default_value)
+
+ @patch("codecarbon.cli.main.Confirm.ask")
+ @patch("codecarbon.cli.main.questionary_prompt")
+ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
+ MockApiClient.return_value = self.mock_api_client
+ mock_prompt.side_effect = [
+ "Create New Organization",
+ "Create New Team",
+ "Create New Project",
+ "Create New Experiment",
+ ]
+ mock_confirm.side_effect = [False, False, False]
+ result = self.runner.invoke(
+ codecarbon,
+ ["init"],
+ input="n",
+ )
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(
+ "CodeCarbon Initialization achieved, here is your experiment id:",
+ result.stdout,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
From 2e67fec298e616fd91a8d97db9810ea38bc4bbf9 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sun, 3 Mar 2024 16:30:34 +0100
Subject: [PATCH 14/57] feat(cli): :sparkles: add more functionalities to
python API client and CLI
---
codecarbon/cli/cli_utils.py | 38 +++-
codecarbon/cli/main.py | 326 +++++++++++++++++++---------------
codecarbon/core/api_client.py | 39 +++-
tests/test_cli.py | 34 ++--
4 files changed, 263 insertions(+), 174 deletions(-)
diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py
index 3539c12cf..015bc1379 100644
--- a/codecarbon/cli/cli_utils.py
+++ b/codecarbon/cli/cli_utils.py
@@ -1,9 +1,20 @@
import configparser
from pathlib import Path
+from typing import Optional
-def get_api_endpoint():
- p = Path.cwd().resolve() / ".codecarbon.config"
+def get_config(path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
+ if p.exists():
+ config = configparser.ConfigParser()
+ config.read(str(p))
+ if "codecarbon" in config.sections():
+ d = dict(config["codecarbon"])
+ return d
+
+
+def get_api_endpoint(path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
if p.exists():
config = configparser.ConfigParser()
config.read(str(p))
@@ -14,8 +25,8 @@ def get_api_endpoint():
return "https://api.codecarbon.io"
-def get_existing_local_exp_id():
- p = Path.cwd().resolve() / ".codecarbon.config"
+def get_existing_local_exp_id(path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
if p.exists():
config = configparser.ConfigParser()
config.read(str(p))
@@ -25,8 +36,9 @@ def get_existing_local_exp_id():
return d["experiment_id"]
-def write_local_exp_id(exp_id):
- p = Path.cwd().resolve() / ".codecarbon.config"
+def write_local_exp_id(exp_id, path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
+
config = configparser.ConfigParser()
if p.exists():
config.read(str(p))
@@ -37,3 +49,17 @@ def write_local_exp_id(exp_id):
with p.open("w") as f:
config.write(f)
+
+
+def overwrite_local_config(config_name, value, path: Optional[Path] = None):
+ p = path or Path.cwd().resolve() / ".codecarbon.config"
+
+ config = configparser.ConfigParser()
+ if p.exists():
+ config.read(str(p))
+ if "codecarbon" not in config.sections():
+ config.add_section("codecarbon")
+
+ config["codecarbon"][config_name] = value
+ with p.open("w") as f:
+ config.write(f)
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index d7dc28f85..2c4a35127 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -2,17 +2,18 @@
import time
from typing import Optional
-import click
import questionary
import typer
+from rich import print
from rich.prompt import Confirm
from typing_extensions import Annotated
from codecarbon import __app_name__, __version__
from codecarbon.cli.cli_utils import (
get_api_endpoint,
+ get_config,
get_existing_local_exp_id,
- write_local_exp_id,
+ overwrite_local_config,
)
from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone
from codecarbon.core.schemas import (
@@ -49,174 +50,202 @@ def main(
return
-@codecarbon.command("init", short_help="Create an experiment id in a public project.")
-def init():
+def show_config():
+ d = get_config()
+ api_endpoint = get_api_endpoint()
+ api = ApiClient(endpoint_url=api_endpoint)
+ try:
+ org = api.get_organization(d["organization_id"])
+ team = api.get_team(d["team_id"])
+ project = api.get_project(d["project_id"])
+ experiment = api.get_experiment(d["experiment_id"])
+ print(
+ "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n "
+ )
+ print("Experiment: \n ")
+ print(experiment)
+ print("Project: \n")
+ print(project)
+ print("Team: \n")
+ print(team)
+ print("Organization: \n")
+ print(org)
+ except:
+ raise ValueError(
+ "Your configuration is invalid, please run `codecarbon config --init` first!"
+ )
+
+
+@codecarbon.command("config", short_help="Generate or show config")
+def config(
+ init: Annotated[
+ bool, typer.Option(help="Initialise or modify configuration")
+ ] = None,
+ show: Annotated[bool, typer.Option(help="Show configuration details")] = None,
+):
"""
Initialize CodeCarbon, this will prompt you for configuration of Organisation/Team/Project/Experiment.
"""
- typer.echo("Welcome to CodeCarbon configuration wizard")
- use_config = Confirm.ask(
- "Use existing /.codecarbonconfig to configure ?",
- )
- if use_config is True:
- experiment_id = get_existing_local_exp_id()
- else:
- experiment_id = None
- new_local = False
- if experiment_id is None:
- api_endpoint = get_api_endpoint()
- api_endpoint = typer.prompt(
- f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
- type=str,
- default=api_endpoint,
- )
- api = ApiClient(endpoint_url=api_endpoint)
- organizations = api.get_list_organizations()
- org = questionary_prompt(
- "Pick existing organization from list or Create new organization ?",
- [org["name"] for org in organizations] + ["Create New Organization"],
- default="Create New Organization",
+ if show:
+ show_config()
+ elif init:
+ typer.echo("Welcome to CodeCarbon configuration wizard")
+ use_config = questionary_prompt(
+ "Use existing /.codecarbonconfig to configure or overwrite ? ",
+ ["/.codecarbonconfig", "Create New Config"],
+ default="/.codecarbonconfig",
)
- if org == "Create New Organization":
- org_name = typer.prompt(
- "Organization name", default="Code Carbon user test"
+ if use_config == "/.codecarbonconfig":
+ typer.echo("Using existing config file :")
+ show_config()
+ pass
+
+ else:
+ typer.echo("Creating new config file")
+ api_endpoint = get_api_endpoint()
+ api_endpoint = typer.prompt(
+ f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
+ type=str,
+ default=api_endpoint,
)
- org_description = typer.prompt(
- "Organization description", default="Code Carbon user test"
+ api = ApiClient(endpoint_url=api_endpoint)
+ organizations = api.get_list_organizations()
+ org = questionary_prompt(
+ "Pick existing organization from list or Create new organization ?",
+ [org["name"] for org in organizations] + ["Create New Organization"],
+ default="Create New Organization",
)
- if org_name in organizations:
- typer.echo(
- f"Organization {org_name} already exists, using it for this experiment."
+
+ if org == "Create New Organization":
+ org_name = typer.prompt(
+ "Organization name", default="Code Carbon user test"
)
+ org_description = typer.prompt(
+ "Organization description", default="Code Carbon user test"
+ )
+ if org_name in organizations:
+ typer.echo(
+ f"Organization {org_name} already exists, using it for this experiment."
+ )
+ organization = [
+ orga for orga in organizations if orga["name"] == org
+ ][0]
+ else:
+ organization_create = OrganizationCreate(
+ name=org_name,
+ description=org_description,
+ )
+ organization = api.create_organization(
+ organization=organization_create
+ )
+ typer.echo(f"Created organization : {organization}")
+ else:
organization = [orga for orga in organizations if orga["name"] == org][
0
]
- else:
- organization_create = OrganizationCreate(
- name=org_name,
- description=org_description,
- )
- organization = api.create_organization(organization=organization_create)
- typer.echo(f"Created organization : {organization}")
- else:
- organization = [orga for orga in organizations if orga["name"] == org][0]
- teams = api.list_teams_from_organization(organization["id"])
+ overwrite_local_config("organization_id", organization["id"])
+ teams = api.list_teams_from_organization(organization["id"])
- team = questionary_prompt(
- "Pick existing team from list or create new team in organization ?",
- [team["name"] for team in teams] + ["Create New Team"],
- default="Create New Team",
- )
- if team == "Create New Team":
- team_name = typer.prompt("Team name", default="Code Carbon user test")
- team_description = typer.prompt(
- "Team description", default="Code Carbon user test"
- )
- team_create = TeamCreate(
- name=team_name,
- description=team_description,
- organization_id=organization["id"],
- )
- team = api.create_team(
- team=team_create,
- )
- typer.echo(f"Created team : {team}")
- else:
- team = [t for t in teams if t["name"] == team][0]
- projects = api.list_projects_from_team(team["id"])
- project = questionary_prompt(
- "Pick existing project from list or Create new project ?",
- [project["name"] for project in projects] + ["Create New Project"],
- default="Create New Project",
- )
- if project == "Create New Project":
- project_name = typer.prompt("Project name", default="Code Carbon user test")
- project_description = typer.prompt(
- "Project description", default="Code Carbon user test"
- )
- project_create = ProjectCreate(
- name=project_name,
- description=project_description,
- team_id=team["id"],
+ team = questionary_prompt(
+ "Pick existing team from list or create new team in organization ?",
+ [team["name"] for team in teams] + ["Create New Team"],
+ default="Create New Team",
)
- project = api.create_project(project=project_create)
- typer.echo(f"Created project : {project}")
- else:
- project = [p for p in projects if p["name"] == project][0]
+ if team == "Create New Team":
+ team_name = typer.prompt("Team name", default="Code Carbon user test")
+ team_description = typer.prompt(
+ "Team description", default="Code Carbon user test"
+ )
+ team_create = TeamCreate(
+ name=team_name,
+ description=team_description,
+ organization_id=organization["id"],
+ )
+ team = api.create_team(
+ team=team_create,
+ )
+ typer.echo(f"Created team : {team}")
+ else:
+ team = [t for t in teams if t["name"] == team][0]
+ overwrite_local_config("team_id", team["id"])
- experiments = api.list_experiments_from_project(project["id"])
- experiment = questionary_prompt(
- "Pick existing experiment from list or Create new experiment ?",
- [experiment["name"] for experiment in experiments]
- + ["Create New Experiment"],
- default="Create New Experiment",
- )
- if experiment == "Create New Experiment":
- typer.echo("Creating new experiment")
- exp_name = typer.prompt(
- "Experiment name :", default="Code Carbon user test"
- )
- exp_description = typer.prompt(
- "Experiment description :",
- default="Code Carbon user test ",
+ projects = api.list_projects_from_team(team["id"])
+ project = questionary_prompt(
+ "Pick existing project from list or Create new project ?",
+ [project["name"] for project in projects] + ["Create New Project"],
+ default="Create New Project",
)
-
- exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
- if exp_on_cloud is True:
- cloud_provider = typer.prompt(
- "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
+ if project == "Create New Project":
+ project_name = typer.prompt(
+ "Project name", default="Code Carbon user test"
)
- cloud_region = typer.prompt(
- "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
+ project_description = typer.prompt(
+ "Project description", default="Code Carbon user test"
)
+ project_create = ProjectCreate(
+ name=project_name,
+ description=project_description,
+ team_id=team["id"],
+ )
+ project = api.create_project(project=project_create)
+ typer.echo(f"Created project : {project}")
else:
- cloud_provider = None
- cloud_region = None
- country_name = typer.prompt("Country name :", default="France")
- country_iso_code = typer.prompt("Country ISO code :", default="FRA")
- region = typer.prompt("Region :", default="france")
- experiment_create = ExperimentCreate(
- timestamp=get_datetime_with_timezone(),
- name=exp_name,
- description=exp_description,
- on_cloud=exp_on_cloud,
- project_id=project["id"],
- country_name=country_name,
- country_iso_code=country_iso_code,
- region=region,
- cloud_provider=cloud_provider,
- cloud_region=cloud_region,
+ project = [p for p in projects if p["name"] == project][0]
+ overwrite_local_config("project_id", project["id"])
+
+ experiments = api.list_experiments_from_project(project["id"])
+ experiment = questionary_prompt(
+ "Pick existing experiment from list or Create new experiment ?",
+ [experiment["name"] for experiment in experiments]
+ + ["Create New Experiment"],
+ default="Create New Experiment",
)
- experiment_id = api.add_experiment(experiment=experiment_create)
+ if experiment == "Create New Experiment":
+ typer.echo("Creating new experiment")
+ exp_name = typer.prompt(
+ "Experiment name :", default="Code Carbon user test"
+ )
+ exp_description = typer.prompt(
+ "Experiment description :",
+ default="Code Carbon user test ",
+ )
- else:
- experiment_id = [e for e in experiments if e["name"] == experiment][0]["id"]
+ exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
+ if exp_on_cloud is True:
+ cloud_provider = typer.prompt(
+ "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
+ )
+ cloud_region = typer.prompt(
+ "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
+ )
+ else:
+ cloud_provider = None
+ cloud_region = None
+ country_name = typer.prompt("Country name :", default="France")
+ country_iso_code = typer.prompt("Country ISO code :", default="FRA")
+ region = typer.prompt("Region :", default="france")
+ experiment_create = ExperimentCreate(
+ timestamp=get_datetime_with_timezone(),
+ name=exp_name,
+ description=exp_description,
+ on_cloud=exp_on_cloud,
+ project_id=project["id"],
+ country_name=country_name,
+ country_iso_code=country_iso_code,
+ region=region,
+ cloud_provider=cloud_provider,
+ cloud_region=cloud_region,
+ )
+ experiment_id = api.create_experiment(experiment=experiment_create)
- write_to_config = Confirm.ask(
- "Write experiment_id to /.codecarbonconfig ? (Press enter to continue)"
- )
+ else:
+ experiment_id = [e for e in experiments if e["name"] == experiment][0][
+ "id"
+ ]
- if write_to_config is True:
- write_local_exp_id(experiment_id)
- new_local = True
- typer.echo(
- "\nCodeCarbon Initialization achieved, here is your experiment id:\n"
- + click.style(f"{experiment_id}", fg="bright_green")
- + (
- ""
- if new_local
- else " (from "
- + click.style("./.codecarbon.config", fg="bright_blue")
- + ")\n"
- )
- )
- if new_local:
- click.echo(
- "\nCodeCarbon added this id to your local config: "
- + click.style("./.codecarbon.config", fg="bright_blue")
- + "\n"
- )
+ overwrite_local_config("experiment_id", experiment_id["id"])
+ show_config()
@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.")
@@ -264,3 +293,4 @@ def questionary_prompt(prompt, list_options, default):
if __name__ == "__main__":
codecarbon()
+ codecarbon()
diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py
index ca89f27aa..2727a3d33 100644
--- a/codecarbon/core/api_client.py
+++ b/codecarbon/core/api_client.py
@@ -98,9 +98,7 @@ def list_teams_from_organization(self, organization_id):
"""
List all teams
"""
- url = (
- self.url + "/teams/organization/" + organization_id
- ) # TODO : check if this is the right url
+ url = self.url + "/teams/organization/" + organization_id
r = requests.get(url=url, timeout=2)
if r.status_code != 200:
self._log_error(url, {}, r)
@@ -119,6 +117,17 @@ def create_team(self, team: TeamCreate):
return None
return r.json()
+ def get_team(self, team_id):
+ """
+ Get a team
+ """
+ url = self.url + "/team/" + team_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
def list_projects_from_team(self, team_id):
"""
List all projects
@@ -142,6 +151,17 @@ def create_project(self, project: ProjectCreate):
return None
return r.json()
+ def get_project(self, project_id):
+ """
+ Get a project
+ """
+ url = self.url + "/project/" + project_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
def add_emission(self, carbon_emission: dict):
assert self.experiment_id is not None
self._previous_call = time.time()
@@ -255,7 +275,7 @@ def set_experiment(self, experiment_id: str):
"""
self.experiment_id = experiment_id
- def add_experiment(self, experiment: ExperimentCreate):
+ def create_experiment(self, experiment: ExperimentCreate):
"""
Create an experiment, used by the CLI, not the package.
::experiment:: The experiment to create.
@@ -269,6 +289,17 @@ def add_experiment(self, experiment: ExperimentCreate):
self.experiment_id = r.json()["id"]
return self.experiment_id
+ def get_experiment(self, experiment_id):
+ """
+ Get an experiment by id
+ """
+ url = self.url + "/experiment/" + experiment_id
+ r = requests.get(url=url, timeout=2)
+ if r.status_code != 200:
+ self._log_error(url, {}, r)
+ return None
+ return r.json()
+
def _log_error(self, url, payload, response):
if len(payload) > 0:
logger.error(
diff --git a/tests/test_cli.py b/tests/test_cli.py
index a6e3d6db0..7d39b0b4b 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -15,32 +15,32 @@ def setUp(self):
self.runner = CliRunner()
self.mock_api_client = MagicMock()
self.mock_api_client.get_list_organizations.return_value = [
- {"id": 1, "name": "test org Code Carbon"}
+ {"id": "1", "name": "test org Code Carbon"}
]
self.mock_api_client.list_teams_from_organization.return_value = [
- {"id": 1, "name": "test team Code Carbon"}
+ {"id": "1", "name": "test team Code Carbon"}
]
self.mock_api_client.list_projects_from_team.return_value = [
- {"id": 1, "name": "test project Code Carbon"}
+ {"id": "1", "name": "test project Code Carbon"}
]
self.mock_api_client.list_experiments_from_project.return_value = [
- {"id": 1, "name": "test experiment Code Carbon"}
+ {"id": "1", "name": "test experiment Code Carbon"}
]
self.mock_api_client.create_organization.return_value = {
- "id": 1,
+ "id": "1",
"name": "test org Code Carbon",
}
self.mock_api_client.create_team.return_value = {
- "id": 1,
+ "id": "1",
"name": "test team Code Carbon",
}
self.mock_api_client.create_project.return_value = {
- "id": 1,
+ "id": "1",
"name": "test project Code Carbon",
}
self.mock_api_client.create_experiment.return_value = {
- "id": 1,
+ "id": "1",
"name": "test experiment Code Carbon",
}
@@ -51,18 +51,19 @@ def test_app(self, MockApiClient):
self.assertIn(__version__, result.stdout)
def test_init_aborted(self, MockApiClient):
- result = self.runner.invoke(codecarbon, ["init"])
+ result = self.runner.invoke(codecarbon, ["config", "--init"])
self.assertEqual(result.exit_code, 1)
self.assertIn("Welcome to CodeCarbon configuration wizard", result.stdout)
- def test_init_use_local(self, MockApiClient):
- result = self.runner.invoke(codecarbon, ["init"], input="y")
+ @patch("codecarbon.cli.main.questionary_prompt")
+ def test_init_use_local(self, mock_prompt, MockApiClient):
+ mock_prompt.return_value = "/.codecarbonconfig"
+ result = self.runner.invoke(codecarbon, ["config", "--init"], input="y")
self.assertEqual(result.exit_code, 0)
self.assertIn(
- "CodeCarbon Initialization achieved, here is your experiment id:",
+ "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n ",
result.stdout,
)
- self.assertIn("(from ./.codecarbon.config)", result.stdout)
def custom_questionary_side_effect(*args, **kwargs):
default_value = kwargs.get("default")
@@ -73,6 +74,7 @@ def custom_questionary_side_effect(*args, **kwargs):
def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
MockApiClient.return_value = self.mock_api_client
mock_prompt.side_effect = [
+ "Create New Config",
"Create New Organization",
"Create New Team",
"Create New Project",
@@ -81,12 +83,12 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
mock_confirm.side_effect = [False, False, False]
result = self.runner.invoke(
codecarbon,
- ["init"],
- input="n",
+ ["config", "--init"],
+ input="y",
)
self.assertEqual(result.exit_code, 0)
self.assertIn(
- "CodeCarbon Initialization achieved, here is your experiment id:",
+ "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n ",
result.stdout,
)
From c09d39bac1bc7e34f755d06622bac6953ebc88e9 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Sun, 3 Mar 2024 16:31:46 +0100
Subject: [PATCH 15/57] feat(core): :sparkles: allows picking up API endpoint
from conf file for dashboard
---
dashboard/data/data_loader.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/dashboard/data/data_loader.py b/dashboard/data/data_loader.py
index e19d66000..15721e8dd 100644
--- a/dashboard/data/data_loader.py
+++ b/dashboard/data/data_loader.py
@@ -7,10 +7,16 @@
import requests
+from codecarbon.core.config import get_hierarchical_config
+
API_PATH = os.getenv("CODECARBON_API_URL")
if API_PATH is None:
+ conf = get_hierarchical_config()
+ if "api_endpoint" in conf:
+ API_PATH = conf["api_endpoint"]
# API_PATH = "http://carbonserver.cleverapps.io"
- API_PATH = "https://api.codecarbon.io"
+ else:
+ API_PATH = "https://api.codecarbon.io"
# API_PATH = "http://localhost:8008" # export CODECARBON_API_URL=http://localhost:8008
# API_PATH = "http://carbonserver.cleverapps.io"
USER = "jessica"
@@ -217,3 +223,4 @@ def load_run_infos(run_id: str, **kwargs) -> tuple:
"""
path = f"{API_PATH}/run/{run_id}"
return path, kwargs
+ return path, kwargs
From f86a666f0d74be0abd577fca9e1d580af4244e07 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 10:52:54 +0100
Subject: [PATCH 16/57] feat(cli): :sparkles: allow to create a new custom
configuration file anywhere
---
codecarbon/cli/cli_utils.py | 1 +
codecarbon/cli/main.py | 19 +++++++++++++++++--
2 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py
index 015bc1379..727356bc7 100644
--- a/codecarbon/cli/cli_utils.py
+++ b/codecarbon/cli/cli_utils.py
@@ -5,6 +5,7 @@
def get_config(path: Optional[Path] = None):
p = path or Path.cwd().resolve() / ".codecarbon.config"
+
if p.exists():
config = configparser.ConfigParser()
config.read(str(p))
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index 2c4a35127..cdb727e1e 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -1,5 +1,6 @@
import sys
import time
+from pathlib import Path
from typing import Optional
import questionary
@@ -98,11 +99,23 @@ def config(
if use_config == "/.codecarbonconfig":
typer.echo("Using existing config file :")
+ file_path = Path("/.codecarbonconfig")
show_config()
pass
else:
typer.echo("Creating new config file")
+ file_path = typer.prompt(
+ "Where do you want to put your config file ?",
+ type=str,
+ default="/.codecarbonconfig",
+ )
+ file_path = Path(file_path)
+ if not file_path.parent.exists():
+ Confirm.ask(
+ "Parent folder does not exist do you want to create it (and parents) ?"
+ )
+
api_endpoint = get_api_endpoint()
api_endpoint = typer.prompt(
f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
@@ -144,7 +157,9 @@ def config(
organization = [orga for orga in organizations if orga["name"] == org][
0
]
- overwrite_local_config("organization_id", organization["id"])
+ overwrite_local_config(
+ "organization_id", organization["id"], file_path=file_path
+ )
teams = api.list_teams_from_organization(organization["id"])
team = questionary_prompt(
@@ -244,7 +259,7 @@ def config(
"id"
]
- overwrite_local_config("experiment_id", experiment_id["id"])
+ overwrite_local_config("experiment_id", experiment_id)
show_config()
From fd906dfa276694a14a76554d7f2c14943829cba4 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 12:39:52 +0100
Subject: [PATCH 17/57] feat(CLI): :sparkles: allow to use or modify existing
config file or create one from cli
---
codecarbon/cli/cli_utils.py | 34 +++-
codecarbon/cli/main.py | 369 ++++++++++++++++++----------------
codecarbon/core/api_client.py | 3 +-
tests/test_cli.py | 14 +-
4 files changed, 236 insertions(+), 184 deletions(-)
diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py
index 727356bc7..82b58a205 100644
--- a/codecarbon/cli/cli_utils.py
+++ b/codecarbon/cli/cli_utils.py
@@ -2,6 +2,9 @@
from pathlib import Path
from typing import Optional
+import typer
+from rich.prompt import Confirm
+
def get_config(path: Optional[Path] = None):
p = path or Path.cwd().resolve() / ".codecarbon.config"
@@ -11,7 +14,12 @@ def get_config(path: Optional[Path] = None):
config.read(str(p))
if "codecarbon" in config.sections():
d = dict(config["codecarbon"])
- return d
+ return d
+
+ else:
+ raise FileNotFoundError(
+ "No .codecarbon.config file found in the current directory."
+ )
def get_api_endpoint(path: Optional[Path] = None):
@@ -23,6 +31,9 @@ def get_api_endpoint(path: Optional[Path] = None):
d = dict(config["codecarbon"])
if "api_endpoint" in d:
return d["api_endpoint"]
+ else:
+ with p.open("a") as f:
+ f.write("api_endpoint=https://api.codecarbon.io\n")
return "https://api.codecarbon.io"
@@ -64,3 +75,24 @@ def overwrite_local_config(config_name, value, path: Optional[Path] = None):
config["codecarbon"][config_name] = value
with p.open("w") as f:
config.write(f)
+
+
+def create_new_config_file():
+ typer.echo("Creating new config file")
+ file_path = typer.prompt(
+ "Where do you want to put your config file ?",
+ type=str,
+ default="./.codecarbon.config",
+ )
+ file_path = Path(file_path)
+ if not file_path.parent.exists():
+ create = Confirm.ask(
+ "Parent folder does not exist do you want to create it (and parents) ?"
+ )
+ if create:
+ file_path.parent.mkdir(parents=True, exist_ok=True)
+ file_path.touch()
+ with open(file_path, "w") as f:
+ f.write("[codecarbon]\n")
+ typer.echo(f"Config file created at {file_path}")
+ return file_path
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index cdb727e1e..b41d84a48 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -1,4 +1,3 @@
-import sys
import time
from pathlib import Path
from typing import Optional
@@ -11,6 +10,7 @@
from codecarbon import __app_name__, __version__
from codecarbon.cli.cli_utils import (
+ create_new_config_file,
get_api_endpoint,
get_config,
get_existing_local_exp_id,
@@ -33,7 +33,7 @@
def _version_callback(value: bool) -> None:
if value:
- typer.echo(f"{__app_name__} v{__version__}")
+ print(f"{__app_name__} v{__version__}")
raise typer.Exit()
@@ -51,29 +51,54 @@ def main(
return
-def show_config():
- d = get_config()
- api_endpoint = get_api_endpoint()
+def show_config(path: Path = Path("./.codecarbon.config")) -> None:
+ d = get_config(path)
+ api_endpoint = get_api_endpoint(path)
api = ApiClient(endpoint_url=api_endpoint)
+ print("Current configuration : \n")
+ print("Config file content : ")
+ print(d)
try:
- org = api.get_organization(d["organization_id"])
- team = api.get_team(d["team_id"])
- project = api.get_project(d["project_id"])
- experiment = api.get_experiment(d["experiment_id"])
- print(
- "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n "
- )
- print("Experiment: \n ")
- print(experiment)
- print("Project: \n")
- print(project)
- print("Team: \n")
- print(team)
- print("Organization: \n")
- print(org)
- except:
+ if "organization_id" not in d:
+ print(
+ "No organization_id in config, follow setup instruction to complete your configuration file!",
+ color="red",
+ )
+ else:
+ org = api.get_organization(d["organization_id"])
+
+ if "team_id" not in d:
+ print(
+ "No team_id in config, follow setup instruction to complete your configuration file!",
+ color="red",
+ )
+ else:
+ team = api.get_team(d["team_id"])
+ if "project_id" not in d:
+ print(
+ "No project_id in config, follow setup instruction to complete your configuration file!",
+ color="red",
+ )
+ else:
+ project = api.get_project(d["project_id"])
+ if "experiment_id" not in d:
+ print(
+ "No experiment_id in config, follow setup instruction to complete your configuration file!",
+ color="red",
+ )
+ else:
+ experiment = api.get_experiment(d["experiment_id"])
+ print("\nExperiment :")
+ print(experiment)
+ print("\nProject :")
+ print(project)
+ print("\nTeam :")
+ print(team)
+ print("\nOrganization :")
+ print(org)
+ except Exception as e:
raise ValueError(
- "Your configuration is invalid, please run `codecarbon config --init` first!"
+ f"Your configuration is invalid, please run `codecarbon config --init` first! (error: {e})"
)
@@ -90,177 +115,171 @@ def config(
if show:
show_config()
elif init:
- typer.echo("Welcome to CodeCarbon configuration wizard")
- use_config = questionary_prompt(
- "Use existing /.codecarbonconfig to configure or overwrite ? ",
- ["/.codecarbonconfig", "Create New Config"],
- default="/.codecarbonconfig",
- )
+ print("Welcome to CodeCarbon configuration wizard")
+ default_path = Path("./.codecarbon.config")
- if use_config == "/.codecarbonconfig":
- typer.echo("Using existing config file :")
- file_path = Path("/.codecarbonconfig")
- show_config()
- pass
+ if default_path.exists():
+ print("Existing config file found :")
+ show_config(default_path)
- else:
- typer.echo("Creating new config file")
- file_path = typer.prompt(
- "Where do you want to put your config file ?",
- type=str,
- default="/.codecarbonconfig",
+ use_config = questionary_prompt(
+ "Use existing ./.codecarbon.config to configure or create a new file somwhere else ? ",
+ ["./.codecarbon.config", "Create New Config"],
+ default="./.codecarbon.config",
)
- file_path = Path(file_path)
- if not file_path.parent.exists():
- Confirm.ask(
- "Parent folder does not exist do you want to create it (and parents) ?"
- )
- api_endpoint = get_api_endpoint()
- api_endpoint = typer.prompt(
- f"Default API endpoint is {api_endpoint}. You can change it in /.codecarbonconfig. Press enter to continue or input other url",
- type=str,
- default=api_endpoint,
+ if use_config == "./.codecarbon.config":
+ modify = Confirm.ask("Do you want to modify the existing config file ?")
+ if modify:
+ print(f"Modifying existing config file {default_path}:")
+ file_path = default_path
+ else:
+ print(f"Using already existing config file {default_path}")
+ return
+ else:
+ file_path = create_new_config_file()
+ else:
+ file_path = create_new_config_file()
+
+ api_endpoint = get_api_endpoint(file_path)
+ api_endpoint = typer.prompt(
+ f"Current API endpoint is {api_endpoint}. Press enter to continue or input other url",
+ type=str,
+ default=api_endpoint,
+ )
+ overwrite_local_config("api_endpoint", api_endpoint, path=file_path)
+ api = ApiClient(endpoint_url=api_endpoint)
+ organizations = api.get_list_organizations()
+ org = questionary_prompt(
+ "Pick existing organization from list or Create new organization ?",
+ [org["name"] for org in organizations] + ["Create New Organization"],
+ default="Create New Organization",
+ )
+
+ if org == "Create New Organization":
+ org_name = typer.prompt(
+ "Organization name", default="Code Carbon user test"
)
- api = ApiClient(endpoint_url=api_endpoint)
- organizations = api.get_list_organizations()
- org = questionary_prompt(
- "Pick existing organization from list or Create new organization ?",
- [org["name"] for org in organizations] + ["Create New Organization"],
- default="Create New Organization",
+ org_description = typer.prompt(
+ "Organization description", default="Code Carbon user test"
)
-
- if org == "Create New Organization":
- org_name = typer.prompt(
- "Organization name", default="Code Carbon user test"
- )
- org_description = typer.prompt(
- "Organization description", default="Code Carbon user test"
+ if org_name in organizations:
+ print(
+ f"Organization {org_name} already exists, using it for this experiment."
)
- if org_name in organizations:
- typer.echo(
- f"Organization {org_name} already exists, using it for this experiment."
- )
- organization = [
- orga for orga in organizations if orga["name"] == org
- ][0]
- else:
- organization_create = OrganizationCreate(
- name=org_name,
- description=org_description,
- )
- organization = api.create_organization(
- organization=organization_create
- )
- typer.echo(f"Created organization : {organization}")
- else:
organization = [orga for orga in organizations if orga["name"] == org][
0
]
- overwrite_local_config(
- "organization_id", organization["id"], file_path=file_path
+ else:
+ organization_create = OrganizationCreate(
+ name=org_name,
+ description=org_description,
+ )
+ organization = api.create_organization(organization=organization_create)
+ print(f"Created organization : {organization}")
+ else:
+ organization = [orga for orga in organizations if orga["name"] == org][0]
+ overwrite_local_config("organization_id", organization["id"], path=file_path)
+ teams = api.list_teams_from_organization(organization["id"])
+
+ team = questionary_prompt(
+ "Pick existing team from list or create new team in organization ?",
+ [team["name"] for team in teams] + ["Create New Team"],
+ default="Create New Team",
+ )
+ if team == "Create New Team":
+ team_name = typer.prompt("Team name", default="Code Carbon user test")
+ team_description = typer.prompt(
+ "Team description", default="Code Carbon user test"
+ )
+ team_create = TeamCreate(
+ name=team_name,
+ description=team_description,
+ organization_id=organization["id"],
)
- teams = api.list_teams_from_organization(organization["id"])
+ team = api.create_team(
+ team=team_create,
+ )
+ print(f"Created team : {team}")
+ else:
+ team = [t for t in teams if t["name"] == team][0]
+ overwrite_local_config("team_id", team["id"], path=file_path)
- team = questionary_prompt(
- "Pick existing team from list or create new team in organization ?",
- [team["name"] for team in teams] + ["Create New Team"],
- default="Create New Team",
+ projects = api.list_projects_from_team(team["id"])
+ project = questionary_prompt(
+ "Pick existing project from list or Create new project ?",
+ [project["name"] for project in projects] + ["Create New Project"],
+ default="Create New Project",
+ )
+ if project == "Create New Project":
+ project_name = typer.prompt("Project name", default="Code Carbon user test")
+ project_description = typer.prompt(
+ "Project description", default="Code Carbon user test"
)
- if team == "Create New Team":
- team_name = typer.prompt("Team name", default="Code Carbon user test")
- team_description = typer.prompt(
- "Team description", default="Code Carbon user test"
- )
- team_create = TeamCreate(
- name=team_name,
- description=team_description,
- organization_id=organization["id"],
- )
- team = api.create_team(
- team=team_create,
- )
- typer.echo(f"Created team : {team}")
- else:
- team = [t for t in teams if t["name"] == team][0]
- overwrite_local_config("team_id", team["id"])
+ project_create = ProjectCreate(
+ name=project_name,
+ description=project_description,
+ team_id=team["id"],
+ )
+ project = api.create_project(project=project_create)
+ print(f"Created project : {project}")
+ else:
+ project = [p for p in projects if p["name"] == project][0]
+ overwrite_local_config("project_id", project["id"], path=file_path)
- projects = api.list_projects_from_team(team["id"])
- project = questionary_prompt(
- "Pick existing project from list or Create new project ?",
- [project["name"] for project in projects] + ["Create New Project"],
- default="Create New Project",
+ experiments = api.list_experiments_from_project(project["id"])
+ experiment = questionary_prompt(
+ "Pick existing experiment from list or Create new experiment ?",
+ [experiment["name"] for experiment in experiments]
+ + ["Create New Experiment"],
+ default="Create New Experiment",
+ )
+ if experiment == "Create New Experiment":
+ print("Creating new experiment")
+ exp_name = typer.prompt(
+ "Experiment name :", default="Code Carbon user test"
)
- if project == "Create New Project":
- project_name = typer.prompt(
- "Project name", default="Code Carbon user test"
- )
- project_description = typer.prompt(
- "Project description", default="Code Carbon user test"
+ exp_description = typer.prompt(
+ "Experiment description :",
+ default="Code Carbon user test ",
+ )
+
+ exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
+ if exp_on_cloud is True:
+ cloud_provider = typer.prompt(
+ "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
)
- project_create = ProjectCreate(
- name=project_name,
- description=project_description,
- team_id=team["id"],
+ cloud_region = typer.prompt(
+ "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
)
- project = api.create_project(project=project_create)
- typer.echo(f"Created project : {project}")
else:
- project = [p for p in projects if p["name"] == project][0]
- overwrite_local_config("project_id", project["id"])
-
- experiments = api.list_experiments_from_project(project["id"])
- experiment = questionary_prompt(
- "Pick existing experiment from list or Create new experiment ?",
- [experiment["name"] for experiment in experiments]
- + ["Create New Experiment"],
- default="Create New Experiment",
+ cloud_provider = None
+ cloud_region = None
+ country_name = typer.prompt("Country name :", default="France")
+ country_iso_code = typer.prompt("Country ISO code :", default="FRA")
+ region = typer.prompt("Region :", default="france")
+ experiment_create = ExperimentCreate(
+ timestamp=get_datetime_with_timezone(),
+ name=exp_name,
+ description=exp_description,
+ on_cloud=exp_on_cloud,
+ project_id=project["id"],
+ country_name=country_name,
+ country_iso_code=country_iso_code,
+ region=region,
+ cloud_provider=cloud_provider,
+ cloud_region=cloud_region,
)
- if experiment == "Create New Experiment":
- typer.echo("Creating new experiment")
- exp_name = typer.prompt(
- "Experiment name :", default="Code Carbon user test"
- )
- exp_description = typer.prompt(
- "Experiment description :",
- default="Code Carbon user test ",
- )
-
- exp_on_cloud = Confirm.ask("Is this experiment running on the cloud ?")
- if exp_on_cloud is True:
- cloud_provider = typer.prompt(
- "Cloud provider (AWS, GCP, Azure, ...)", default="AWS"
- )
- cloud_region = typer.prompt(
- "Cloud region (eu-west-1, us-east-1, ...)", default="eu-west-1"
- )
- else:
- cloud_provider = None
- cloud_region = None
- country_name = typer.prompt("Country name :", default="France")
- country_iso_code = typer.prompt("Country ISO code :", default="FRA")
- region = typer.prompt("Region :", default="france")
- experiment_create = ExperimentCreate(
- timestamp=get_datetime_with_timezone(),
- name=exp_name,
- description=exp_description,
- on_cloud=exp_on_cloud,
- project_id=project["id"],
- country_name=country_name,
- country_iso_code=country_iso_code,
- region=region,
- cloud_provider=cloud_provider,
- cloud_region=cloud_region,
- )
- experiment_id = api.create_experiment(experiment=experiment_create)
+ experiment = api.create_experiment(experiment=experiment_create)
- else:
- experiment_id = [e for e in experiments if e["name"] == experiment][0][
- "id"
- ]
+ else:
+ experiment = [e for e in experiments if e["name"] == experiment][0]
- overwrite_local_config("experiment_id", experiment_id)
- show_config()
+ overwrite_local_config("experiment_id", experiment["id"], path=file_path)
+ show_config(file_path)
+ print(
+ "Consult [link=https://mlco2.github.io/codecarbon/usage.html#configuration]configuration documentation[/link] for more configuration options"
+ )
@codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.")
@@ -284,9 +303,8 @@ def monitor(
"""
experiment_id = get_existing_local_exp_id()
if api and experiment_id is None:
- typer.echo("ERROR: No experiment id, call 'codecarbon init' first.")
- sys.exit(1)
- typer.echo("CodeCarbon is going in an infinite loop to monitor this machine.")
+ print("ERROR: No experiment id, call 'codecarbon init' first.", err=True)
+ print("CodeCarbon is going in an infinite loop to monitor this machine.")
with EmissionsTracker(
measure_power_secs=measure_power_secs,
api_call_interval=api_call_interval,
@@ -308,4 +326,3 @@ def questionary_prompt(prompt, list_options, default):
if __name__ == "__main__":
codecarbon()
- codecarbon()
diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py
index 2727a3d33..db735ba2f 100644
--- a/codecarbon/core/api_client.py
+++ b/codecarbon/core/api_client.py
@@ -286,8 +286,7 @@ def create_experiment(self, experiment: ExperimentCreate):
if r.status_code != 201:
self._log_error(url, payload, r)
return None
- self.experiment_id = r.json()["id"]
- return self.experiment_id
+ return r.json()
def get_experiment(self, experiment_id):
"""
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 7d39b0b4b..71559361d 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -57,11 +57,11 @@ def test_init_aborted(self, MockApiClient):
@patch("codecarbon.cli.main.questionary_prompt")
def test_init_use_local(self, mock_prompt, MockApiClient):
- mock_prompt.return_value = "/.codecarbonconfig"
- result = self.runner.invoke(codecarbon, ["config", "--init"], input="y")
+ mock_prompt.return_value = "./.codecarbon.config"
+ result = self.runner.invoke(codecarbon, ["config", "--init"], input="n")
self.assertEqual(result.exit_code, 0)
self.assertIn(
- "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n ",
+ "Using already existing config file ",
result.stdout,
)
@@ -80,7 +80,7 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
"Create New Project",
"Create New Experiment",
]
- mock_confirm.side_effect = [False, False, False]
+ mock_confirm.side_effect = [True, False, False, False]
result = self.runner.invoke(
codecarbon,
["config", "--init"],
@@ -88,7 +88,11 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
)
self.assertEqual(result.exit_code, 0)
self.assertIn(
- "Succesfully initiated Code Carbon ! \n Here is your detailed config : \n ",
+ "Creating new experiment",
+ result.stdout,
+ )
+ self.assertIn(
+ "Consult configuration documentation for more configuration options",
result.stdout,
)
From ae09ce33cf70013ab69eb9607497488e46b755af Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 13:32:10 +0100
Subject: [PATCH 18/57] test(CLI): :white_check_mark: fix tests
---
codecarbon/cli/main.py | 4 ---
codecarbon/cli/test_cli_utils.py | 32 ++++++++++++++++++++++++
tests/test_cli.py | 42 ++++++++++++++++++++------------
3 files changed, 58 insertions(+), 20 deletions(-)
create mode 100644 codecarbon/cli/test_cli_utils.py
diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py
index b41d84a48..5f8927f10 100644
--- a/codecarbon/cli/main.py
+++ b/codecarbon/cli/main.py
@@ -62,7 +62,6 @@ def show_config(path: Path = Path("./.codecarbon.config")) -> None:
if "organization_id" not in d:
print(
"No organization_id in config, follow setup instruction to complete your configuration file!",
- color="red",
)
else:
org = api.get_organization(d["organization_id"])
@@ -70,21 +69,18 @@ def show_config(path: Path = Path("./.codecarbon.config")) -> None:
if "team_id" not in d:
print(
"No team_id in config, follow setup instruction to complete your configuration file!",
- color="red",
)
else:
team = api.get_team(d["team_id"])
if "project_id" not in d:
print(
"No project_id in config, follow setup instruction to complete your configuration file!",
- color="red",
)
else:
project = api.get_project(d["project_id"])
if "experiment_id" not in d:
print(
"No experiment_id in config, follow setup instruction to complete your configuration file!",
- color="red",
)
else:
experiment = api.get_experiment(d["experiment_id"])
diff --git a/codecarbon/cli/test_cli_utils.py b/codecarbon/cli/test_cli_utils.py
new file mode 100644
index 000000000..c67e95722
--- /dev/null
+++ b/codecarbon/cli/test_cli_utils.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+from unittest.mock import patch
+
+from typer.testing import CliRunner
+
+from codecarbon.cli.cli_utils import create_new_config_file
+
+
+def test_create_new_config_file():
+ runner = CliRunner()
+
+ # Mock the typer.prompt function
+ with patch("codecarbon.cli.cli_utils.typer.prompt") as mock_prompt:
+ mock_prompt.return_value = "./.codecarbon.config"
+
+ result = runner.invoke(create_new_config_file)
+
+ assert result.exit_code == 0
+ assert "Config file created at" in result.stdout
+
+ # Verify that the prompt was called with the correct arguments
+ mock_prompt.assert_called_once_with(
+ "Where do you want to put your config file ?",
+ type=str,
+ default="./.codecarbon.config",
+ )
+
+ # Verify that the file was created
+ file_path = Path("./.codecarbon.config")
+ assert file_path.exists()
+ assert file_path.is_file()
+ assert file_path.read_text() == "[codecarbon]\n"
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 71559361d..4c5ce3cd1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,4 +1,6 @@
+import tempfile
import unittest
+from pathlib import Path
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
@@ -55,36 +57,24 @@ def test_init_aborted(self, MockApiClient):
self.assertEqual(result.exit_code, 1)
self.assertIn("Welcome to CodeCarbon configuration wizard", result.stdout)
- @patch("codecarbon.cli.main.questionary_prompt")
- def test_init_use_local(self, mock_prompt, MockApiClient):
- mock_prompt.return_value = "./.codecarbon.config"
- result = self.runner.invoke(codecarbon, ["config", "--init"], input="n")
- self.assertEqual(result.exit_code, 0)
- self.assertIn(
- "Using already existing config file ",
- result.stdout,
- )
-
- def custom_questionary_side_effect(*args, **kwargs):
- default_value = kwargs.get("default")
- return MagicMock(return_value=default_value)
-
@patch("codecarbon.cli.main.Confirm.ask")
@patch("codecarbon.cli.main.questionary_prompt")
def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
+ temp_codecarbon_config = tempfile.NamedTemporaryFile(mode="w+t", delete=False)
+
MockApiClient.return_value = self.mock_api_client
mock_prompt.side_effect = [
- "Create New Config",
"Create New Organization",
"Create New Team",
"Create New Project",
"Create New Experiment",
]
mock_confirm.side_effect = [True, False, False, False]
+
result = self.runner.invoke(
codecarbon,
["config", "--init"],
- input="y",
+ input=f"{temp_codecarbon_config.name}\n",
)
self.assertEqual(result.exit_code, 0)
self.assertIn(
@@ -96,6 +86,26 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
result.stdout,
)
+ @patch("codecarbon.cli.main.Path")
+ @patch("codecarbon.cli.main.questionary_prompt")
+ def test_init_use_local(self, mock_prompt, mock_path, MockApiClient):
+ temp_codecarbon_config = tempfile.NamedTemporaryFile(mode="w+t", delete=False)
+ mock_path.return_value = Path(temp_codecarbon_config.name)
+ test_data = "[codecarbon]\nexperiment_id = 12345"
+ temp_codecarbon_config.write(test_data)
+ temp_codecarbon_config.seek(0)
+ mock_prompt.return_value = "./.codecarbon.config"
+ result = self.runner.invoke(codecarbon, ["config", "--init"], input="n")
+ self.assertEqual(result.exit_code, 0)
+ self.assertIn(
+ "Using already existing config file ",
+ result.stdout,
+ )
+
+ def custom_questionary_side_effect(*args, **kwargs):
+ default_value = kwargs.get("default")
+ return MagicMock(return_value=default_value)
+
if __name__ == "__main__":
unittest.main()
From 315542d32d4f695723a3a71e8d77880777bd8bd5 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 13:39:53 +0100
Subject: [PATCH 19/57] fix(CLI): :white_check_mark: use gihut action TEMP DIR
---
tests/test_cli.py | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4c5ce3cd1..f364a6339 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,3 +1,4 @@
+import os
import tempfile
import unittest
from pathlib import Path
@@ -60,7 +61,11 @@ def test_init_aborted(self, MockApiClient):
@patch("codecarbon.cli.main.Confirm.ask")
@patch("codecarbon.cli.main.questionary_prompt")
def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
- temp_codecarbon_config = tempfile.NamedTemporaryFile(mode="w+t", delete=False)
+ temp_dir = os.getenv("RUNNER_TEMP", tempfile.gettempdir())
+
+ temp_codecarbon_config = tempfile.NamedTemporaryFile(
+ mode="w+t", delete=False, dir=temp_dir
+ )
MockApiClient.return_value = self.mock_api_client
mock_prompt.side_effect = [
@@ -89,7 +94,11 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
@patch("codecarbon.cli.main.Path")
@patch("codecarbon.cli.main.questionary_prompt")
def test_init_use_local(self, mock_prompt, mock_path, MockApiClient):
- temp_codecarbon_config = tempfile.NamedTemporaryFile(mode="w+t", delete=False)
+ temp_dir = os.getenv("RUNNER_TEMP", tempfile.gettempdir())
+
+ temp_codecarbon_config = tempfile.NamedTemporaryFile(
+ mode="w+t", delete=False, dir=temp_dir
+ )
mock_path.return_value = Path(temp_codecarbon_config.name)
test_data = "[codecarbon]\nexperiment_id = 12345"
temp_codecarbon_config.write(test_data)
From bb1672c0d68861981b2a132ca54e5cef4edc0fe6 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 13:54:25 +0100
Subject: [PATCH 20/57] ci(CLI): :green_heart: debu CI
---
.github/workflows/build.yml | 4 ++++
tests/test_cli.py | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a898d75f3..39ff70f25 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,6 +13,10 @@ jobs:
python-version: ["3.8", "3.11"]
steps:
+ - name: List environment variables and directory permissions
+ run: |
+ printenv
+ ls -ld /tmp
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
diff --git a/tests/test_cli.py b/tests/test_cli.py
index f364a6339..89d7e0dbe 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -62,7 +62,7 @@ def test_init_aborted(self, MockApiClient):
@patch("codecarbon.cli.main.questionary_prompt")
def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
temp_dir = os.getenv("RUNNER_TEMP", tempfile.gettempdir())
-
+ print("temp_dir", temp_dir)
temp_codecarbon_config = tempfile.NamedTemporaryFile(
mode="w+t", delete=False, dir=temp_dir
)
From 7818242f0bcab9f28a93428abc4e994341da9710 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 13:59:05 +0100
Subject: [PATCH 21/57] ci(CLI): :green_heart: try modifying tox ini
---
tox.ini | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tox.ini b/tox.ini
index 1f2c8f779..34db1f7bf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,6 +17,7 @@ deps =
pytest
-rrequirements-dev.txt
-rrequirements-test.txt
+passenv = RUNNER_TEMP
commands =
pip install -e .
@@ -32,3 +33,4 @@ python =
3.9: py39
3.10: py310
3.11: py311
+
From d813861026687ccb8f9d1ea045d2b7f1f9dc24ef Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 17:58:51 +0100
Subject: [PATCH 22/57] fix(CLI): :white_check_mark: remove useless test that
created a .codecarbon.config file
---
tests/test_cli.py | 6 ------
1 file changed, 6 deletions(-)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 89d7e0dbe..c28c88308 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -53,16 +53,10 @@ def test_app(self, MockApiClient):
self.assertIn(__app_name__, result.stdout)
self.assertIn(__version__, result.stdout)
- def test_init_aborted(self, MockApiClient):
- result = self.runner.invoke(codecarbon, ["config", "--init"])
- self.assertEqual(result.exit_code, 1)
- self.assertIn("Welcome to CodeCarbon configuration wizard", result.stdout)
-
@patch("codecarbon.cli.main.Confirm.ask")
@patch("codecarbon.cli.main.questionary_prompt")
def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient):
temp_dir = os.getenv("RUNNER_TEMP", tempfile.gettempdir())
- print("temp_dir", temp_dir)
temp_codecarbon_config = tempfile.NamedTemporaryFile(
mode="w+t", delete=False, dir=temp_dir
)
From f3f28c336a1c9c81bb2bbb8f3f7aca8e63a890f6 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 17:59:34 +0100
Subject: [PATCH 23/57] ci(CLI): :rewind: remove debug step
---
.github/workflows/build.yml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 39ff70f25..a898d75f3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,10 +13,6 @@ jobs:
python-version: ["3.8", "3.11"]
steps:
- - name: List environment variables and directory permissions
- run: |
- printenv
- ls -ld /tmp
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
From 3d8ab509d3b5f1929c48ff9b57712c40430af128 Mon Sep 17 00:00:00 2001
From: LuisBlanche
Date: Fri, 22 Mar 2024 18:54:58 +0100
Subject: [PATCH 24/57] docs(CLI): :memo: test asciinema
---
.gitignore | 4 ++++
docs/_sources/usage.rst.txt | 16 +++++++++++++++-
docs/_static/pygments.css | 1 +
docs/api.html | 8 +++-----
docs/comet.html | 8 +++-----
docs/edit/usage.rst | 17 ++++++++++++++++-
docs/examples.html | 8 +++-----
docs/faq.html | 8 +++-----
docs/genindex.html | 8 +++-----
docs/index.html | 8 +++-----
docs/installation.html | 8 +++-----
docs/methodology.html | 8 +++-----
docs/model_examples.html | 8 +++-----
docs/motivation.html | 8 +++-----
docs/objects.inv | Bin 818 -> 575 bytes
docs/output.html | 8 +++-----
docs/parameters.html | 8 +++-----
docs/search.html | 8 +++-----
docs/searchindex.js | 2 +-
docs/to_logger.html | 8 +++-----
docs/usage.html | 27 +++++++++++++++++++--------
docs/visualize.html | 8 +++-----
22 files changed, 101 insertions(+), 86 deletions(-)
diff --git a/.gitignore b/.gitignore
index 03321b53b..ec6e5c99b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -125,3 +125,7 @@ code_carbon.db
# Local file
emissions*.csv*
tests/test_data/rapl/*
+
+
+#asciinema
+*.cast
\ No newline at end of file
diff --git a/docs/_sources/usage.rst.txt b/docs/_sources/usage.rst.txt
index e9dafe568..52ad93b5d 100644
--- a/docs/_sources/usage.rst.txt
+++ b/docs/_sources/usage.rst.txt
@@ -16,12 +16,26 @@ Command line
If you want to track the emissions of a computer without having to modify your code, you can use the command line interface:
+Create a minimal configuration file (just follow the prompts)
.. code-block:: console
+
+ codecarbon config --init
- codecarbon monitor --no-api
+Start monitoring the emissions of the computer
+.. code-block:: console
+
+ codecarbon monitor
You have to stop the monitoring manually with ``Ctrl+C``.
+In the following example you will see how to use the CLI to monitor all the emissions of you computer and sending everything
+to an API running on localhost:8008 (that you can start with the docke-compose)
+.. raw:: html
+
+
+
+
+
Implementing CodeCarbon in your code allows you to track the emissions of a specific block of code.
Explicit Object
diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css
index 691aeb82d..0d49244ed 100644
--- a/docs/_static/pygments.css
+++ b/docs/_static/pygments.css
@@ -17,6 +17,7 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left:
.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #A00000 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
+.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #FF0000 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
diff --git a/docs/api.html b/docs/api.html
index 6fc0cf945..27530aec9 100644
--- a/docs/api.html
+++ b/docs/api.html
@@ -1,14 +1,12 @@
-
+
CodeCarbon API — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/comet.html b/docs/comet.html
index 459117b3b..07ed9f02b 100644
--- a/docs/comet.html
+++ b/docs/comet.html
@@ -1,14 +1,12 @@
-
+
Comet Integration — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst
index e9dafe568..6b258399c 100644
--- a/docs/edit/usage.rst
+++ b/docs/edit/usage.rst
@@ -16,12 +16,27 @@ Command line
If you want to track the emissions of a computer without having to modify your code, you can use the command line interface:
+Create a minimal configuration file (just follow the prompts)
.. code-block:: console
- codecarbon monitor --no-api
+ codecarbon config --init
+
+Start monitoring the emissions of the computer
+.. code-block:: console
+
+ codecarbon monitor
You have to stop the monitoring manually with ``Ctrl+C``.
+In the following example you will see how to use the CLI to monitor all the emissions of you computer and sending everything
+to an API running on localhost:8008 (that you can start with the docke-compose)
+
+.. raw:: html
+
+
+
+
+
Implementing CodeCarbon in your code allows you to track the emissions of a specific block of code.
Explicit Object
diff --git a/docs/examples.html b/docs/examples.html
index 76ebeeee2..5d4d38041 100644
--- a/docs/examples.html
+++ b/docs/examples.html
@@ -1,14 +1,12 @@
-
+
Examples — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/faq.html b/docs/faq.html
index 06792d78c..1c6eb15f6 100644
--- a/docs/faq.html
+++ b/docs/faq.html
@@ -1,14 +1,12 @@
-
+
Frequently Asked Questions — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/genindex.html b/docs/genindex.html
index 16e62aac7..428e18372 100644
--- a/docs/genindex.html
+++ b/docs/genindex.html
@@ -1,13 +1,11 @@
-
+
Index — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/index.html b/docs/index.html
index ae51971dc..fe185fd00 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1,14 +1,12 @@
-
+
CodeCarbon — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/installation.html b/docs/installation.html
index 1e4c56f20..f4386d55b 100644
--- a/docs/installation.html
+++ b/docs/installation.html
@@ -1,14 +1,12 @@
-
+
Installing CodeCarbon — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/methodology.html b/docs/methodology.html
index ee1212bf1..b2191b9fe 100644
--- a/docs/methodology.html
+++ b/docs/methodology.html
@@ -1,14 +1,12 @@
-
+
Methodology — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/model_examples.html b/docs/model_examples.html
index ebe1fdb3b..e3d9cfc48 100644
--- a/docs/model_examples.html
+++ b/docs/model_examples.html
@@ -1,14 +1,12 @@
-
+
Model Comparisons — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/motivation.html b/docs/motivation.html
index c1c16cc6a..ca88a5c96 100644
--- a/docs/motivation.html
+++ b/docs/motivation.html
@@ -1,14 +1,12 @@
-
+
Motivation — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/objects.inv b/docs/objects.inv
index 0251f17b6909708d6646a7fc27d9e75d9d7fc4bb..3ca2bac60c0c1116f350fc8ee471a522c19e4577 100644
GIT binary patch
delta 465
zcmV;?0WSWs2EPQ5cz=bI%Wi`(5JmTVg{5{&rEa^6qE^bHDp93wBL}9$iowK>DD>+)
zVEh7R*7j+FX&C%juWt$e@+{3vRIn5_{`+p!E%Qv9oD~*D?)bMQA
zYN#dEh-Pr3O~*mSfYU7VKAlpl?>{l)FueVjUgA(Z+Pn`57Xz*4J=v%;RAg^J4ZQ@!
zhdT&!?_f2wtgYadvQD*Ccf-#t*ru)`N7y3Tl%|}I6H47dcd?@~Y~vp9n_48`&DJw;
zQ72IeTxuV;5P!+GgU=u=7+-cyVbz4gviz|2Dx^=Qii0-1N*$F<4GUP|L+NG0)2QX%
zzZ$r*(XP+k1K)|^FzvPxnq3zw3#vohc@9J!C&)0X012Zzll;A`V$;9I-?7Q3Mm9{G
zjCf&+y8)RDJ*YOd@aH)YMw22~A%?=-9t^bT@ug_fxk@UavOJ}r)hzREpBJ=-%zw<{
z>!e?}#~(1GFe(lh1q<&DE6@0Mkvg*Zwt^7{5LvHG8OqsTlIlg9{Ns)@w+~PKSRfGp
HJ}p7{#{B7x
delta 710
zcmV;%0y+J^1hNK@cz?ZCU2obj6n*DcSn3|Oha_uP?O~cGRj8Ag+KsAJUn`T`f`?2V8&--hlboM2aDT&TWe5zSI(>W(!g?}WPX%628M>yWna$Xhp
z^ckXyUQ#@4*dVtIADvLNeqH{r_V3d4R|QEqRq8FPXac->8M(|V{-)C~5&m7s
z=gVG~Nq4($DSvicD5qI=OSRilImz*;wX!clKZ~W;2;G|B({kxI#D6Gd&w}YW##S-3
zV18UY8p=>+Fr?)~3UW0bSsJGk;6Cx}-6l1aoI|Ram=egDQcMa(v?P>pIh{fo`e^xN
zVB_Y*krhW;Ml@QBhX>p-d=A<#sR7!e>bsH9FfFM@>3;y06q^OXd(>h^bG<%=RGWW5
z#%5UhGkx(*@o4lRxI$%pdS?Qt*J}-k9_g~GkPqMBa4IS4+ESTbKpbVPsP~$qb=S_iY
zO(?+#zR30MKtspPrVab5fR^bj6dKHE?#=xeH$U>gE;GNJaKjy5V3k5E-=G1E_^x8<
s96y&*do-JtBVq$Q>Vm0?a{LG4yIRmVKeopWwlxp=7{Dq10zKtYqRbXtb^rhX
diff --git a/docs/output.html b/docs/output.html
index 3c3445a75..72ac89dd3 100644
--- a/docs/output.html
+++ b/docs/output.html
@@ -1,14 +1,12 @@
-
+
Output — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/parameters.html b/docs/parameters.html
index 4ead0433d..0bf6ba1b0 100644
--- a/docs/parameters.html
+++ b/docs/parameters.html
@@ -1,14 +1,12 @@
-
+
Parameters — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/search.html b/docs/search.html
index 1b492a54c..bd518185a 100644
--- a/docs/search.html
+++ b/docs/search.html
@@ -1,13 +1,11 @@
-
+
Search — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
diff --git a/docs/usage.html b/docs/usage.html
index d08332d34..2acf6cb2b 100644
--- a/docs/usage.html
+++ b/docs/usage.html
@@ -1,14 +1,12 @@
-
+
Quickstart — CodeCarbon 2.3.4 documentation
-
-
-
-
+
+
@@ -122,10 +120,23 @@ Online Mode
+[![asciicast](https://asciinema.org/a/667984.svg)](https://asciinema.org/a/667984 )
Implementing CodeCarbon in your code allows you to track the emissions of a specific block of code.
diff --git a/docs/visualize.html b/docs/visualize.html
index f084d69dc..142e069ac 100644
--- a/docs/visualize.html
+++ b/docs/visualize.html
@@ -1,12 +1,14 @@
-
+
Visualize — CodeCarbon 2.3.4 documentation
-
-
+
+
+
+