Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions codecarbon/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ def api_get():
api_endpoint = get_api_endpoint()
api = ApiClient(endpoint_url=api_endpoint)
api.set_access_token(get_access_token())
organizations = api.get_list_organizations()
try:
organizations = api.get_list_organizations()
except Exception as e:
print(f"[yellow]API request failed[/yellow]. (error: {e})")
raise typer.Exit(1)
print(organizations)


Expand All @@ -130,7 +134,11 @@ def login():
api = ApiClient(endpoint_url=api_endpoint)
access_token = get_access_token()
api.set_access_token(access_token)
api.check_auth()
try:
api.check_auth()
except Exception as e:
print(f"[yellow]Authentication check failed[/yellow]. (error: {e})")
raise typer.Exit(1)


def get_api_key(project_id: str):
Expand Down Expand Up @@ -212,7 +220,14 @@ def config():
overwrite_local_config("api_endpoint", api_endpoint, path=file_path)
api = ApiClient(endpoint_url=api_endpoint)
api.set_access_token(get_access_token())
organizations = api.get_list_organizations()
try:
organizations = api.get_list_organizations()
except Exception as e:
print(
f"[yellow]Could not list organizations from API[/yellow]. "
f"Please check your login and API endpoint. (error: {e})"
)
return
org = questionary_prompt(
"Pick existing organization from list or Create new organization ?",
[org["name"] for org in organizations] + ["Create New Organization"],
Expand All @@ -230,17 +245,14 @@ def config():
description=org_description,
)
organization = api.create_organization(organization=organization_create)
if organization is None:
print("Error creating organization")
return
print(f"Created organization : {organization}")
else:
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_organization(org_id)
project_names = [project["name"] for project in projects] if projects else []
project_names = [project["name"] for project in projects]
project = questionary_prompt(
"Pick existing project from list or Create new project ?",
project_names + ["Create New Project"],
Expand All @@ -264,9 +276,7 @@ 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 []
)
experiments_names = [experiment["name"] for experiment in experiments]

