codecarbon 3.3.0__tar.gz → 3.3.2__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 (148) hide show
  1. {codecarbon-3.3.0 → codecarbon-3.3.2}/PKG-INFO +15 -1
  2. codecarbon-3.3.2/codecarbon/_version.py +1 -0
  3. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/cli/auth.py +10 -2
  4. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/cli/main.py +53 -7
  5. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/cli/monitor.py +14 -11
  6. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/api_client.py +7 -2
  7. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/config.py +85 -30
  8. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/cpu.py +120 -14
  9. codecarbon-3.3.2/codecarbon/core/electricitymaps_api.py +152 -0
  10. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/emissions.py +49 -4
  11. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/gpu.py +13 -1
  12. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/gpu_amd.py +11 -3
  13. codecarbon-3.3.2/codecarbon/core/gpu_device.py +298 -0
  14. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/gpu_nvidia.py +3 -2
  15. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/hardware_cache.py +16 -6
  16. codecarbon-3.3.2/codecarbon/core/powermetrics.py +242 -0
  17. codecarbon-3.3.2/codecarbon/core/rapl.py +157 -0
  18. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/resource_tracker.py +27 -2
  19. codecarbon-3.3.2/codecarbon/core/slurm.py +30 -0
  20. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/windows_emi.py +4 -23
  21. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_power.csv +15 -0
  22. codecarbon-3.3.2/codecarbon/data/private_infra/2023/china_provincial_emissions.json +292 -0
  23. {codecarbon-3.3.0/codecarbon/data/private_infra/2016 → codecarbon-3.3.2/codecarbon/data/private_infra/2023}/usa_emissions.json +157 -151
  24. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/private_infra/carbon_intensity_per_source.json +7 -1
  25. codecarbon-3.3.2/codecarbon/data/private_infra/nordic_emissions.json +89 -0
  26. codecarbon-3.3.2/codecarbon/data/private_infra/update_usa_emissions.py +145 -0
  27. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/emissions_tracker.py +162 -46
  28. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/geography.py +1 -1
  29. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/hardware.py +209 -50
  30. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/ram.py +4 -3
  31. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/input.py +14 -1
  32. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/output.py +29 -0
  33. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/file.py +31 -19
  34. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/carbonboard.py +2 -2
  35. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon.egg-info/PKG-INFO +15 -1
  36. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon.egg-info/SOURCES.txt +10 -1
  37. {codecarbon-3.3.0 → codecarbon-3.3.2}/pyproject.toml +38 -7
  38. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_api_call.py +33 -0
  39. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_boamps_output.py +6 -0
  40. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_config.py +61 -0
  41. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_cpu.py +55 -0
  42. codecarbon-3.3.2/tests/test_cpu_load.py +384 -0
  43. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_electricitymaps_api.py +13 -0
  44. codecarbon-3.3.2/tests/test_electricitymaps_cache.py +183 -0
  45. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_emissions.py +235 -4
  46. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_emissions_tracker.py +290 -1
  47. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_emissions_tracker_constant.py +36 -1
  48. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_emissions_tracker_flush.py +13 -0
  49. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_geography.py +15 -0
  50. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_gpu_amd.py +35 -6
  51. codecarbon-3.3.2/tests/test_gpu_nvidia.py +938 -0
  52. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_hardware_cache.py +34 -0
  53. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_powermetrics.py +167 -8
  54. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_ram.py +6 -0
  55. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_rapl_mmio_scanning.py +229 -0
  56. codecarbon-3.3.2/tests/test_rapl_permissions.py +278 -0
  57. codecarbon-3.3.2/tests/test_raspberry.py +253 -0
  58. codecarbon-3.3.2/tests/test_readme_links.py +26 -0
  59. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_resource_tracker.py +73 -0
  60. codecarbon-3.3.2/tests/test_slurm.py +40 -0
  61. codecarbon-3.3.2/tests/test_update_usa_emissions.py +102 -0
  62. codecarbon-3.3.2/tests/test_zero_carbon_intensity.py +34 -0
  63. codecarbon-3.3.0/codecarbon/_version.py +0 -1
  64. codecarbon-3.3.0/codecarbon/core/electricitymaps_api.py +0 -69
  65. codecarbon-3.3.0/codecarbon/core/gpu_device.py +0 -130
  66. codecarbon-3.3.0/codecarbon/core/powermetrics.py +0 -211
  67. codecarbon-3.3.0/codecarbon/core/rapl.py +0 -84
  68. codecarbon-3.3.0/codecarbon/data/private_infra/nordic_emissions.json +0 -69
  69. codecarbon-3.3.0/tests/test_cpu_load.py +0 -195
  70. codecarbon-3.3.0/tests/test_gpu_nvidia.py +0 -469
  71. codecarbon-3.3.0/tests/test_rapl_permissions.py +0 -112
  72. {codecarbon-3.3.0 → codecarbon-3.3.2}/LICENSE +0 -0
  73. {codecarbon-3.3.0 → codecarbon-3.3.2}/README.md +0 -0
  74. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/__init__.py +0 -0
  75. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/cli/__init__.py +0 -0
  76. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/cli/cli_utils.py +0 -0
  77. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/__init__.py +0 -0
  78. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/cloud.py +0 -0
  79. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/measure.py +0 -0
  80. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/schemas.py +0 -0
  81. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/telemetry_client.py +0 -0
  82. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/telemetry_schemas.py +0 -0
  83. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/units.py +0 -0
  84. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/core/util.py +0 -0
  85. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/canada_provinces.geojson +0 -0
  86. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/cloud/impact.csv +0 -0
  87. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/amd_cpu_scrapper.py +0 -0
  88. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/intel_cpu_scrapper.py +0 -0
  89. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/hardware/cpu_dataset_builder/merge_scrapped_cpu_power.py +0 -0
  90. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/private_infra/2023/canada_energy_mix.json +0 -0
  91. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/data/private_infra/global_energy_mix.json +0 -0
  92. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/__init__.py +0 -0
  93. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/logger.py +0 -0
  94. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/scheduler.py +0 -0
  95. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/external/task.py +0 -0
  96. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/lock.py +0 -0
  97. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output.py +0 -0
  98. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/__init__.py +0 -0
  99. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/base_output.py +0 -0
  100. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/__init__.py +0 -0
  101. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/mapper.py +0 -0
  102. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/boamps/models.py +0 -0
  103. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/emissions_data.py +0 -0
  104. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/http.py +0 -0
  105. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/logger.py +0 -0
  106. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/__init__.py +0 -0
  107. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/logfire.py +0 -0
  108. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/metric_docs.py +0 -0
  109. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/output_methods/metrics/prometheus.py +0 -0
  110. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/py.typed +0 -0
  111. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/__init__.py +0 -0
  112. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/assets/__init__.py +0 -0
  113. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/assets/car_icon.png +0 -0
  114. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/assets/house_icon.png +0 -0
  115. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/assets/tv_icon.png +0 -0
  116. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/carbonboard_on_api.py +0 -0
  117. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/components.py +0 -0
  118. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/data.py +0 -0
  119. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon/viz/units.py +0 -0
  120. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon.egg-info/dependency_links.txt +0 -0
  121. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon.egg-info/entry_points.txt +0 -0
  122. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon.egg-info/requires.txt +0 -0
  123. {codecarbon-3.3.0 → codecarbon-3.3.2}/codecarbon.egg-info/top_level.txt +0 -0
  124. {codecarbon-3.3.0 → codecarbon-3.3.2}/setup.cfg +0 -0
  125. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_cloud.py +0 -0
  126. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_core_util.py +0 -0
  127. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_custom_handler.py +0 -0
  128. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_docs_examples.py +0 -0
  129. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_electricitymaps_backward_compatibility.py +0 -0
  130. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_electricitymaps_config_backward_compatibility.py +0 -0
  131. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_energy.py +0 -0
  132. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_gpu.py +0 -0
  133. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_input.py +0 -0
  134. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_lock.py +0 -0
  135. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_logger.py +0 -0
  136. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_logging_output.py +0 -0
  137. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_offline_emissions_tracker.py +0 -0
  138. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_package_integrity.py +0 -0
  139. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_rapl_parameters.py +0 -0
  140. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_telemetry_client.py +0 -0
  141. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_tracking_inference.py +0 -0
  142. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_unsupported_gpu.py +0 -0
  143. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_utilization_tracking.py +0 -0
  144. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_viz_data.py +0 -0
  145. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_viz_units.py +0 -0
  146. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/test_windows_emi.py +0 -0
  147. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/testdata.py +0 -0
  148. {codecarbon-3.3.0 → codecarbon-3.3.2}/tests/testutils.py +0 -0
