diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d8dc086..15e7007 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,9 @@ jobs: - name: Check Python run: | make init - make style + pip install pre-commit + pre-commit install + make format - name: Check Data run: make check-data diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97950d6..902b2e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,15 +11,9 @@ repos: hooks: - id: poetry-check - id: poetry-lock - - repo: https://github.com/timothycrosley/isort - rev: '5.12.0' + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.290 hooks: - - id: isort - args: [ --check-only, --diff ] - types: [ python ] - - repo: https://github.com/psf/black - rev: '24.1.1' - hooks: - - id: black - args: [ --check, --diff ] - types: [ python ] + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/Makefile b/Makefile index 44078ad..f8e4f02 100755 --- a/Makefile +++ b/Makefile @@ -8,15 +8,7 @@ init: ## Init the requirements .PHONY: format format: ## Format the code $(info --- 🐍 Check Python format ---) - @poetry run black . - @poetry run isort . - -.PHONY: style -style: ## Run style - $(info --- 🐍 Style Python ---) - @poetry run isort --diff --check-only . - @poetry run black --check . - @poetry run mypy + pre-commit run -a .PHONY: security security: ## Run security checks diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py index ec2a0c2..68d4784 100644 --- a/docs/source/_ext/edit_on_github.py +++ b/docs/source/_ext/edit_on_github.py @@ -25,7 +25,7 @@ def html_page_context(app, pagename, templatename, context, doctree): return if not app.config.edit_on_github_project: - warnings.warn("edit_on_github_project not specified") + warnings.warn("edit_on_github_project not specified", stacklevel=1) return path = os.path.relpath(doctree.get("source"), app.builder.srcdir) @@ -39,9 +39,7 @@ def html_page_context(app, pagename, templatename, context, doctree): context["display_github"] = True context["github_user"] = app.config.edit_on_github_project.split("/")[0] context["github_repo"] = app.config.edit_on_github_project.split("/")[1] - context["github_version"] = ( - f"{app.config.edit_on_github_branch}/{app.config.page_source_prefix}/" - ) + context["github_version"] = f"{app.config.edit_on_github_branch}/{app.config.page_source_prefix}/" def setup(app): diff --git a/poetry.lock b/poetry.lock index 3f8bd53..52f9188 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -164,13 +164,13 @@ files = [ [[package]] name = "anyio" -version = "4.2.0" +version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [package.dependencies] @@ -197,13 +197,13 @@ files = [ [[package]] name = "asyncer" -version = "0.0.4" +version = "0.0.5" description = "Asyncer, async and await, focused on developer experience." optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "asyncer-0.0.4-py3-none-any.whl", hash = "sha256:bc27f6bcea231a78c6fb730dff5977c74b0ee5a6f2ea080e273a03fac17ca3d1"}, - {file = "asyncer-0.0.4.tar.gz", hash = "sha256:d9400418d4b508e5f3e819809bd3e7b9cd43d99a65c0a5eada14d41583164ca6"}, + {file = "asyncer-0.0.5-py3-none-any.whl", hash = "sha256:ba06d6de3c750763868dffacf89b18d40b667605b0241d31c2ee43f188e2ab74"}, + {file = "asyncer-0.0.5.tar.gz", hash = "sha256:2979f3e04cbedfe5cfeb79027dcf7d004fcc4430a0ca0066ae20490f218ec06e"}, ] [package.dependencies] @@ -311,61 +311,15 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "black" -version = "24.2.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "cachetools" -version = "5.3.2" +version = "5.3.3" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, - {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, ] [[package]] @@ -505,63 +459,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.1" +version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, - {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, - {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, - {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, - {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, - {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, - {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, - {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, - {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, - {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, - {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, - {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, - {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, - {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, - {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, - {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, - {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, - {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, - {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, - {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, - {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, - {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, - {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, - {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, - {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, - {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, - {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, - {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, + {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, + {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, + {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, + {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, + {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, + {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, + {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, + {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, + {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, + {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, + {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, + {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, ] [package.dependencies] @@ -725,13 +679,13 @@ files = [ [[package]] name = "google-auth" -version = "2.27.0" +version = "2.28.1" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.27.0.tar.gz", hash = "sha256:e863a56ccc2d8efa83df7a80272601e43487fa9a728a376205c86c26aaefa821"}, - {file = "google_auth-2.27.0-py2.py3-none-any.whl", hash = "sha256:8e4bad367015430ff253fe49d500fdc3396c1a434db5740828c728e45bcce245"}, + {file = "google-auth-2.28.1.tar.gz", hash = "sha256:34fc3046c257cedcf1622fc4b31fc2be7923d9b4d44973d481125ecc50d83885"}, + {file = "google_auth-2.28.1-py2.py3-none-any.whl", hash = "sha256:25141e2d7a14bfcba945f5e9827f98092716e99482562f15306e5b026e21aa72"}, ] [package.dependencies] @@ -798,20 +752,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - [[package]] name = "jinja2" version = "3.1.3" @@ -1244,17 +1184,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "pbr" version = "6.0.0" @@ -1266,21 +1195,6 @@ files = [ {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, ] -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - [[package]] name = "pluggy" version = "1.4.0" @@ -1470,13 +1384,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] @@ -1484,7 +1398,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" +pluggy = ">=1.3.0,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] @@ -1580,13 +1494,13 @@ testing = ["filelock"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -1736,13 +1650,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -1767,6 +1681,32 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "ruff" +version = "0.3.0" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, + {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, + {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, + {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, + {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, +] + [[package]] name = "six" version = "1.16.0" @@ -1780,13 +1720,13 @@ files = [ [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] @@ -1937,13 +1877,13 @@ test = ["pytest"] [[package]] name = "stevedore" -version = "5.1.0" +version = "5.2.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ - {file = "stevedore-5.1.0-py3-none-any.whl", hash = "sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d"}, - {file = "stevedore-5.1.0.tar.gz", hash = "sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c"}, + {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, + {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, ] [package.dependencies] @@ -2030,13 +1970,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -2115,13 +2055,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.0" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, - {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] @@ -2286,4 +2226,4 @@ prometheus = ["prometheus-client"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "2f81e2aba1686c1d97cc3c8cbf6003dae0d89a12f34758716340e0166536b63d" +content-hash = "b97f97dc0cecd6e3bb744ecc30826bd3cd30c1e10fbd7367157dffc51fc0ddc6" diff --git a/pyproject.toml b/pyproject.toml index 384b87c..3af3448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,26 +16,25 @@ include = ["tracarbon/py.typed"] [tool.poetry.dependencies] python = "^3.8" loguru = ">=0.6,<0.8" -aiohttp = "^3.8.4" +aiohttp = "^3.9.3" aiocache = "^0.12.1" -aiofiles = "^23.1.0" -psutil = "^5.9.4" -ujson = "^5.8.0" +aiofiles = "^23.2.0" +psutil = "^5.9.8" +ujson = "^5.9.0" msgpack = "^1.0.4" pydantic = ">=1.10.7,<3.0.0" typer = ">=0.7,<0.10" ec2-metadata = "^2.13.0" python-dotenv = ">=0.21,<1.1" -asyncer = ">=0.0.2,<0.0.5" +asyncer = "^0.0.5" datadog = {version = ">=0.44,<0.49", optional = true} prometheus-client = {version = ">=0.16,<0.21", optional = true} kubernetes = {version = ">=26.1,<30.0", optional = true} [tool.poetry.dev-dependencies] mypy = "^1.8" -black = "^24.1.1" -isort = "^5.13.2" -pytest = "^7.4.4" +ruff = "^0.3.0" +pytest = "^8.0.2" pytest-mock = "^3.12.0" pytest-asyncio = "^0.23.5" pytest-cov = "^4.1.0" @@ -86,16 +85,20 @@ strict_equality = true strict_optional = false [tool.bandit] -skips = ["B404", "B607", "B602"] +skips = ["B404", "B607", "B602", "B603"] exclude_dirs = ["tests", "scripts"] -[tool.isort] -profile = "black" -src_paths = ["tracarbon", "tests"] +[tool.ruff] +fix = true +select = ["I", "S", "B"] +line-length = 120 +ignore = ["B023", "S603"] -[tool.black] -include = '\.pyi?$' -exclude = "venv" +[tool.ruff.per-file-ignores] # Don’t apply ruff rules to our tests +"**/tests/*" = ["S"] + +[tool.ruff.isort] +force-single-line = true [tool.pytest.ini_options] addopts = "--cov=tracarbon -v --asyncio-mode=auto" diff --git a/scripts/check_data.py b/scripts/check_data.py index 8c997a6..c57d9a9 100755 --- a/scripts/check_data.py +++ b/scripts/check_data.py @@ -1,11 +1,20 @@ import urllib.request +from urllib.parse import urlparse + + +def is_valid_url(url: str) -> bool: + parsed_url = urlparse(url) + return parsed_url.scheme in ["http", "https"] def check_content_length(url: str, expected_content_length: str) -> bool: - site = urllib.request.urlopen(url) - assert ( - site.getheader("Content-Length") == expected_content_length - ), f"This url content changed {url}" + if not is_valid_url(url): + raise ValueError(f"Invalid or unsafe URL scheme for URL: {url}") + + site = urllib.request.urlopen(url) # noqa: S310 + if site.getheader("Content-Length") != expected_content_length: + raise ValueError(f"This url content changed {url}") + return True if __name__ == "__main__": @@ -24,6 +33,4 @@ def check_content_length(url: str, expected_content_length: str) -> bool: }, ] for url in urls: - check_content_length( - url=url["url"], expected_content_length=url["content_length"] - ) + check_content_length(url=url["url"], expected_content_length=url["content_length"]) diff --git a/tests/carbon_emissions/test_carbon_emissions.py b/tests/carbon_emissions/test_carbon_emissions.py index 7ea91a7..ec13eb4 100644 --- a/tests/carbon_emissions/test_carbon_emissions.py +++ b/tests/carbon_emissions/test_carbon_emissions.py @@ -1,14 +1,12 @@ import pytest -from tracarbon import ( - CarbonEmission, - CarbonUsage, - CarbonUsageUnit, - EnergyUsage, - LinuxEnergyConsumption, - MacEnergyConsumption, - UsageType, -) +from tracarbon import CarbonEmission +from tracarbon import CarbonUsage +from tracarbon import CarbonUsageUnit +from tracarbon import EnergyUsage +from tracarbon import LinuxEnergyConsumption +from tracarbon import MacEnergyConsumption +from tracarbon import UsageType from tracarbon.locations import Country @@ -32,9 +30,7 @@ async def test_carbon_emission_should_run_to_convert_watt_hours_to_co2g_on_mac(m ) name_alpha_iso_2 = "fr" mocker.patch.object(Country, "get_latest_co2g_kwh", return_value=co2g_per_kwh) - mocker.patch.object( - MacEnergyConsumption, "get_energy_usage", return_value=energy_usage - ) + mocker.patch.object(MacEnergyConsumption, "get_energy_usage", return_value=energy_usage) carbon_emission = CarbonEmission( location=Country(name=name_alpha_iso_2, co2g_kwh=co2g_per_kwh), ) @@ -56,9 +52,7 @@ async def test_carbon_emission_should_run_to_convert_watt_hours_to_co2g_on_linux name_alpha_iso_2 = "fr" energy_usage = EnergyUsage(host_energy_usage=60.0) mocker.patch.object(Country, "get_latest_co2g_kwh", return_value=co2g_per_kwh) - mocker.patch.object( - LinuxEnergyConsumption, "get_energy_usage", return_value=energy_usage - ) + mocker.patch.object(LinuxEnergyConsumption, "get_energy_usage", return_value=energy_usage) carbon_emission = CarbonEmission( location=Country(name=name_alpha_iso_2, co2g_kwh=co2g_per_kwh), ) @@ -83,26 +77,14 @@ def test_carbon_usage_with_type_and_conversion(): assert carbon_usage.get_carbon_usage_on_type(UsageType.HOST) == host_carbon_usage assert carbon_usage.get_carbon_usage_on_type(UsageType.CPU) == cpu_carbon_usage - assert ( - carbon_usage.get_carbon_usage_on_type(UsageType.MEMORY) == memory_carbon_usage - ) + assert carbon_usage.get_carbon_usage_on_type(UsageType.MEMORY) == memory_carbon_usage assert carbon_usage.get_carbon_usage_on_type(UsageType.GPU) == gpu_carbon_usage assert carbon_usage.unit == CarbonUsageUnit.CO2_G carbon_usage.convert_unit(CarbonUsageUnit.CO2_MG) - assert ( - carbon_usage.get_carbon_usage_on_type(UsageType.HOST) - == host_carbon_usage * 1000 - ) - assert ( - carbon_usage.get_carbon_usage_on_type(UsageType.CPU) == cpu_carbon_usage * 1000 - ) - assert ( - carbon_usage.get_carbon_usage_on_type(UsageType.MEMORY) - == memory_carbon_usage * 1000 - ) - assert ( - carbon_usage.get_carbon_usage_on_type(UsageType.GPU) == gpu_carbon_usage * 1000 - ) + assert carbon_usage.get_carbon_usage_on_type(UsageType.HOST) == host_carbon_usage * 1000 + assert carbon_usage.get_carbon_usage_on_type(UsageType.CPU) == cpu_carbon_usage * 1000 + assert carbon_usage.get_carbon_usage_on_type(UsageType.MEMORY) == memory_carbon_usage * 1000 + assert carbon_usage.get_carbon_usage_on_type(UsageType.GPU) == gpu_carbon_usage * 1000 assert carbon_usage.unit == CarbonUsageUnit.CO2_MG diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index ec6e414..5671016 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,10 +1,16 @@ import pytest from kubernetes import config -from tracarbon import Country, EnergyUsage, Kubernetes, MacEnergyConsumption -from tracarbon.cli import get_exporter, run_metrics -from tracarbon.exporters import DatadogExporter, StdoutExporter -from tracarbon.hardwares import Container, Pod +from tracarbon import Country +from tracarbon import EnergyUsage +from tracarbon import Kubernetes +from tracarbon import MacEnergyConsumption +from tracarbon.cli import get_exporter +from tracarbon.cli import run_metrics +from tracarbon.exporters import DatadogExporter +from tracarbon.exporters import StdoutExporter +from tracarbon.hardwares import Container +from tracarbon.hardwares import Pod def test_get_exporter_by_name(): @@ -32,9 +38,7 @@ def test_run_metrics_should_be_ok(mocker, caplog): ) energy_usage = EnergyUsage(host_energy_usage=60.0) mocker.patch.object(config, "load_kube_config", return_value=None) - mocker.patch.object( - MacEnergyConsumption, "get_energy_usage", return_value=energy_usage - ) + mocker.patch.object(MacEnergyConsumption, "get_energy_usage", return_value=energy_usage) run_metrics(exporter_name=exporter, running=False) @@ -44,9 +48,7 @@ def test_run_metrics_should_be_ok(mocker, caplog): assert "units:watts" in caplog.text energy_usage = EnergyUsage(cpu_energy_usage=15.0, memory_energy_usage=12.0) - mocker.patch.object( - MacEnergyConsumption, "get_energy_usage", return_value=energy_usage - ) + mocker.patch.object(MacEnergyConsumption, "get_energy_usage", return_value=energy_usage) mocker.patch.object( Kubernetes, "get_pods_usage", @@ -54,9 +56,7 @@ def test_run_metrics_should_be_ok(mocker, caplog): Pod( name="pod_name", namespace="default", - containers=[ - Container(name="container_name", cpu_usage="1", memory_usage=2) - ], + containers=[Container(name="container_name", cpu_usage="1", memory_usage=2)], ) ], ) diff --git a/tests/exporters/test_exporter.py b/tests/exporters/test_exporter.py index 11e7d28..7fb70de 100644 --- a/tests/exporters/test_exporter.py +++ b/tests/exporters/test_exporter.py @@ -3,8 +3,11 @@ import psutil import pytest -from tracarbon import Country, MetricGenerator -from tracarbon.exporters import Metric, StdoutExporter, Tag +from tracarbon import Country +from tracarbon import MetricGenerator +from tracarbon.exporters import Metric +from tracarbon.exporters import StdoutExporter +from tracarbon.exporters import Tag def test_exporters_should_run_and_print_the_metrics(mocker, caplog): @@ -32,10 +35,7 @@ async def get_memory_usage() -> float: assert memory_metric.name in caplog.text assert str(memory_metric.value) in caplog.text assert str(memory_metric.tags) in caplog.text - assert ( - exporter.metric_report["test_metric_1"].exporter_name - == StdoutExporter.get_name() - ) + assert exporter.metric_report["test_metric_1"].exporter_name == StdoutExporter.get_name() assert exporter.metric_report["test_metric_1"].metric == memory_metric assert exporter.metric_report["test_metric_1"].total > 0 assert exporter.metric_report["test_metric_1"].average > 0 diff --git a/tests/exporters/test_json_exporter.py b/tests/exporters/test_json_exporter.py index 1181d2a..1f25ec2 100644 --- a/tests/exporters/test_json_exporter.py +++ b/tests/exporters/test_json_exporter.py @@ -4,8 +4,11 @@ import psutil import ujson -from tracarbon import Country, MetricGenerator -from tracarbon.exporters import JSONExporter, Metric, Tag +from tracarbon import Country +from tracarbon import MetricGenerator +from tracarbon.exporters import JSONExporter +from tracarbon.exporters import Metric +from tracarbon.exporters import Tag def test_json_exporter_should_write_well_formatted_metrics_in_json_file(mocker, tmpdir): @@ -48,9 +51,7 @@ async def get_memory_usage() -> float: ) metric_generators = [MetricGenerator(metrics=[memory_metric])] - exporter = JSONExporter( - quit=True, metric_generators=metric_generators, path=str(test_json_file) - ) + exporter = JSONExporter(quit=True, metric_generators=metric_generators, path=str(test_json_file)) exporter.start(interval_in_seconds=interval_in_seconds) exporter.stop() @@ -61,9 +62,7 @@ async def get_memory_usage() -> float: with open(test_json_file, "r") as file: assert ujson.load(file) == expected - assert ( - exporter.metric_report["test_metric_1"].exporter_name == JSONExporter.get_name() - ) + assert exporter.metric_report["test_metric_1"].exporter_name == JSONExporter.get_name() assert exporter.metric_report["test_metric_1"].metric == memory_metric assert exporter.metric_report["test_metric_1"].total > 0 assert exporter.metric_report["test_metric_1"].average > 0 diff --git a/tests/exporters/test_prometheus_exporter.py b/tests/exporters/test_prometheus_exporter.py index b790431..f710b28 100644 --- a/tests/exporters/test_prometheus_exporter.py +++ b/tests/exporters/test_prometheus_exporter.py @@ -2,8 +2,11 @@ import psutil -from tracarbon import Country, MetricGenerator -from tracarbon.exporters import Metric, PrometheusExporter, Tag +from tracarbon import Country +from tracarbon import MetricGenerator +from tracarbon.exporters import Metric +from tracarbon.exporters import PrometheusExporter +from tracarbon.exporters import Tag def test_prometheus_exporter(mocker): @@ -27,19 +30,12 @@ async def get_memory_usage() -> float: tags=[Tag(key="test", value="tags")], ) metric_generators = [MetricGenerator(metrics=[memory_metric])] - exporter = PrometheusExporter( - quit=True, metric_generators=metric_generators, metric_prefix_name="tracarbon" - ) + exporter = PrometheusExporter(quit=True, metric_generators=metric_generators, metric_prefix_name="tracarbon") exporter.start(interval_in_seconds=interval_in_seconds) exporter.stop() - assert ( - str(exporter.prometheus_metrics["tracarbon_test_metric_1"]) == expected_metric_1 - ) - assert ( - exporter.metric_report["test_metric_1"].exporter_name - == PrometheusExporter.get_name() - ) + assert str(exporter.prometheus_metrics["tracarbon_test_metric_1"]) == expected_metric_1 + assert exporter.metric_report["test_metric_1"].exporter_name == PrometheusExporter.get_name() assert exporter.metric_report["test_metric_1"].metric == memory_metric assert exporter.metric_report["test_metric_1"].total > 0 assert exporter.metric_report["test_metric_1"].average > 0 diff --git a/tests/hardwares/test_containers.py b/tests/hardwares/test_containers.py index 6f8f913..f4aa972 100644 --- a/tests/hardwares/test_containers.py +++ b/tests/hardwares/test_containers.py @@ -1,14 +1,14 @@ from kubernetes import config -from kubernetes.client import ( - CoreV1Api, - CustomObjectsApi, - V1Namespace, - V1NamespaceList, - V1ObjectMeta, -) +from kubernetes.client import CoreV1Api +from kubernetes.client import CustomObjectsApi +from kubernetes.client import V1Namespace +from kubernetes.client import V1NamespaceList +from kubernetes.client import V1ObjectMeta from tracarbon import HardwareInfo -from tracarbon.hardwares.containers import Container, Kubernetes, Pod +from tracarbon.hardwares.containers import Container +from tracarbon.hardwares.containers import Kubernetes +from tracarbon.hardwares.containers import Pod def test_get_pods_usage(mocker): @@ -50,9 +50,7 @@ def test_get_pods_usage(mocker): }, "timestamp": "2023-01-09T08:01:31Z", "window": "18s", - "containers": [ - {"name": "shorty", "usage": {"cpu": "380444n", "memory": "3304Ki"}} - ], + "containers": [{"name": "shorty", "usage": {"cpu": "380444n", "memory": "3304Ki"}}], }, { "metadata": { @@ -73,20 +71,14 @@ def test_get_pods_usage(mocker): ], } number_of_cores = 2 - mocker.patch.object( - HardwareInfo, "get_number_of_cores", return_value=number_of_cores - ) + mocker.patch.object(HardwareInfo, "get_number_of_cores", return_value=number_of_cores) memory_total = 1000000000 mocker.patch.object(HardwareInfo, "get_memory_total", return_value=memory_total) - mocker.patch.object( - CustomObjectsApi, "list_namespaced_custom_object", return_value=return_value - ) + mocker.patch.object(CustomObjectsApi, "list_namespaced_custom_object", return_value=return_value) mocker.patch.object( CoreV1Api, "list_namespace", - return_value=V1NamespaceList( - items=[V1Namespace(metadata=V1ObjectMeta(name="default"))] - ), + return_value=V1NamespaceList(items=[V1Namespace(metadata=V1ObjectMeta(name="default"))]), ) mocker.patch.object(config, "load_kube_config", return_value=None) pods_usage_expected = [ @@ -98,16 +90,12 @@ def test_get_pods_usage(mocker): Pod( name="shorty-5469f85799-n4k2x", namespace="default", - containers=[ - Container(name="shorty", cpu_usage=0.000190222, memory_usage=0.003304) - ], + containers=[Container(name="shorty", cpu_usage=0.000190222, memory_usage=0.003304)], ), Pod( name="subnet-router", namespace="default", - containers=[ - Container(name="tailscale", cpu_usage=0.0070081, memory_usage=0.014912) - ], + containers=[Container(name="tailscale", cpu_usage=0.0070081, memory_usage=0.014912)], ), ] diff --git a/tests/hardwares/test_energy.py b/tests/hardwares/test_energy.py index d4ff258..24d9112 100644 --- a/tests/hardwares/test_energy.py +++ b/tests/hardwares/test_energy.py @@ -1,6 +1,8 @@ import datetime -from tracarbon import EnergyUsage, EnergyUsageUnit, UsageType +from tracarbon import EnergyUsage +from tracarbon import EnergyUsageUnit +from tracarbon import UsageType from tracarbon.hardwares import Power @@ -53,26 +55,14 @@ def test_energy_usage_with_type_and_conversion(): assert energy_usage.get_energy_usage_on_type(UsageType.HOST) == host_energy_usage assert energy_usage.get_energy_usage_on_type(UsageType.CPU) == cpu_energy_usage - assert ( - energy_usage.get_energy_usage_on_type(UsageType.MEMORY) == memory_energy_usage - ) + assert energy_usage.get_energy_usage_on_type(UsageType.MEMORY) == memory_energy_usage assert energy_usage.get_energy_usage_on_type(UsageType.GPU) == gpu_energy_usage assert energy_usage.unit == EnergyUsageUnit.WATT energy_usage.convert_unit(EnergyUsageUnit.MILLIWATT) - assert ( - energy_usage.get_energy_usage_on_type(UsageType.HOST) - == host_energy_usage * 1000 - ) - assert ( - energy_usage.get_energy_usage_on_type(UsageType.CPU) == cpu_energy_usage * 1000 - ) - assert ( - energy_usage.get_energy_usage_on_type(UsageType.MEMORY) - == memory_energy_usage * 1000 - ) - assert ( - energy_usage.get_energy_usage_on_type(UsageType.GPU) == gpu_energy_usage * 1000 - ) + assert energy_usage.get_energy_usage_on_type(UsageType.HOST) == host_energy_usage * 1000 + assert energy_usage.get_energy_usage_on_type(UsageType.CPU) == cpu_energy_usage * 1000 + assert energy_usage.get_energy_usage_on_type(UsageType.MEMORY) == memory_energy_usage * 1000 + assert energy_usage.get_energy_usage_on_type(UsageType.GPU) == gpu_energy_usage * 1000 assert energy_usage.unit == EnergyUsageUnit.MILLIWATT diff --git a/tests/hardwares/test_gpu.py b/tests/hardwares/test_gpu.py index 41256c1..fb7c6ac 100644 --- a/tests/hardwares/test_gpu.py +++ b/tests/hardwares/test_gpu.py @@ -1,3 +1,5 @@ +import shutil + import pytest from tracarbon.exceptions import TracarbonException @@ -7,9 +9,8 @@ def test_get_nvidia_gpu_power_usage(mocker): gpu_power_usage_returned = "226 W" gpu_usage_expected = 226 - mocker.patch.object( - NvidiaGPU, "launch_shell_command", return_value=[gpu_power_usage_returned, 0] - ) + mocker.patch.object(shutil, "which", return_value=True) + mocker.patch.object(NvidiaGPU, "launch_shell_command", return_value=[gpu_power_usage_returned, 0]) gpu_usage = NvidiaGPU.get_gpu_power_usage() @@ -19,4 +20,4 @@ def test_get_nvidia_gpu_power_usage(mocker): def test_get_nvidia_gpu_power_usage_should_throw_error(): with pytest.raises(TracarbonException) as exception: NvidiaGPU.get_gpu_power_usage() - assert exception.value.args[0] == "No Nvidia GPU detected." + assert exception.value.args[0] == "Nvidia GPU with nvidia-smi not found in PATH." diff --git a/tests/hardwares/test_hardware.py b/tests/hardwares/test_hardware.py index 803e90c..9144693 100644 --- a/tests/hardwares/test_hardware.py +++ b/tests/hardwares/test_hardware.py @@ -1,10 +1,11 @@ import platform +import shutil from collections import namedtuple import psutil import pytest -from tracarbon import RAPL, HardwareInfo +from tracarbon import HardwareInfo from tracarbon.exceptions import TracarbonException from tracarbon.hardwares.gpu import NvidiaGPU @@ -60,9 +61,7 @@ def test_get_cpu_count(mocker): def test_get_gpu_power_usage(mocker): gpu_power_usage_returned = "226 W" gpu_usage_expected = 226 - mocker.patch.object( - NvidiaGPU, "launch_shell_command", return_value=[gpu_power_usage_returned, 0] - ) + mocker.patch.object(NvidiaGPU, "launch_shell_command", return_value=[gpu_power_usage_returned, 0]) gpu_usage = HardwareInfo.get_gpu_power_usage() @@ -71,9 +70,8 @@ def test_get_gpu_power_usage(mocker): def test_get_gpu_power_usage_with_no_0(mocker): gpu_power_usage_returned = "0 W" - mocker.patch.object( - NvidiaGPU, "launch_shell_command", return_value=[gpu_power_usage_returned, -1] - ) + mocker.patch.object(shutil, "which", return_value=True) + mocker.patch.object(NvidiaGPU, "launch_shell_command", return_value=[gpu_power_usage_returned, -1]) with pytest.raises(TracarbonException) as exception: HardwareInfo.get_gpu_power_usage() @@ -83,4 +81,4 @@ def test_get_gpu_power_usage_with_no_0(mocker): def test_get_gpu_power_usage_with_no_gpu(): with pytest.raises(TracarbonException) as exception: HardwareInfo.get_gpu_power_usage() - assert exception.value.args[0] == "No Nvidia GPU detected." + assert exception.value.args[0] == "Nvidia GPU with nvidia-smi not found in PATH." diff --git a/tests/hardwares/test_rapl.py b/tests/hardwares/test_rapl.py index 22d60a2..8d85b3d 100644 --- a/tests/hardwares/test_rapl.py +++ b/tests/hardwares/test_rapl.py @@ -4,7 +4,8 @@ import pytest from tracarbon import RAPL -from tracarbon.hardwares import EnergyUsageUnit, RAPLResult +from tracarbon.hardwares import EnergyUsageUnit +from tracarbon.hardwares import RAPLResult @pytest.mark.linux @@ -22,9 +23,7 @@ async def test_get_rapl_power_usage(): path = f"{pathlib.Path(__file__).parent.resolve()}/data/intel-rapl" rapl_separator_for_windows = "T" - rapl_results = await RAPL( - path=path, rapl_separator=rapl_separator_for_windows - ).get_rapl_power_usage() + rapl_results = await RAPL(path=path, rapl_separator=rapl_separator_for_windows).get_rapl_power_usage() def by_energy_uj(rapl_result: RAPLResult) -> str: return rapl_result.energy_uj @@ -52,15 +51,9 @@ async def test_get_rapl_power_wrap_around_when_0(): two_seconds_ago = datetime.datetime.now() - datetime.timedelta(seconds=2) rapl_separator_for_windows = "T" rapl_results = dict() - rapl_results["package-0"] = RAPLResult( - name="package", energy_uj=2, max_energy_uj=70000, timestamp=two_seconds_ago - ) - rapl_results["core"] = RAPLResult( - name="core", energy_uj=1, max_energy_uj=70000, timestamp=two_seconds_ago - ) - rapl = RAPL( - path=path, rapl_separator=rapl_separator_for_windows, rapl_results=rapl_results - ) + rapl_results["package-0"] = RAPLResult(name="package", energy_uj=2, max_energy_uj=70000, timestamp=two_seconds_ago) + rapl_results["core"] = RAPLResult(name="core", energy_uj=1, max_energy_uj=70000, timestamp=two_seconds_ago) + rapl = RAPL(path=path, rapl_separator=rapl_separator_for_windows, rapl_results=rapl_results) host_energy_usage_expected = 35 cpu_energy_usage_expected = 35 @@ -82,12 +75,8 @@ async def test_get_total_uj_one_call(): rapl_results["package-0"] = RAPLResult( name="package", energy_uj=50000, max_energy_uj=70000, timestamp=one_minute_ago ) - rapl_results["core"] = RAPLResult( - name="core", energy_uj=40000, max_energy_uj=70000, timestamp=one_minute_ago - ) - rapl = RAPL( - path=path, rapl_separator=rapl_separator_for_windows, rapl_results=rapl_results - ) + rapl_results["core"] = RAPLResult(name="core", energy_uj=40000, max_energy_uj=70000, timestamp=one_minute_ago) + rapl = RAPL(path=path, rapl_separator=rapl_separator_for_windows, rapl_results=rapl_results) host_energy_usage_expected = 0.33 cpu_energy_usage_expected = 0.5 diff --git a/tests/hardwares/test_sensors.py b/tests/hardwares/test_sensors.py index 828b5f1..aa0e6b4 100644 --- a/tests/hardwares/test_sensors.py +++ b/tests/hardwares/test_sensors.py @@ -1,14 +1,14 @@ import pytest import requests -from tracarbon import ( - RAPL, - AWSEC2EnergyConsumption, - EnergyConsumption, - LinuxEnergyConsumption, - TracarbonException, -) -from tracarbon.hardwares import EnergyUsage, HardwareInfo, WindowsEnergyConsumption +from tracarbon import RAPL +from tracarbon import AWSEC2EnergyConsumption +from tracarbon import EnergyConsumption +from tracarbon import LinuxEnergyConsumption +from tracarbon import TracarbonException +from tracarbon.hardwares import EnergyUsage +from tracarbon.hardwares import HardwareInfo +from tracarbon.hardwares import WindowsEnergyConsumption from tracarbon.hardwares.cloud_providers import AWS @@ -51,14 +51,9 @@ async def test_aws_sensor_with_gpu_should_return_energy_consumption(mocker): mocker.patch.object(HardwareInfo, "get_cpu_usage", return_value=50) mocker.patch.object(HardwareInfo, "get_memory_usage", return_value=50) gpu_power_usage = 1805.4 - mocker.patch.object( - HardwareInfo, "get_gpu_power_usage", return_value=gpu_power_usage - ) + mocker.patch.object(HardwareInfo, "get_gpu_power_usage", return_value=gpu_power_usage) value_expected = ( - aws_ec2_sensor.cpu_at_50 - + aws_ec2_sensor.memory_at_50 - + aws_ec2_sensor.delta_full_machine - + gpu_power_usage + aws_ec2_sensor.cpu_at_50 + aws_ec2_sensor.memory_at_50 + aws_ec2_sensor.delta_full_machine + gpu_power_usage ) energy_usage = await aws_ec2_sensor.get_energy_usage() @@ -83,11 +78,7 @@ async def test_aws_sensor_without_gpu_should_return_energy_consumption(mocker): mocker.patch.object(HardwareInfo, "get_cpu_usage", return_value=50) mocker.patch.object(HardwareInfo, "get_memory_usage", return_value=50) - value_expected = ( - aws_ec2_sensor.cpu_at_50 - + aws_ec2_sensor.memory_at_50 - + aws_ec2_sensor.delta_full_machine - ) + value_expected = aws_ec2_sensor.cpu_at_50 + aws_ec2_sensor.memory_at_50 + aws_ec2_sensor.delta_full_machine energy_usage = await aws_ec2_sensor.get_energy_usage() diff --git a/tests/locations/test_country.py b/tests/locations/test_country.py index 93a9c94..5e3d74d 100644 --- a/tests/locations/test_country.py +++ b/tests/locations/test_country.py @@ -1,6 +1,7 @@ import pytest -from tracarbon.locations import CarbonIntensitySource, Country +from tracarbon.locations import CarbonIntensitySource +from tracarbon.locations import Country @pytest.mark.asyncio diff --git a/tests/locations/test_location.py b/tests/locations/test_location.py index 5119ccf..f334c35 100644 --- a/tests/locations/test_location.py +++ b/tests/locations/test_location.py @@ -1,8 +1,11 @@ import pytest -from tracarbon.exceptions import CloudProviderRegionIsMissing, CountryIsMissing +from tracarbon.exceptions import CloudProviderRegionIsMissing +from tracarbon.exceptions import CountryIsMissing from tracarbon.hardwares import CloudProviders -from tracarbon.locations import AWSLocation, Country, Location +from tracarbon.locations import AWSLocation +from tracarbon.locations import Country +from tracarbon.locations import Location @pytest.mark.asyncio @@ -44,9 +47,7 @@ def test_unknown_location(mocker): with pytest.raises(CountryIsMissing) as exception: Country.get_location() - assert ( - exception.value.args[0] == "The country [ze] is not in the co2 emission file." - ) + assert exception.value.args[0] == "The country [ze] is not in the co2 emission file." def test_world_emission_should_get_country(): @@ -68,10 +69,7 @@ def test_world_emission_should_raise_error_when_country_is_missing(): with pytest.raises(CountryIsMissing) as exception: Country.from_eu_file(country_code_alpha_iso_2=country_code_alpha_iso_2) - assert ( - exception.value.args[0] - == f"The country [{country_code_alpha_iso_2}] is not in the co2 emission file." - ) + assert exception.value.args[0] == f"The country [{country_code_alpha_iso_2}] is not in the co2 emission file." def test_aws_location_should_return_an_error_if_region_not_exists(): @@ -79,10 +77,7 @@ def test_aws_location_should_return_an_error_if_region_not_exists(): with pytest.raises(CloudProviderRegionIsMissing) as exception: AWSLocation(region_name=region_name) - assert ( - exception.value.args[0] - == f"The region [{region_name}] is not in the AWS grid emissions factors file." - ) + assert exception.value.args[0] == f"The region [{region_name}] is not in the AWS grid emissions factors file." def test_aws_location_should_return_ok_if_region_exists(): diff --git a/tests/test_builder.py b/tests/test_builder.py index 902c67c..e1ef16a 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,6 +1,8 @@ import pytest -from tracarbon.builder import TracarbonBuilder, TracarbonConfiguration, TracarbonReport +from tracarbon.builder import TracarbonBuilder +from tracarbon.builder import TracarbonConfiguration +from tracarbon.builder import TracarbonReport from tracarbon.exporters import StdoutExporter from tracarbon.general_metrics import CarbonEmissionGenerator from tracarbon.locations import Country @@ -12,18 +14,14 @@ def test_builder_without_configuration(mocker): mocker.patch.object(Country, "get_current_country", return_value=location) builder = TracarbonBuilder() expected_exporter = StdoutExporter( - metric_generators=[ - CarbonEmissionGenerator(location=Country(name=location, co2g_kwh=51.1)) - ] + metric_generators=[CarbonEmissionGenerator(location=Country(name=location, co2g_kwh=51.1))] ) tracarbon = builder.build() assert tracarbon.configuration == TracarbonConfiguration() assert type(tracarbon.exporter) == type(expected_exporter) - assert type(tracarbon.exporter.metric_generators[0]) == type( - expected_exporter.metric_generators[0] - ) + assert type(tracarbon.exporter.metric_generators[0]) == type(expected_exporter.metric_generators[0]) assert tracarbon.location == Country(name=location, co2g_kwh=51.1) @@ -31,18 +29,12 @@ def test_builder_without_configuration(mocker): def test_builder_with_configuration(): configuration = TracarbonConfiguration(co2signal_api_key="API_KEY") expected_location = Country(name="fr", co2g_kwh=51.1) - expected_exporter = StdoutExporter( - metric_generators=[CarbonEmissionGenerator(location=expected_location)] - ) + expected_exporter = StdoutExporter(metric_generators=[CarbonEmissionGenerator(location=expected_location)]) builder = TracarbonBuilder(configuration=configuration) - tracarbon = ( - builder.with_exporter(exporter=expected_exporter) - .with_location(location=expected_location) - .build() - ) + tracarbon = builder.with_exporter(exporter=expected_exporter).with_location(location=expected_location).build() assert tracarbon.configuration == configuration assert tracarbon.location == expected_location assert tracarbon.exporter == expected_exporter - assert tracarbon.report == TracarbonReport() + assert tracarbon.report is not None diff --git a/tests/test_general_metrics.py b/tests/test_general_metrics.py index 45394fe..b1507b0 100644 --- a/tests/test_general_metrics.py +++ b/tests/test_general_metrics.py @@ -1,23 +1,20 @@ import pytest from kubernetes import config -from tracarbon import ( - CarbonEmission, - CarbonUsage, - EnergyConsumption, - EnergyUsage, - HardwareInfo, - Kubernetes, - MacEnergyConsumption, -) +from tracarbon import CarbonEmission +from tracarbon import CarbonUsage +from tracarbon import EnergyConsumption +from tracarbon import EnergyUsage +from tracarbon import HardwareInfo +from tracarbon import Kubernetes +from tracarbon import MacEnergyConsumption from tracarbon.exporters import Tag -from tracarbon.general_metrics import ( - CarbonEmissionGenerator, - CarbonEmissionKubernetesGenerator, - EnergyConsumptionGenerator, - EnergyConsumptionKubernetesGenerator, -) -from tracarbon.hardwares import Container, Pod +from tracarbon.general_metrics import CarbonEmissionGenerator +from tracarbon.general_metrics import CarbonEmissionKubernetesGenerator +from tracarbon.general_metrics import EnergyConsumptionGenerator +from tracarbon.general_metrics import EnergyConsumptionKubernetesGenerator +from tracarbon.hardwares import Container +from tracarbon.hardwares import Pod from tracarbon.locations.country import Country @@ -34,33 +31,25 @@ async def test_carbon_emission_metric(mocker): assert carbon_emission_metric.name == "carbon_emission_host" assert carbon_emission_metric.tags[1] == Tag(key="location", value=location_name) - assert carbon_emission_metric.tags[2] == Tag( - key="source", value=location.co2g_kwh_source.value - ) + assert carbon_emission_metric.tags[2] == Tag(key="source", value=location.co2g_kwh_source.value) assert carbon_emission_metric.tags[3] == Tag(key="units", value="co2g") carbon_emission_metric = await generator.__anext__() assert carbon_emission_metric.name == "carbon_emission_cpu" assert carbon_emission_metric.tags[1] == Tag(key="location", value=location_name) - assert carbon_emission_metric.tags[2] == Tag( - key="source", value=location.co2g_kwh_source.value - ) + assert carbon_emission_metric.tags[2] == Tag(key="source", value=location.co2g_kwh_source.value) assert carbon_emission_metric.tags[3] == Tag(key="units", value="co2g") carbon_emission_metric = await generator.__anext__() assert carbon_emission_metric.name == "carbon_emission_memory" assert carbon_emission_metric.tags[1] == Tag(key="location", value=location_name) - assert carbon_emission_metric.tags[2] == Tag( - key="source", value=location.co2g_kwh_source.value - ) + assert carbon_emission_metric.tags[2] == Tag(key="source", value=location.co2g_kwh_source.value) assert carbon_emission_metric.tags[3] == Tag(key="units", value="co2g") carbon_emission_metric = await generator.__anext__() assert carbon_emission_metric.name == "carbon_emission_gpu" assert carbon_emission_metric.tags[1] == Tag(key="location", value=location_name) - assert carbon_emission_metric.tags[2] == Tag( - key="source", value=location.co2g_kwh_source.value - ) + assert carbon_emission_metric.tags[2] == Tag(key="source", value=location.co2g_kwh_source.value) assert carbon_emission_metric.tags[3] == Tag(key="units", value="co2g") @@ -68,13 +57,9 @@ async def test_carbon_emission_metric(mocker): async def test_energy_consumption_metric(mocker): location_name = "fr" energy_usage = EnergyUsage(cpu_energy_usage=12.0, memory_energy_usage=4.0) - mocker.patch.object( - EnergyConsumption, "from_platform", return_value=MacEnergyConsumption() - ) + mocker.patch.object(EnergyConsumption, "from_platform", return_value=MacEnergyConsumption()) mocker.patch.object(Country, "get_current_country", return_value=location_name) - mocker.patch.object( - MacEnergyConsumption, "get_energy_usage", return_value=energy_usage - ) + mocker.patch.object(MacEnergyConsumption, "get_energy_usage", return_value=energy_usage) location = Country(name=location_name, co2g_kwh=51.1) energy_consumption_generator = EnergyConsumptionGenerator(location=location) generator = energy_consumption_generator.generate() @@ -105,17 +90,13 @@ async def test_energy_consumption_kubernetes_generator(mocker): location_name = "fr" memory_total = 101200121856 energy_usage = EnergyUsage(cpu_energy_usage=12.0, memory_energy_usage=4.0) - mocker.patch.object( - EnergyConsumption, "from_platform", return_value=MacEnergyConsumption() - ) + mocker.patch.object(EnergyConsumption, "from_platform", return_value=MacEnergyConsumption()) mocker.patch.object(config, "load_kube_config", return_value=None) mocker.patch.object(Country, "get_current_country", return_value=location_name) mocker.patch.object(HardwareInfo, "get_memory_total", return_value=memory_total) cores_number = 2 mocker.patch.object(HardwareInfo, "get_number_of_cores", return_value=cores_number) - mocker.patch.object( - MacEnergyConsumption, "get_energy_usage", return_value=energy_usage - ) + mocker.patch.object(MacEnergyConsumption, "get_energy_usage", return_value=energy_usage) container_name = "grafana" pod_name = "grafana-5745b58656-8q4q8" namespace = "default" @@ -188,9 +169,7 @@ async def test_carbon_emission_kubernetes_generator(mocker): location_name = "fr" memory_total = 1000000000 carbon_usage = CarbonUsage(cpu_carbon_usage=0.2, memory_carbon_usage=0.1) - mocker.patch.object( - EnergyConsumption, "from_platform", return_value=MacEnergyConsumption() - ) + mocker.patch.object(EnergyConsumption, "from_platform", return_value=MacEnergyConsumption()) mocker.patch.object(config, "load_kube_config", return_value=None) mocker.patch.object(Country, "get_current_country", return_value=location_name) mocker.patch.object(HardwareInfo, "get_memory_total", return_value=memory_total) @@ -217,9 +196,7 @@ async def test_carbon_emission_kubernetes_generator(mocker): mocker.patch.object(Kubernetes, "get_pods_usage", return_value=pods_usage) location = Country(name=location_name, co2g_kwh=55) - carbon_emission_kubernertes_generator = CarbonEmissionKubernetesGenerator( - location=location, platform="Darwin" - ) + carbon_emission_kubernertes_generator = CarbonEmissionKubernetesGenerator(location=location, platform="Darwin") async_generator = carbon_emission_kubernertes_generator.generate() metric = await async_generator.__anext__() diff --git a/tracarbon/builder.py b/tracarbon/builder.py index 2e2be16..e8e0ffb 100644 --- a/tracarbon/builder.py +++ b/tracarbon/builder.py @@ -1,12 +1,16 @@ import datetime -from typing import Dict, Optional +from typing import Dict +from typing import Optional from pydantic import BaseModel from tracarbon.conf import TracarbonConfiguration -from tracarbon.exporters import Exporter, MetricReport, StdoutExporter +from tracarbon.exporters import Exporter +from tracarbon.exporters import MetricReport +from tracarbon.exporters import StdoutExporter from tracarbon.general_metrics import CarbonEmissionGenerator -from tracarbon.locations import Country, Location +from tracarbon.locations import Country +from tracarbon.locations import Location class TracarbonReport(BaseModel): @@ -103,9 +107,7 @@ def build(self) -> Tracarbon: co2signal_url=self.configuration.co2signal_url, ) if not self.exporter: - self.exporter = StdoutExporter( - metric_generators=[CarbonEmissionGenerator(location=self.location)] - ) + self.exporter = StdoutExporter(metric_generators=[CarbonEmissionGenerator(location=self.location)]) return Tracarbon( configuration=self.configuration, diff --git a/tracarbon/cli/__init__.py b/tracarbon/cli/__init__.py index 60f0cc0..985590b 100644 --- a/tracarbon/cli/__init__.py +++ b/tracarbon/cli/__init__.py @@ -1,16 +1,16 @@ import time -from typing import List, Optional +from typing import List +from typing import Optional import typer from loguru import logger from tracarbon.builder import TracarbonBuilder from tracarbon.conf import KUBERNETES_INSTALLED -from tracarbon.exporters import Exporter, MetricGenerator -from tracarbon.general_metrics import ( - CarbonEmissionGenerator, - EnergyConsumptionGenerator, -) +from tracarbon.exporters import Exporter +from tracarbon.exporters import MetricGenerator +from tracarbon.general_metrics import CarbonEmissionGenerator +from tracarbon.general_metrics import EnergyConsumptionGenerator from tracarbon.locations import Country app = typer.Typer() @@ -30,7 +30,7 @@ def list_exporters(displayed: bool = True) -> List[str]: def get_exporter( exporter_name: str, metric_generators: List[MetricGenerator], - tracarbon_builder: TracarbonBuilder = TracarbonBuilder(), + tracarbon_builder: Optional[TracarbonBuilder] = None, ) -> Exporter: """ Get the exporter based on the name with its metrics. @@ -40,18 +40,20 @@ def get_exporter( :param tracarbon_builder: the configuration of Tracarbon :return: the configured exporter """ + if not tracarbon_builder: + tracarbon_builder = TracarbonBuilder() exporters = list_exporters(displayed=False) if exporter_name not in exporters: raise ValueError(f"This exporter is not available in the list: {exporters}") try: - selected_exporter = next( - cls for cls in Exporter.__subclasses__() if cls.get_name() == exporter_name - ) + selected_exporter = next(cls for cls in Exporter.__subclasses__() if cls.get_name() == exporter_name) except Exception as exception: logger.exception("This exporter initiation failed.") raise exception - return selected_exporter(metric_generators=metric_generators, metric_prefix_name=tracarbon_builder.configuration.metric_prefix_name) # type: ignore + return selected_exporter( + metric_generators=metric_generators, metric_prefix_name=tracarbon_builder.configuration.metric_prefix_name + ) # type: ignore def add_containers_generator(location: Country) -> List[MetricGenerator]: @@ -62,10 +64,8 @@ def add_containers_generator(location: Country) -> List[MetricGenerator]: :return: the list of metric generators for containers """ if KUBERNETES_INSTALLED: - from tracarbon.general_metrics import ( - CarbonEmissionKubernetesGenerator, - EnergyConsumptionKubernetesGenerator, - ) + from tracarbon.general_metrics import CarbonEmissionKubernetesGenerator + from tracarbon.general_metrics import EnergyConsumptionKubernetesGenerator return [ EnergyConsumptionKubernetesGenerator(location=location), @@ -110,11 +110,7 @@ def run_metrics( metric_generators=metric_generators, tracarbon_builder=tracarbon_builder, ) - tracarbon = ( - tracarbon_builder.with_location(location=location) - .with_exporter(exporter=exporter) - .build() - ) + tracarbon = tracarbon_builder.with_location(location=location).with_exporter(exporter=exporter).build() from loguru import logger logger.info("Tracarbon CLI started.") diff --git a/tracarbon/conf.py b/tracarbon/conf.py index 96f100c..5066af9 100755 --- a/tracarbon/conf.py +++ b/tracarbon/conf.py @@ -1,6 +1,7 @@ import os import sys -from typing import Any, Optional +from typing import Any +from typing import Optional from dotenv import load_dotenv from pydantic import BaseModel @@ -64,16 +65,10 @@ def __init__( log_level = os.environ.get("TRACARBON_LOG_LEVEL", log_level) logger_configuration(level=log_level) super().__init__( - metric_prefix_name=os.environ.get( - "TRACARBON_METRIC_PREFIX_NAME", metric_prefix_name - ), + metric_prefix_name=os.environ.get("TRACARBON_METRIC_PREFIX_NAME", metric_prefix_name), log_level=log_level, - interval_in_seconds=os.environ.get( - "TRACARBON_INTERVAL_IN_SECONDS", interval_in_seconds - ), - co2signal_api_key=os.environ.get( - "TRACARBON_CO2SIGNAL_API_KEY", co2signal_api_key - ), + interval_in_seconds=os.environ.get("TRACARBON_INTERVAL_IN_SECONDS", interval_in_seconds), + co2signal_api_key=os.environ.get("TRACARBON_CO2SIGNAL_API_KEY", co2signal_api_key), co2signal_url=os.environ.get("TRACARBON_CO2SIGNAL_URL", co2signal_url), **data, ) diff --git a/tracarbon/emissions/__init__.py b/tracarbon/emissions/__init__.py index 2337f4d..6e2afb5 100644 --- a/tracarbon/emissions/__init__.py +++ b/tracarbon/emissions/__init__.py @@ -1,5 +1,3 @@ -from tracarbon.emissions.carbon_emissions import ( - CarbonEmission, - CarbonUsage, - CarbonUsageUnit, -) +from tracarbon.emissions.carbon_emissions import CarbonEmission +from tracarbon.emissions.carbon_emissions import CarbonUsage +from tracarbon.emissions.carbon_emissions import CarbonUsageUnit diff --git a/tracarbon/emissions/carbon_emissions.py b/tracarbon/emissions/carbon_emissions.py index cd81830..67ab6bf 100644 --- a/tracarbon/emissions/carbon_emissions.py +++ b/tracarbon/emissions/carbon_emissions.py @@ -1,13 +1,19 @@ from datetime import datetime from enum import Enum -from typing import Any, Optional +from typing import Any +from typing import Optional from loguru import logger from pydantic import BaseModel -from tracarbon.hardwares import EnergyConsumption, Power, Sensor -from tracarbon.hardwares.energy import EnergyUsage, EnergyUsageUnit, UsageType -from tracarbon.locations import Country, Location +from tracarbon.hardwares import EnergyConsumption +from tracarbon.hardwares import Power +from tracarbon.hardwares import Sensor +from tracarbon.hardwares.energy import EnergyUsage +from tracarbon.hardwares.energy import EnergyUsageUnit +from tracarbon.hardwares.energy import UsageType +from tracarbon.locations import Country +from tracarbon.locations import Location class CarbonUsageUnit(Enum): @@ -56,31 +62,15 @@ def convert_unit(self, unit: CarbonUsageUnit) -> None: if self.unit != unit: if unit == CarbonUsageUnit.CO2_G and self.unit == CarbonUsageUnit.CO2_MG: self.host_carbon_usage = self.host_carbon_usage / 1000 - self.cpu_carbon_usage = ( - self.cpu_carbon_usage / 1000 if self.cpu_carbon_usage else None - ) - self.memory_carbon_usage = ( - self.memory_carbon_usage / 1000 - if self.memory_carbon_usage - else None - ) - self.gpu_carbon_usage = ( - self.gpu_carbon_usage / 1000 if self.gpu_carbon_usage else None - ) + self.cpu_carbon_usage = self.cpu_carbon_usage / 1000 if self.cpu_carbon_usage else None + self.memory_carbon_usage = self.memory_carbon_usage / 1000 if self.memory_carbon_usage else None + self.gpu_carbon_usage = self.gpu_carbon_usage / 1000 if self.gpu_carbon_usage else None self.unit = CarbonUsageUnit.CO2_G elif unit == CarbonUsageUnit.CO2_MG and self.unit == CarbonUsageUnit.CO2_G: self.host_carbon_usage = self.host_carbon_usage * 1000 - self.cpu_carbon_usage = ( - self.cpu_carbon_usage * 1000 if self.cpu_carbon_usage else None - ) - self.memory_carbon_usage = ( - self.memory_carbon_usage * 1000 - if self.memory_carbon_usage - else None - ) - self.gpu_carbon_usage = ( - self.gpu_carbon_usage * 1000 if self.gpu_carbon_usage else None - ) + self.cpu_carbon_usage = self.cpu_carbon_usage * 1000 if self.cpu_carbon_usage else None + self.memory_carbon_usage = self.memory_carbon_usage * 1000 if self.memory_carbon_usage else None + self.gpu_carbon_usage = self.gpu_carbon_usage * 1000 if self.gpu_carbon_usage else None self.unit = CarbonUsageUnit.CO2_MG @@ -160,9 +150,7 @@ async def get_co2_usage(self) -> CarbonUsage: return CarbonUsage( host_carbon_usage=host_carbon_usage, cpu_carbon_usage=cpu_carbon_usage if cpu_carbon_usage > 0 else None, - memory_carbon_usage=( - memory_carbon_usage if memory_carbon_usage > 0 else None - ), + memory_carbon_usage=(memory_carbon_usage if memory_carbon_usage > 0 else None), gpu_carbon_usage=gpu_carbon_usage if gpu_carbon_usage > 0 else None, unit=CarbonUsageUnit.CO2_G, ) diff --git a/tracarbon/exporters/datadog_exporter.py b/tracarbon/exporters/datadog_exporter.py index 816fbab..fd78a7a 100755 --- a/tracarbon/exporters/datadog_exporter.py +++ b/tracarbon/exporters/datadog_exporter.py @@ -1,13 +1,16 @@ import os -from typing import Any, Optional +from typing import Any +from typing import Optional from loguru import logger from tracarbon.conf import DATADOG_INSTALLED -from tracarbon.exporters.exporter import Exporter, MetricGenerator +from tracarbon.exporters.exporter import Exporter +from tracarbon.exporters.exporter import MetricGenerator if DATADOG_INSTALLED: - from datadog import ThreadStats, initialize + from datadog import ThreadStats + from datadog import initialize class DatadogExporter(Exporter): """ @@ -47,15 +50,11 @@ async def launch(self, metric_generator: MetricGenerator) -> None: metric_value = await metric.value() if metric_value: await self.add_metric_to_report(metric=metric, value=metric_value) - metric_name = metric.format_name( - metric_prefix_name=self.metric_prefix_name - ) + metric_name = metric.format_name(metric_prefix_name=self.metric_prefix_name) logger.info( f"Sending metric[{metric_name}] with value [{metric_value}] and tags{metric.format_tags()} to Datadog." ) - self.stats.gauge( - metric_name, metric_value, tags=metric.format_tags() - ) + self.stats.gauge(metric_name, metric_value, tags=metric.format_tags()) @classmethod def get_name(cls) -> str: diff --git a/tracarbon/exporters/exporter.py b/tracarbon/exporters/exporter.py index 854397a..f91c0e6 100755 --- a/tracarbon/exporters/exporter.py +++ b/tracarbon/exporters/exporter.py @@ -1,9 +1,16 @@ import asyncio import sys -from abc import ABCMeta, abstractmethod +from abc import ABCMeta +from abc import abstractmethod from datetime import datetime -from threading import Event, Timer -from typing import AsyncGenerator, Awaitable, Callable, Dict, List, Optional +from threading import Event +from threading import Timer +from typing import AsyncGenerator +from typing import Awaitable +from typing import Callable +from typing import Dict +from typing import List +from typing import Optional from asyncer import asyncify from loguru import logger @@ -31,9 +38,7 @@ class Metric(BaseModel): value: Callable[[], Awaitable[float]] tags: List[Tag] = list() - def format_name( - self, metric_prefix_name: Optional[str] = None, separator: str = "." - ) -> str: + def format_name(self, metric_prefix_name: Optional[str] = None, separator: str = ".") -> str: """ Format the name of the metric with a prefix and separator. @@ -153,9 +158,7 @@ async def _launch_all(self) -> None: logger.debug(f"Running MetricGenerator[{metric_generator}].") await self.launch(metric_generator=metric_generator) - async def add_metric_to_report( - self, metric: "Metric", value: float - ) -> "MetricReport": + async def add_metric_to_report(self, metric: "Metric", value: float) -> "MetricReport": """ Add the generated metric to the report asynchronously. @@ -166,22 +169,15 @@ async def add_metric_to_report( def add_metric_to_report() -> MetricReport: if metric.name not in self.metric_report: - self.metric_report[metric.name] = MetricReport( - exporter_name=self.get_name(), metric=metric - ) + self.metric_report[metric.name] = MetricReport(exporter_name=self.get_name(), metric=metric) metric_report = self.metric_report[metric.name] now = datetime.now() if metric_report.last_report_time: - time_difference_in_s = ( - now - metric_report.last_report_time - ).total_seconds() + time_difference_in_s = (now - metric_report.last_report_time).total_seconds() metric_report.average_interval_in_seconds = ( time_difference_in_s if not metric_report.average_interval_in_seconds - else ( - metric_report.average_interval_in_seconds + time_difference_in_s - ) - / 2 + else (metric_report.average_interval_in_seconds + time_difference_in_s) / 2 ) metric_report.last_report_time = now diff --git a/tracarbon/exporters/json_exporter.py b/tracarbon/exporters/json_exporter.py index 990a3d7..6c74149 100644 --- a/tracarbon/exporters/json_exporter.py +++ b/tracarbon/exporters/json_exporter.py @@ -6,7 +6,8 @@ import aiofiles import ujson -from tracarbon.exporters.exporter import Exporter, MetricGenerator +from tracarbon.exporters.exporter import Exporter +from tracarbon.exporters.exporter import MetricGenerator class JSONExporter(Exporter): @@ -49,9 +50,7 @@ async def launch(self, metric_generator: MetricGenerator) -> None: ujson.dumps( { "timestamp": str(datetime.utcnow()), - "metric_name": metric.format_name( - metric_prefix_name=self.metric_prefix_name - ), + "metric_name": metric.format_name(metric_prefix_name=self.metric_prefix_name), "metric_value": metric_value, "metric_tags": metric.format_tags(), }, diff --git a/tracarbon/exporters/prometheus_exporter.py b/tracarbon/exporters/prometheus_exporter.py index 00e3b49..99f4c7f 100644 --- a/tracarbon/exporters/prometheus_exporter.py +++ b/tracarbon/exporters/prometheus_exporter.py @@ -1,14 +1,18 @@ import os -from typing import Any, Dict, Optional +from typing import Any +from typing import Dict +from typing import Optional from loguru import logger from tracarbon.conf import PROMETHEUS_INSTALLED -from tracarbon.exporters.exporter import Exporter, MetricGenerator +from tracarbon.exporters.exporter import Exporter +from tracarbon.exporters.exporter import MetricGenerator if PROMETHEUS_INSTALLED: import prometheus_client - from prometheus_client import Gauge, start_http_server + from prometheus_client import Gauge + from prometheus_client import start_http_server class PrometheusExporter(Exporter): """ @@ -22,14 +26,8 @@ class PrometheusExporter(Exporter): def __init__(self, **data: Any) -> None: super().__init__(**data) prometheus_client.REGISTRY.unregister(prometheus_client.GC_COLLECTOR) - addr = ( - self.address - if self.address - else os.environ.get("PROMETHEUS_ADDRESS", "::") - ) - port = ( - self.port if self.port else int(os.environ.get("PROMETHEUS_PORT", 8081)) - ) + addr = self.address if self.address else os.environ.get("PROMETHEUS_ADDRESS", "::") + port = self.port if self.port else int(os.environ.get("PROMETHEUS_PORT", 8081)) start_http_server( addr=addr, port=port, @@ -42,9 +40,7 @@ async def launch(self, metric_generator: MetricGenerator) -> None: :param metric_generator: the metric generator """ async for metric in metric_generator.generate(): - metric_name = metric.format_name( - metric_prefix_name=self.metric_prefix_name, separator="_" - ) + metric_name = metric.format_name(metric_prefix_name=self.metric_prefix_name, separator="_") if metric_name not in self.prometheus_metrics: self.prometheus_metrics[metric_name] = Gauge( metric_name, @@ -57,9 +53,7 @@ async def launch(self, metric_generator: MetricGenerator) -> None: logger.info( f"Sending metric[{metric_name}] with value [{metric_value}] and labels{metric.format_tags()} to Prometheus." ) - self.prometheus_metrics[metric_name].labels( - *[tag.value for tag in metric.tags] - ).set(metric_value) + self.prometheus_metrics[metric_name].labels(*[tag.value for tag in metric.tags]).set(metric_value) @classmethod def get_name(cls) -> str: diff --git a/tracarbon/exporters/stdout.py b/tracarbon/exporters/stdout.py index 164a904..37a25b2 100755 --- a/tracarbon/exporters/stdout.py +++ b/tracarbon/exporters/stdout.py @@ -1,6 +1,7 @@ from loguru import logger -from tracarbon.exporters.exporter import Exporter, MetricGenerator +from tracarbon.exporters.exporter import Exporter +from tracarbon.exporters.exporter import MetricGenerator class StdoutExporter(Exporter): diff --git a/tracarbon/general_metrics.py b/tracarbon/general_metrics.py index f9a57e4..f23bdc3 100644 --- a/tracarbon/general_metrics.py +++ b/tracarbon/general_metrics.py @@ -1,10 +1,18 @@ -from typing import Any, AsyncGenerator, Optional +from typing import Any +from typing import AsyncGenerator +from typing import Optional from tracarbon.conf import KUBERNETES_INSTALLED -from tracarbon.emissions import CarbonEmission, CarbonUsageUnit -from tracarbon.exporters import Metric, MetricGenerator, Tag -from tracarbon.hardwares import EnergyConsumption, EnergyUsageUnit, UsageType -from tracarbon.locations import Country, Location +from tracarbon.emissions import CarbonEmission +from tracarbon.emissions import CarbonUsageUnit +from tracarbon.exporters import Metric +from tracarbon.exporters import MetricGenerator +from tracarbon.exporters import Tag +from tracarbon.hardwares import EnergyConsumption +from tracarbon.hardwares import EnergyUsageUnit +from tracarbon.hardwares import UsageType +from tracarbon.locations import Country +from tracarbon.locations import Location class EnergyConsumptionGenerator(MetricGenerator): @@ -62,15 +70,9 @@ def __init__(self, location: Optional[Location] = None, **data: Any) -> None: if "carbon_emission" not in data: data["carbon_emission"] = CarbonEmission( co2signal_api_key=( - data["co2signal_api_key"] - if "co2signal_api_key" in data - else location.co2signal_api_key - ), - co2signal_url=( - data["co2signal_url"] - if "co2signal_url" in data - else location.co2signal_url + data["co2signal_api_key"] if "co2signal_api_key" in data else location.co2signal_api_key ), + co2signal_url=(data["co2signal_url"] if "co2signal_url" in data else location.co2signal_url), location=location, ) super().__init__(location=location, metrics=[], **data) @@ -148,10 +150,7 @@ async def get_pod_total_energy_consumption() -> Optional[float]: """ Get the total energy consumption of the pod. """ - total = ( - await get_pod_memory_energy_consumption() - + await get_pod_cpu_energy_consumption() - ) + total = await get_pod_memory_energy_consumption() + await get_pod_cpu_energy_consumption() return total tags = [ @@ -193,15 +192,9 @@ def __init__(self, location: Location, **data: Any) -> None: if "carbon_emission" not in data: data["carbon_emission"] = CarbonEmission( co2signal_api_key=( - data["co2signal_api_key"] - if "co2signal_api_key" in data - else location.co2signal_api_key - ), - co2signal_url=( - data["co2signal_url"] - if "co2signal_url" in data - else location.co2signal_url + data["co2signal_api_key"] if "co2signal_api_key" in data else location.co2signal_api_key ), + co2signal_url=(data["co2signal_url"] if "co2signal_url" in data else location.co2signal_url), location=location, ) if "kubernetes" not in data: @@ -236,10 +229,7 @@ async def get_total_pod_carbon_emission() -> Optional[float]: """ Get the total carbon emission of the pod. """ - total = ( - await get_cpu_pod_carbon_emission() - + await get_memory_pod_carbon_emission() - ) + total = await get_cpu_pod_carbon_emission() + await get_memory_pod_carbon_emission() return total tags = [ diff --git a/tracarbon/hardwares/cloud_providers.py b/tracarbon/hardwares/cloud_providers.py index fb51ca9..49673f7 100644 --- a/tracarbon/hardwares/cloud_providers.py +++ b/tracarbon/hardwares/cloud_providers.py @@ -1,7 +1,8 @@ from typing import Optional import requests -from ec2_metadata import EC2Metadata, ec2_metadata +from ec2_metadata import EC2Metadata +from ec2_metadata import ec2_metadata from pydantic import BaseModel diff --git a/tracarbon/hardwares/containers.py b/tracarbon/hardwares/containers.py index bed2337..fa37a96 100644 --- a/tracarbon/hardwares/containers.py +++ b/tracarbon/hardwares/containers.py @@ -1,4 +1,7 @@ -from typing import Any, Iterator, List, Optional +from typing import Any +from typing import Iterator +from typing import List +from typing import Optional from pydantic import BaseModel @@ -8,7 +11,8 @@ if KUBERNETES_INSTALLED: from kubernetes import config - from kubernetes.client import CoreV1Api, CustomObjectsApi + from kubernetes.client import CoreV1Api + from kubernetes.client import CustomObjectsApi class Container(BaseModel): """ @@ -27,31 +31,19 @@ def __init__(self, **data: Any) -> None: memory_total = HardwareInfo.get_memory_total() if isinstance(data["cpu_usage"], str): if "n" in data["cpu_usage"]: - data["cpu_usage"] = ( - float(data["cpu_usage"].replace("n", "")) / 1000000000 - ) / cores + data["cpu_usage"] = (float(data["cpu_usage"].replace("n", "")) / 1000000000) / cores elif "u" in data["cpu_usage"]: - data["cpu_usage"] = ( - float(data["cpu_usage"].replace("u", "")) / 1000000 - ) / cores + data["cpu_usage"] = (float(data["cpu_usage"].replace("u", "")) / 1000000) / cores elif "m" in data["cpu_usage"]: - data["cpu_usage"] = ( - float(data["cpu_usage"].replace("m", "")) / 1000 - ) / cores + data["cpu_usage"] = (float(data["cpu_usage"].replace("m", "")) / 1000) / cores if isinstance(data["memory_usage"], str): if "Ki" in data["memory_usage"]: - data["memory_usage"] = ( - float(data["memory_usage"].replace("Ki", "")) * 1000 - ) / memory_total + data["memory_usage"] = (float(data["memory_usage"].replace("Ki", "")) * 1000) / memory_total elif "Mi" in data["memory_usage"]: - data["memory_usage"] = ( - float(data["memory_usage"].replace("Mi", "")) * 1000000 - ) / memory_total + data["memory_usage"] = (float(data["memory_usage"].replace("Mi", "")) * 1000000) / memory_total elif "Gi" in data["memory_usage"]: - data["memory_usage"] = ( - float(data["memory_usage"].replace("Gi", "")) * 1000000000 - ) / memory_total + data["memory_usage"] = (float(data["memory_usage"].replace("Gi", "")) * 1000000000) / memory_total super().__init__(**data) @@ -93,9 +85,7 @@ def refresh_namespaces(self) -> None: """ Refresh the names of the namespaces. """ - self.namespaces = [ - item.metadata.name for item in CoreV1Api().list_namespace().items - ] + self.namespaces = [item.metadata.name for item in CoreV1Api().list_namespace().items] def get_pods_usage(self, namespace: Optional[str] = None) -> Iterator[Pod]: """ diff --git a/tracarbon/hardwares/energy.py b/tracarbon/hardwares/energy.py index 5f5363c..ec23783 100644 --- a/tracarbon/hardwares/energy.py +++ b/tracarbon/hardwares/energy.py @@ -1,6 +1,7 @@ from datetime import datetime from enum import Enum -from typing import ClassVar, Optional +from typing import ClassVar +from typing import Optional from pydantic import BaseModel @@ -62,42 +63,20 @@ def convert_unit(self, unit: EnergyUsageUnit) -> None: if self.unit != unit: if unit == EnergyUsageUnit.WATT and self.unit == EnergyUsageUnit.MILLIWATT: self.host_energy_usage = self.host_energy_usage * 1000 - self.cpu_energy_usage = ( - self.cpu_energy_usage * 1000 - if self.cpu_energy_usage is not None - else None - ) + self.cpu_energy_usage = self.cpu_energy_usage * 1000 if self.cpu_energy_usage is not None else None self.memory_energy_usage = ( - self.memory_energy_usage * 1000 - if self.memory_energy_usage is not None - else None - ) - self.gpu_energy_usage = ( - self.gpu_energy_usage / 1000 - if self.gpu_energy_usage is not None - else None + self.memory_energy_usage * 1000 if self.memory_energy_usage is not None else None ) + self.gpu_energy_usage = self.gpu_energy_usage / 1000 if self.gpu_energy_usage is not None else None self.unit = EnergyUsageUnit.MILLIWATT - elif ( - unit == EnergyUsageUnit.MILLIWATT and self.unit == EnergyUsageUnit.WATT - ): + elif unit == EnergyUsageUnit.MILLIWATT and self.unit == EnergyUsageUnit.WATT: self.host_energy_usage = self.host_energy_usage * 1000 - self.cpu_energy_usage = ( - self.cpu_energy_usage * 1000 - if self.cpu_energy_usage is not None - else None - ) + self.cpu_energy_usage = self.cpu_energy_usage * 1000 if self.cpu_energy_usage is not None else None self.memory_energy_usage = ( - self.memory_energy_usage * 1000 - if self.memory_energy_usage is not None - else None - ) - self.gpu_energy_usage = ( - self.gpu_energy_usage * 1000 - if self.gpu_energy_usage is not None - else None + self.memory_energy_usage * 1000 if self.memory_energy_usage is not None else None ) + self.gpu_energy_usage = self.gpu_energy_usage * 1000 if self.gpu_energy_usage is not None else None self.unit = EnergyUsageUnit.MILLIWATT @@ -109,9 +88,7 @@ class Power(BaseModel): SECONDS_TO_HOURS_FACTOR: ClassVar[int] = 3600 @staticmethod - def watts_to_watt_hours( - watts: float, previous_energy_measurement_time: Optional[datetime] = None - ) -> float: + def watts_to_watt_hours(watts: float, previous_energy_measurement_time: Optional[datetime] = None) -> float: """ Convert current watts to watt-hours W/h using the previous energy measurement. @@ -121,9 +98,7 @@ def watts_to_watt_hours( """ now = datetime.now() if previous_energy_measurement_time: - time_difference_in_seconds = ( - now - previous_energy_measurement_time - ).total_seconds() + time_difference_in_seconds = (now - previous_energy_measurement_time).total_seconds() else: time_difference_in_seconds = 1 return watts * (time_difference_in_seconds / Power.SECONDS_TO_HOURS_FACTOR) diff --git a/tracarbon/hardwares/gpu.py b/tracarbon/hardwares/gpu.py index 669fdbc..1d5df0c 100644 --- a/tracarbon/hardwares/gpu.py +++ b/tracarbon/hardwares/gpu.py @@ -1,3 +1,4 @@ +import shutil import subprocess from abc import ABC from typing import Tuple @@ -34,10 +35,13 @@ def launch_shell_command(cls) -> Tuple[bytes, int]: :return: result of the shell command and returncode """ + nvidia_smi_path = shutil.which("nvidia-smi") + if nvidia_smi_path is None: + raise HardwareNoGPUDetectedException("Nvidia GPU with nvidia-smi not found in PATH.") + process = subprocess.Popen( - "nvidia-smi --query-gpu=power.draw --format=csv,noheader", + [nvidia_smi_path, "--query-gpu=power.draw", "--format=csv,noheader"], stdout=subprocess.PIPE, - shell=True, ) stdout, _ = process.communicate() return stdout, process.returncode diff --git a/tracarbon/hardwares/rapl.py b/tracarbon/hardwares/rapl.py index 66d403e..0c276a6 100644 --- a/tracarbon/hardwares/rapl.py +++ b/tracarbon/hardwares/rapl.py @@ -1,14 +1,17 @@ import os import re from datetime import datetime -from typing import Dict, List, Optional +from typing import Dict +from typing import List +from typing import Optional import aiofiles from loguru import logger from pydantic import BaseModel from tracarbon.exceptions import HardwareRAPLException -from tracarbon.hardwares.energy import EnergyUsage, Power +from tracarbon.hardwares.energy import EnergyUsage +from tracarbon.hardwares.energy import Power class RAPLResult(BaseModel): @@ -47,14 +50,10 @@ def get_rapl_files_list(self) -> None: :return: the list of files path containing RAPL energy measurements. """ if not self.is_rapl_compatible(): - raise ValueError( - f"Path f{self.path} doest not exists for reading RAPL energy measurements" - ) + raise ValueError(f"Path f{self.path} doest not exists for reading RAPL energy measurements") logger.debug(f"The hardware is RAPL compatible.") intel_rapl_regex = re.compile("intel-rapl") - for directory_path, directory_names, filenames in os.walk( - self.path, topdown=True - ): + for directory_path, directory_names, _filenames in os.walk(self.path, topdown=True): for directory in directory_names: if not intel_rapl_regex.search(directory): directory_names.remove(directory) @@ -79,13 +78,9 @@ async def get_rapl_power_usage(self) -> List[RAPLResult]: for file_path in self.file_list: async with aiofiles.open(f"{file_path}/name", "r") as rapl_name: name = await rapl_name.read() - async with aiofiles.open( - f"{file_path}/energy_uj", "r" - ) as rapl_energy: + async with aiofiles.open(f"{file_path}/energy_uj", "r") as rapl_energy: energy_uj = float(await rapl_energy.read()) - async with aiofiles.open( - f"{file_path}/max_energy_range_uj", "r" - ) as rapl_max_energy: + async with aiofiles.open(f"{file_path}/max_energy_range_uj", "r") as rapl_max_energy: max_energy_uj = float(await rapl_max_energy.read()) rapl_results.append( RAPLResult( @@ -97,7 +92,7 @@ async def get_rapl_power_usage(self) -> List[RAPLResult]: ) except Exception as exception: logger.exception(f"The RAPL read encountered an issue.") - raise HardwareRAPLException(exception) + raise HardwareRAPLException(exception) from exception logger.debug(f"The RAPL results: {rapl_results}.") return rapl_results @@ -114,9 +109,7 @@ async def get_energy_report(self) -> EnergyUsage: gpu_energy_usage_watts = 0.0 for rapl_result in rapl_results: previous_rapl_result = self.rapl_results.get(rapl_result.name, rapl_result) - time_difference = ( - rapl_result.timestamp - previous_rapl_result.timestamp - ).total_seconds() + time_difference = (rapl_result.timestamp - previous_rapl_result.timestamp).total_seconds() energy_uj = rapl_result.energy_uj if previous_rapl_result.energy_uj > rapl_result.energy_uj: logger.debug( @@ -124,11 +117,7 @@ async def get_energy_report(self) -> EnergyUsage: ) energy_uj = energy_uj + rapl_result.max_energy_uj watts = Power.watts_from_microjoules( - ( - (energy_uj - previous_rapl_result.energy_uj) / time_difference - if time_difference > 0 - else 1 - ) + ((energy_uj - previous_rapl_result.energy_uj) / time_difference if time_difference > 0 else 1) ) self.rapl_results[rapl_result.name] = rapl_result if "package" in rapl_result.name or "ram" in rapl_result.name: @@ -141,17 +130,9 @@ async def get_energy_report(self) -> EnergyUsage: gpu_energy_usage_watts += watts energy_usage_report = EnergyUsage( host_energy_usage=host_energy_usage_watts, - cpu_energy_usage=( - cpu_energy_usage_watts if cpu_energy_usage_watts > 0 else None - ), - memory_energy_usage=( - memory_energy_usage_watts if memory_energy_usage_watts > 0 else None - ), - gpu_energy_usage=( - gpu_energy_usage_watts if gpu_energy_usage_watts > 0 else None - ), - ) - logger.debug( - f"The usage energy report measured with RAPL is {energy_usage_report}." + cpu_energy_usage=(cpu_energy_usage_watts if cpu_energy_usage_watts > 0 else None), + memory_energy_usage=(memory_energy_usage_watts if memory_energy_usage_watts > 0 else None), + gpu_energy_usage=(gpu_energy_usage_watts if gpu_energy_usage_watts > 0 else None), ) + logger.debug(f"The usage energy report measured with RAPL is {energy_usage_report}.") return energy_usage_report diff --git a/tracarbon/hardwares/sensors.py b/tracarbon/hardwares/sensors.py index b178196..0a98e73 100644 --- a/tracarbon/hardwares/sensors.py +++ b/tracarbon/hardwares/sensors.py @@ -1,13 +1,15 @@ import asyncio import csv import importlib -from abc import ABC, abstractmethod +from abc import ABC +from abc import abstractmethod from typing import Any from loguru import logger from pydantic import BaseModel -from tracarbon.exceptions import AWSSensorException, TracarbonException +from tracarbon.exceptions import AWSSensorException +from tracarbon.exceptions import TracarbonException from tracarbon.hardwares import EnergyUsage from tracarbon.hardwares.cloud_providers import CloudProviders from tracarbon.hardwares.hardware import HardwareInfo @@ -79,9 +81,7 @@ class MacEnergyConsumption(EnergyConsumption): Energy Consumption of the Mac, working only if it's plugged into plugged-in wall adapter, in watts. """ - shell_command: str = ( - """/usr/sbin/ioreg -rw0 -c AppleSmartBattery | grep BatteryData | grep -o '"AdapterPower"=[0-9]*' | cut -c 16- | xargs -I % lldb --batch -o "print/f %" | grep -o '$0 = [0-9.]*' | cut -c 6-""" - ) + shell_command: str = """/usr/sbin/ioreg -rw0 -c AppleSmartBattery | grep BatteryData | grep -o '"AdapterPower"=[0-9]*' | cut -c 16- | xargs -I % lldb --batch -o "print/f %" | grep -o '$0 = [0-9.]*' | cut -c 6-""" async def get_energy_usage(self) -> EnergyUsage: """ @@ -89,9 +89,7 @@ async def get_energy_usage(self) -> EnergyUsage: :return: the generated energy usage. """ - proc = await asyncio.create_subprocess_shell( - self.shell_command, stdout=asyncio.subprocess.PIPE - ) + proc = await asyncio.create_subprocess_shell(self.shell_command, stdout=asyncio.subprocess.PIPE) result, _ = await proc.communicate() return EnergyUsage(host_energy_usage=float(result)) @@ -146,9 +144,7 @@ class AWSEC2EnergyConsumption(EnergyConsumption): delta_full_machine: float def __init__(self, instance_type: str, **data: Any) -> None: - with importlib.resources.path( - "tracarbon.hardwares.data", "aws-instances.csv" - ) as resource: + with importlib.resources.path("tracarbon.hardwares.data", "aws-instances.csv") as resource: try: with open(str(resource)) as csvfile: reader = csv.reader(csvfile) @@ -164,9 +160,7 @@ def __init__(self, instance_type: str, **data: Any) -> None: data["memory_at_50"] = float(row[20].replace(",", ".")) data["memory_at_100"] = float(row[21].replace(",", ".")) data["has_gpu"] = float(row[22].replace(",", ".")) > 0 - data["delta_full_machine"] = float( - row[26].replace(",", ".") - ) + data["delta_full_machine"] = float(row[26].replace(",", ".")) super().__init__( **data, ) @@ -176,7 +170,7 @@ def __init__(self, instance_type: str, **data: Any) -> None: ) except Exception as exception: logger.exception("Error in the AWSSensor") - raise AWSSensorException(exception) + raise AWSSensorException(exception) from exception async def get_energy_usage(self) -> EnergyUsage: """ diff --git a/tracarbon/locations/country.py b/tracarbon/locations/country.py index d578214..667a967 100644 --- a/tracarbon/locations/country.py +++ b/tracarbon/locations/country.py @@ -1,19 +1,19 @@ import csv import importlib.resources -from typing import Any, Optional +from typing import Any +from typing import Optional import requests import ujson from aiocache import cached from loguru import logger -from tracarbon.exceptions import ( - CloudProviderRegionIsMissing, - CO2SignalAPIKeyIsMissing, - CountryIsMissing, -) +from tracarbon.exceptions import CloudProviderRegionIsMissing +from tracarbon.exceptions import CO2SignalAPIKeyIsMissing +from tracarbon.exceptions import CountryIsMissing from tracarbon.hardwares import CloudProviders -from tracarbon.locations.location import CarbonIntensitySource, Location +from tracarbon.locations.location import CarbonIntensitySource +from tracarbon.locations.location import Location class Country(Location): @@ -29,22 +29,16 @@ def from_eu_file(cls, country_code_alpha_iso_2: str) -> "Country": :param country_code_alpha_iso_2: the alpha_iso_2 name of the country :return: """ - with importlib.resources.path( - "tracarbon.locations.data", "co2-emission-intensity-9.exhibit.json" - ) as resource: + with importlib.resources.path("tracarbon.locations.data", "co2-emission-intensity-9.exhibit.json") as resource: with open(str(resource)) as json_file: countries_values = ujson.load(json_file)["countries"] for country in countries_values: if country_code_alpha_iso_2.lower() == country["name"]: return cls.parse_obj(country) - raise CountryIsMissing( - f"The country [{country_code_alpha_iso_2}] is not in the co2 emission file." - ) + raise CountryIsMissing(f"The country [{country_code_alpha_iso_2}] is not in the co2 emission file.") @classmethod - def get_current_country( - cls, url: str = "http://ipinfo.io/json", timeout: int = 300 - ) -> str: + def get_current_country(cls, url: str = "http://ipinfo.io/json", timeout: int = 300) -> str: """ Get the client's country using an internet access. @@ -105,9 +99,7 @@ async def get_latest_co2g_kwh(self) -> float: if self.co2g_kwh_source == CarbonIntensitySource.FILE: return self.co2g_kwh - logger.info( - f"Request the latest carbon intensity in Co2g/kwh for your country {self.name}." - ) + logger.info(f"Request the latest carbon intensity in Co2g/kwh for your country {self.name}.") if not self.co2signal_api_key: raise CO2SignalAPIKeyIsMissing() url = f"{self.co2signal_url}{self.name}" @@ -121,9 +113,7 @@ async def get_latest_co2g_kwh(self) -> float: if "data" in response: response = response["data"] self.co2g_kwh = float(response["carbonIntensity"]) - logger.info( - f"The latest carbon intensity of your country {self.name} is: {self.co2g_kwh} CO2g/kwh." - ) + logger.info(f"The latest carbon intensity of your country {self.name} is: {self.co2g_kwh} CO2g/kwh.") except Exception: logger.error( f'Failed to get the latest carbon intensity of your country {self.name} {response if response else ""}.' @@ -142,18 +132,14 @@ class AWSLocation(Country): """ def __init__(self, region_name: str, **data: Any) -> None: - with importlib.resources.path( - "tracarbon.locations.data", "grid-emissions-factors-aws.csv" - ) as resource: + with importlib.resources.path("tracarbon.locations.data", "grid-emissions-factors-aws.csv") as resource: co2g_kwh = None with open(str(resource)) as csv_file: reader = csv.reader(csv_file) for row in reader: if row[0] == region_name: co2g_kwh = float(row[3]) * 1000000 - super().__init__( - name=f"AWS({region_name})", co2g_kwh=co2g_kwh, **data - ) + super().__init__(name=f"AWS({region_name})", co2g_kwh=co2g_kwh, **data) if not co2g_kwh: raise CloudProviderRegionIsMissing( f"The region [{region_name}] is not in the AWS grid emissions factors file." diff --git a/tracarbon/locations/location.py b/tracarbon/locations/location.py index 74113b3..bec2a71 100755 --- a/tracarbon/locations/location.py +++ b/tracarbon/locations/location.py @@ -1,6 +1,9 @@ -from abc import ABC, abstractmethod +from abc import ABC +from abc import abstractmethod from enum import Enum -from typing import Any, Dict, Optional +from typing import Any +from typing import Dict +from typing import Optional import aiohttp import ujson @@ -26,9 +29,7 @@ class Location(ABC, BaseModel): co2g_kwh: float = 0.0 @classmethod - async def request( - cls, url: str, headers: Optional[Dict[str, str]] = None - ) -> Dict[str, Any]: + async def request(cls, url: str, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]: """ Launch an async request.