experiment = questionary_prompt(
"Pick existing experiment from list or Create new experiment ?",
Expand Down
56 changes: 26 additions & 30 deletions codecarbon/core/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ def check_auth(self):
headers = self._get_headers()
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return None
self._raise_api_error(url, {}, r)
return r.json()

def get_list_organizations(self):
Expand All @@ -97,17 +96,14 @@ def get_list_organizations(self):
headers = self._get_headers()
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return None
self._raise_api_error(url, {}, r)
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
Expand All @@ -128,8 +124,7 @@ def create_organization(self, organization: OrganizationCreate):
headers = self._get_headers()
r = requests.post(url=url, json=payload, timeout=2, headers=headers)
if r.status_code != 201:
self._log_error(url, payload, r)
return None
self._raise_api_error(url, payload, r)
return r.json()

def get_organization(self, organization_id):
Expand All @@ -140,8 +135,7 @@ def get_organization(self, organization_id):
url = self.url + "/organizations/" + organization_id
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return None
self._raise_api_error(url, {}, r)
return r.json()

def update_organization(self, organization: OrganizationCreate):
Expand All @@ -153,8 +147,7 @@ def update_organization(self, organization: OrganizationCreate):
url = self.url + "/organizations/" + organization.id
r = requests.patch(url=url, json=payload, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, payload, r)
return None
self._raise_api_error(url, payload, r)
return r.json()

def list_projects_from_organization(self, organization_id):
Expand All @@ -165,8 +158,7 @@ def list_projects_from_organization(self, organization_id):
headers = self._get_headers()
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return None
self._raise_api_error(url, {}, r)
return r.json()

def create_project(self, project: ProjectCreate):
Expand All @@ -178,8 +170,7 @@ def create_project(self, project: ProjectCreate):
headers = self._get_headers()
r = requests.post(url=url, json=payload, timeout=2, headers=headers)
if r.status_code != 201:
self._log_error(url, payload, r)
return None
self._raise_api_error(url, payload, r)
return r.json()

def get_project(self, project_id):
Expand All @@ -190,8 +181,7 @@ def get_project(self, project_id):
headers = self._get_headers()
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return None
self._raise_api_error(url, {}, r)
return r.json()

def add_emission(self, carbon_emission: dict):
Expand Down Expand Up @@ -236,12 +226,13 @@ def add_emission(self, carbon_emission: dict):
headers = self._get_headers()
r = requests.post(url=url, json=payload, timeout=2, headers=headers)
if r.status_code != 201:
self._log_error(url, payload, r)
return False
self._raise_api_error(url, payload, r)
logger.debug(f"ApiClient - Successful upload emission {payload} to {url}")
except requests.exceptions.HTTPError:
raise
except Exception as e:
logger.error(e, exc_info=True)
return False
raise
return True

def _create_run(self, experiment_id: str):
Expand Down Expand Up @@ -278,8 +269,7 @@ def _create_run(self, experiment_id: str):
headers = self._get_headers()
r = requests.post(url=url, json=payload, timeout=2, headers=headers)
if r.status_code != 201:
self._log_error(url, payload, r)
return None
self._raise_api_error(url, payload, r)
self.run_id = r.json()["id"]
logger.info(
"ApiClient Successfully registered your run on the API.\n\n"
Expand All @@ -292,8 +282,12 @@ def _create_run(self, experiment_id: str):
f"Failed to connect to API, please check the configuration. {e}",
exc_info=False,
)
raise
except requests.exceptions.HTTPError:
raise
except Exception as e:
logger.error(e, exc_info=True)
raise

def list_experiments_from_project(self, project_id: str):
"""
Expand All @@ -303,8 +297,7 @@ def list_experiments_from_project(self, project_id: str):
headers = self._get_headers()
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return []
self._raise_api_error(url, {}, r)
return r.json()

def set_experiment(self, experiment_id: str):
Expand All @@ -323,8 +316,7 @@ def add_experiment(self, experiment: ExperimentCreate):
headers = self._get_headers()
r = requests.post(url=url, json=payload, timeout=2, headers=headers)
if r.status_code != 201:
self._log_error(url, payload, r)
return None
self._raise_api_error(url, payload, r)
return r.json()

def get_experiment(self, experiment_id):
Expand All @@ -335,11 +327,10 @@ def get_experiment(self, experiment_id):
headers = self._get_headers()
r = requests.get(url=url, timeout=2, headers=headers)
if r.status_code != 200:
self._log_error(url, {}, r)
return None
self._raise_api_error(url, {}, r)
return r.json()

def _log_error(self, url, payload, response):
def _raise_api_error(self, url, payload, response):
if len(payload) > 0:
logger.error(
f"ApiClient Error when calling the API on {url} with : {json.dumps(payload)}"
Expand All @@ -349,6 +340,11 @@ def _log_error(self, url, payload, response):
logger.error(
f"ApiClient API return http code {response.status_code} and answer : {response.text}"
)
response.raise_for_status()
# 2xx/3xx that still isn't what the caller expected
raise requests.exceptions.HTTPError(
f"Unexpected status {response.status_code} from {url}", response=response
)

def close_experiment(self):
"""
Expand Down
25 changes: 17 additions & 8 deletions codecarbon/emissions_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,14 +637,23 @@ def _init_output_methods(self, *, api_key: str = None):
self._output_handlers.append(HTTPOutput(self._emissions_endpoint))

if OutputMethod.API in methods:
cc_api__out = CodeCarbonAPIOutput(
endpoint_url=self._api_endpoint,
experiment_id=self._experiment_id,
api_key=api_key,
conf=self._conf,
)
self.run_id = cc_api__out.run_id
self._output_handlers.append(cc_api__out)
try:
cc_api__out = CodeCarbonAPIOutput(
endpoint_url=self._api_endpoint,
experiment_id=self._experiment_id,
api_key=api_key,
conf=self._conf,
)
self.run_id = cc_api__out.run_id
self._output_handlers.append(cc_api__out)
except Exception as e:
logger.error(
"Failed to initialize Code Carbon API output; "
"continuing without API reporting. %s",
e,
exc_info=True,
)
self.run_id = uuid.uuid4()
else:
self.run_id = uuid.uuid4()

Expand Down
41 changes: 41 additions & 0 deletions tests/cli/test_cli_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from types import SimpleNamespace

import pytest
import requests
import typer
from typer.testing import CliRunner

Expand Down Expand Up @@ -43,6 +44,21 @@ def test_api_get_calls_api_and_prints(monkeypatch):
assert "fake-org" in result.output


def test_api_get_prints_friendly_error_on_api_failure(monkeypatch):
class FailingApiClient(FakeApiClient):
def get_list_organizations(self):
raise requests.exceptions.HTTPError("401 Unauthorized")

runner = CliRunner()
monkeypatch.setattr("codecarbon.core.api_client.ApiClient", FailingApiClient)
monkeypatch.setattr("codecarbon.cli.auth.get_access_token", fake_get_access_token)

result = runner.invoke(cli_main.codecarbon, ["test-api"])
assert result.exit_code == 1
assert "API request failed" in result.output
assert "401 Unauthorized" in result.output


def test_api_get_uses_get_api_endpoint(monkeypatch):
call_info = {}

Expand Down Expand Up @@ -185,6 +201,31 @@ def check_auth(self):
assert calls["endpoint_url"] == "https://custom-login.codecarbon.io"


def test_login_prints_friendly_error_on_auth_failure(monkeypatch):
class FailingApiClient:
def __init__(self, endpoint_url=None):
pass

def set_access_token(self, token):
pass

def check_auth(self):
raise requests.exceptions.HTTPError("403 Forbidden")

monkeypatch.setattr("codecarbon.core.api_client.ApiClient", FailingApiClient)
monkeypatch.setattr("codecarbon.cli.auth.authorize", lambda: None)
monkeypatch.setattr(
cli_main, "get_api_endpoint", lambda: "https://api.codecarbon.io"
)
monkeypatch.setattr("codecarbon.cli.auth.get_access_token", lambda: "bad-token")

runner = CliRunner()
result = runner.invoke(cli_main.codecarbon, ["login"])
assert result.exit_code == 1
assert "Authentication check failed" in result.output
assert "403 Forbidden" in result.output


def test_get_api_key_uses_bearer_token(monkeypatch):
captured = {}

Expand Down
Loading