@@ -1,6 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecarbon
3
- Version: 3.3.0
3
+ Version: 3.3.2
4
+ Summary: Track and reduce the CO2 emissions of your computing
4
5
  Author: Mila, DataForGood, BCG GAMMA, Comet.ml, Haverford College
5
6
  License-Expression: MIT
6
7
  Project-URL: Homepage, https://codecarbon.io/
@@ -9,12 +10,25 @@ Project-URL: Dashboard, http://dashboard.codecarbon.io/
9
10
  Project-URL: Documentation, https://docs.codecarbon.io/
10
11
  Project-URL: Issues, https://github.com/mlco2/codecarbon/issues
11
12
  Project-URL: Changelog, https://github.com/mlco2/codecarbon/releases
13
+ Keywords: carbon,co2,emissions,carbon-footprint,sustainability,green-ai,green-computing,energy,energy-consumption,power-consumption,rapl,gpu,machine-learning,mlops,monitoring
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Intended Audience :: System Administrators
12
19
  Classifier: Natural Language :: English
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Operating System :: MacOS
22
+ Classifier: Operating System :: Microsoft :: Windows
13
23
  Classifier: Programming Language :: Python :: 3.10
14
24
  Classifier: Programming Language :: Python :: 3.11
15
25
  Classifier: Programming Language :: Python :: 3.12
