codecarbon 3.2.2__tar.gz → 3.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codecarbon-3.2.2 → codecarbon-3.2.3}/PKG-INFO +6 -5
- {codecarbon-3.2.2 → codecarbon-3.2.3}/README.md +1 -0
- codecarbon-3.2.3/codecarbon/_version.py +1 -0
- codecarbon-3.2.3/codecarbon/cli/auth.py +225 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/cli/main.py +40 -51
- codecarbon-3.2.3/codecarbon/cli/monitor.py +127 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/cpu.py +20 -5
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/resource_tracker.py +14 -8
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/util.py +23 -1
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_power.csv +1 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/emissions_tracker.py +23 -6
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/hardware.py +62 -3
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/PKG-INFO +6 -5
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/SOURCES.txt +2 -1
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/requires.txt +3 -2
- {codecarbon-3.2.2 → codecarbon-3.2.3}/pyproject.toml +49 -10
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_cpu.py +186 -2
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions_tracker.py +95 -0
- codecarbon-3.2.2/codecarbon/_version.py +0 -1
- codecarbon-3.2.2/tests/test_cli.py +0 -190
- {codecarbon-3.2.2 → codecarbon-3.2.3}/LICENSE +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/cli/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/cli/cli_utils.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/api_client.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/cloud.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/config.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/electricitymaps_api.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/emissions.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/gpu.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/measure.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/powermetrics.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/rapl.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/schemas.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/units.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/canada_provinces.geojson +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/cloud/impact.csv +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_dataset_builder/amd_cpu_scrapper.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/2016/usa_emissions.json +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/2023/canada_energy_mix.json +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/carbon_intensity_per_source.json +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/geography.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/logger.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/ram.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/scheduler.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/task.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/input.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/lock.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/base_output.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/emissions_data.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/file.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/http.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/logger.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/logfire.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/metric_docs.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/prometheus.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/__init__.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/car_icon.png +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/house_icon.png +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/tv_icon.png +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/carbonboard.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/carbonboard_on_api.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/components.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/data.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/units.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/dependency_links.txt +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/entry_points.txt +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/top_level.txt +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/setup.cfg +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_api_call.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_cloud.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_config.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_core_util.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_cpu_load.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_custom_handler.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_electricitymaps_api.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_electricitymaps_backward_compatibility.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_electricitymaps_config_backward_compatibility.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions_tracker_constant.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions_tracker_flush.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_energy.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_geography.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_gpu.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_input.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_lock.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_logging_output.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_offline_emissions_tracker.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_package_integrity.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_powermetrics.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_ram.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_rapl_mmio_scanning.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_rapl_parameters.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_rapl_permissions.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_tracking_inference.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_unsupported_gpu.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_utilization_tracking.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_viz_data.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_viz_units.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/testdata.py +0 -0
- {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/testutils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecarbon
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.3
|
|
4
4
|
Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://codecarbon.io/
|
|
@@ -10,7 +10,6 @@ Project-URL: Documentation, https://mlco2.github.io/codecarbon/
|
|
|
10
10
|
Project-URL: Issues, https://github.com/mlco2/codecarbon/issues
|
|
11
11
|
Project-URL: Changelog, https://github.com/mlco2/codecarbon/releases
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -18,12 +17,12 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
-
Requires-Python: >=3.
|
|
20
|
+
Requires-Python: >=3.8
|
|
22
21
|
Description-Content-Type: text/markdown
|
|
23
22
|
License-File: LICENSE
|
|
24
23
|
Requires-Dist: arrow
|
|
24
|
+
Requires-Dist: authlib>=1.2.1
|
|
25
25
|
Requires-Dist: click
|
|
26
|
-
Requires-Dist: fief-client[cli]
|
|
27
26
|
Requires-Dist: pandas>=2.3.3; python_version >= "3.14"
|
|
28
27
|
Requires-Dist: pandas; python_version < "3.14"
|
|
29
28
|
Requires-Dist: prometheus_client
|
|
@@ -46,12 +45,14 @@ Requires-Dist: dash_bootstrap_components>1.0.0; extra == "viz-legacy"
|
|
|
46
45
|
Requires-Dist: fire; extra == "viz-legacy"
|
|
47
46
|
Provides-Extra: api
|
|
48
47
|
Requires-Dist: alembic<2.0.0; extra == "api"
|
|
48
|
+
Requires-Dist: authlib>=1.2.1; extra == "api"
|
|
49
49
|
Requires-Dist: bcrypt<5.0.0; extra == "api"
|
|
50
50
|
Requires-Dist: python-dateutil<3.0.0; extra == "api"
|
|
51
51
|
Requires-Dist: dependency-injector<5.0.0; extra == "api"
|
|
52
52
|
Requires-Dist: fastapi<1.0.0; extra == "api"
|
|
53
53
|
Requires-Dist: fief-client[fastapi]; extra == "api"
|
|
54
54
|
Requires-Dist: httpx; extra == "api"
|
|
55
|
+
Requires-Dist: itsdangerous; extra == "api"
|
|
55
56
|
Requires-Dist: pydantic[email]<2.0.0; extra == "api"
|
|
56
57
|
Requires-Dist: psycopg2-binary<3.0.0; extra == "api"
|
|
57
58
|
Requires-Dist: requests<3.0.0; extra == "api"
|
|
@@ -67,7 +68,6 @@ Requires-Dist: psutil; extra == "api"
|
|
|
67
68
|
Requires-Dist: requests-mock; extra == "api"
|
|
68
69
|
Requires-Dist: rapidfuzz; extra == "api"
|
|
69
70
|
Requires-Dist: PyJWT; extra == "api"
|
|
70
|
-
Requires-Dist: logfire[fastapi]>=1.0.1; extra == "api"
|
|
71
71
|
Dynamic: license-file
|
|
72
72
|
|
|
73
73
|

|
|
@@ -89,6 +89,7 @@ CodeCarbon websites:
|
|
|
89
89
|
[](https://zenodo.org/badge/latestdoi/263364731)
|
|
90
90
|
<!-- [](https://pepy.tech/project/codecarbon) -->
|
|
91
91
|
[](https://scorecard.dev/viewer/?uri=github.com/mlco2/codecarbon)
|
|
92
|
+
[](https://codecov.io/gh/mlco2/codecarbon)
|
|
92
93
|
|
|
93
94
|
|
|
94
95
|
- [About CodeCarbon 💡](#about-codecarbon-)
|
|
@@ -17,6 +17,7 @@ CodeCarbon websites:
|
|
|
17
17
|
[](https://zenodo.org/badge/latestdoi/263364731)
|
|
18
18
|
<!-- [](https://pepy.tech/project/codecarbon) -->
|
|
19
19
|
[](https://scorecard.dev/viewer/?uri=github.com/mlco2/codecarbon)
|
|
20
|
+
[](https://codecov.io/gh/mlco2/codecarbon)
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
- [About CodeCarbon 💡](#about-codecarbon-)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.2.3"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OIDC Authentication helpers for the CodeCarbon CLI.
|
|
3
|
+
|
|
4
|
+
Handles the full token lifecycle: browser-based login (Authorization Code +
|
|
5
|
+
PKCE), credential storage, JWKS validation, and transparent refresh.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import webbrowser
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from urllib.parse import parse_qs, urlparse
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
from authlib.common.security import generate_token
|
|
17
|
+
from authlib.integrations.requests_client import OAuth2Session
|
|
18
|
+
from authlib.jose import JsonWebKey
|
|
19
|
+
from authlib.jose import jwt as jose_jwt
|
|
20
|
+
from authlib.oauth2.rfc7636 import create_s256_code_challenge
|
|
21
|
+
|
|
22
|
+
AUTH_CLIENT_ID = os.environ.get(
|
|
23
|
+
"AUTH_CLIENT_ID",
|
|
24
|
+
"codecarbon-cli",
|
|
25
|
+
)
|
|
26
|
+
AUTH_SERVER_WELL_KNOWN = os.environ.get(
|
|
27
|
+
"AUTH_SERVER_WELL_KNOWN",
|
|
28
|
+
"https://authentication.codecarbon.io/realms/codecarbon/.well-known/openid-configuration",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_REDIRECT_PORT = 8090
|
|
32
|
+
_REDIRECT_URI = f"http://localhost:{_REDIRECT_PORT}/callback"
|
|
33
|
+
_CREDENTIALS_FILE = Path("./credentials.json")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── OAuth callback server ───────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
40
|
+
"""HTTP handler that captures the OAuth2 authorization callback."""
|
|
41
|
+
|
|
42
|
+
callback_url = None
|
|
43
|
+
error = None
|
|
44
|
+
|
|
45
|
+
def do_GET(self):
|
|
46
|
+
_CallbackHandler.callback_url = f"http://localhost:{_REDIRECT_PORT}{self.path}"
|
|
47
|
+
parsed = urlparse(self.path)
|
|
48
|
+
params = parse_qs(parsed.query)
|
|
49
|
+
|
|
50
|
+
if "error" in params:
|
|
51
|
+
_CallbackHandler.error = params["error"][0]
|
|
52
|
+
self.send_response(400)
|
|
53
|
+
self.send_header("Content-Type", "text/html")
|
|
54
|
+
self.end_headers()
|
|
55
|
+
msg = params.get("error_description", [params["error"][0]])[0]
|
|
56
|
+
self.wfile.write(
|
|
57
|
+
f"<html><body><h1>Login failed</h1><p>{msg}</p></body></html>".encode()
|
|
58
|
+
)
|
|
59
|
+
else:
|
|
60
|
+
self.send_response(200)
|
|
61
|
+
self.send_header("Content-Type", "text/html")
|
|
62
|
+
self.end_headers()
|
|
63
|
+
self.wfile.write(
|
|
64
|
+
b"<html><body><h1>Login successful!</h1>"
|
|
65
|
+
b"<p>You can close this window.</p></body></html>"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def log_message(self, format, *args):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── OIDC discovery ──────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _discover_endpoints():
|
|
76
|
+
"""Fetch OpenID Connect discovery document."""
|
|
77
|
+
resp = requests.get(AUTH_SERVER_WELL_KNOWN)
|
|
78
|
+
resp.raise_for_status()
|
|
79
|
+
return resp.json()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Credential storage ──────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _save_credentials(tokens):
|
|
86
|
+
"""Save OAuth tokens to the local credentials file."""
|
|
87
|
+
with open(_CREDENTIALS_FILE, "w") as f:
|
|
88
|
+
json.dump(tokens, f)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _load_credentials():
|
|
92
|
+
"""Load OAuth tokens from the local credentials file."""
|
|
93
|
+
with open(_CREDENTIALS_FILE, "r") as f:
|
|
94
|
+
return json.load(f)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Token validation & refresh ──────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _validate_access_token(access_token: str) -> bool:
|
|
101
|
+
"""Validate access token against the current OIDC provider's JWKS.
|
|
102
|
+
|
|
103
|
+
Returns False when the signature or expiry check fails (wrong provider,
|
|
104
|
+
expired, tampered). Returns True on network errors so the caller can
|
|
105
|
+
fall through to the API and let the server decide.
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
discovery = _discover_endpoints()
|
|
109
|
+
jwks_resp = requests.get(discovery["jwks_uri"])
|
|
110
|
+
jwks_resp.raise_for_status()
|
|
111
|
+
keyset = JsonWebKey.import_key_set(jwks_resp.json())
|
|
112
|
+
claims = jose_jwt.decode(access_token, keyset)
|
|
113
|
+
claims.validate()
|
|
114
|
+
return True
|
|
115
|
+
except requests.RequestException:
|
|
116
|
+
return True # Can't reach auth server — let the API handle it
|
|
117
|
+
except Exception:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _refresh_tokens(refresh_token: str) -> dict:
|
|
122
|
+
"""Exchange a refresh token for a new token set via the OIDC token endpoint."""
|
|
123
|
+
discovery = _discover_endpoints()
|
|
124
|
+
resp = requests.post(
|
|
125
|
+
discovery["token_endpoint"],
|
|
126
|
+
data={
|
|
127
|
+
"grant_type": "refresh_token",
|
|
128
|
+
"refresh_token": refresh_token,
|
|
129
|
+
"client_id": AUTH_CLIENT_ID,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
resp.raise_for_status()
|
|
133
|
+
return resp.json()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── Public API ──────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def authorize():
|
|
140
|
+
"""Run the OAuth2 Authorization Code flow with PKCE."""
|
|
141
|
+
discovery = _discover_endpoints()
|
|
142
|
+
|
|
143
|
+
session = OAuth2Session(
|
|
144
|
+
client_id=AUTH_CLIENT_ID,
|
|
145
|
+
redirect_uri=_REDIRECT_URI,
|
|
146
|
+
scope="openid offline_access",
|
|
147
|
+
token_endpoint_auth_method="none",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
code_verifier = generate_token(48)
|
|
151
|
+
code_challenge = create_s256_code_challenge(code_verifier)
|
|
152
|
+
|
|
153
|
+
uri, state = session.create_authorization_url(
|
|
154
|
+
discovery["authorization_endpoint"],
|
|
155
|
+
code_challenge=code_challenge,
|
|
156
|
+
code_challenge_method="S256",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
_CallbackHandler.callback_url = None
|
|
160
|
+
_CallbackHandler.error = None
|
|
161
|
+
|
|
162
|
+
server = HTTPServer(("localhost", _REDIRECT_PORT), _CallbackHandler)
|
|
163
|
+
|
|
164
|
+
print("Opening browser for authentication...")
|
|
165
|
+
webbrowser.open(uri)
|
|
166
|
+
|
|
167
|
+
server.handle_request()
|
|
168
|
+
server.server_close()
|
|
169
|
+
|
|
170
|
+
if _CallbackHandler.error:
|
|
171
|
+
raise ValueError(f"Authorization failed: {_CallbackHandler.error}")
|
|
172
|
+
|
|
173
|
+
if not _CallbackHandler.callback_url:
|
|
174
|
+
raise ValueError("Authorization failed: no callback received")
|
|
175
|
+
|
|
176
|
+
token = session.fetch_token(
|
|
177
|
+
discovery["token_endpoint"],
|
|
178
|
+
authorization_response=_CallbackHandler.callback_url,
|
|
179
|
+
code_verifier=code_verifier,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
_save_credentials(token)
|
|
183
|
+
return token
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_access_token() -> str:
|
|
187
|
+
"""Return a valid access token, refreshing or failing with a clear message."""
|
|
188
|
+
try:
|
|
189
|
+
creds = _load_credentials()
|
|
190
|
+
except Exception as e:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
"Not able to retrieve the access token, "
|
|
193
|
+
f"please run `codecarbon login` first! (error: {e})"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
access_token = creds.get("access_token")
|
|
197
|
+
if not access_token:
|
|
198
|
+
raise ValueError("No access token found. Please run `codecarbon login` first.")
|
|
199
|
+
|
|
200
|
+
# Fast path: token is still valid for the current OIDC provider
|
|
201
|
+
if _validate_access_token(access_token):
|
|
202
|
+
return access_token
|
|
203
|
+
|
|
204
|
+
# Token is expired or was issued by a different provider — try refresh
|
|
205
|
+
refresh_token = creds.get("refresh_token")
|
|
206
|
+
if refresh_token:
|
|
207
|
+
try:
|
|
208
|
+
new_tokens = _refresh_tokens(refresh_token)
|
|
209
|
+
_save_credentials(new_tokens)
|
|
210
|
+
return new_tokens["access_token"]
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# Refresh failed — credentials are stale (e.g. auth provider migrated)
|
|
215
|
+
_CREDENTIALS_FILE.unlink(missing_ok=True)
|
|
216
|
+
raise ValueError(
|
|
217
|
+
"Your session has expired or the authentication provider has changed. "
|
|
218
|
+
"Please run `codecarbon login` again."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_id_token() -> str:
|
|
223
|
+
"""Return the stored OIDC id_token."""
|
|
224
|
+
creds = _load_credentials()
|
|
225
|
+
return creds["id_token"]
|
|
@@ -8,13 +8,12 @@ from typing import Optional
|
|
|
8
8
|
import questionary
|
|
9
9
|
import requests
|
|
10
10
|
import typer
|
|
11
|
-
from fief_client import Fief
|
|
12
|
-
from fief_client.integrations.cli import FiefAuth
|
|
13
11
|
from rich import print
|
|
14
12
|
from rich.prompt import Confirm
|
|
15
13
|
from typing_extensions import Annotated
|
|
16
14
|
|
|
17
15
|
from codecarbon import __app_name__, __version__
|
|
16
|
+
from codecarbon.cli.auth import authorize, get_access_token
|
|
18
17
|
from codecarbon.cli.cli_utils import (
|
|
19
18
|
create_new_config_file,
|
|
20
19
|
get_api_endpoint,
|
|
@@ -22,17 +21,11 @@ from codecarbon.cli.cli_utils import (
|
|
|
22
21
|
get_existing_local_exp_id,
|
|
23
22
|
overwrite_local_config,
|
|
24
23
|
)
|
|
24
|
+
from codecarbon.cli.monitor import run_and_monitor
|
|
25
25
|
from codecarbon.core.api_client import ApiClient, get_datetime_with_timezone
|
|
26
26
|
from codecarbon.core.schemas import ExperimentCreate, OrganizationCreate, ProjectCreate
|
|
27
27
|
from codecarbon.emissions_tracker import EmissionsTracker, OfflineEmissionsTracker
|
|
28
28
|
|
|
29
|
-
AUTH_CLIENT_ID = os.environ.get(
|
|
30
|
-
"AUTH_CLIENT_ID",
|
|
31
|
-
"jsUPWIcUECQFE_ouanUuVhXx52TTjEVcVNNtNGeyAtU",
|
|
32
|
-
)
|
|
33
|
-
AUTH_SERVER_URL = os.environ.get(
|
|
34
|
-
"AUTH_SERVER_URL", "https://auth.codecarbon.io/codecarbon"
|
|
35
|
-
)
|
|
36
29
|
API_URL = os.environ.get("API_URL", "https://dashboard.codecarbon.io/api")
|
|
37
30
|
|
|
38
31
|
DEFAULT_PROJECT_ID = "e60afa92-17b7-4720-91a0-1ae91e409ba1"
|
|
@@ -78,7 +71,7 @@ def show_config(path: Path = Path("./.codecarbon.config")) -> None:
|
|
|
78
71
|
d = get_config(path)
|
|
79
72
|
api_endpoint = get_api_endpoint(path)
|
|
80
73
|
api = ApiClient(endpoint_url=api_endpoint)
|
|
81
|
-
api.set_access_token(
|
|
74
|
+
api.set_access_token(get_access_token())
|
|
82
75
|
print("Current configuration : \n")
|
|
83
76
|
print("Config file content : ")
|
|
84
77
|
print(d)
|
|
@@ -114,28 +107,6 @@ def show_config(path: Path = Path("./.codecarbon.config")) -> None:
|
|
|
114
107
|
)
|
|
115
108
|
|
|
116
109
|
|
|
117
|
-
def get_fief_auth():
|
|
118
|
-
fief = Fief(AUTH_SERVER_URL, AUTH_CLIENT_ID)
|
|
119
|
-
fief_auth = FiefAuth(fief, "./credentials.json")
|
|
120
|
-
return fief_auth
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _get_access_token():
|
|
124
|
-
try:
|
|
125
|
-
access_token_info = get_fief_auth().access_token_info()
|
|
126
|
-
access_token = access_token_info["access_token"]
|
|
127
|
-
return access_token
|
|
128
|
-
except Exception as e:
|
|
129
|
-
raise ValueError(
|
|
130
|
-
f"Not able to retrieve the access token, please run `codecarbon login` first! (error: {e})"
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _get_id_token():
|
|
135
|
-
id_token = get_fief_auth()._tokens["id_token"]
|
|
136
|
-
return id_token
|
|
137
|
-
|
|
138
|
-
|
|
139
110
|
@codecarbon.command(
|
|
140
111
|
"test-api", short_help="Make an authenticated GET request to an API endpoint"
|
|
141
112
|
)
|
|
@@ -144,16 +115,16 @@ def api_get():
|
|
|
144
115
|
ex: test-api
|
|
145
116
|
"""
|
|
146
117
|
api = ApiClient(endpoint_url=API_URL) # TODO: get endpoint from config
|
|
147
|
-
api.set_access_token(
|
|
118
|
+
api.set_access_token(get_access_token())
|
|
148
119
|
organizations = api.get_list_organizations()
|
|
149
120
|
print(organizations)
|
|
150
121
|
|
|
151
122
|
|
|
152
123
|
@codecarbon.command("login", short_help="Login to CodeCarbon")
|
|
153
124
|
def login():
|
|
154
|
-
|
|
125
|
+
authorize()
|
|
155
126
|
api = ApiClient(endpoint_url=API_URL) # TODO: get endpoint from config
|
|
156
|
-
access_token =
|
|
127
|
+
access_token = get_access_token()
|
|
157
128
|
api.set_access_token(access_token)
|
|
158
129
|
api.check_auth()
|
|
159
130
|
|
|
@@ -166,7 +137,7 @@ def get_api_key(project_id: str):
|
|
|
166
137
|
"name": "api token",
|
|
167
138
|
"x_token": "???",
|
|
168
139
|
},
|
|
169
|
-
headers={"Authorization": f"Bearer {
|
|
140
|
+
headers={"Authorization": f"Bearer {get_access_token()}"},
|
|
170
141
|
)
|
|
171
142
|
api_key = req.json()["token"]
|
|
172
143
|
return api_key
|
|
@@ -175,7 +146,7 @@ def get_api_key(project_id: str):
|
|
|
175
146
|
@codecarbon.command("get-token", short_help="Get project token")
|
|
176
147
|
def get_token(project_id: str):
|
|
177
148
|
# api = ApiClient(endpoint_url=API_URL) # TODO: get endpoint from config
|
|
178
|
-
# api.set_access_token(
|
|
149
|
+
# api.set_access_token(get_access_token())
|
|
179
150
|
token = get_api_key(project_id)
|
|
180
151
|
print("Your token: " + token)
|
|
181
152
|
print("Add it to the api_key field in your configuration file")
|
|
@@ -223,7 +194,7 @@ def config():
|
|
|
223
194
|
)
|
|
224
195
|
overwrite_local_config("api_endpoint", api_endpoint, path=file_path)
|
|
225
196
|
api = ApiClient(endpoint_url=api_endpoint)
|
|
226
|
-
api.set_access_token(
|
|
197
|
+
api.set_access_token(get_access_token())
|
|
227
198
|
organizations = api.get_list_organizations()
|
|
228
199
|
org = questionary_prompt(
|
|
229
200
|
"Pick existing organization from list or Create new organization ?",
|
|
@@ -339,13 +310,18 @@ def config():
|
|
|
339
310
|
)
|
|
340
311
|
|
|
341
312
|
|
|
342
|
-
@codecarbon.command(
|
|
313
|
+
@codecarbon.command(
|
|
314
|
+
"monitor",
|
|
315
|
+
short_help="Monitor your machine's carbon emissions.",
|
|
316
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
317
|
+
)
|
|
343
318
|
def monitor(
|
|
319
|
+
ctx: typer.Context,
|
|
344
320
|
measure_power_secs: Annotated[
|
|
345
|
-
int, typer.
|
|
321
|
+
int, typer.Option(help="Interval between two measures.")
|
|
346
322
|
] = 10,
|
|
347
323
|
api_call_interval: Annotated[
|
|
348
|
-
int, typer.
|
|
324
|
+
int, typer.Option(help="Number of measures between API calls.")
|
|
349
325
|
] = 30,
|
|
350
326
|
api: Annotated[
|
|
351
327
|
bool, typer.Option(help="Choose to call Code Carbon API or not")
|
|
@@ -359,6 +335,13 @@ def monitor(
|
|
|
359
335
|
] = None,
|
|
360
336
|
):
|
|
361
337
|
"""Monitor your machine's carbon emissions."""
|
|
338
|
+
|
|
339
|
+
# Shared tracker args so monitor and run_and_monitor behave the same
|
|
340
|
+
tracker_args = {
|
|
341
|
+
"measure_power_secs": measure_power_secs,
|
|
342
|
+
"api_call_interval": api_call_interval,
|
|
343
|
+
}
|
|
344
|
+
# Set up the tracker arguments based on mode (offline vs online) and validate required args for each mode
|
|
362
345
|
if offline:
|
|
363
346
|
if not country_iso_code:
|
|
364
347
|
print(
|
|
@@ -366,11 +349,11 @@ def monitor(
|
|
|
366
349
|
)
|
|
367
350
|
raise typer.Exit(1)
|
|
368
351
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
country_iso_code
|
|
372
|
-
region
|
|
373
|
-
|
|
352
|
+
tracker_args = {
|
|
353
|
+
**tracker_args,
|
|
354
|
+
"country_iso_code": country_iso_code,
|
|
355
|
+
"region": region,
|
|
356
|
+
}
|
|
374
357
|
else:
|
|
375
358
|
experiment_id = get_existing_local_exp_id()
|
|
376
359
|
if api and experiment_id is None:
|
|
@@ -380,11 +363,17 @@ def monitor(
|
|
|
380
363
|
)
|
|
381
364
|
raise typer.Exit(1)
|
|
382
365
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
366
|
+
tracker_args = {**tracker_args, "save_to_api": api}
|
|
367
|
+
|
|
368
|
+
# If extra args are provided (e.g. `codecarbon monitor -- my_script.py`), delegate to `run_and_monitor`
|
|
369
|
+
if getattr(ctx, "args", None):
|
|
370
|
+
return run_and_monitor(ctx, **tracker_args)
|
|
371
|
+
|
|
372
|
+
# Instantiate the tracker
|
|
373
|
+
if offline:
|
|
374
|
+
tracker = OfflineEmissionsTracker(**tracker_args)
|
|
375
|
+
else:
|
|
376
|
+
tracker = EmissionsTracker(**tracker_args)
|
|
388
377
|
|
|
389
378
|
def signal_handler(signum, frame):
|
|
390
379
|
print("\nReceived signal to stop. Saving emissions data...")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""CodeCarbon CLI - Monitor Command"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich import print
|
|
9
|
+
from typing_extensions import Annotated
|
|
10
|
+
|
|
11
|
+
from codecarbon.emissions_tracker import EmissionsTracker
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_and_monitor(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
log_level: Annotated[
|
|
17
|
+
str, typer.Option(help="Log level (critical, error, warning, info, debug)")
|
|
18
|
+
] = "error",
|
|
19
|
+
**tracker_args,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Run a command and track its carbon emissions.
|
|
23
|
+
|
|
24
|
+
This command wraps any executable and measures the process's total power
|
|
25
|
+
consumption during its execution. When the command completes, a summary
|
|
26
|
+
report is displayed and emissions data is saved to a CSV file.
|
|
27
|
+
|
|
28
|
+
Note: This tracks process-level emissions (only the specific command), not the
|
|
29
|
+
entire machine. For machine-level tracking, use the `monitor` command.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
|
|
33
|
+
Do not use quotes around the command. Use -- to separate CodeCarbon args.
|
|
34
|
+
|
|
35
|
+
# Run any shell command:
|
|
36
|
+
codecarbon monitor -- ./benchmark.sh
|
|
37
|
+
|
|
38
|
+
# Commands with arguments (use single quotes for special chars):
|
|
39
|
+
codecarbon monitor -- python -c 'print("Hello World!")'
|
|
40
|
+
|
|
41
|
+
# Pipe the command output:
|
|
42
|
+
codecarbon monitor -- npm run test > output.txt
|
|
43
|
+
|
|
44
|
+
# Display the CodeCarbon detailed logs:
|
|
45
|
+
codecarbon monitor --log-level debug -- python --version
|
|
46
|
+
|
|
47
|
+
The emissions data is appended to emissions.csv (default) in the current
|
|
48
|
+
directory. The file path is shown in the final report.
|
|
49
|
+
"""
|
|
50
|
+
# Suppress all CodeCarbon logs during execution
|
|
51
|
+
from codecarbon.external.logger import set_logger_level
|
|
52
|
+
|
|
53
|
+
set_logger_level(log_level)
|
|
54
|
+
|
|
55
|
+
# Get the command from remaining args
|
|
56
|
+
command = ctx.args
|
|
57
|
+
|
|
58
|
+
if not command:
|
|
59
|
+
print(
|
|
60
|
+
"ERROR: No command provided. Use: codecarbon monitor -- <command>",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
|
|
65
|
+
# Initialize tracker with specified logging level and shared args
|
|
66
|
+
tracker = EmissionsTracker(
|
|
67
|
+
log_level=log_level,
|
|
68
|
+
save_to_logger=False,
|
|
69
|
+
tracking_mode="process",
|
|
70
|
+
**tracker_args,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
print("🌱 CodeCarbon: Starting emissions tracking...")
|
|
74
|
+
print(f" Command: {' '.join(command)}")
|
|
75
|
+
print()
|
|
76
|
+
|
|
77
|
+
tracker.start()
|
|
78
|
+
|
|
79
|
+
process = None
|
|
80
|
+
try:
|
|
81
|
+
# Run the command, streaming output to console
|
|
82
|
+
# Let the child inherit the parent's std streams so Click's
|
|
83
|
+
# `CliRunner` can capture output (don't pass StringIO objects).
|
|
84
|
+
process = subprocess.Popen(command, text=True)
|
|
85
|
+
|
|
86
|
+
# Wait for completion
|
|
87
|
+
exit_code = process.wait()
|
|
88
|
+
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
print(f"❌ Error: Command not found: {command[0]}", file=sys.stderr)
|
|
91
|
+
exit_code = 127
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
print("\n⚠️ Interrupted by user", file=sys.stderr)
|
|
94
|
+
if process is not None:
|
|
95
|
+
process.terminate()
|
|
96
|
+
try:
|
|
97
|
+
process.wait(timeout=5)
|
|
98
|
+
except subprocess.TimeoutExpired:
|
|
99
|
+
process.kill()
|
|
100
|
+
exit_code = 130
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"❌ Error running command: {e}", file=sys.stderr)
|
|
103
|
+
exit_code = 1
|
|
104
|
+
finally:
|
|
105
|
+
emissions = tracker.stop()
|
|
106
|
+
print()
|
|
107
|
+
print("=" * 60)
|
|
108
|
+
print("🌱 CodeCarbon Emissions Report")
|
|
109
|
+
print("=" * 60)
|
|
110
|
+
print(f" Command: {' '.join(command)}")
|
|
111
|
+
if emissions is not None:
|
|
112
|
+
print(f" Emissions: {emissions * 1000:.4f} g CO2eq")
|
|
113
|
+
else:
|
|
114
|
+
print(" Emissions: N/A")
|
|
115
|
+
|
|
116
|
+
# Show where the data was saved
|
|
117
|
+
if hasattr(tracker, "_conf") and "output_file" in tracker._conf:
|
|
118
|
+
output_path = tracker._conf["output_file"]
|
|
119
|
+
# Make it absolute if it's relative
|
|
120
|
+
if not os.path.isabs(output_path):
|
|
121
|
+
output_path = os.path.abspath(output_path)
|
|
122
|
+
print(f" Saved to: {output_path}")
|
|
123
|
+
|
|
124
|
+
print(" ⚠️ Note: Tracked the command process and its children")
|
|
125
|
+
print("=" * 60)
|
|
126
|
+
|
|
127
|
+
raise typer.Exit(exit_code)
|
|
@@ -208,14 +208,29 @@ def is_rapl_available(rapl_dir: Optional[str] = None) -> bool:
|
|
|
208
208
|
|
|
209
209
|
def is_psutil_available():
|
|
210
210
|
try:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
cpu_times = psutil.cpu_times()
|
|
212
|
+
|
|
213
|
+
# platforms like Windows do not have 'nice' attribute
|
|
214
|
+
if hasattr(cpu_times, "nice"):
|
|
215
|
+
nice = cpu_times.nice
|
|
216
|
+
if nice > 0.0001:
|
|
217
|
+
return True
|
|
218
|
+
else:
|
|
219
|
+
logger.debug(
|
|
220
|
+
f"is_psutil_available(): psutil.cpu_times().nice is too small: {nice}"
|
|
221
|
+
)
|
|
222
|
+
return False
|
|
223
|
+
|
|
214
224
|
else:
|
|
225
|
+
# Fallback: check if psutil works by calling cpu_percent
|
|
215
226
|
logger.debug(
|
|
216
|
-
|
|
227
|
+
"is_psutil_available(): no 'nice' attribute, using fallback check."
|
|
217
228
|
)
|
|
218
|
-
|
|
229
|
+
|
|
230
|
+
# check CPU utilization usable
|
|
231
|
+
psutil.cpu_percent(interval=0.0, percpu=False)
|
|
232
|
+
return True
|
|
233
|
+
|
|
219
234
|
except Exception as e:
|
|
220
235
|
logger.debug(
|
|
221
236
|
"Not using the psutil interface, an exception occurred while instantiating "
|