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

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:

-
codecarbon monitor --no-api
-
-
+

Create a minimal configuration file (just follow the prompts) +.. code-block:: console

+
+

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

+
+

<script src=”https://asciinema.org/a/bJMOlPe5F4mFLY0Rl6fiJSOp3.js” id=”asciicast-bJMOlPe5F4mFLY0Rl6fiJSOp3” async></script>

+

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 8007bd041..06c33468c 100644 --- a/docs/visualize.html +++ b/docs/visualize.html @@ -1,14 +1,12 @@ - + Visualize — CodeCarbon 2.3.4 documentation - - - - + + From 903e3f3c89e66c7ce8d006fc11be0be968188c50 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 19:01:44 +0100 Subject: [PATCH 25/57] docs(CLI): :memo: fix code block --- docs/edit/usage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index 6b258399c..6510fd0fd 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -17,9 +17,10 @@ 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 config --init Start monitoring the emissions of the computer .. code-block:: console From 56d4f968f46d63d3b8339721becd583bc25b7610 Mon Sep 17 00:00:00 2001 From: benoit-cty <4-benoit-cty@users.noreply.git.leximpact.dev> Date: Fri, 29 Mar 2024 21:22:16 +0100 Subject: [PATCH 26/57] empty line at end of file --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ec6e5c99b..0967e2314 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,5 @@ code_carbon.db emissions*.csv* tests/test_data/rapl/* - #asciinema -*.cast \ No newline at end of file +*.cast From 54ca5945eca6e0cac4190fb74d9980fddf91daa3 Mon Sep 17 00:00:00 2001 From: Luis Blanche Date: Wed, 8 May 2024 11:14:07 +0200 Subject: [PATCH 27/57] Update docs/edit/usage.rst Co-authored-by: Benoit Courty <6603048+benoit-cty@users.noreply.github.com> --- docs/edit/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index 6510fd0fd..250a30b6f 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -30,7 +30,7 @@ Start monitoring the emissions of the computer 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) +to an API running on "https://api.codecarbon.io" (Or you can start a private local API with "docke-compose up") .. raw:: html From 00548c9080d9bce090ec3a7f7681f460a7b48c8e Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Tue, 12 Dec 2023 09:10:27 +0100 Subject: [PATCH 28/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 6a2791c0f39d3db0cda836134d3ab6824c24a689 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Tue, 12 Dec 2023 09:12:04 +0100 Subject: [PATCH 29/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 5f83a3065eea16c8fac888ce3bd647d4a4fbf9ec Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Tue, 12 Dec 2023 09:12:41 +0100 Subject: [PATCH 30/57] build: :arrow_up: add new dependencies for cli --- codecarbon/__init__.py | 1 + 1 file changed, 1 insertion(+) 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" From b4866c08c434631d04666f0b9033834eed2b850a Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 13 Jan 2024 17:34:47 +0100 Subject: [PATCH 31/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 60bbea0b86269ba18b22458161aff474fedba4d7 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 13 Jan 2024 17:35:03 +0100 Subject: [PATCH 32/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 d1dca1f71980a2f134f8a9ec76ff6e7188da919d Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 3 Mar 2024 16:30:34 +0100 Subject: [PATCH 33/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 b342381ab37673b0dad58792c0bb9de5bcf2a3f9 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 3 Mar 2024 16:31:46 +0100 Subject: [PATCH 34/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 0a2b469b600a72b34494570705bd81a870570709 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 10:52:54 +0100 Subject: [PATCH 35/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 8230ec834e4d5a784131071f7e2766f305083a5d Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 13 Jan 2024 17:34:47 +0100 Subject: [PATCH 36/57] fix(cli): :art: add a questionary prompt function allows easier testing --- codecarbon/cli/main.py | 106 +++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 14 deletions(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index cdb727e1e..b2aeeaaed 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -96,6 +96,13 @@ def config( ["/.codecarbonconfig", "Create New Config"], default="/.codecarbonconfig", ) + 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 use_config == "/.codecarbonconfig": typer.echo("Using existing config file :") @@ -185,15 +192,69 @@ def config( team = [t for t in teams if t["name"] == team][0] overwrite_local_config("team_id", team["id"]) - 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", + 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"], + ) + 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_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 ", ) - if project == "Create New Project": - project_name = typer.prompt( - "Project name", 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_description = typer.prompt( "Project description", default="Code Carbon user test" @@ -254,13 +315,30 @@ def config( ) experiment_id = api.create_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)" + ) - overwrite_local_config("experiment_id", experiment_id) - show_config() + 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" + ) @codecarbon.command("monitor", short_help="Monitor your machine's carbon emissions.") From f1208c5163973e112b033b433fdab90b0d720cd5 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 3 Mar 2024 16:30:34 +0100 Subject: [PATCH 37/57] feat(cli): :sparkles: add more functionalities to python API client and CLI --- codecarbon/cli/cli_utils.py | 1 - codecarbon/cli/main.py | 123 +++++------------------------------- 2 files changed, 15 insertions(+), 109 deletions(-) diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py index 727356bc7..015bc1379 100644 --- a/codecarbon/cli/cli_utils.py +++ b/codecarbon/cli/cli_utils.py @@ -5,7 +5,6 @@ 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 b2aeeaaed..2c4a35127 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -1,6 +1,5 @@ import sys import time -from pathlib import Path from typing import Optional import questionary @@ -96,33 +95,14 @@ def config( ["/.codecarbonconfig", "Create New Config"], default="/.codecarbonconfig", ) - 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 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", @@ -164,9 +144,7 @@ def config( organization = [orga for orga in organizations if orga["name"] == org][ 0 ] - overwrite_local_config( - "organization_id", organization["id"], file_path=file_path - ) + overwrite_local_config("organization_id", organization["id"]) teams = api.list_teams_from_organization(organization["id"]) team = questionary_prompt( @@ -192,69 +170,15 @@ def config( team = [t for t in teams if t["name"] == team][0] overwrite_local_config("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", - ) - 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"], + 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", ) - 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_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 ", - ) - - 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" ) project_description = typer.prompt( "Project description", default="Code Carbon user test" @@ -315,30 +239,13 @@ def config( ) 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.") From d95d41faac80b4f7097a661aee915afa3b442ed7 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 10:52:54 +0100 Subject: [PATCH 38/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 793eec56ecc7f5722de70b205120aa832942480b Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 12:39:52 +0100 Subject: [PATCH 39/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 12859c21075be52d98be5c9ffd277f2a8da9ec44 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 13:32:10 +0100 Subject: [PATCH 40/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 ab1c67c8c880d1c888ae2a57094875989e9a6811 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 13:39:53 +0100 Subject: [PATCH 41/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 78e9deb56883da4295fd7fcdeba462057de8d557 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 13:54:25 +0100 Subject: [PATCH 42/57] ci(CLI): :green_heart: debu CI --- tests/test_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8c005053a8aa5291173557af9491153a8fae9dc0 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 17:58:51 +0100 Subject: [PATCH 43/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 c6dd83f1c99b690b95e700ae13b92a9ec3871a55 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 18:54:58 +0100 Subject: [PATCH 44/57] docs(CLI): :memo: test asciinema --- .gitignore | 4 ++++ docs/_sources/usage.rst.txt | 16 +++++++++++++++- 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 +++----- 21 files changed, 100 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 f8e06dfb0..e2f216ba0 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/api.html b/docs/api.html index 5f77dbb84..100e3c7ef 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 440d02acd..ed90e192c 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 f8e06dfb0..4971a90ed 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 9d23b4f84..a33fbf92a 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 7eafc0dca..a95fc0d85 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 f94b0b98b..9e412f23c 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 01c677d7c..9d794133b 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 f5980c094..c9acefafb 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 f702e9f45..b2a34359a 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 7024b203c..0be112e20 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 5c06ecedd..686ebfbf5 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 6bc0e4249..1c4950e51 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 87379c93e..7fb623d3c 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 aa79b6612..d98ffadfd 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 b24d1b57d..e7cbe6616 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

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:

-
codecarbon monitor --no-api
-
-
+

Create a minimal configuration file (just follow the prompts) +.. code-block:: console

+
+

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

+
+

<script src=”https://asciinema.org/a/bJMOlPe5F4mFLY0Rl6fiJSOp3.js” id=”asciicast-bJMOlPe5F4mFLY0Rl6fiJSOp3” async></script>

+

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 142e069ac..f084d69dc 100644 --- a/docs/visualize.html +++ b/docs/visualize.html @@ -1,14 +1,12 @@ - + Visualize — CodeCarbon 2.3.4 documentation - - - - + + From 6332283504cb1004041803000a7510f8cbe69629 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Fri, 22 Mar 2024 19:01:44 +0100 Subject: [PATCH 45/57] docs(CLI): :memo: fix code block --- docs/edit/usage.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index 4971a90ed..973a4b584 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -17,9 +17,10 @@ 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 config --init Start monitoring the emissions of the computer .. code-block:: console From a36aa3017cbc4ab6561595ac2cc50d09f99a13d8 Mon Sep 17 00:00:00 2001 From: benoit-cty <4-benoit-cty@users.noreply.git.leximpact.dev> Date: Fri, 29 Mar 2024 21:22:16 +0100 Subject: [PATCH 46/57] empty line at end of file --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ec6e5c99b..0967e2314 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,5 @@ code_carbon.db emissions*.csv* tests/test_data/rapl/* - #asciinema -*.cast \ No newline at end of file +*.cast From a82ce6709099fe46634fd3d39ae118b780b6ede0 Mon Sep 17 00:00:00 2001 From: Luis Blanche Date: Wed, 8 May 2024 11:14:07 +0200 Subject: [PATCH 47/57] Update docs/edit/usage.rst Co-authored-by: Benoit Courty <6603048+benoit-cty@users.noreply.github.com> --- docs/edit/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index 973a4b584..be17f699c 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -30,7 +30,7 @@ Start monitoring the emissions of the computer 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) +to an API running on "https://api.codecarbon.io" (Or you can start a private local API with "docke-compose up") .. raw:: html From a65474caede0a261607f178d9b6d339ef67bb148 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 18 May 2024 12:00:03 +0200 Subject: [PATCH 48/57] build(CLI): :arrow_up: adapt to hatch --- pyproject.toml | 3 +++ requirements.txt | 27 ++++++++++++++++++++- requirements/requirements-test.py3.8.txt | 31 ++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f9548bc8d..bcc0bdd3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,9 @@ dependencies = [ "pynvml", "rapidfuzz", "requests", + "questionary", + "rich", + "typer" ] [project.urls] diff --git a/requirements.txt b/requirements.txt index 10bc20cc4..ef4f18e00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.8 +# This file is autogenerated by hatch-pip-compile with Python 3.9 # # - arrow # - click @@ -8,8 +8,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # arrow==1.3.0 @@ -22,16 +25,24 @@ click==8.1.7 # via hatch.envs.default idna==3.7 # via requests +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py numpy==1.24.4 # via pandas pandas==2.0.3 # via hatch.envs.default prometheus-client==0.20.0 # via hatch.envs.default +prompt-toolkit==3.0.36 + # via questionary psutil==5.9.8 # via hatch.envs.default py-cpuinfo==9.0.0 # via hatch.envs.default +pygments==2.18.0 + # via rich pynvml==11.5.0 # via hatch.envs.default python-dateutil==2.9.0.post0 @@ -40,15 +51,29 @@ python-dateutil==2.9.0.post0 # pandas pytz==2024.1 # via pandas +questionary==2.0.1 + # via hatch.envs.default rapidfuzz==3.6.2 # via hatch.envs.default requests==2.32.0 # via hatch.envs.default +rich==13.7.1 + # via + # hatch.envs.default + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via python-dateutil +typer==0.12.3 + # via hatch.envs.default types-python-dateutil==2.9.0.20240316 # via arrow +typing-extensions==4.11.0 + # via typer tzdata==2024.1 # via pandas urllib3==2.2.1 # via requests +wcwidth==0.2.13 + # via prompt-toolkit diff --git a/requirements/requirements-test.py3.8.txt b/requirements/requirements-test.py3.8.txt index d76703ca6..740dd4ccb 100644 --- a/requirements/requirements-test.py3.8.txt +++ b/requirements/requirements-test.py3.8.txt @@ -15,8 +15,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # - dash # - dash-bootstrap-components<1.0.0 # - fire @@ -34,6 +37,7 @@ click==8.1.7 # via # hatch.envs.test.py3.8 # flask + # typer dash==2.16.1 # via # hatch.envs.test.py3.8 @@ -64,10 +68,14 @@ itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py mock==5.1.0 # via hatch.envs.test.py3.8 nest-asyncio==1.6.0 @@ -88,10 +96,14 @@ pluggy==1.5.0 # via pytest prometheus-client==0.20.0 # via hatch.envs.test.py3.8 +prompt-toolkit==3.0.36 + # via questionary psutil==5.9.8 # via hatch.envs.test.py3.8 py-cpuinfo==9.0.0 # via hatch.envs.test.py3.8 +pygments==2.18.0 + # via rich pynvml==11.5.0 # via hatch.envs.test.py3.8 pytest==8.2.0 @@ -104,6 +116,8 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via responses +questionary==2.0.1 + # via hatch.envs.test.py3.8 rapidfuzz==3.7.0 # via hatch.envs.test.py3.8 requests==2.32.0 @@ -118,6 +132,12 @@ responses==0.25.0 # via hatch.envs.test.py3.8 retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.test.py3.8 + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # fire @@ -129,17 +149,24 @@ termcolor==2.4.0 # via fire tomli==2.0.1 # via pytest +typer==0.12.3 + # via hatch.envs.test.py3.8 types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.11.0 - # via dash + # via + # dash + # rich + # typer tzdata==2024.1 # via pandas urllib3==2.2.1 # via # requests # responses -werkzeug==3.0.3 +wcwidth==0.2.13 + # via prompt-toolkit +werkzeug==3.0.2 # via # dash # flask From 00a45f50d0d9105aca82249d484027238d03c114 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 29 Jun 2024 12:10:15 +0200 Subject: [PATCH 49/57] merging master --- carbonserver/carbonserver/api/domain/teams.py | 17 --- .../infra/repositories/repository_teams.py | 91 ------------ .../carbonserver/api/routers/teams.py | 70 --------- .../carbonserver/api/services/team_service.py | 20 --- carbonserver/tests/api/routers/test_teams.py | 136 ------------------ .../tests/api/service/test_teams_service.py | 85 ----------- .../metrics/prometheus/__init__.py | 0 .../metrics/prometheus/metrics.py | 65 --------- requirements/requirements-docs.txt | 115 --------------- requirements/requirements-test.py3.7.txt | 98 ------------- 10 files changed, 697 deletions(-) delete mode 100644 carbonserver/carbonserver/api/domain/teams.py delete mode 100644 carbonserver/carbonserver/api/infra/repositories/repository_teams.py delete mode 100644 carbonserver/carbonserver/api/routers/teams.py delete mode 100644 carbonserver/carbonserver/api/services/team_service.py delete mode 100644 carbonserver/tests/api/routers/test_teams.py delete mode 100644 carbonserver/tests/api/service/test_teams_service.py delete mode 100644 codecarbon/output_methods/metrics/prometheus/__init__.py delete mode 100644 codecarbon/output_methods/metrics/prometheus/metrics.py delete mode 100644 requirements/requirements-docs.txt delete mode 100644 requirements/requirements-test.py3.7.txt diff --git a/carbonserver/carbonserver/api/domain/teams.py b/carbonserver/carbonserver/api/domain/teams.py deleted file mode 100644 index 0c85211fc..000000000 --- a/carbonserver/carbonserver/api/domain/teams.py +++ /dev/null @@ -1,17 +0,0 @@ -import abc - -from carbonserver.api import schemas - - -class Teams(abc.ABC): - @abc.abstractmethod - def add_team(self, team: schemas.TeamCreate): - raise NotImplementedError - - @abc.abstractmethod - def get_one_team(self, team_id): - raise NotImplementedError - - @abc.abstractmethod - def list_teams(self): - raise NotImplementedError diff --git a/carbonserver/carbonserver/api/infra/repositories/repository_teams.py b/carbonserver/carbonserver/api/infra/repositories/repository_teams.py deleted file mode 100644 index 9e79f24d0..000000000 --- a/carbonserver/carbonserver/api/infra/repositories/repository_teams.py +++ /dev/null @@ -1,91 +0,0 @@ -from contextlib import AbstractContextManager -from typing import List -from uuid import UUID, uuid4 - -from dependency_injector.providers import Callable -from fastapi import HTTPException - -from carbonserver.api.domain.teams import Teams -from carbonserver.api.infra.api_key_service import generate_api_key -from carbonserver.api.infra.database.sql_models import Team as SqlModelTeam -from carbonserver.api.schemas import Team, TeamCreate - -""" -Here there is all the method to manipulate the team data -""" - - -class SqlAlchemyRepository(Teams): - def __init__(self, session_factory) -> Callable[..., AbstractContextManager]: - self.session_factory = session_factory - - def add_team(self, team: TeamCreate) -> Team: - with self.session_factory() as session: - db_team = SqlModelTeam( - id=uuid4(), - name=team.name, - description=team.description, - api_key=generate_api_key(), - organization_id=team.organization_id, - ) - session.add(db_team) - session.commit() - session.refresh(db_team) - return self.map_sql_to_schema(db_team) - - def get_one_team(self, team_id): - """Find the team in database and return it - - :team_id: The id of the team to retreive. - :returns: An Team in pyDantic BaseModel format. - :rtype: schemas.Team - """ - with self.session_factory() as session: - e = session.query(SqlModelTeam).filter(SqlModelTeam.id == team_id).first() - if e is None: - raise HTTPException(status_code=404, detail=f"Team {team_id} not found") - return self.map_sql_to_schema(e) - - def list_teams(self): - with self.session_factory() as session: - e = session.query(SqlModelTeam) - if e is None: - return None - teams: List[Team] = [] - for team in e: - teams.append(self.map_sql_to_schema(team)) - return teams - - def get_teams_from_organization(self, organization_id) -> List[Team]: - """Find the list of teams from an organization in database and return it - - :organization_id: The id of the organization to retreive teams from. - :returns: List of Teams in pyDantic BaseModel format. - :rtype: List[schemas.Team] - """ - with self.session_factory() as session: - res = session.query(SqlModelTeam).filter( - SqlModelTeam.organization_id == organization_id - ) - if res.first() is None: - return [] - return [self.map_sql_to_schema(e) for e in res] - - def is_api_key_valid(self, organization_id: UUID, api_key: str): - with self.session_factory() as session: - return bool( - session.query(SqlModelTeam) - .filter(SqlModelTeam.id == organization_id) - .filter(SqlModelTeam.api_key == api_key) - .first() - ) - - @staticmethod - def map_sql_to_schema(team: SqlModelTeam) -> Team: - return Team( - id=str(team.id), - name=team.name, - api_key=team.api_key, - description=team.description, - organization_id=str(team.organization_id), - ) diff --git a/carbonserver/carbonserver/api/routers/teams.py b/carbonserver/carbonserver/api/routers/teams.py deleted file mode 100644 index 1b88468a8..000000000 --- a/carbonserver/carbonserver/api/routers/teams.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import List - -from container import ServerContainer -from dependency_injector.wiring import Provide, inject -from fastapi import APIRouter, Depends -from starlette import status - -from carbonserver.api.dependencies import get_token_header -from carbonserver.api.schemas import Team, TeamCreate -from carbonserver.api.services.team_service import TeamService - -TEAMS_ROUTER_TAGS = ["Teams"] - -router = APIRouter( - dependencies=[Depends(get_token_header)], -) - - -@router.post( - "/team", - tags=TEAMS_ROUTER_TAGS, - status_code=status.HTTP_201_CREATED, - response_model=Team, -) -@inject -def add_team( - team: TeamCreate, - team_service: TeamService = Depends(Provide[ServerContainer.team_service]), -) -> Team: - return team_service.add_team(team) - - -@router.get( - "/team/{team_id}", - tags=TEAMS_ROUTER_TAGS, - status_code=status.HTTP_200_OK, - response_model=Team, -) -@inject -def read_team( - team_id: str, - team_service: TeamService = Depends(Provide[ServerContainer.team_service]), -) -> Team: - return team_service.read_team(team_id) - - -@router.get( - "/teams", - tags=TEAMS_ROUTER_TAGS, - status_code=status.HTTP_200_OK, - response_model=List[Team], -) -@inject -def list_teams( - team_service: TeamService = Depends(Provide[ServerContainer.team_service]), -) -> List[Team]: - return team_service.list_teams() - - -@router.get( - "/teams/organization/{organization_id}", - tags=TEAMS_ROUTER_TAGS, - status_code=status.HTTP_200_OK, -) -@inject -def read_teams_from_organization( - organization_id: str, - team_service: TeamService = Depends(Provide[ServerContainer.team_service]), -): - return team_service.list_teams_from_organization(organization_id) diff --git a/carbonserver/carbonserver/api/services/team_service.py b/carbonserver/carbonserver/api/services/team_service.py deleted file mode 100644 index 5363e549d..000000000 --- a/carbonserver/carbonserver/api/services/team_service.py +++ /dev/null @@ -1,20 +0,0 @@ -from carbonserver.api.infra.repositories.repository_teams import SqlAlchemyRepository -from carbonserver.api.schemas import Team, TeamCreate - - -class TeamService: - def __init__(self, team_repository: SqlAlchemyRepository): - self._repository = team_repository - - def add_team(self, team: TeamCreate) -> Team: - created_team = self._repository.add_team(team) - return created_team - - def read_team(self, team_id: str) -> Team: - return self._repository.get_one_team(team_id) - - def list_teams(self): - return self._repository.list_teams() - - def list_teams_from_organization(self, organization_id: str): - return self._repository.get_teams_from_organization(organization_id) diff --git a/carbonserver/tests/api/routers/test_teams.py b/carbonserver/tests/api/routers/test_teams.py deleted file mode 100644 index f81d484dd..000000000 --- a/carbonserver/tests/api/routers/test_teams.py +++ /dev/null @@ -1,136 +0,0 @@ -from unittest import mock - -import pytest -from container import ServerContainer -from fastapi import FastAPI, status -from fastapi.testclient import TestClient - -from carbonserver.api.infra.repositories.repository_teams import SqlAlchemyRepository -from carbonserver.api.routers import teams -from carbonserver.api.schemas import Team - -API_KEY = "U5W0EUP9y6bBENOnZWJS0g" - -ORG_ID = "e52fe339-164d-4c2b-a8c0-f562dfce066d" -ORG_ID_2 = "f688133d-2cb9-41f0-9362-a4c05ceb0dd8" - -TEAM_ID = "8edb03e1-9a28-452a-9c93-a3b6560136d7" -TEAM_ID_2 = "8edb03e1-9a28-452a-9c93-a3b6560136d6" - -TEAM_TO_CREATE = { - "name": "Data For Good Code Carbon", - "description": "DFG Code Carbon Team", - "organization_id": ORG_ID, -} - -TEAM_1 = { - "id": TEAM_ID, - "api_key": API_KEY, - "name": "Data For Good Code Carbon", - "description": "Data For Good Code Carbon Team", - "organization_id": ORG_ID, - "projects": [], -} - -TEAM_WITH_NO_ORG = { - "id": TEAM_ID, - "name": "Data For Good Code Carbon", - "description": "Data For Good Code Carbon Team", -} - - -TEAM_2 = { - "id": TEAM_ID_2, - "api_key": API_KEY, - "name": "Data For Good Code Carbon 2", - "description": "Data For Good Code Carbon Team 2", - "organization_id": ORG_ID_2, - "projects": [], -} - - -@pytest.fixture -def custom_test_server(): - container = ServerContainer() - container.wire(modules=[teams]) - app = FastAPI() - app.container = container - app.include_router(teams.router) - yield app - - -@pytest.fixture -def client(custom_test_server): - yield TestClient(custom_test_server) - - -def test_add_team(client, custom_test_server): - repository_mock = mock.Mock(spec=SqlAlchemyRepository) - expected_team = TEAM_1 - repository_mock.add_team.return_value = Team(**TEAM_1) - with custom_test_server.container.team_repository.override(repository_mock): - response = client.post("/team", json=TEAM_TO_CREATE) - actual_team = response.json() - - assert response.status_code == status.HTTP_201_CREATED - assert actual_team == expected_team - - -def test_add_team_with_no_org_links_to_data_for_good_org(client, custom_test_server): - repository_mock = mock.Mock(spec=SqlAlchemyRepository) - default_org_id = ORG_ID - repository_mock.add_team.return_value = Team(**TEAM_1) - - with custom_test_server.container.team_repository.override(repository_mock): - response = client.post("/team", json=TEAM_TO_CREATE) - actual_team = response.json() - - assert response.status_code == status.HTTP_201_CREATED - assert actual_team["organization_id"] == default_org_id - - -def test_get_one_team_returns_correct_team(client, custom_test_server): - repository_mock = mock.Mock(spec=SqlAlchemyRepository) - expected_team = TEAM_1 - repository_mock.get_one_team.return_value = Team(**expected_team) - - with custom_test_server.container.team_repository.override(repository_mock): - response = client.get("/team/read_team/", params={"id": TEAM_ID}) - actual_team = response.json() - - assert response.status_code == status.HTTP_200_OK - assert actual_team == expected_team - - -def test_list_teams_returns_all_teams(client, custom_test_server): - repository_mock = mock.Mock(spec=SqlAlchemyRepository) - expected_team_1 = TEAM_1 - expected_team_2 = TEAM_2 - expected_team_list = [expected_team_1, expected_team_2] - repository_mock.list_teams.return_value = [ - Team(**expected_team_1), - Team(**expected_team_2), - ] - - with custom_test_server.container.team_repository.override(repository_mock): - response = client.get("/teams") - actual_team_list = response.json() - - assert response.status_code == status.HTTP_200_OK - assert actual_team_list == expected_team_list - - -def test_get_teams_from_organization_returns_correct_team(client, custom_test_server): - repository_mock = mock.Mock(spec=SqlAlchemyRepository) - expected_team_1 = TEAM_1 - expected_team_list = [expected_team_1] - repository_mock.get_teams_from_organization.return_value = [ - Team(**expected_team_1), - ] - - with custom_test_server.container.team_repository.override(repository_mock): - response = client.get("/teams/organization/" + ORG_ID) - actual_team_list = response.json() - - assert response.status_code == status.HTTP_200_OK - assert actual_team_list == expected_team_list diff --git a/carbonserver/tests/api/service/test_teams_service.py b/carbonserver/tests/api/service/test_teams_service.py deleted file mode 100644 index 3ca900a1c..000000000 --- a/carbonserver/tests/api/service/test_teams_service.py +++ /dev/null @@ -1,85 +0,0 @@ -from unittest import mock -from uuid import UUID - -from carbonserver.api.infra.repositories.repository_teams import SqlAlchemyRepository -from carbonserver.api.schemas import Team, TeamCreate -from carbonserver.api.services.team_service import TeamService - -API_KEY = "9INn3JsdhCGzLAuOUC6rAw" - -ORG_ID = UUID("e52fe339-164d-4c2b-a8c0-f562dfce066d") -ORG_ID_2 = UUID("e395767d-0255-40f3-a314-5d2e01f56fbd") - -TEAM_ID = UUID("c13e851f-5c2f-403d-98d0-51fe15df3bc3") -TEAM_ID_2 = UUID("dd011783-7d05-4376-ab60-9537738be25f") - -TEAM_1 = Team( - id=TEAM_ID, - name="DFG Code Carbon", - description="DFG Code Carbon Team", - api_key=API_KEY, - organization_id=ORG_ID, -) - -TEAM_2 = Team( - id=TEAM_ID_2, - name="DFG Code Carbon 2", - description="DFG Code Carbon 2", - api_key=API_KEY, - organization_id=ORG_ID_2, -) - - -@mock.patch("uuid.uuid4", return_value=TEAM_ID) -def test_teams_service_creates_correct_team(_): - repository_mock: SqlAlchemyRepository = mock.Mock(spec=SqlAlchemyRepository) - expected_id = TEAM_ID - user_service: TeamService = TeamService(repository_mock) - repository_mock.add_team.return_value = TEAM_1 - team_to_create = TeamCreate( - name="DFG Code Carbon", - description="DFG Code Carbon Team", - organization_id=ORG_ID, - ) - - actual_saved_org = user_service.add_team(team_to_create) - - repository_mock.add_team.assert_called_with(team_to_create) - assert actual_saved_org.id == expected_id - - -def test_teams_service_retrieves_all_existing_teams(): - repository_mock: SqlAlchemyRepository = mock.Mock(spec=SqlAlchemyRepository) - expected_team_ids_list = [TEAM_ID, TEAM_ID_2] - organization_service: TeamService = TeamService(repository_mock) - repository_mock.list_teams.return_value = [TEAM_1, TEAM_2] - - team_list = organization_service.list_teams() - actual_team_ids_list = map(lambda x: x.id, iter(team_list)) - diff = set(actual_team_ids_list) ^ set(expected_team_ids_list) - - assert not diff - assert len(team_list) == len(expected_team_ids_list) - - -def test_teams_service_retrieves_correct_team_by_id(): - repository_mock: SqlAlchemyRepository = mock.Mock(spec=SqlAlchemyRepository) - expected_org: Team = TEAM_1 - organization_service: TeamService = TeamService(repository_mock) - repository_mock.get_one_team.return_value = TEAM_1 - - actual_saved_org = organization_service.read_team(TEAM_ID) - - assert actual_saved_org.id == expected_org.id - assert actual_saved_org.name == expected_org.name - - -def test_teams_service_retrieves_correct_team_by_organization_id(): - repository_mock: SqlAlchemyRepository = mock.Mock(spec=SqlAlchemyRepository) - expected_organization_id = ORG_ID - team_service: TeamService = TeamService(repository_mock) - repository_mock.get_teams_from_organization.return_value = [TEAM_1] - - actual_teams = team_service.list_teams_from_organization(ORG_ID) - - assert actual_teams[0].organization_id == expected_organization_id diff --git a/codecarbon/output_methods/metrics/prometheus/__init__.py b/codecarbon/output_methods/metrics/prometheus/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/codecarbon/output_methods/metrics/prometheus/metrics.py b/codecarbon/output_methods/metrics/prometheus/metrics.py deleted file mode 100644 index 53397b7a4..000000000 --- a/codecarbon/output_methods/metrics/prometheus/metrics.py +++ /dev/null @@ -1,65 +0,0 @@ -from prometheus_client import CollectorRegistry, Gauge - -from codecarbon.output_methods.metrics.metric_docs import ( - MetricDocumentation, - cpu_energy_doc, - cpu_power_doc, - duration_doc, - emissions_doc, - emissions_rate_doc, - energy_consumed_doc, - gpu_energy_doc, - gpu_power_doc, - ram_energy_doc, - ram_power_doc, -) - -registry = CollectorRegistry() - - -# TODO: add labelnames -# timestamp: str -# run_id: str -# python_version: str -# longitude: float -# latitude: float -# on_cloud: str = "N" - -# TODO: Set up the possible labels -labelnames = [ - "project_name", - "country_name", - "country_iso_code", - "region", - "cloud_provider", - "cloud_region", - "os", - "codecarbon_version", - "cpu_model", - "cpu_count", - "gpu_model", - "gpu_count", - "tracking_mode", - "ram_total_size", -] - - -def generate_gauge(metric_doc: MetricDocumentation): - return Gauge( - metric_doc.name, - metric_doc.description, - labelnames, - registry=registry, - ) - - -duration_gauge = generate_gauge(duration_doc) -emissions_gauge = generate_gauge(emissions_doc) -emissions_rate_gauge = generate_gauge(emissions_rate_doc) -cpu_power_gauge = generate_gauge(cpu_power_doc) -gpu_power_gauge = generate_gauge(gpu_power_doc) -ram_power_gauge = generate_gauge(ram_power_doc) -cpu_energy_gauge = generate_gauge(cpu_energy_doc) -gpu_energy_gauge = generate_gauge(gpu_energy_doc) -ram_energy_gauge = generate_gauge(ram_energy_doc) -energy_consumed_gauge = generate_gauge(energy_consumed_doc) diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt deleted file mode 100644 index 3c0949ddd..000000000 --- a/requirements/requirements-docs.txt +++ /dev/null @@ -1,115 +0,0 @@ -# -# This file is autogenerated by hatch-pip-compile with Python 3.8 -# -# - make -# - sphinx -# - sphinx_rtd_theme -# - arrow -# - click -# - pandas -# - prometheus-client -# - psutil -# - py-cpuinfo -# - pynvml -# - rapidfuzz -# - requests -# - -alabaster==0.7.13 - # via sphinx -arrow==1.3.0 - # via - # hatch.envs.docs - # jinja2-time -babel==2.14.0 - # via sphinx -certifi==2024.2.2 - # via requests -charset-normalizer==3.3.2 - # via requests -click==8.1.7 - # via hatch.envs.docs -docutils==0.20.1 - # via - # sphinx - # sphinx-rtd-theme -idna==3.7 - # via requests -imagesize==1.4.1 - # via sphinx -importlib-metadata==7.1.0 - # via sphinx -jinja2==3.1.4 - # via - # jinja2-time - # make - # sphinx -jinja2-time==0.2.0 - # via make -make==0.1.6.post2 - # via hatch.envs.docs -markupsafe==2.1.5 - # via jinja2 -numpy==1.24.4 - # via pandas -packaging==24.0 - # via sphinx -pandas==2.0.3 - # via hatch.envs.docs -prometheus-client==0.20.0 - # via hatch.envs.docs -psutil==5.9.8 - # via hatch.envs.docs -py-cpuinfo==9.0.0 - # via hatch.envs.docs -pygments==2.17.2 - # via sphinx -pynvml==11.5.0 - # via hatch.envs.docs -python-dateutil==2.9.0.post0 - # via - # arrow - # pandas -pytz==2024.1 - # via - # babel - # pandas -rapidfuzz==3.6.2 - # via hatch.envs.docs -requests==2.32.0 - # via - # hatch.envs.docs - # sphinx -six==1.16.0 - # via python-dateutil -snowballstemmer==2.2.0 - # via sphinx -sphinx==7.1.2 - # via - # hatch.envs.docs - # sphinx-rtd-theme - # sphinxcontrib-jquery -sphinx-rtd-theme==2.0.0 - # via hatch.envs.docs -sphinxcontrib-applehelp==1.0.4 - # via sphinx -sphinxcontrib-devhelp==1.0.2 - # via sphinx -sphinxcontrib-htmlhelp==2.0.1 - # via sphinx -sphinxcontrib-jquery==4.1 - # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==1.0.3 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 - # via sphinx -types-python-dateutil==2.9.0.20240316 - # via arrow -tzdata==2024.1 - # via pandas -urllib3==2.2.1 - # via requests -zipp==3.18.1 - # via importlib-metadata diff --git a/requirements/requirements-test.py3.7.txt b/requirements/requirements-test.py3.7.txt deleted file mode 100644 index 8fb8d6917..000000000 --- a/requirements/requirements-test.py3.7.txt +++ /dev/null @@ -1,98 +0,0 @@ -# -# This file is autogenerated by hatch-pip-compile with Python 3.7 -# -# - mock -# - pytest -# - responses -# - numpy -# - psutil -# - requests-mock -# - rapidfuzz -# - arrow -# - click -# - pandas -# - prometheus-client -# - psutil -# - py-cpuinfo -# - pynvml -# - rapidfuzz -# - requests -# - -arrow==1.2.3 - # via hatch.envs.test.py3.7 -certifi==2024.2.2 - # via requests -charset-normalizer==3.3.2 - # via requests -click==8.1.7 - # via hatch.envs.test.py3.7 -exceptiongroup==1.2.0 - # via pytest -idna==3.7 - # via requests -importlib-metadata==6.7.0 - # via - # click - # pluggy - # pytest -iniconfig==2.0.0 - # via pytest -mock==5.1.0 - # via hatch.envs.test.py3.7 -numpy==1.21.6 - # via - # hatch.envs.test.py3.7 - # pandas -packaging==24.0 - # via pytest -pandas==1.3.5 - # via hatch.envs.test.py3.7 -pluggy==1.2.0 - # via pytest -prometheus-client==0.17.1 - # via hatch.envs.test.py3.7 -psutil==5.9.8 - # via hatch.envs.test.py3.7 -py-cpuinfo==9.0.0 - # via hatch.envs.test.py3.7 -pynvml==11.5.0 - # via hatch.envs.test.py3.7 -pytest==7.4.4 - # via hatch.envs.test.py3.7 -python-dateutil==2.9.0.post0 - # via - # arrow - # pandas -pytz==2024.1 - # via pandas -pyyaml==6.0.1 - # via responses -rapidfuzz==3.4.0 - # via hatch.envs.test.py3.7 -requests==2.32.0 - # via - # hatch.envs.test.py3.7 - # requests-mock - # responses -requests-mock==1.12.1 - # via hatch.envs.test.py3.7 -responses==0.23.3 - # via hatch.envs.test.py3.7 -six==1.16.0 - # via python-dateutil -tomli==2.0.1 - # via pytest -types-pyyaml==6.0.12.12 - # via responses -typing-extensions==4.7.1 - # via - # arrow - # importlib-metadata - # responses -urllib3==2.0.7 - # via - # requests - # responses -zipp==3.15.0 - # via importlib-metadata From ca869ea969597c10419b84adb4adcc26a70f2dc0 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 13 Jul 2024 19:37:02 +0200 Subject: [PATCH 50/57] WIP CLI 2 --- codecarbon/cli/main.py | 95 +++++++++---------------------- codecarbon/core/api_client.py | 76 ++++++++++++------------- codecarbon/core/schemas.py | 17 +----- requirements/requirements-api.txt | 18 +++++- 4 files changed, 81 insertions(+), 125 deletions(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 59b2f2b01..3218d2c24 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -17,12 +17,7 @@ overwrite_local_config, ) from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone -from codecarbon.core.schemas import ( - ExperimentCreate, - OrganizationCreate, - ProjectCreate, - TeamCreate, -) +from codecarbon.core.schemas import ExperimentCreate, OrganizationCreate, ProjectCreate from codecarbon.emissions_tracker import EmissionsTracker DEFAULT_PROJECT_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1" @@ -66,32 +61,24 @@ def show_config(path: Path = Path("./.codecarbon.config")) -> None: else: org = api.get_organization(d["organization_id"]) - if "team_id" not in d: + if "project_id" not in d: print( - "No team_id in config, follow setup instruction to complete your configuration file!", + "No project_id in config, follow setup instruction to complete your configuration file!", ) else: - team = api.get_team(d["team_id"]) - if "project_id" not in d: + project = api.get_project(d["project_id"]) + if "experiment_id" not in d: print( - "No project_id in config, follow setup instruction to complete your configuration file!", + "No experiment_id in config, follow setup instruction to complete your configuration file!", ) 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!", - ) - else: - experiment = api.get_experiment(d["experiment_id"]) - print("\nExperiment :") - print(experiment) - print("\nProject :") - print(project) - print("\nTeam :") - print(team) - print("\nOrganization :") - print(org) + experiment = api.get_experiment(d["experiment_id"]) + print("\nExperiment :") + print(experiment) + print("\nProject :") + print(project) + print("\nOrganization :") + print(org) except Exception as e: raise ValueError( f"Your configuration is invalid, please run `codecarbon config --init` first! (error: {e})" @@ -152,50 +139,23 @@ def config(): 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." - ) - 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) - 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"], + organization_create = OrganizationCreate( + name=org_name, + description=org_description, ) - team = api.create_team( - team=team_create, - ) - print(f"Created team : {team}") + organization = api.create_organization(organization=organization_create) + print(f"Created organization : {organization}") else: - team = [t for t in teams if t["name"] == team][0] - overwrite_local_config("team_id", team["id"], path=file_path) + organization = [orga for orga in organizations if orga["name"] == org][0] + org_id = organization["id"] + overwrite_local_config("organization_id", org_id, path=file_path) - projects = api.list_projects_from_team(team["id"]) + projects = api.list_projects_from_organization(org_id) + project_names = [project["name"] for project in projects] if projects else [] project = questionary_prompt( "Pick existing project from list or Create new project ?", - [project["name"] for project in projects] + ["Create New Project"], + project_names + ["Create New Project"], default="Create New Project", ) if project == "Create New Project": @@ -206,15 +166,16 @@ def config(): project_create = ProjectCreate( name=project_name, description=project_description, - team_id=team["id"], + organization_id=org_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) + project_id = project["id"] + overwrite_local_config("project_id", project_id, path=file_path) - experiments = api.list_experiments_from_project(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"], diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py index ae557a6f8..d0539ff46 100644 --- a/codecarbon/core/api_client.py +++ b/codecarbon/core/api_client.py @@ -20,7 +20,6 @@ OrganizationCreate, ProjectCreate, RunCreate, - TeamCreate, ) from codecarbon.external.logger import logger @@ -78,68 +77,65 @@ def get_list_organizations(self): return None return r.json() + def check_organization_exists(self, organization_name: str): + """ + Check if an organization exists + """ + organizations = self.get_list_organizations() + if organizations is None: + return False + for organization in organizations: + if organization["name"] == organization_name: + return organization + return False + 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() + url = self.url + "/organizations" + if organization := self.check_organization_exists(organization.name): + logger.warning( + f"Organization {organization['name']} already exists. Skipping creation." + ) + return organization + else: + 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 + url = self.url + "/organizations/" + 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): + def update_organization(self, organization: OrganizationCreate): """ - List all teams + Update an organization """ - url = self.url + "/teams/organization/" + organization_id - r = requests.get(url=url, timeout=2) + payload = dataclasses.asdict(organization) + url = self.url + "/organizations/" + organization.id + r = requests.patch(url=url, json=payload, 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 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): + def list_projects_from_organization(self, organization_id): """ List all projects """ - url = self.url + "/projects/team/" + team_id + url = self.url + "/projects/" + organization_id + r = requests.get(url=url, timeout=2) if r.status_code != 200: self._log_error(url, {}, r) @@ -151,7 +147,7 @@ def create_project(self, project: ProjectCreate): Create a project """ payload = dataclasses.asdict(project) - url = self.url + "/project" + url = self.url + "/projects" r = requests.post(url=url, json=payload, timeout=2) if r.status_code != 201: self._log_error(url, payload, r) @@ -162,7 +158,7 @@ def get_project(self, project_id): """ Get a project """ - url = self.url + "/project/" + project_id + url = self.url + "/projects/" + project_id r = requests.get(url=url, timeout=2) if r.status_code != 200: self._log_error(url, {}, r) @@ -273,7 +269,7 @@ def list_experiments_from_project(self, project_id: str): r = requests.get(url=url, timeout=2) if r.status_code != 200: self._log_error(url, {}, r) - return None + return [] return r.json() def set_experiment(self, experiment_id: str): diff --git a/codecarbon/core/schemas.py b/codecarbon/core/schemas.py index d8969dfa1..225deb6a5 100644 --- a/codecarbon/core/schemas.py +++ b/codecarbon/core/schemas.py @@ -95,26 +95,11 @@ 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 + organization_id: str class ProjectCreate(ProjectBase): diff --git a/requirements/requirements-api.txt b/requirements/requirements-api.txt index 9d88c8c10..67ba70645 100644 --- a/requirements/requirements-api.txt +++ b/requirements/requirements-api.txt @@ -30,8 +30,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # alembic==1.13.1 @@ -134,6 +137,8 @@ pluggy==1.5.0 # via pytest prometheus-client==0.20.0 # via hatch.envs.api +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.api psycopg2-binary==2.9.9 @@ -170,6 +175,8 @@ pyyaml==6.0.1 # via # responses # uvicorn +questionary==2.0.1 + # via hatch.envs.api rapidfuzz==3.9.3 # via hatch.envs.api requests==2.32.3 @@ -182,7 +189,9 @@ requests-mock==1.12.1 responses==0.25.3 # via hatch.envs.api rich==13.7.1 - # via typer + # via + # hatch.envs.api + # typer shellingham==1.5.4 # via typer six==1.16.0 @@ -200,7 +209,9 @@ sqlalchemy==1.4.52 starlette==0.37.2 # via fastapi typer==0.12.3 - # via fastapi-cli + # via + # hatch.envs.api + # fastapi-cli types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 @@ -210,6 +221,7 @@ typing-extensions==4.12.2 # fastapi-pagination # jwcrypto # pydantic + # starlette # typer tzdata==2024.1 # via pandas @@ -227,5 +239,7 @@ uvloop==0.19.0 # via uvicorn watchfiles==0.22.0 # via uvicorn +wcwidth==0.2.13 + # via prompt-toolkit websockets==12.0 # via uvicorn From fe7609f69e1eb13e9e27720d3771d25f8672dcc1 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sat, 13 Jul 2024 20:12:40 +0200 Subject: [PATCH 51/57] fix(CLI): :bug: make cli work --- codecarbon/cli/main.py | 8 ++++++-- codecarbon/core/api_client.py | 14 +++++++------- requirements/requirements-api.txt | 15 +++++++++------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/codecarbon/cli/main.py b/codecarbon/cli/main.py index 3218d2c24..8c4e0a4e0 100644 --- a/codecarbon/cli/main.py +++ b/codecarbon/cli/main.py @@ -176,9 +176,13 @@ def config(): overwrite_local_config("project_id", project_id, path=file_path) experiments = api.list_experiments_from_project(project_id) + experiments_names = ( + [experiment["name"] for experiment in experiments] if experiments else [] + ) + experiment = questionary_prompt( "Pick existing experiment from list or Create new experiment ?", - [experiment["name"] for experiment in experiments] + ["Create New Experiment"], + experiments_names + ["Create New Experiment"], default="Create New Experiment", ) if experiment == "Create New Experiment": @@ -221,7 +225,7 @@ def config(): cloud_provider=cloud_provider, cloud_region=cloud_region, ) - experiment = api.create_experiment(experiment=experiment_create) + experiment = api.add_experiment(experiment=experiment_create) else: experiment = [e for e in experiments if e["name"] == experiment][0] diff --git a/codecarbon/core/api_client.py b/codecarbon/core/api_client.py index d0539ff46..366cd42b7 100644 --- a/codecarbon/core/api_client.py +++ b/codecarbon/core/api_client.py @@ -134,7 +134,7 @@ def list_projects_from_organization(self, organization_id): """ List all projects """ - url = self.url + "/projects/" + organization_id + url = self.url + "/organizations/" + organization_id + "/projects" r = requests.get(url=url, timeout=2) if r.status_code != 200: @@ -201,7 +201,7 @@ def add_emission(self, carbon_emission: dict): ) try: payload = dataclasses.asdict(emission) - url = self.url + "/emission" + url = self.url + "/emissions" r = requests.post(url=url, json=payload, timeout=2) if r.status_code != 201: self._log_error(url, payload, r) @@ -241,7 +241,7 @@ def _create_run(self, experiment_id): tracking_mode=self.conf.get("tracking_mode"), ) payload = dataclasses.asdict(run) - url = self.url + "/run" + url = self.url + "/runs" r = requests.post(url=url, json=payload, timeout=2) if r.status_code != 201: self._log_error(url, payload, r) @@ -265,7 +265,7 @@ def list_experiments_from_project(self, project_id: str): """ List all experiments for a project """ - url = self.url + "/experiments/project/" + project_id + url = self.url + "/projects/" + project_id + "/experiments" r = requests.get(url=url, timeout=2) if r.status_code != 200: self._log_error(url, {}, r) @@ -278,13 +278,13 @@ def set_experiment(self, experiment_id: str): """ self.experiment_id = experiment_id - def create_experiment(self, experiment: ExperimentCreate): + def add_experiment(self, experiment: ExperimentCreate): """ Create an experiment, used by the CLI, not the package. ::experiment:: The experiment to create. """ payload = dataclasses.asdict(experiment) - url = self.url + "/experiment" + url = self.url + "/experiments" r = requests.post(url=url, json=payload, timeout=2) if r.status_code != 201: self._log_error(url, payload, r) @@ -295,7 +295,7 @@ def get_experiment(self, experiment_id): """ Get an experiment by id """ - url = self.url + "/experiment/" + experiment_id + url = self.url + "/experiments/" + experiment_id r = requests.get(url=url, timeout=2) if r.status_code != 200: self._log_error(url, {}, r) diff --git a/requirements/requirements-api.txt b/requirements/requirements-api.txt index 67ba70645..73a566598 100644 --- a/requirements/requirements-api.txt +++ b/requirements/requirements-api.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.11 +# This file is autogenerated by hatch-pip-compile with Python 3.9 # # - alembic<2.0.0 # - bcrypt<5.0.0 @@ -7,7 +7,6 @@ # - dependency-injector<5.0.0 # - fastapi<1.0.0 # - fief-client[fastapi] -# - pyjwt # - httpx # - pydantic[email]<2.0.0 # - psycopg2-binary<3.0.0 @@ -72,6 +71,10 @@ email-validator==2.2.0 # via # fastapi # pydantic +exceptiongroup==1.2.2 + # via + # anyio + # pytest fastapi==0.111.0 # via # hatch.envs.api @@ -82,8 +85,6 @@ fastapi-pagination==0.12.25 # via hatch.envs.api fief-client==0.19.0 # via hatch.envs.api -greenlet==3.0.3 - # via sqlalchemy h11==0.14.0 # via # httpcore @@ -154,8 +155,6 @@ pydantic==1.10.16 # fastapi-pagination pygments==2.18.0 # via rich -pyjwt==2.8.0 - # via hatch.envs.api pynvml==11.5.0 # via hatch.envs.api pytest==8.2.2 @@ -208,6 +207,8 @@ sqlalchemy==1.4.52 # alembic starlette==0.37.2 # via fastapi +tomli==2.0.1 + # via pytest typer==0.12.3 # via # hatch.envs.api @@ -217,12 +218,14 @@ types-python-dateutil==2.9.0.20240316 typing-extensions==4.12.2 # via # alembic + # anyio # fastapi # fastapi-pagination # jwcrypto # pydantic # starlette # typer + # uvicorn tzdata==2024.1 # via pandas ujson==5.10.0 From a23b72118293e8f47149b68b1f169543af0a1bf9 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 14 Jul 2024 10:34:00 +0200 Subject: [PATCH 52/57] test(CLI): :white_check_mark: pass tests --- tests/test_cli.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c28c88308..0b363e101 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,6 @@ import os import tempfile import unittest -from pathlib import Path from unittest.mock import MagicMock, patch from typer.testing import CliRunner @@ -42,7 +41,7 @@ def setUp(self): "id": "1", "name": "test project Code Carbon", } - self.mock_api_client.create_experiment.return_value = { + self.mock_api_client.add_experiment.return_value = { "id": "1", "name": "test experiment Code Carbon", } @@ -55,7 +54,7 @@ def test_app(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): + def test_config_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient): temp_dir = os.getenv("RUNNER_TEMP", tempfile.gettempdir()) temp_codecarbon_config = tempfile.NamedTemporaryFile( mode="w+t", delete=False, dir=temp_dir @@ -63,8 +62,8 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient): MockApiClient.return_value = self.mock_api_client mock_prompt.side_effect = [ + "/tmp/.codecarbon.config", "Create New Organization", - "Create New Team", "Create New Project", "Create New Experiment", ] @@ -72,12 +71,12 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient): result = self.runner.invoke( codecarbon, - ["config", "--init"], + ["config"], input=f"{temp_codecarbon_config.name}\n", ) self.assertEqual(result.exit_code, 0) self.assertIn( - "Creating new experiment", + "Creating new experiment\nExperiment name : [Code Carbon user test]", result.stdout, ) self.assertIn( @@ -85,23 +84,20 @@ def test_init_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient): result.stdout, ) - @patch("codecarbon.cli.main.Path") + @patch("codecarbon.cli.main.get_config") @patch("codecarbon.cli.main.questionary_prompt") - def test_init_use_local(self, mock_prompt, mock_path, MockApiClient): - 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) - temp_codecarbon_config.seek(0) - mock_prompt.return_value = "./.codecarbon.config" - result = self.runner.invoke(codecarbon, ["config", "--init"], input="n") + def test_init_use_local(self, mock_prompt, mock_config, MockApiClient): + mock_prompt.return_value = "~/.codecarbon.config" + mock_config.return_value = { + "api_endpoint": "http://localhost:8008", + "organization_id": "114", + "project_id": "133", + "experiment_id": "yolo123", + } + result = self.runner.invoke(codecarbon, ["config"], input="n") self.assertEqual(result.exit_code, 0) self.assertIn( - "Using already existing config file ", + "Using already existing global config file ", result.stdout, ) From 64cd7d3f047f0c998034483e03b149e3ca30756a Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 14 Jul 2024 10:34:19 +0200 Subject: [PATCH 53/57] fix: :white_check_mark: fix API tests --- tests/test_api_call.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_api_call.py b/tests/test_api_call.py index a413dd94b..be6201d39 100644 --- a/tests/test_api_call.py +++ b/tests/test_api_call.py @@ -26,7 +26,7 @@ def test_call_api(): with requests_mock.Mocker() as m: m.post( - "http://test.com/run", + "http://test.com/runs", json={ "id": "82ba0923-0713-4da1-9e57-cea70b460ee9", "timestamp": "2021-04-04T08:43:00+02:00", @@ -56,7 +56,7 @@ def test_call_api(): assert api.run_id == "82ba0923-0713-4da1-9e57-cea70b460ee9" with requests_mock.Mocker() as m: - m.post("http://test.com/emission", status_code=201) + m.post("http://test.com/emissions", status_code=201) carbon_emission = EmissionsData( timestamp="222", project_name="", From 8c254a5bfee161d461c54254209817e9167376d3 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 14 Jul 2024 11:17:26 +0200 Subject: [PATCH 54/57] fix(CLI): :wrench: default config should be global --- codecarbon/cli/cli_utils.py | 2 +- tests/test_cli.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py index 82b58a205..33d7ab307 100644 --- a/codecarbon/cli/cli_utils.py +++ b/codecarbon/cli/cli_utils.py @@ -82,7 +82,7 @@ def create_new_config_file(): file_path = typer.prompt( "Where do you want to put your config file ?", type=str, - default="./.codecarbon.config", + default="~/.codecarbon.config", ) file_path = Path(file_path) if not file_path.parent.exists(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 0b363e101..062a3fb23 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -52,17 +52,29 @@ def test_app(self, MockApiClient): self.assertIn(__app_name__, result.stdout) self.assertIn(__version__, result.stdout) + @patch("codecarbon.cli.main.Path.exists") @patch("codecarbon.cli.main.Confirm.ask") @patch("codecarbon.cli.main.questionary_prompt") - def test_config_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient): + def test_config_no_local_new_all( + self, mock_prompt, mock_confirm, mock_path_exists, MockApiClient + ): temp_dir = os.getenv("RUNNER_TEMP", tempfile.gettempdir()) temp_codecarbon_config = tempfile.NamedTemporaryFile( mode="w+t", delete=False, dir=temp_dir ) + def side_effect_wrapper(*args, **kwargs): + """Side effect wrapper to simulate the first call to path.exists to avoid picking up global config""" + if side_effect_wrapper.call_count == 0: + side_effect_wrapper.call_count += 1 + return False + else: + return True + + side_effect_wrapper.call_count = 0 + mock_path_exists.side_effect = side_effect_wrapper MockApiClient.return_value = self.mock_api_client mock_prompt.side_effect = [ - "/tmp/.codecarbon.config", "Create New Organization", "Create New Project", "Create New Experiment", @@ -84,9 +96,12 @@ def test_config_no_local_new_all(self, mock_prompt, mock_confirm, MockApiClient) result.stdout, ) + @patch("codecarbon.cli.main.Path.exists") @patch("codecarbon.cli.main.get_config") @patch("codecarbon.cli.main.questionary_prompt") - def test_init_use_local(self, mock_prompt, mock_config, MockApiClient): + def test_init_use_local( + self, mock_prompt, mock_config, mock_path_exists, MockApiClient + ): mock_prompt.return_value = "~/.codecarbon.config" mock_config.return_value = { "api_endpoint": "http://localhost:8008", @@ -94,6 +109,18 @@ def test_init_use_local(self, mock_prompt, mock_config, MockApiClient): "project_id": "133", "experiment_id": "yolo123", } + + def side_effect_wrapper(*args, **kwargs): + """Side effect wrapper to simulate the first call to path.exists to avoid picking up global config""" + if side_effect_wrapper.call_count == 1: + side_effect_wrapper.call_count += 1 + return False + else: + side_effect_wrapper.call_count += 1 + return True + + side_effect_wrapper.call_count = 0 + mock_path_exists.side_effects = side_effect_wrapper result = self.runner.invoke(codecarbon, ["config"], input="n") self.assertEqual(result.exit_code, 0) self.assertIn( From 42ddc4e0dc04b2f2828b1627ba4f55d18d5d3b3e Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 14 Jul 2024 11:49:54 +0200 Subject: [PATCH 55/57] fix(CLI): :bug: fix home path for config file --- codecarbon/cli/cli_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/codecarbon/cli/cli_utils.py b/codecarbon/cli/cli_utils.py index 33d7ab307..a6629e57e 100644 --- a/codecarbon/cli/cli_utils.py +++ b/codecarbon/cli/cli_utils.py @@ -84,13 +84,18 @@ def create_new_config_file(): type=str, default="~/.codecarbon.config", ) - file_path = Path(file_path) + if file_path[0] == "~": + file_path = Path.home() / file_path[2:] + else: + 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") From 714d005900c512a1481a82b3dd3ea4b197d562f4 Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Sun, 4 Aug 2024 12:09:33 +0200 Subject: [PATCH 56/57] fixes requirements issues before merging --- requirements.txt | 14 +++-- requirements/requirements-api.txt | 66 +++++++--------------- requirements/requirements-carbonboard.txt | 46 +++++++++++++--- requirements/requirements-dashboard.txt | 46 +++++++++++++--- requirements/requirements-dev.txt | 47 ++++++++++++---- requirements/requirements-test.py3.10.txt | 44 ++++++++++++--- requirements/requirements-test.py3.11.txt | 42 +++++++++++--- requirements/requirements-test.py3.12.txt | 42 +++++++++++--- requirements/requirements-test.py3.8.txt | 22 +++++--- requirements/requirements-test.py3.9.txt | 44 ++++++++++++--- setup.py | 67 ----------------------- tox.ini | 36 ------------ 12 files changed, 289 insertions(+), 227 deletions(-) delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/requirements.txt b/requirements.txt index 5e8756d78..e1b2af52b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.10 +# This file is autogenerated by hatch-pip-compile with Python 3.9 # # - arrow # - click @@ -22,14 +22,16 @@ certifi==2024.7.4 charset-normalizer==3.3.2 # via requests click==8.1.7 - # via hatch.envs.default + # via + # hatch.envs.default + # typer idna==3.7 # via requests markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -numpy==2.0.0 +numpy==2.0.1 # via pandas pandas==2.2.2 # via hatch.envs.default @@ -43,7 +45,7 @@ py-cpuinfo==9.0.0 # via hatch.envs.default pygments==2.18.0 # via rich -pynvml==11.5.0 +pynvml==11.5.3 # via hatch.envs.default python-dateutil==2.9.0.post0 # via @@ -53,7 +55,7 @@ pytz==2024.1 # via pandas questionary==2.0.1 # via hatch.envs.default -rapidfuzz==3.9.3 +rapidfuzz==3.9.5 # via hatch.envs.default requests==2.32.3 # via hatch.envs.default @@ -69,7 +71,7 @@ typer==0.12.3 # via hatch.envs.default types-python-dateutil==2.9.0.20240316 # via arrow -typing-extensions==4.11.0 +typing-extensions==4.12.2 # via typer tzdata==2024.1 # via pandas diff --git a/requirements/requirements-api.txt b/requirements/requirements-api.txt index 8fea61a81..64f634fd2 100644 --- a/requirements/requirements-api.txt +++ b/requirements/requirements-api.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.12 +# This file is autogenerated by hatch-pip-compile with Python 3.11 # # - alembic<2.0.0 # - bcrypt<5.0.0 @@ -37,7 +37,7 @@ # - typer # -alembic==1.13.1 +alembic==1.13.2 # via hatch.envs.api anyio==4.4.0 # via @@ -46,7 +46,7 @@ anyio==4.4.0 # watchfiles arrow==1.3.0 # via hatch.envs.api -bcrypt==4.1.3 +bcrypt==4.2.0 # via hatch.envs.api certifi==2024.7.4 # via @@ -62,27 +62,19 @@ click==8.1.7 # hatch.envs.api # typer # uvicorn -cryptography==42.0.8 +cryptography==43.0.0 # via jwcrypto dependency-injector==4.41.0 # via hatch.envs.api dnspython==2.6.1 # via email-validator email-validator==2.2.0 - # via - # fastapi - # pydantic -exceptiongroup==1.2.2 - # via - # anyio - # pytest -fastapi==0.111.0 + # via pydantic +fastapi==0.112.0 # via # hatch.envs.api # fief-client -fastapi-cli==0.0.4 - # via fastapi -fastapi-pagination==0.12.25 +fastapi-pagination==0.12.26 # via hatch.envs.api fief-client==0.19.0 # via hatch.envs.api @@ -97,7 +89,6 @@ httptools==0.6.1 httpx==0.27.0 # via # hatch.envs.api - # fastapi # fief-client idna==3.7 # via @@ -107,30 +98,24 @@ idna==3.7 # requests iniconfig==2.0.0 # via pytest -jinja2==3.1.4 - # via fastapi jwcrypto==1.5.6 # via fief-client -makefun==1.15.3 +makefun==1.15.4 # via fief-client mako==1.3.5 # via alembic markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 - # via - # jinja2 - # mako + # via mako mdurl==0.1.2 # via markdown-it-py mock==5.1.0 # via hatch.envs.api -numpy==2.0.0 +numpy==2.0.1 # via # hatch.envs.api # pandas -orjson==3.10.5 - # via fastapi packaging==24.1 # via pytest pandas==2.2.2 @@ -149,16 +134,18 @@ py-cpuinfo==9.0.0 # via hatch.envs.api pycparser==2.22 # via cffi -pydantic==1.10.16 +pydantic==1.10.17 # via # hatch.envs.api # fastapi # fastapi-pagination pygments==2.18.0 # via rich -pynvml==11.5.0 +pyjwt==2.9.0 + # via hatch.envs.api +pynvml==11.5.3 # via hatch.envs.api -pytest==8.2.2 +pytest==8.3.2 # via hatch.envs.api python-dateutil==2.9.0.post0 # via @@ -167,8 +154,6 @@ python-dateutil==2.9.0.post0 # pandas python-dotenv==1.0.1 # via uvicorn -python-multipart==0.0.9 - # via fastapi pytz==2024.1 # via pandas pyyaml==6.0.1 @@ -177,7 +162,7 @@ pyyaml==6.0.1 # uvicorn questionary==2.0.1 # via hatch.envs.api -rapidfuzz==3.9.3 +rapidfuzz==3.9.5 # via hatch.envs.api requests==2.32.3 # via @@ -202,43 +187,32 @@ sniffio==1.3.1 # via # anyio # httpx -sqlalchemy==1.4.52 +sqlalchemy==1.4.53 # via # hatch.envs.api # alembic starlette==0.37.2 # via fastapi -tomli==2.0.1 - # via pytest typer==0.12.3 - # via - # hatch.envs.api - # fastapi-cli + # via hatch.envs.api types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 # via # alembic - # anyio # fastapi # fastapi-pagination # jwcrypto # pydantic - # starlette # typer - # uvicorn tzdata==2024.1 # via pandas -ujson==5.10.0 - # via fastapi urllib3==2.2.2 # via # requests # responses -uvicorn==0.30.1 - # via - # hatch.envs.api - # fastapi +uvicorn==0.30.5 + # via hatch.envs.api uvloop==0.19.0 # via uvicorn watchfiles==0.22.0 diff --git a/requirements/requirements-carbonboard.txt b/requirements/requirements-carbonboard.txt index edc5c3dde..c026af346 100644 --- a/requirements/requirements-carbonboard.txt +++ b/requirements/requirements-carbonboard.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.10 +# This file is autogenerated by hatch-pip-compile with Python 3.9 # # - arrow # - click @@ -8,8 +8,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # - dash # - dash-bootstrap-components<1.0.0 # - fire @@ -27,6 +30,7 @@ click==8.1.7 # via # hatch.envs.carbonboard # flask + # typer dash==2.17.1 # via # hatch.envs.carbonboard @@ -45,33 +49,43 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 - # via dash +importlib-metadata==8.2.0 + # via + # dash + # flask itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py nest-asyncio==1.6.0 # via dash -numpy==2.0.0 +numpy==2.0.1 # via pandas packaging==24.1 # via plotly pandas==2.2.2 # via hatch.envs.carbonboard -plotly==5.22.0 +plotly==5.23.0 # via dash prometheus-client==0.20.0 # via hatch.envs.carbonboard +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.carbonboard py-cpuinfo==9.0.0 # via hatch.envs.carbonboard -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.carbonboard python-dateutil==2.9.0.post0 # via @@ -79,7 +93,9 @@ python-dateutil==2.9.0.post0 # pandas pytz==2024.1 # via pandas -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.carbonboard +rapidfuzz==3.9.5 # via hatch.envs.carbonboard requests==2.32.3 # via @@ -87,23 +103,35 @@ requests==2.32.3 # dash retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.carbonboard + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # fire # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly termcolor==2.4.0 # via fire +typer==0.12.3 + # via hatch.envs.carbonboard types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via requests +wcwidth==0.2.13 + # via prompt-toolkit werkzeug==3.0.3 # via # dash diff --git a/requirements/requirements-dashboard.txt b/requirements/requirements-dashboard.txt index ade674ef8..d17b95926 100644 --- a/requirements/requirements-dashboard.txt +++ b/requirements/requirements-dashboard.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.10 +# This file is autogenerated by hatch-pip-compile with Python 3.9 # # - dash>=2.2.0 # - dash_bootstrap_components @@ -11,8 +11,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # arrow==1.3.0 @@ -27,6 +30,7 @@ click==8.1.7 # via # hatch.envs.dashboard # flask + # typer dash==2.17.1 # via # hatch.envs.dashboard @@ -43,35 +47,45 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 - # via dash +importlib-metadata==8.2.0 + # via + # dash + # flask itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py nest-asyncio==1.6.0 # via dash -numpy==2.0.0 +numpy==2.0.1 # via pandas packaging==24.1 # via plotly pandas==2.2.2 # via hatch.envs.dashboard -plotly==5.22.0 +plotly==5.23.0 # via # hatch.envs.dashboard # dash prometheus-client==0.20.0 # via hatch.envs.dashboard +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.dashboard py-cpuinfo==9.0.0 # via hatch.envs.dashboard -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.dashboard python-dateutil==2.9.0.post0 # via @@ -79,7 +93,9 @@ python-dateutil==2.9.0.post0 # pandas pytz==2024.1 # via pandas -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.dashboard +rapidfuzz==3.9.5 # via hatch.envs.dashboard requests==2.32.3 # via @@ -87,20 +103,32 @@ requests==2.32.3 # dash retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.dashboard + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly +typer==0.12.3 + # via hatch.envs.dashboard types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via requests +wcwidth==0.2.13 + # via prompt-toolkit werkzeug==3.0.3 # via # dash diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index bde339988..9c43e9ca7 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by hatch-pip-compile with Python 3.10 +# This file is autogenerated by hatch-pip-compile with Python 3.9 # # - pre-commit # - ruff @@ -12,13 +12,16 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # arrow==1.3.0 # via hatch.envs.dev -black==24.4.2 +black==24.8.0 # via hatch.envs.dev certifi==2024.7.4 # via requests @@ -30,15 +33,20 @@ click==8.1.7 # via # hatch.envs.dev # black + # typer distlib==0.3.8 # via virtualenv -filelock==3.15.3 +filelock==3.15.4 # via virtualenv -identify==2.5.36 +identify==2.6.0 # via pre-commit idna==3.7 # via requests -mypy==1.10.0 +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.11.1 # via hatch.envs.dev mypy-extensions==1.0.0 # via @@ -46,7 +54,7 @@ mypy-extensions==1.0.0 # mypy nodeenv==1.9.1 # via pre-commit -numpy==2.0.0 +numpy==2.0.1 # via pandas packaging==24.1 # via black @@ -58,15 +66,19 @@ platformdirs==4.2.2 # via # black # virtualenv -pre-commit==3.7.1 +pre-commit==3.8.0 # via hatch.envs.dev prometheus-client==0.20.0 # via hatch.envs.dev +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.dev py-cpuinfo==9.0.0 # via hatch.envs.dev -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.dev python-dateutil==2.9.0.post0 # via @@ -76,27 +88,40 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via pre-commit -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.dev +rapidfuzz==3.9.5 # via hatch.envs.dev requests==2.32.3 # via hatch.envs.dev -ruff==0.4.9 +rich==13.7.1 + # via + # hatch.envs.dev + # typer +ruff==0.5.6 # via hatch.envs.dev +shellingham==1.5.4 + # via typer six==1.16.0 # via python-dateutil tomli==2.0.1 # via # black # mypy +typer==0.12.3 + # via hatch.envs.dev types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 # via # black # mypy + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via requests -virtualenv==20.26.2 +virtualenv==20.26.3 # via pre-commit +wcwidth==0.2.13 + # via prompt-toolkit diff --git a/requirements/requirements-test.py3.10.txt b/requirements/requirements-test.py3.10.txt index 72bd5964c..a5ca36fff 100644 --- a/requirements/requirements-test.py3.10.txt +++ b/requirements/requirements-test.py3.10.txt @@ -15,8 +15,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # - dash # - dash-bootstrap-components<1.0.0 # - fire @@ -34,6 +37,7 @@ click==8.1.7 # via # hatch.envs.test.py3.10 # flask + # typer dash==2.17.1 # via # hatch.envs.test.py3.10 @@ -46,7 +50,7 @@ dash-html-components==2.0.0 # via dash dash-table==5.0.0 # via dash -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via pytest fire==0.6.0 # via hatch.envs.test.py3.10 @@ -54,7 +58,7 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 +importlib-metadata==8.2.0 # via dash iniconfig==2.0.0 # via pytest @@ -62,15 +66,19 @@ itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py mock==5.1.0 # via hatch.envs.test.py3.10 nest-asyncio==1.6.0 # via dash -numpy==2.0.0 +numpy==2.0.1 # via # hatch.envs.test.py3.10 # pandas @@ -80,19 +88,23 @@ packaging==24.1 # pytest pandas==2.2.2 # via hatch.envs.test.py3.10 -plotly==5.22.0 +plotly==5.23.0 # via dash pluggy==1.5.0 # via pytest prometheus-client==0.20.0 # via hatch.envs.test.py3.10 +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.test.py3.10 py-cpuinfo==9.0.0 # via hatch.envs.test.py3.10 -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.test.py3.10 -pytest==8.2.2 +pytest==8.3.2 # via hatch.envs.test.py3.10 python-dateutil==2.9.0.post0 # via @@ -102,7 +114,9 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via responses -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.test.py3.10 +rapidfuzz==3.9.5 # via hatch.envs.test.py3.10 requests==2.32.3 # via @@ -116,27 +130,39 @@ responses==0.25.3 # via hatch.envs.test.py3.10 retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.test.py3.10 + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # fire # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly termcolor==2.4.0 # via fire tomli==2.0.1 # via pytest +typer==0.12.3 + # via hatch.envs.test.py3.10 types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via # requests # responses +wcwidth==0.2.13 + # via prompt-toolkit werkzeug==3.0.3 # via # dash diff --git a/requirements/requirements-test.py3.11.txt b/requirements/requirements-test.py3.11.txt index 145ff4942..5dcb50645 100644 --- a/requirements/requirements-test.py3.11.txt +++ b/requirements/requirements-test.py3.11.txt @@ -15,8 +15,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # - dash # - dash-bootstrap-components<1.0.0 # - fire @@ -34,6 +37,7 @@ click==8.1.7 # via # hatch.envs.test.py3.11 # flask + # typer dash==2.17.1 # via # hatch.envs.test.py3.11 @@ -52,7 +56,7 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 +importlib-metadata==8.2.0 # via dash iniconfig==2.0.0 # via pytest @@ -60,15 +64,19 @@ itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py mock==5.1.0 # via hatch.envs.test.py3.11 nest-asyncio==1.6.0 # via dash -numpy==2.0.0 +numpy==2.0.1 # via # hatch.envs.test.py3.11 # pandas @@ -78,19 +86,23 @@ packaging==24.1 # pytest pandas==2.2.2 # via hatch.envs.test.py3.11 -plotly==5.22.0 +plotly==5.23.0 # via dash pluggy==1.5.0 # via pytest prometheus-client==0.20.0 # via hatch.envs.test.py3.11 +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.test.py3.11 py-cpuinfo==9.0.0 # via hatch.envs.test.py3.11 -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.test.py3.11 -pytest==8.2.2 +pytest==8.3.2 # via hatch.envs.test.py3.11 python-dateutil==2.9.0.post0 # via @@ -100,7 +112,9 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via responses -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.test.py3.11 +rapidfuzz==3.9.5 # via hatch.envs.test.py3.11 requests==2.32.3 # via @@ -114,25 +128,37 @@ responses==0.25.3 # via hatch.envs.test.py3.11 retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.test.py3.11 + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # fire # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly termcolor==2.4.0 # via fire +typer==0.12.3 + # via hatch.envs.test.py3.11 types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via # requests # responses +wcwidth==0.2.13 + # via prompt-toolkit werkzeug==3.0.3 # via # dash diff --git a/requirements/requirements-test.py3.12.txt b/requirements/requirements-test.py3.12.txt index 609b7c7cc..0b0b41603 100644 --- a/requirements/requirements-test.py3.12.txt +++ b/requirements/requirements-test.py3.12.txt @@ -15,8 +15,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # - dash # - dash-bootstrap-components<1.0.0 # - fire @@ -34,6 +37,7 @@ click==8.1.7 # via # hatch.envs.test.py3.12 # flask + # typer dash==2.17.1 # via # hatch.envs.test.py3.12 @@ -52,7 +56,7 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 +importlib-metadata==8.2.0 # via dash iniconfig==2.0.0 # via pytest @@ -60,15 +64,19 @@ itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py mock==5.1.0 # via hatch.envs.test.py3.12 nest-asyncio==1.6.0 # via dash -numpy==2.0.0 +numpy==2.0.1 # via # hatch.envs.test.py3.12 # pandas @@ -78,19 +86,23 @@ packaging==24.1 # pytest pandas==2.2.2 # via hatch.envs.test.py3.12 -plotly==5.22.0 +plotly==5.23.0 # via dash pluggy==1.5.0 # via pytest prometheus-client==0.20.0 # via hatch.envs.test.py3.12 +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.test.py3.12 py-cpuinfo==9.0.0 # via hatch.envs.test.py3.12 -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.test.py3.12 -pytest==8.2.2 +pytest==8.3.2 # via hatch.envs.test.py3.12 python-dateutil==2.9.0.post0 # via @@ -100,7 +112,9 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via responses -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.test.py3.12 +rapidfuzz==3.9.5 # via hatch.envs.test.py3.12 requests==2.32.3 # via @@ -114,25 +128,37 @@ responses==0.25.3 # via hatch.envs.test.py3.12 retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.test.py3.12 + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # fire # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly termcolor==2.4.0 # via fire +typer==0.12.3 + # via hatch.envs.test.py3.12 types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via # requests # responses +wcwidth==0.2.13 + # via prompt-toolkit werkzeug==3.0.3 # via # dash diff --git a/requirements/requirements-test.py3.8.txt b/requirements/requirements-test.py3.8.txt index 3acf417d0..19d193616 100644 --- a/requirements/requirements-test.py3.8.txt +++ b/requirements/requirements-test.py3.8.txt @@ -37,6 +37,7 @@ click==8.1.7 # via # hatch.envs.test.py3.8 # flask + # typer dash==2.17.1 # via # hatch.envs.test.py3.8 @@ -49,7 +50,7 @@ dash-html-components==2.0.0 # via dash dash-table==5.0.0 # via dash -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via pytest fire==0.6.0 # via hatch.envs.test.py3.8 @@ -57,7 +58,7 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 +importlib-metadata==8.2.0 # via # dash # flask @@ -89,7 +90,7 @@ packaging==24.1 # pytest pandas==2.0.3 # via hatch.envs.test.py3.8 -plotly==5.22.0 +plotly==5.23.0 # via dash pluggy==1.5.0 # via pytest @@ -103,9 +104,9 @@ py-cpuinfo==9.0.0 # via hatch.envs.test.py3.8 pygments==2.18.0 # via rich -pynvml==11.5.0 +pynvml==11.5.3 # via hatch.envs.test.py3.8 -pytest==8.2.2 +pytest==8.3.2 # via hatch.envs.test.py3.8 python-dateutil==2.9.0.post0 # via @@ -117,7 +118,7 @@ pyyaml==6.0.1 # via responses questionary==2.0.1 # via hatch.envs.test.py3.8 -rapidfuzz==3.7.0 +rapidfuzz==3.9.5 # via hatch.envs.test.py3.8 requests==2.32.3 # via @@ -142,7 +143,7 @@ six==1.16.0 # fire # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly termcolor==2.4.0 # via fire @@ -153,7 +154,10 @@ typer==0.12.3 types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # rich + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 @@ -162,7 +166,7 @@ urllib3==2.2.2 # responses wcwidth==0.2.13 # via prompt-toolkit -werkzeug==3.0.2 +werkzeug==3.0.3 # via # dash # flask diff --git a/requirements/requirements-test.py3.9.txt b/requirements/requirements-test.py3.9.txt index c7999eead..3a8192d13 100644 --- a/requirements/requirements-test.py3.9.txt +++ b/requirements/requirements-test.py3.9.txt @@ -15,8 +15,11 @@ # - psutil # - py-cpuinfo # - pynvml +# - questionary # - rapidfuzz # - requests +# - rich +# - typer # - dash # - dash-bootstrap-components<1.0.0 # - fire @@ -34,6 +37,7 @@ click==8.1.7 # via # hatch.envs.test.py3.9 # flask + # typer dash==2.17.1 # via # hatch.envs.test.py3.9 @@ -46,7 +50,7 @@ dash-html-components==2.0.0 # via dash dash-table==5.0.0 # via dash -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via pytest fire==0.6.0 # via hatch.envs.test.py3.9 @@ -54,7 +58,7 @@ flask==3.0.3 # via dash idna==3.7 # via requests -importlib-metadata==7.1.0 +importlib-metadata==8.2.0 # via # dash # flask @@ -64,15 +68,19 @@ itsdangerous==2.2.0 # via flask jinja2==3.1.4 # via flask +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via # jinja2 # werkzeug +mdurl==0.1.2 + # via markdown-it-py mock==5.1.0 # via hatch.envs.test.py3.9 nest-asyncio==1.6.0 # via dash -numpy==2.0.0 +numpy==2.0.1 # via # hatch.envs.test.py3.9 # pandas @@ -82,19 +90,23 @@ packaging==24.1 # pytest pandas==2.2.2 # via hatch.envs.test.py3.9 -plotly==5.22.0 +plotly==5.23.0 # via dash pluggy==1.5.0 # via pytest prometheus-client==0.20.0 # via hatch.envs.test.py3.9 +prompt-toolkit==3.0.36 + # via questionary psutil==6.0.0 # via hatch.envs.test.py3.9 py-cpuinfo==9.0.0 # via hatch.envs.test.py3.9 -pynvml==11.5.0 +pygments==2.18.0 + # via rich +pynvml==11.5.3 # via hatch.envs.test.py3.9 -pytest==8.2.2 +pytest==8.3.2 # via hatch.envs.test.py3.9 python-dateutil==2.9.0.post0 # via @@ -104,7 +116,9 @@ pytz==2024.1 # via pandas pyyaml==6.0.1 # via responses -rapidfuzz==3.9.3 +questionary==2.0.1 + # via hatch.envs.test.py3.9 +rapidfuzz==3.9.5 # via hatch.envs.test.py3.9 requests==2.32.3 # via @@ -118,27 +132,39 @@ responses==0.25.3 # via hatch.envs.test.py3.9 retrying==1.3.4 # via dash +rich==13.7.1 + # via + # hatch.envs.test.py3.9 + # typer +shellingham==1.5.4 + # via typer six==1.16.0 # via # fire # python-dateutil # retrying -tenacity==8.4.1 +tenacity==9.0.0 # via plotly termcolor==2.4.0 # via fire tomli==2.0.1 # via pytest +typer==0.12.3 + # via hatch.envs.test.py3.9 types-python-dateutil==2.9.0.20240316 # via arrow typing-extensions==4.12.2 - # via dash + # via + # dash + # typer tzdata==2024.1 # via pandas urllib3==2.2.2 # via # requests # responses +wcwidth==0.2.13 + # via prompt-toolkit werkzeug==3.0.3 # via # dash diff --git a/setup.py b/setup.py deleted file mode 100644 index 52308f51a..000000000 --- a/setup.py +++ /dev/null @@ -1,67 +0,0 @@ -import setuptools - -with open("README.md", "r", encoding="utf-8") as f: - long_description = f.read() - -DEPENDENCIES = [ - "arrow", - "pandas", - "pynvml", - "requests", - "psutil", - "py-cpuinfo", - "rapidfuzz", - "click", - "typer", - "questionary", - "rich", - "prometheus_client", -] - -TEST_DEPENDENCIES = ["mock", "pytest", "responses", "tox", "numpy", "requests-mock"] - - -setuptools.setup( - name="codecarbon", - version="2.3.4", - author="Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College", - long_description=long_description, - long_description_content_type="text/markdown", - license_files=("LICENSE",), - packages=setuptools.find_packages( - exclude=["*.tests", "*.tests.*", "tests.*", "tests"] - ), - install_requires=DEPENDENCIES, - tests_require=TEST_DEPENDENCIES, - extras_require={ - "viz": ["dash", "dash_bootstrap_components < 1.0.0", "fire"], - "dashboard": ["dash>=2.2.0", "plotly>=5.6.0", "dash_bootstrap_components"], - }, - classifiers=[ - "Natural Language :: English", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "License :: OSI Approved :: MIT License", - ], - package_data={ - "codecarbon": [ - "data/cloud/impact.csv", - "data/hardware/cpu_power.csv", - "data/private_infra/2016/usa_emissions.json", - "data/private_infra/2016/canada_energy_mix.json", - "data/private_infra/carbon_intensity_per_source.json", - "data/private_infra/global_energy_mix.json", - "viz/assets/*.png", - ], - }, - python_requires=">=3.7", - entry_points={ - "console_scripts": [ - "carbonboard = codecarbon.viz.carbonboard:main", - "codecarbon = codecarbon.cli.main:codecarbon", - ] - }, -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 34db1f7bf..000000000 --- a/tox.ini +++ /dev/null @@ -1,36 +0,0 @@ -[tox] -envlist = py37, py38, py39, py310, py311 - -[testenv] -deps = - pytest - -rrequirements-dev.txt - -rrequirements-test.txt - -recreate = - false - -commands = pytest -vv -m "not integ_test" tests/ - -[testenv:all] -deps = - pytest - -rrequirements-dev.txt - -rrequirements-test.txt -passenv = RUNNER_TEMP - -commands = - pip install -e . - python -m pytest -vv tests/ - -recreate = - false - -[gh-actions] -python = - 3.7: py37 - 3.8: py38 - 3.9: py39 - 3.10: py310 - 3.11: py311 - From f4c70f96abcb97457247aa57b83902d0e9829b3c Mon Sep 17 00:00:00 2001 From: LuisBlanche Date: Thu, 8 Aug 2024 19:06:10 +0200 Subject: [PATCH 57/57] docs(CLI): :memo: update docs and add asciinema links --- docs/.buildinfo | 2 +- docs/_sources/usage.rst.txt | 21 ++++++++++------ docs/_static/documentation_options.js | 4 +-- docs/_static/searchtools.js | 7 +++--- docs/api.html | 8 +++--- docs/comet.html | 8 +++--- docs/edit/usage.rst | 19 ++++++++------ 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/output.html | 8 +++--- docs/parameters.html | 8 +++--- docs/search.html | 8 +++--- docs/searchindex.js | 2 +- docs/to_logger.html | 8 +++--- docs/usage.html | 36 +++++++++++++-------------- docs/visualize.html | 8 +++--- 22 files changed, 125 insertions(+), 86 deletions(-) diff --git a/docs/.buildinfo b/docs/.buildinfo index ada98111f..f4300015c 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 960d5ac230b3e4a0815590a992a80b6d +config: 3cf6aa4ee352d32c08f3e49d0567a08a tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_sources/usage.rst.txt b/docs/_sources/usage.rst.txt index e2f216ba0..861191c98 100644 --- a/docs/_sources/usage.rst.txt +++ b/docs/_sources/usage.rst.txt @@ -17,11 +17,18 @@ 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 -Start monitoring the emissions of the computer + codecarbon config + +[![asciicast](https://asciinema.org/a/667970.svg)](https://asciinema.org/a/667970) + +You can use the same command to modify an existing config + +[![asciicast](https://asciinema.org/a/667971.svg)](https://asciinema.org/a/667971) + + .. code-block:: console codecarbon monitor @@ -29,12 +36,10 @@ Start monitoring the emissions of the computer 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 - - - +to an API running on "localhost:8008" (Or you can start a private local API with "docker-compose up"). Using the public API with +this is not supported yet (coming soon!) +[![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/_static/documentation_options.js b/docs/_static/documentation_options.js index 61e433a28..9e94264b5 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '2.5.0', + VERSION: '2.3.4', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', @@ -10,4 +10,4 @@ const DOCUMENTATION_OPTIONS = { NAVIGATION_WITH_KEYS: false, SHOW_SEARCH_SUMMARY: true, ENABLE_SEARCH_SHORTCUTS: true, -}; +}; \ No newline at end of file diff --git a/docs/_static/searchtools.js b/docs/_static/searchtools.js index 92da3f8b2..b08d58c9b 100644 --- a/docs/_static/searchtools.js +++ b/docs/_static/searchtools.js @@ -178,7 +178,7 @@ const Search = { htmlToText: (htmlString, anchor) => { const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - for (const removalQuery of [".headerlinks", "script", "style"]) { + for (const removalQuery of [".headerlink", "script", "style"]) { htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); } if (anchor) { @@ -328,13 +328,14 @@ const Search = { for (const [title, foundTitles] of Object.entries(allTitles)) { if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { for (const [file, id] of foundTitles) { - let score = Math.round(100 * queryLower.length / title.length) + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, id !== null ? "#" + id : "", null, - score, + score + boost, filenames[file], ]); } diff --git a/docs/api.html b/docs/api.html index 100e3c7ef..5f77dbb84 100644 --- a/docs/api.html +++ b/docs/api.html @@ -1,12 +1,14 @@ - + CodeCarbon API — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/comet.html b/docs/comet.html index ed90e192c..440d02acd 100644 --- a/docs/comet.html +++ b/docs/comet.html @@ -1,12 +1,14 @@ - + Comet Integration — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/edit/usage.rst b/docs/edit/usage.rst index be17f699c..861191c98 100644 --- a/docs/edit/usage.rst +++ b/docs/edit/usage.rst @@ -20,9 +20,15 @@ Create a minimal configuration file (just follow the prompts) .. code-block:: console - codecarbon config --init + codecarbon config + +[![asciicast](https://asciinema.org/a/667970.svg)](https://asciinema.org/a/667970) + +You can use the same command to modify an existing config + +[![asciicast](https://asciinema.org/a/667971.svg)](https://asciinema.org/a/667971) + -Start monitoring the emissions of the computer .. code-block:: console codecarbon monitor @@ -30,13 +36,10 @@ Start monitoring the emissions of the computer 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 "https://api.codecarbon.io" (Or you can start a private local API with "docke-compose up") - -.. raw:: html - - - +to an API running on "localhost:8008" (Or you can start a private local API with "docker-compose up"). Using the public API with +this is not supported yet (coming soon!) +[![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/examples.html b/docs/examples.html index a33fbf92a..9d23b4f84 100644 --- a/docs/examples.html +++ b/docs/examples.html @@ -1,12 +1,14 @@ - + Examples — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/faq.html b/docs/faq.html index a95fc0d85..7eafc0dca 100644 --- a/docs/faq.html +++ b/docs/faq.html @@ -1,12 +1,14 @@ - + Frequently Asked Questions — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/genindex.html b/docs/genindex.html index 9e412f23c..f94b0b98b 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -1,11 +1,13 @@ - + Index — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/index.html b/docs/index.html index 876e2e1d1..12c79956d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,12 +1,14 @@ - + CodeCarbon — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/installation.html b/docs/installation.html index c9acefafb..f5980c094 100644 --- a/docs/installation.html +++ b/docs/installation.html @@ -1,12 +1,14 @@ - + Installing CodeCarbon — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/methodology.html b/docs/methodology.html index b2a34359a..f702e9f45 100644 --- a/docs/methodology.html +++ b/docs/methodology.html @@ -1,12 +1,14 @@ - + Methodology — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/model_examples.html b/docs/model_examples.html index 0be112e20..7024b203c 100644 --- a/docs/model_examples.html +++ b/docs/model_examples.html @@ -1,12 +1,14 @@ - + Model Comparisons — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/motivation.html b/docs/motivation.html index 686ebfbf5..5c06ecedd 100644 --- a/docs/motivation.html +++ b/docs/motivation.html @@ -1,12 +1,14 @@ - + Motivation — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/output.html b/docs/output.html index 9872c85d9..94e8b7d3b 100644 --- a/docs/output.html +++ b/docs/output.html @@ -1,12 +1,14 @@ - + Output — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/parameters.html b/docs/parameters.html index 7fb623d3c..87379c93e 100644 --- a/docs/parameters.html +++ b/docs/parameters.html @@ -1,12 +1,14 @@ - + Parameters — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/search.html b/docs/search.html index d98ffadfd..aa79b6612 100644 --- a/docs/search.html +++ b/docs/search.html @@ -1,11 +1,13 @@ - + Search — CodeCarbon 2.3.4 documentation - - + + + + diff --git a/docs/usage.html b/docs/usage.html index e7cbe6616..af3608f7f 100644 --- a/docs/usage.html +++ b/docs/usage.html @@ -1,12 +1,14 @@ - + Quickstart — CodeCarbon 2.3.4 documentation - - + + + + @@ -120,23 +122,21 @@

Online Mode

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

-
-

Start monitoring the emissions of the computer -.. code-block:: console

-
-

codecarbon monitor

-
+

Create a minimal configuration file (just follow the prompts)

+
codecarbon config
+
+
+

[![asciicast](https://asciinema.org/a/667970.svg)](https://asciinema.org/a/667970)

+

You can use the same command to modify an existing config

+

[![asciicast](https://asciinema.org/a/667971.svg)](https://asciinema.org/a/667971)

+
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

-
-

<script src=”https://asciinema.org/a/bJMOlPe5F4mFLY0Rl6fiJSOp3.js” id=”asciicast-bJMOlPe5F4mFLY0Rl6fiJSOp3” async></script>

-
+to an API running on “localhost:8008” (Or you can start a private local API with “docker-compose up”). Using the public API with +this is not supported yet (coming soon!)

+

[![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 - - + + + +