16
26
  Classifier: Programming Language :: Python :: 3.13
17
27
  Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
29
+ Classifier: Topic :: System :: Monitoring
30
+ Classifier: Topic :: System :: Benchmark
31
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
32
  Requires-Python: >=3.10
19
33
  Description-Content-Type: text/markdown
20
34
  License-File: LICENSE
@@ -0,0 +1 @@
1
+ __version__ = "3.3.2"
@@ -86,8 +86,15 @@ def _discover_endpoints():
86
86
 
87
87
  def _save_credentials(tokens):
88
88
  """Save OAuth tokens to the local credentials file."""
89
- with open(_CREDENTIALS_FILE, "w") as f:
90
- json.dump(tokens, f)
89
+ try:
90
+ with open(_CREDENTIALS_FILE, "w") as f:
91
+ json.dump(tokens, f)
92
+ except OSError as e:
93
+ raise ValueError(
94
+ f"Could not write the credentials file {_CREDENTIALS_FILE.resolve()} "
95
+ f"(error: {e}). Please run the command from a directory you can "
96
+ "write to."
97
+ )
91
98
 
92
99
 
93
100
  def _load_credentials():
@@ -169,6 +176,7 @@ def authorize():
169
176
  server = HTTPServer(("localhost", _REDIRECT_PORT), _CallbackHandler)
170
177
 
171
178
  print("Opening browser for authentication...")
179
+ print(f"If no browser opens, copy this URL into your browser:\n{uri}")
172
180
  webbrowser.open(uri)
173
181
 
174
182
  server.handle_request()
@@ -358,6 +358,34 @@ def config():
358
358
  )
359
359
 
360
360
 
361
+ def _cli_provided(ctx, name: str) -> bool:
362
+ """
363
+ Whether the option `name` was typed on the command line (or set through its
364
+ environment variable) rather than left at its Typer default.
365
+
366
+ Options left at their default must not be forwarded to the tracker: doing so
367
+ would silently override the values coming from `.codecarbon.config` and the
368
+ `CODECARBON_*` environment variables, which the tracker reads itself.
369
+ """
370
+ get_source = getattr(ctx, "get_parameter_source", None)
371
+ if get_source is None:
372
+ # `monitor` called directly from Python, not through Click: every value
373
+ # given is explicit.
374
+ return True
375
+ source = get_source(name)
376
+ return source is None or source.name in ("COMMANDLINE", "ENVIRONMENT")
377
+
378
+
379
+ def _external_config() -> dict:
380
+ """The configuration files and CODECARBON_* variables, as a plain dict."""
381
+ from codecarbon.core.config import get_hierarchical_config
382
+
383
+ try:
384
+ return dict(get_hierarchical_config())
385
+ except Exception:
386
+ return {}
387
+
388
+
361
389
  @codecarbon.command(
362
390
  "monitor",
363
391
  short_help="Monitor your machine's carbon emissions.",
@@ -365,6 +393,10 @@ def config():
365
393
  )
