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.
Files changed (110) hide show
  1. {codecarbon-3.2.2 → codecarbon-3.2.3}/PKG-INFO +6 -5
  2. {codecarbon-3.2.2 → codecarbon-3.2.3}/README.md +1 -0
  3. codecarbon-3.2.3/codecarbon/_version.py +1 -0
  4. codecarbon-3.2.3/codecarbon/cli/auth.py +225 -0
  5. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/cli/main.py +40 -51
  6. codecarbon-3.2.3/codecarbon/cli/monitor.py +127 -0
  7. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/cpu.py +20 -5
  8. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/resource_tracker.py +14 -8
  9. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/util.py +23 -1
  10. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_power.csv +1 -0
  11. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/emissions_tracker.py +23 -6
  12. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/hardware.py +62 -3
  13. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/PKG-INFO +6 -5
  14. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/SOURCES.txt +2 -1
  15. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/requires.txt +3 -2
  16. {codecarbon-3.2.2 → codecarbon-3.2.3}/pyproject.toml +49 -10
  17. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_cpu.py +186 -2
  18. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions_tracker.py +95 -0
  19. codecarbon-3.2.2/codecarbon/_version.py +0 -1
  20. codecarbon-3.2.2/tests/test_cli.py +0 -190
  21. {codecarbon-3.2.2 → codecarbon-3.2.3}/LICENSE +0 -0
  22. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/__init__.py +0 -0
  23. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/cli/__init__.py +0 -0
  24. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/cli/cli_utils.py +0 -0
  25. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/__init__.py +0 -0
  26. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/api_client.py +0 -0
  27. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/cloud.py +0 -0
  28. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/config.py +0 -0
  29. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/electricitymaps_api.py +0 -0
  30. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/emissions.py +0 -0
  31. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/gpu.py +0 -0
  32. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/measure.py +0 -0
  33. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/powermetrics.py +0 -0
  34. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/rapl.py +0 -0
  35. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/schemas.py +0 -0
  36. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/core/units.py +0 -0
  37. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/canada_provinces.geojson +0 -0
  38. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/cloud/impact.csv +0 -0
  39. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_dataset_builder/amd_cpu_scrapper.py +0 -0
  40. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py +0 -0
  41. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py +0 -0
  42. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/2016/usa_emissions.json +0 -0
  43. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/2023/canada_energy_mix.json +0 -0
  44. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/carbon_intensity_per_source.json +0 -0
  45. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
  46. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/__init__.py +0 -0
  47. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/geography.py +0 -0
  48. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/logger.py +0 -0
  49. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/ram.py +0 -0
  50. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/scheduler.py +0 -0
  51. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/external/task.py +0 -0
  52. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/input.py +0 -0
  53. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/lock.py +0 -0
  54. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output.py +0 -0
  55. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/__init__.py +0 -0
  56. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/base_output.py +0 -0
  57. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/emissions_data.py +0 -0
  58. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/file.py +0 -0
  59. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/http.py +0 -0
  60. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/logger.py +0 -0
  61. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/__init__.py +0 -0
  62. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/logfire.py +0 -0
  63. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/metric_docs.py +0 -0
  64. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/output_methods/metrics/prometheus.py +0 -0
  65. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/__init__.py +0 -0
  66. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/__init__.py +0 -0
  67. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/car_icon.png +0 -0
  68. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/house_icon.png +0 -0
  69. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/assets/tv_icon.png +0 -0
  70. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/carbonboard.py +0 -0
  71. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/carbonboard_on_api.py +0 -0
  72. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/components.py +0 -0
  73. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/data.py +0 -0
  74. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon/viz/units.py +0 -0
  75. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/dependency_links.txt +0 -0
  76. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/entry_points.txt +0 -0
  77. {codecarbon-3.2.2 → codecarbon-3.2.3}/codecarbon.egg-info/top_level.txt +0 -0
  78. {codecarbon-3.2.2 → codecarbon-3.2.3}/setup.cfg +0 -0
  79. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_api_call.py +0 -0
  80. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_cloud.py +0 -0
  81. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_config.py +0 -0
  82. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_core_util.py +0 -0
  83. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_cpu_load.py +0 -0
  84. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_custom_handler.py +0 -0
  85. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_electricitymaps_api.py +0 -0
  86. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_electricitymaps_backward_compatibility.py +0 -0
  87. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_electricitymaps_config_backward_compatibility.py +0 -0
  88. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions.py +0 -0
  89. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions_tracker_constant.py +0 -0
  90. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_emissions_tracker_flush.py +0 -0
  91. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_energy.py +0 -0
  92. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_geography.py +0 -0
  93. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_gpu.py +0 -0
  94. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_input.py +0 -0
  95. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_lock.py +0 -0
  96. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_logging_output.py +0 -0
  97. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_offline_emissions_tracker.py +0 -0
  98. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_package_integrity.py +0 -0
  99. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_powermetrics.py +0 -0
  100. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_ram.py +0 -0
  101. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_rapl_mmio_scanning.py +0 -0
  102. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_rapl_parameters.py +0 -0
  103. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_rapl_permissions.py +0 -0
  104. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_tracking_inference.py +0 -0
  105. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_unsupported_gpu.py +0 -0
  106. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_utilization_tracking.py +0 -0
  107. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_viz_data.py +0 -0
  108. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/test_viz_units.py +0 -0
  109. {codecarbon-3.2.2 → codecarbon-3.2.3}/tests/testdata.py +0 -0
  110. {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.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.7
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
  ![banner](docs/edit/images/banner.png)
@@ -89,6 +89,7 @@ CodeCarbon websites:
89
89
  [![DOI](https://zenodo.org/badge/263364731.svg)](https://zenodo.org/badge/latestdoi/263364731)
90
90
  <!-- [![Downloads](https://static.pepy.tech/badge/codecarbon/month)](https://pepy.tech/project/codecarbon) -->
91
91
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/mlco2/codecarbon/badge)](https://scorecard.dev/viewer/?uri=github.com/mlco2/codecarbon)
92
+ [![codecov](https://codecov.io/gh/mlco2/codecarbon/graph/badge.svg)](https://codecov.io/gh/mlco2/codecarbon)
92
93
 
93
94
 
94
95
  - [About CodeCarbon 💡](#about-codecarbon-)
@@ -17,6 +17,7 @@ CodeCarbon websites:
17
17
  [![DOI](https://zenodo.org/badge/263364731.svg)](https://zenodo.org/badge/latestdoi/263364731)
18
18
  <!-- [![Downloads](https://static.pepy.tech/badge/codecarbon/month)](https://pepy.tech/project/codecarbon) -->
19
19
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/mlco2/codecarbon/badge)](https://scorecard.dev/viewer/?uri=github.com/mlco2/codecarbon)
20
+ [![codecov](https://codecov.io/gh/mlco2/codecarbon/graph/badge.svg)](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(_get_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(_get_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
- get_fief_auth().authorize()
125
+ authorize()
155
126
  api = ApiClient(endpoint_url=API_URL) # TODO: get endpoint from config
156
- access_token = _get_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 {_get_access_token()}"},
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(_get_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(_get_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("monitor", short_help="Monitor your machine's carbon emissions.")
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.Argument(help="Interval between two measures.")
321
+ int, typer.Option(help="Interval between two measures.")
346
322
  ] = 10,
347
323
  api_call_interval: Annotated[
348
- int, typer.Argument(help="Number of measures between API calls.")
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
- tracker = OfflineEmissionsTracker(
370
- measure_power_secs=measure_power_secs,
371
- country_iso_code=country_iso_code,
372
- region=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
- tracker = EmissionsTracker(
384
- measure_power_secs=measure_power_secs,
385
- api_call_interval=api_call_interval,
386
- save_to_api=api,
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
- nice = psutil.cpu_times().nice
212
- if nice > 0.0001:
213
- return True
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
- f"is_psutil_available() : psutil.cpu_times().nice is too small : {nice} !"
227
+ "is_psutil_available(): no 'nice' attribute, using fallback check."
217
228
  )
218
- return False
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 "