366
394
  def monitor(
367
395
  ctx: typer.Context,
396
+ project_name: Annotated[
397
+ str,
398
+ typer.Option(help="Project name for the current experiment."),
399
+ ] = None,
368
400
  measure_power_secs: Annotated[
369
401
  int,
370
402
  typer.Option(help="Interval between two measures."),
@@ -393,15 +425,29 @@ def monitor(
393
425
  ):
394
426
  """Monitor your machine's carbon emissions."""
395
427
 
396
- # Shared tracker args so monitor and run_and_monitor behave the same
428
+ external_conf = _external_config()
429
+
430
+ # Shared tracker args so monitor and run_and_monitor behave the same.
431
+ # Only the options actually given are forwarded: the others are left to the
432
+ # tracker, which resolves them from the configuration file and environment.
433
+ cli_defaults = (
434
+ ("project_name", project_name),
435
+ ("measure_power_secs", measure_power_secs),
436
+ ("api_call_interval", api_call_interval),
437
+ ("log_level", log_level),
438
+ )
397
439
  tracker_args = {
398
- "measure_power_secs": measure_power_secs,
399
- "api_call_interval": api_call_interval,
400
- "log_level": log_level,
440
+ name: value for name, value in cli_defaults if _cli_provided(ctx, name)
401
441
  }
442
+ for name, value in cli_defaults:
443
+ if name not in tracker_args and name not in external_conf:
444
+ # Nothing configures it: keep the defaults advertised by `--help`
445
+ # (and an unattended monitor quiet) instead of the tracker's own.
446
+ tracker_args[name] = value
447
+
402
448
  # Set up the tracker arguments based on mode (offline vs online) and validate required args for each mode
403
449
  if offline:
404
- if not country_iso_code:
450
+ if not country_iso_code and not external_conf.get("country_iso_code"):
405
451
  print(
406
452
  "ERROR: Country ISO code is required for offline mode. Add it to your configuration or provide it via the command line: `--country-iso-code FRA`",
407
453
  file=sys.stderr,
@@ -410,8 +456,8 @@ def monitor(
410
456
 
411
457
  tracker_args = {
412
458
  **tracker_args,
413
- "country_iso_code": country_iso_code,
414
- "region": region,
459
+ **({"country_iso_code": country_iso_code} if country_iso_code else {}),
460
+ **({"region": region} if region else {}),
415
461
  }
416
462
  else:
417
463
  experiment_id = get_existing_exp_id()
@@ -3,6 +3,7 @@
3
3
  import os
4
4
  import subprocess
5
5
  import sys
6
+ from typing import Optional
6
7
 
7
8
  import typer
8
9
  from rich import print
@@ -12,9 +13,9 @@ from typing_extensions import Annotated
12
13
  def run_and_monitor(
13
14
  ctx: typer.Context,
14
15
  log_level: Annotated[
15
- str,
16
+ Optional[str],
16
17
  typer.Option(help="Log level (critical, error, warning, info, debug)"),
17
- ] = "error",
18
+ ] = None,
18
19
  offline: bool = False,
19
20
  **tracker_args,
20
21
  ):
@@ -51,7 +52,11 @@ def run_and_monitor(
51
52
  from codecarbon.emissions_tracker import EmissionsTracker, OfflineEmissionsTracker
52
53
  from codecarbon.external.logger import set_logger_level
53
54
 
54
- set_logger_level(log_level)
55
+ # `log_level` is None when nothing set it: leave it to the tracker, which
56
+ # resolves it from the configuration file and the environment.
57
+ if log_level is not None:
58
+ set_logger_level(log_level)
59
+ tracker_args["log_level"] = log_level
55
60
 
56
61
  # Get the command from remaining args (strip nested subcommand / `--` leftovers)
57
62
  command = list(getattr(ctx, "args", None) or [])
@@ -67,7 +72,6 @@ def run_and_monitor(
67
72
 
68
73
  tracker_cls = OfflineEmissionsTracker if offline else EmissionsTracker
69
74
  tracker = tracker_cls(
70
- log_level=log_level,
71
75
  save_to_logger=False,
72
76
  tracking_mode="process",
73
77
  **tracker_args,
@@ -116,13 +120,12 @@ def run_and_monitor(
116
120
  else:
117
121
  print(" Emissions: N/A")
118
122
 
119
- # Show where the data was saved
120
- if hasattr(tracker, "_conf") and "output_file" in tracker._conf:
121
- output_path = tracker._conf["output_file"]
122
- # Make it absolute if it's relative
123
- if not os.path.isabs(output_path):
124
- output_path = os.path.abspath(output_path)
125
- print(f" Saved to: {output_path}")
123
+ # Show where the data was saved, asking the output handlers themselves
124
+ # so the path stays right whatever `output_dir` / `output_file` are.
125
+ for handler in getattr(tracker, "_output_handlers", []):
126
+ save_file_path = getattr(handler, "save_file_path", None)
127
+ if save_file_path:
128
+ print(f" Saved to: {os.path.abspath(save_file_path)}")
126
129
 
127
130
  print(" ⚠️ Note: Tracked the command process and its children")
128
131
  print("=" * 60)
@@ -22,6 +22,11 @@ from codecarbon.core.schemas import (
22
22
  from codecarbon.external.logger import logger
23
23
 
24
24
 
25
+ def _round_or_none(value: float | None) -> float | None:
26
+ """Round a coordinate, keeping None when it is unknown."""
27
+ return None if value is None else round(value, 1)
28
+
29
+
25
30
  def get_datetime_with_timezone():
26
31
  import arrow
27
32
 
@@ -242,8 +247,8 @@ class ApiClient: # (AsyncClient)
242
247
  gpu_count=self.conf.get("gpu_count"),
243
248
  gpu_model=self.conf.get("gpu_model"),
244
249
  # Reduce precision for Privacy
245
- longitude=round(self.conf.get("longitude", 0), 1),
246
- latitude=round(self.conf.get("latitude", 0), 1),
250
+ longitude=_round_or_none(self.conf.get("longitude")),
251
+ latitude=_round_or_none(self.conf.get("latitude")),
247
252
  region=self.conf.get("region"),
248
253
  provider=self.conf.get("provider"),
249
254
  ram_total_size=self.conf.get("ram_total_size"),
@@ -1,10 +1,13 @@
1
1
  import configparser
2
2
  import os
3
3
  from pathlib import Path
4
- from typing import List, Optional, Union
4
+ from typing import Dict, List, Optional, Tuple, Union
5
5
 
6
6
  from codecarbon.external.logger import logger
7
7
 
8
+ CONFIG_FILE_NAME = ".codecarbon.config"
9
+ CONFIG_SECTION = "codecarbon"
10
+
8
11
 
9
12
  def clean_env_key(k: str) -> str:
10
13
  """
@@ -36,7 +39,7 @@ def parse_env_config() -> dict:
36
39
  dict: existing relevant environment variables mapped into a dict
37
40
  """
38
41
  return {
39
- "codecarbon": {
42
+ CONFIG_SECTION: {
40
43
  clean_env_key(k): v
41
44
  for k, v in os.environ.items()
42
45
  if k.lower().startswith("codecarbon_")
@@ -110,10 +113,81 @@ def normalize_gpu_ids(
110
113
  return None
111
114
 
112
115
 
116
+ def read_config_file(path: str) -> dict:
117
+ """
118
+ Read the `[codecarbon]` section of a single configuration file.
119
+
120
+ A file that does not exist, or that cannot be read, is ignored, like
121
+ configparser.ConfigParser.read does.
122
+
123
+ Args:
124
+ path (str): Path of the configuration file to read.
125
+
126
+ Returns:
127
+ dict: The keys defined in this file only, empty if there is nothing to read.
128
+ """
129
+ config = configparser.ConfigParser()
130
+ config.read(path)
131
+ if not config.has_section(CONFIG_SECTION):
132
+ return {}
133
+ return dict(config[CONFIG_SECTION])
134
+
135
+
136
+ def log_config_sources(sources: Dict[str, str]) -> None:
137
+ """
138
+ Log a single line telling, for every configuration key, which layer set it.
139
+
140
+ Only the key names and their source are logged, never the values, because
141
+ this configuration holds secrets such as `api_key` and `experiment_id`.
142
+
143
+ Args:
144
+ sources (dict): The key to source mapping built by
145
+ get_hierarchical_config_with_sources.
146
+ """
147
+ if not sources:
148
+ return
149
+ logger.info(
150
+ "Codecarbon configuration: "
151
+ + ", ".join(f"{key} from {source}" for key, source in sorted(sources.items()))
152
+ )
153
+
154
+
155
+ def get_hierarchical_config_with_sources() -> Tuple[dict, Dict[str, str]]:
156
+ """
157
+ Same as get_hierarchical_config, but also tells where each key comes from.
158
+
159
+ The layers are read from the lowest to the highest priority: the global
160
+ config file, then the local config file, then the environment variables.
161
+ The last layer defining a key wins, and is the one reported as its source.
162
+ A source is either the path of the config file that set the key, or the
163
+ name of the environment variable, e.g. `CODECARBON_PROJECT_NAME`.
164
+
165
+ Returns:
166
+ tuple: The (config, sources) dicts, which share the same keys. `config`
167
+ maps a key to its value (**all values are strings**) and `sources` maps
168
+ a key to the layer it was read from.
169
+ """
170
+ global_path = str((Path.home() / CONFIG_FILE_NAME).expanduser().resolve())
171
+ local_path = str((Path.cwd() / CONFIG_FILE_NAME).expanduser().resolve())
172
+
173
+ config = {}
174
+ sources = {}
175
+
176
+ for path in (global_path, local_path):
177
+ for key, value in read_config_file(path).items():
178
+ config[key] = value
179
+ sources[key] = path
180
+
181
+ for key, value in parse_env_config()[CONFIG_SECTION].items():
182
+ config[key] = value
183
+ sources[key] = f"CODECARBON_{key.upper()}"
184
+
185
+ return config, sources
186
+
187
+
113
188
  def get_hierarchical_config():
114
189
  """
115
- Get the user-defined codecarbon configuration ConfigParser dictionnary
116
- (actually a configparser.SectionProxy instance).
190
+ Get the user-defined codecarbon configuration as a dict.
117
191
 
118
192
  ```
119
193
  >>> from codecarbon.core.config import get_hierarchical_config
@@ -121,10 +195,7 @@ def get_hierarchical_config():
121
195
  >>> print(conf)
122
196
  ```
123
197
 
124
- `conf` works like a regular dict + methods getint(key) getfloat(key)
125
- and getboolean(key) to automatically parse strings into those types.
126
-
127
- All values (outputs of get(key)) are strings.
198
+ All values are strings.
128
199
 
129
200
  It looks for, and reads, a config file .codecarbon.config in the user's $HOME.
130
201
  It then looks for, reads, and updates the previous configuration from a config
@@ -133,29 +204,13 @@ def get_hierarchical_config():
133
204
  with `CODECARBON_` (for instance if `CODECARBON_PROJECT_NAME` is `your-project`
134
205
  then the resulting configuration key `project_name` will have value `your-project`)
135
206
 
207
+ The source of every resolved key is logged, so that a surprising value can be
208
+ traced back to the file or the environment variable that set it.
209
+
136
210
  Returns:
137
211
  dict: The final configuration dict parsed from global,
138
212
  local and environment configurations. **All values are strings**.
139
213
  """
140
-
141
- config = configparser.ConfigParser()
142
-
143
- cwd = Path.cwd()
144
- home = Path.home()
145
- global_path = str((home / ".codecarbon.config").expanduser().resolve())
146
- local_path = str((cwd / ".codecarbon.config").expanduser().resolve())
147
- if Path(global_path).exists():
148
- logger.info(
149
- f"Codecarbon is taking the configuration from global file: {global_path}"
150
- )
151
- if Path(local_path).exists():
152
- logger.info(f"Some variables are overriden by the local file: {local_path}")
153
- elif Path(local_path).exists():
154
- logger.info(
155
- f"Codecarbon is taking the configuration from the local file {local_path}"
156
- )
157
-
158
- config.read([global_path, local_path])
159
- config.read_dict(parse_env_config())
160
-
161
- return dict(config["codecarbon"])
214
+ config, sources = get_hierarchical_config_with_sources()
215
+ log_config_sources(sources)
216
+ return config
@@ -17,7 +17,12 @@ from typing import TYPE_CHECKING, Dict, Optional, Tuple
17
17
  import psutil
18
18
  from rapidfuzz import fuzz, process, utils
19
19
 
20
- from codecarbon.core.rapl import RAPLFile
20
+ from codecarbon.core.rapl import (
21
+ SEQUENTIAL_READ_TOLERANCE_KWH,
22
+ RAPLFile,
23
+ counters_match,
24
+ find_mirrored_counters,
25
+ )
21
26
  from codecarbon.core.units import Time
22
27
  from codecarbon.core.util import count_cpus, detect_cpu_model
23
28
  from codecarbon.external.logger import logger
@@ -28,6 +33,13 @@ if TYPE_CHECKING:
28
33
  # default W value per core for a CPU if no model is found in the ref csv
29
34
  DEFAULT_POWER_PER_CORE = 4
30
35
 
36
+ _TROUBLESHOOTING_URL = "https://docs.codecarbon.io/latest/how-to/troubleshooting/"
37
+ RAPL_PERMISSION_HELP = (
38
+ "You can grant read permission with: sudo chmod -R a+r /sys/class/powercap/* "
39
+ "(this does not persist across reboots, see "
40
+ f"{_TROUBLESHOOTING_URL}#rapl-permission-denied for a permanent setup)"
41
+ )
42
+
31
43
 
32
44
  @lru_cache(maxsize=1)
33
45
  def is_powergadget_available() -> bool:
@@ -175,18 +187,16 @@ def _create_warn_function():
175
187
  nonlocal already_warned
176
188
  if not already_warned:
177
189
  logger.warning(
178
- "\tRAPL - Permission denied reading RAPL file %s. "
179
- "You can grant read permission with: "
180
- "sudo chmod -R a+r /sys/class/powercap/*",
190
+ "\tRAPL - Permission denied reading RAPL file %s. %s",
181
191
  energy_path,
192
+ RAPL_PERMISSION_HELP,
182
193
  )
183
194
  already_warned = True
184
195
  else:
185
196
  logger.debug(
186
- "\tRAPL - Permission denied reading RAPL file %s. "
187
- "You can grant read permission with: "
188
- "sudo chmod -R a+r /sys/class/powercap/*",
197
+ "\tRAPL - Permission denied reading RAPL file %s. %s",
189
198
  energy_path,
199
+ RAPL_PERMISSION_HELP,
190
200
  )
191
201
 
192
202
  return warn_permission_denied
@@ -248,6 +258,24 @@ def is_psutil_available():
248
258
  return False
249
259
 
250
260
 
261
+ def is_raspberry() -> bool:
262
+ """
263
+ Check if the Raspberry Pi PMIC power readings are available.
264
+
265
+ `vcgencmd` ships with every Raspberry Pi OS, but only the Pi 5 answers
266
+ `pmic_read_adc`: older boards fall back to the TDP of their CPU.
267
+ """
268
+ if not os.path.exists("/usr/bin/vcgencmd"):
269
+ return False
270
+ try:
271
+ res = subprocess.run(
272
+ ["vcgencmd", "pmic_read_adc"], capture_output=True, timeout=5
273
+ )
274
+ except (OSError, subprocess.SubprocessError):
275
+ return False
276
+ return res.returncode == 0 and b"volt(" in res.stdout
277
+
278
+
251
279
  class IntelPowerGadget:
252
280
  """
253
281
  A class to interface with Intel Power Gadget for monitoring CPU power consumption on Windows and (non-Apple Silicon) macOS.
@@ -450,6 +478,10 @@ class IntelRAPL:
450
478
  self._lin_rapl_dir = rapl_dir
451
479
  self._system = sys.platform.lower()
452
480
  self._rapl_files = []
481
+ # Files that look like a duplicate of another counter, but whose
482
+ # energy deltas have not confirmed it yet. They are left out of the
483
+ # measurement while pending. Maps file path -> mirrored file path.
484
+ self._mirrored_candidates: Dict[str, str] = {}
453
485
  self.rapl_include_dram = rapl_include_dram
454
486
  self.rapl_prefer_psys = rapl_prefer_psys
455
487
  self._setup_rapl()
@@ -480,7 +512,7 @@ class IntelRAPL:
480
512
  """Scan subdirectories of a RAPL entry for domain directories."""
481
513
  subdirs = []
482
514
  try:
483
- for sub in os.listdir(entry_path):
515
+ for sub in sorted(os.listdir(entry_path)):
484
516
  sub_path = os.path.join(entry_path, sub)
485
517
  if ":" in sub and os.path.isdir(sub_path):
486
518
  if os.path.exists(os.path.join(sub_path, "energy_uj")):
@@ -499,7 +531,7 @@ class IntelRAPL:
499
531
  """Scan a base directory for intel-rapl entries."""
500
532
  domain_dirs = []
501
533
  try:
502
- for entry in os.listdir(base):
534
+ for entry in sorted(os.listdir(base)):
503
535
  if not entry.startswith("intel-rapl"):
504
536
  continue
505
537
  entry_path = os.path.join(base, entry)
@@ -528,7 +560,7 @@ class IntelRAPL:
528
560
  if domain_dirs:
529
561
  return domain_dirs
530
562
  try:
531
- for item in os.listdir(self._lin_rapl_dir):
563
+ for item in sorted(os.listdir(self._lin_rapl_dir)):
532
564
  if ":" in item:
533
565
  path = os.path.join(self._lin_rapl_dir, item)
534
566
  if os.path.isdir(path) and os.path.exists(
@@ -583,8 +615,7 @@ class IntelRAPL:
583
615
  return True, is_required_main
584
616
  except PermissionError:
585
617
  msg = f"\tRAPL - Permission denied reading RAPL file {rapl_file}."
586
- suggestion = "You can grant read permission with: sudo chmod -R a+r /sys/class/powercap/*"
587
- logger.warning("%s %s; skipping.", msg, suggestion)
618
+ logger.warning("%s %s; skipping.", msg, RAPL_PERMISSION_HELP)
588
619
  return False, False
589
620
  except Exception as e:
590
621
  logger.debug(
@@ -842,7 +873,12 @@ class IntelRAPL:
842
873
  for rapl_file in self._rapl_files:
843
874
  rapl_file.delta(duration)
844
875
 
876
+ self._confirm_mirrored_files()
877
+
845
878
  for rapl_file in self._rapl_files:
879
+ if rapl_file.path in self._mirrored_candidates:
880
+ # Still suspected of duplicating another counter
881
+ continue
846
882
  logger.debug(rapl_file)
847
883
  cpu_details[rapl_file.name] = rapl_file.energy_delta.kWh
848
884
  # We fake the name used by Power Gadget when using RAPL
@@ -874,6 +910,75 @@ class IntelRAPL:
874
910
  """
875
911
  for rapl_file in self._rapl_files:
876
912
  rapl_file.start()
913
+ # DRAM domains are a different measurement, never a duplicate
914
+ counters = [
915
+ (rapl_file.path, float(rapl_file.last_energy))
916
+ for rapl_file in self._rapl_files
917
+ if "dram" not in rapl_file.name.lower()
918
+ and rapl_file.last_energy is not None
919
+ ]
920
+ self._mirrored_candidates = find_mirrored_counters(
921
+ counters, SEQUENTIAL_READ_TOLERANCE_KWH
922
+ )
923
+ for path, reference_path in self._mirrored_candidates.items():
924
+ logger.warning(
925
+ "\tRAPL - Energy counter at %s looks like a mirror of %s, it is "
926
+ "left out of the measurement to avoid double-counting",
927
+ path,
928
+ reference_path,
929
+ )
930
+
931
+ def _confirm_mirrored_files(self) -> None:
932
+ """
933
+ Decide the fate of the files flagged as duplicates at ``start()``.
934
+
935
+ Two independent meters could hold indistinguishable counters at the
936
+ single instant of the initial reading, so a candidate is only dropped
937
+ for good once it has also accumulated the very same energy over a
938
+ measurement interval. A candidate whose energy delta differs is a real
939
+ meter and is restored, and a candidate that has not accumulated
940
+ anything yet stays pending until an interval is conclusive.
941
+ """
942
+ if not self._mirrored_candidates:
943
+ return
944
+ files_by_path = {rapl_file.path: rapl_file for rapl_file in self._rapl_files}
945
+ pending = {}
946
+ confirmed = set()
947
+ for path, reference_path in self._mirrored_candidates.items():
948
+ rapl_file = files_by_path.get(path)
949
+ reference = files_by_path.get(reference_path)
950
+ if rapl_file is None or reference is None:
951
+ continue
952
+ delta = float(rapl_file.energy_delta)
953
+ reference_delta = float(reference.energy_delta)
954
+ if rapl_file.sample_skipped or reference.sample_skipped:
955
+ # A skipped sample reports no energy: the interval is not conclusive
956
+ pending[path] = reference_path
957
+ elif delta <= 0 and reference_delta <= 0:
958
+ # Nothing was accumulated: the interval is not conclusive
959
+ pending[path] = reference_path
960
+ elif counters_match(delta, reference_delta, SEQUENTIAL_READ_TOLERANCE_KWH):
961
+ confirmed.add(path)
962
+ logger.warning(
963
+ "\tRAPL - Energy counter at %s mirrors the one at %s, it is "
964
+ "dropped to avoid double-counting the CPU power (multi-die "
965
+ "CPUs mirror the package counter on each die)",
966
+ path,
967
+ reference_path,
968
+ )
969
+ else:
970
+ logger.info(
971
+ "\tRAPL - Energy counter at %s accumulates energy on its own, "
972
+ "it is an independent meter and is measured again",
973
+ path,
974
+ )
975
+ self._mirrored_candidates = pending
976
+ if confirmed:
977
+ self._rapl_files = [
978
+ rapl_file
979
+ for rapl_file in self._rapl_files
980
+ if rapl_file.path not in confirmed
981
+ ]
877
982
 
878
983
 
879
984
  class TDP:
@@ -1009,9 +1114,10 @@ class TDP:
1009
1114
  )
1010
1115
  return cpu_model_detected, power
1011
1116
  logger.warning(
1012
- "We saw that you have a %s but we don't know it."
1013
- + " Please contact us.",
1117
+ "We saw that you have a %s but we don't know it. "
1118
+ "Please help us add it, see %s#unknown-cpu-model",
1014
1119
  cpu_model_detected,
1120
+ _TROUBLESHOOTING_URL,
1015
1121
  )
1016
1122
  if is_psutil_available():
1017
1123
  # Count thread of the CPU