From ca44d443dec048752bc1e4fca40c7aae2c57f0d6 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:48:39 -0700 Subject: [PATCH 01/16] chore: migrate analytics package to pyproject.toml and uv (#4934) Replace the bare setuptools setup.py with a pyproject.toml (setuptools backend, so the downstream git+#subdirectory install path is unchanged), and replace the hand-pinned requirements.txt with a uv workspace and uv.lock. Adds ruff config and a .python-version. --- .gitignore | 2 + analytics/.python-version | 1 + analytics/analytics_package/pyproject.toml | 21 + analytics/analytics_package/setup.py | 15 - analytics/pyproject.toml | 21 + analytics/requirements.txt | 28 - analytics/uv.lock | 731 +++++++++++++++++++++ 7 files changed, 776 insertions(+), 43 deletions(-) create mode 100644 analytics/.python-version create mode 100644 analytics/analytics_package/pyproject.toml delete mode 100644 analytics/analytics_package/setup.py create mode 100644 analytics/pyproject.toml delete mode 100644 analytics/requirements.txt create mode 100644 analytics/uv.lock diff --git a/.gitignore b/.gitignore index 076a0be1a..c261d674f 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ __pycache__/ *.egg-info/ /analytics/**/site/ /analytics/venv +/analytics/.venv +.ruff_cache/ # Credentials .credentials/ diff --git a/analytics/.python-version b/analytics/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/analytics/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/analytics/analytics_package/pyproject.toml b/analytics/analytics_package/pyproject.toml new file mode 100644 index 000000000..37faa2baf --- /dev/null +++ b/analytics/analytics_package/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "analytics" +version = "5.0.4" +requires-python = ">=3.12" +dependencies = [ + "pandas>=3,<4", + "numpy", + "google-auth-oauthlib", + "google-api-python-client", + "requests", +] + +[tool.setuptools] +packages = ["analytics", "analytics.static_site"] + +[tool.setuptools.package-data] +"analytics.static_site" = ["template/*.html"] diff --git a/analytics/analytics_package/setup.py b/analytics/analytics_package/setup.py deleted file mode 100644 index 1262e6d72..000000000 --- a/analytics/analytics_package/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -from setuptools import setup - -setup( - name="analytics", - version="5.0.4", - packages=["analytics", "analytics.static_site"], - package_data={"analytics.static_site": ["template/*.html"]}, - install_requires=[ - "pandas>=3,<4", - "numpy", - "google-auth-oauthlib", - "google-api-python-client", - "requests", - ], -) diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml new file mode 100644 index 000000000..6127dd474 --- /dev/null +++ b/analytics/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "analytics-reports" +version = "0.0.0" +description = "Environment for generating the static analytics site reports." +requires-python = ">=3.12" +dependencies = ["analytics"] + +[tool.uv.workspace] +members = ["analytics_package"] + +[tool.uv.sources] +analytics = { workspace = true } + +[dependency-groups] +dev = ["ruff==0.16.3"] + +[tool.ruff] +extend-exclude = ["venv", ".venv"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I", "W", "B"] diff --git a/analytics/requirements.txt b/analytics/requirements.txt deleted file mode 100644 index 01f61dc35..000000000 --- a/analytics/requirements.txt +++ /dev/null @@ -1,28 +0,0 @@ --e ./analytics_package -certifi==2026.7.22 -cffi==2.1.1 -charset-normalizer==3.5.0 -cryptography==50.0.0 -google-api-core==2.34.0 -google-api-python-client==2.198.0 -google-auth==2.56.3 -google-auth-httplib2==0.4.1 -google-auth-oauthlib==1.4.0 -googleapis-common-protos==1.75.1 -httplib2==0.32.0 -idna==3.18 -numpy==2.5.2 -oauthlib==3.3.1 -pandas==3.0.5 -proto-plus==1.28.3 -protobuf==7.35.1 -pyasn1==0.6.4 -pyasn1_modules==0.4.2 -pycparser==3.0 -pyparsing==3.3.2 -python-dateutil==2.9.0.post0 -requests==2.34.2 -requests-oauthlib==2.0.0 -six==1.17.0 -uritemplate==4.2.0 -urllib3==2.7.0 diff --git a/analytics/uv.lock b/analytics/uv.lock new file mode 100644 index 000000000..d1b44f084 --- /dev/null +++ b/analytics/uv.lock @@ -0,0 +1,731 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[manifest] +members = [ + "analytics", + "analytics-reports", +] + +[[package]] +name = "analytics" +version = "5.0.4" +source = { editable = "analytics_package" } +dependencies = [ + { name = "google-api-python-client" }, + { name = "google-auth-oauthlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "google-api-python-client" }, + { name = "google-auth-oauthlib" }, + { name = "numpy" }, + { name = "pandas", specifier = ">=3,<4" }, + { name = "requests" }, +] + +[[package]] +name = "analytics-reports" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "analytics" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "analytics", editable = "analytics_package" }] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = "==0.16.3" }] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.198.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/53/0cd38e3a29d72ce45e27feba2ce1cd8049d69af9c48cb14fb164f1be9133/google_api_python_client-2.198.0.tar.gz", hash = "sha256:dfe3e16fb241af6e9c460a33f65085b3450e05cea09364f6b5d8997fb7e43e2a", size = 15060142, upload-time = "2026-06-25T14:32:42.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/92/0fc9e7a09eb240c31b879bd8d2e43f81ed1f86c4798b79ead4a083921ab3/google_api_python_client-2.198.0-py3-none-any.whl", hash = "sha256:fabac935474e817da5e662ff61bf7139439d6f92b32d332a7318a2d45931e03e", size = 15644203, upload-time = "2026-06-25T14:32:39.963Z" }, +] + +[[package]] +name = "google-auth" +version = "2.56.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/46/79983cb738f0eb14e6ab4f43457aa9652f8d46bc4376b178f676b68c5c37/google_auth_httplib2-0.4.1.tar.gz", hash = "sha256:125b1bb4fcfdd2d97f19b673c1f46f831603d0acaffe415c8a35dadb312552a1", size = 11158, upload-time = "2026-08-06T06:24:00.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/8a/96410d0c3e02b584d1e7f5d4000b2e1710855650b226cd0601f2b154940a/google_auth_httplib2-0.4.1-py3-none-any.whl", hash = "sha256:67088a8387dc3f66016a690634f1cb9b7a48a31406f8998e088efc64203e7a12", size = 9529, upload-time = "2026-08-06T06:22:50.53Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/18/90c7fac516e63cf2058166fce0c88c353647c677b51cc036c09c49bb5cbb/google_auth_oauthlib-1.4.0.tar.gz", hash = "sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2", size = 21675, upload-time = "2026-05-07T08:03:47.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d3/d7dff0d58a9e9244b48044bfb6a898bfcc8ecc42e0031d1bebc695344725/google_auth_oauthlib-1.4.0-py3-none-any.whl", hash = "sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070", size = 19261, upload-time = "2026-05-07T08:02:13.798Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, +] + +[[package]] +name = "httplib2" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/6a/056256feb4bd000869aba5c16cf2aa911572ca2a2feb185f86e457b5171e/proto_plus-1.28.3.tar.gz", hash = "sha256:5f91b30dafa6bb38d432c5557a6ee1d35ffd40b4b1e0e3ca27260448560b91d9", size = 58051, upload-time = "2026-08-06T06:24:55.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/3a/cfee3c50294f55a2f0f9575052dec2c2a48891ad4b1c2a133b05a87026cd/proto_plus-1.28.3-py3-none-any.whl", hash = "sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281", size = 50795, upload-time = "2026-08-06T06:23:50.653Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] From 2eff1856ed89b6406ae73500ea890e95923c8022 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:49:28 -0700 Subject: [PATCH 02/16] chore: fix ruff lint violations in the analytics package (#4934) Auto-fixes for E714 (not-is-test), I001 (import sorting) and trailing whitespace, plus explicit strict=True on the two zip() calls flagged by B905 -- both zip sequences that are equal-length by construction. --- .../analytics/_report_utils.py | 28 ++++---- analytics/analytics_package/analytics/api.py | 64 +++++++++---------- .../analytics_package/analytics/entities.py | 6 +- .../analytics/report_elements.py | 44 +++++++------ .../analytics/static_site/__init__.py | 2 +- .../analytics/static_site/export.py | 2 +- .../analytics/static_site/fetch.py | 20 +++--- .../analytics/static_site/generator.py | 2 +- .../anvil-catalog/generate_static_site.py | 9 ++- .../anvil-explorer/generate_static_site.py | 16 ++++- .../hca-explorer/generate_static_site.py | 16 ++++- analytics/lungmap/generate_static_site.py | 16 ++++- 12 files changed, 137 insertions(+), 88 deletions(-) diff --git a/analytics/analytics_package/analytics/_report_utils.py b/analytics/analytics_package/analytics/_report_utils.py index edb31987e..eef30e9eb 100644 --- a/analytics/analytics_package/analytics/_report_utils.py +++ b/analytics/analytics_package/analytics/_report_utils.py @@ -1,18 +1,20 @@ import datetime as dt -from . import api as ga -from .entities import ADDITIONAL_DATA_BEHAVIOR + import numpy as np import pandas as pd +from . import api as ga +from .entities import ADDITIONAL_DATA_BEHAVIOR + def get_data_df(metrics, dimensions, percentage_metrics=None, percentage_suffix="_percentage", num_keep_dimensions=None, df_processor=None, **other_params): if metrics is None: df = pd.DataFrame() else: df = ga.get_metrics_by_dimensions(metrics, dimensions, **other_params) - + if dimensions: - if len(dimensions) > 1 and not num_keep_dimensions is None: + if len(dimensions) > 1 and num_keep_dimensions is not None: df.drop(columns=dimensions[num_keep_dimensions:], inplace=True) df.set_index(dimensions[:num_keep_dimensions], inplace=True) for metric in metrics: @@ -22,14 +24,14 @@ def get_data_df(metrics, dimensions, percentage_metrics=None, percentage_suffix= except ValueError: num_column = str_column.astype(float) df[metric] = num_column - + if percentage_metrics: for metric in percentage_metrics: df.insert(list(df.columns).index(metric) + 1, metric + percentage_suffix, df[metric] / df[metric].sum() * 100) - + if df_processor: df = df_processor(df) - + return df def strings_to_lists(*vals): @@ -37,13 +39,13 @@ def strings_to_lists(*vals): def get_df_over_time(xlabels, metrics, dimensions, df_filter=None, **other_params): xlabels, metrics = strings_to_lists(xlabels, metrics) - + df = get_data_df(metrics, dimensions, **other_params) - + # Convert date to datetime object df.index = pd.to_datetime(df.index) - if (not df_filter is None): + if (df_filter is not None): df = df_filter(df) # Rename for display @@ -75,7 +77,7 @@ def get_data_df_from_fields(metrics, dimensions, **other_params): def get_rename_dict(dimensions): """Get a dictionary to rename the columns of a DataFrame.""" return dict( - zip([dimension["id"] for dimension in dimensions], [dimension["alias"] for dimension in dimensions]) + zip([dimension["id"] for dimension in dimensions], [dimension["alias"] for dimension in dimensions], strict=True) ) @@ -173,8 +175,8 @@ def get_change_over_time_df_multiple_events(metric, events, time_dimension, **ch dimension_filter=f"eventName=={event['id']}" ).rename( columns={metric["alias"]: event["alias"], metric["change_alias"]: event["change_alias"]} - ).set_index(time_dimension["alias"]) + ).set_index(time_dimension["alias"]) for event in events ], axis=1 - ) \ No newline at end of file + ) diff --git a/analytics/analytics_package/analytics/api.py b/analytics/analytics_package/analytics/api.py index 86f8dd9b5..5ae1e3b4a 100644 --- a/analytics/analytics_package/analytics/api.py +++ b/analytics/analytics_package/analytics/api.py @@ -1,9 +1,9 @@ -from google_auth_oauthlib.flow import InstalledAppFlow -from googleapiclient.discovery import build import os -import pandas as pd import re +import pandas as pd +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build ga_service_params = ( ['https://www.googleapis.com/auth/analytics.readonly'], @@ -48,7 +48,7 @@ def authenticate(secret_name, first_service_params=ga_service_params, *other_ser flow = InstalledAppFlow.from_client_secrets_file(ANALYTICS_REPORTING_CLIENT_SECRET_PATH, scopes=all_scopes) - + global next_port if port is None: @@ -59,7 +59,7 @@ def authenticate(secret_name, first_service_params=ga_service_params, *other_ser next_port = port + 1 elif next_port is None: next_port = port + 1 - + credentials = flow.run_local_server(port=port) built_systems = [build_service_system(service_params, credentials) for service_params in service_param_sets] @@ -72,32 +72,32 @@ def build_service_system(service_params, credentials): query_func = None else: service_name, service_version, param_subs_or_alt_api, query_func = service_params[1:] - + # Build the service object. service = build(service_name, service_version, credentials=credentials) - + service_system = (service, query_func, param_subs_or_alt_api, credentials) - + global default_service_system if default_service_system is None: default_service_system = service_system - + return service_system def get_metrics_by_dimensions(metrics, dimensions, service_system=None, sort_results=None, **other_params): if service_system is None: service_system = default_service_system - + service, query_func, param_subs_or_alt_api = service_system[:3] - + metrics = normalize_id_list(metrics) dimensions = normalize_id_list(dimensions) sort_results = normalize_id_list(sort_results) - + if query_func is None: return param_subs_or_alt_api(service, metrics, dimensions, sort_results=sort_results, **other_params) - + return get_metrics_by_dimensions_v3_style(service, query_func, param_subs_or_alt_api, metrics, dimensions, sort_results=sort_results, **other_params) @@ -105,15 +105,15 @@ def get_metrics_by_dimensions_v3_style(service, query_func, param_subs, metrics, metrics = join_id_list(metrics) dimensions = join_id_list(dimensions) sort_results = join_id_list(sort_results) - - # Dimensions and Metrics... + + # Dimensions and Metrics... # Dimensions are atrributes, Metrics are quantitative measurements. e.g. city is a Dimension # Sessions is a metric. #https://support.google.com/analytics/answer/1033861?hl=en#site-search-attribution&zippy=%2Cin-this-article - + # Required other params: ids, start_date, end_date # Other notable ones: filters, segment - + params = build_params({ 'ids': property_prefix + property, 'dimensions': dimensions, @@ -129,7 +129,7 @@ def get_metrics_by_dimensions_v3_style(service, query_func, param_subs, metrics, start_index_key = param_subs.get('start_index', 'start_index') max_results_key = param_subs.get('max_results', 'max_results') - + results = [] has_more = True @@ -138,8 +138,8 @@ def get_metrics_by_dimensions_v3_style(service, query_func, param_subs, metrics, has_more = ('rows' in result) and (len(result['rows']) > 0) if has_more or len(results) == 0: results.append(result) - params[start_index_key] += params[max_results_key] - + params[start_index_key] += params[max_results_key] + df = results_to_df(results) return df @@ -147,7 +147,7 @@ def get_metrics_by_dimensions_v3_style(service, query_func, param_subs, metrics, def get_metrics_by_dimensions_v4_style(service, metrics, dimensions, property, start_date, end_date, sort_results, metric_filter=None, dimension_filter=None, base_metric_filter=None, base_dimension_filter=None, property_prefix="properties/", max_results=1000, **other_params): property = property_prefix + property - + params = { "dateRanges": [{"startDate": start_date, "endDate": end_date}], "metrics": [{"name": metric} for metric in metrics], @@ -177,7 +177,7 @@ def get_metrics_by_dimensions_v4_style(service, metrics, dimensions, property, s else: offset += max_results params["offset"] = offset - + df = v4_results_to_df(results, dimensions, metrics) return df @@ -187,11 +187,11 @@ def v4_results_to_df(results, dimensions, metrics): return pd.DataFrame(columns=dimensions + metrics) df = pd.DataFrame() - for result in results: - # Collect column names + for result in results: + # Collect column names column_names = [header["name"] for header in result.get("dimensionHeaders", [])] + [header["name"] for header in result.get("metricHeaders", [])] - - # Get data + + # Get data if "rows" in result: data = [[cell["value"] for cell in row.get("dimensionValues", [])] + [cell["value"] for cell in row.get("metricValues", [])] for row in result["rows"]] else: @@ -285,26 +285,26 @@ def join_id_list(ids): def build_params(source, subs): result = {} - + for key, value in source.items(): if key in subs: - if not subs[key] is None: + if subs[key] is not None: result[subs[key]] = value else: result[key] = value - + return result def results_to_df(results): df = pd.DataFrame() - for result in results: - # Collect column nmes + for result in results: + # Collect column nmes column_names = [] for header in result.get('columnHeaders'): column_names.append(header.get('name')) - # Get data + # Get data data = result.get('rows') # Crete the dataframe diff --git a/analytics/analytics_package/analytics/entities.py b/analytics/analytics_package/analytics/entities.py index 75dffa850..3167b1de0 100644 --- a/analytics/analytics_package/analytics/entities.py +++ b/analytics/analytics_package/analytics/entities.py @@ -55,9 +55,9 @@ "id": "outbound_link_clicked", "alias": "Custom Outbound Link Click" } -# The builtin page view event. +# The builtin page view event. EVENT_PAGE_VIEW = { - "id": "page_view", + "id": "page_view", "alias": "Page View" } EVENT_INDEX_BULK_DOWNLOAD_SELECTED = { @@ -215,4 +215,4 @@ # Used as arguments in get_change_over_time_df class ADDITIONAL_DATA_BEHAVIOR(Enum): ADD = "add" # Sum the cached data with the api data - REPLACE = "replace"# Replace the api data with the cached data \ No newline at end of file + REPLACE = "replace"# Replace the api data with the cached data diff --git a/analytics/analytics_package/analytics/report_elements.py b/analytics/analytics_package/analytics/report_elements.py index adca2b7ca..9e521b6ab 100644 --- a/analytics/analytics_package/analytics/report_elements.py +++ b/analytics/analytics_package/analytics/report_elements.py @@ -1,13 +1,15 @@ +from urllib.parse import urlparse + import pandas as pd +from . import entities as e from ._report_utils import ( - get_data_df_from_fields, - get_one_period_change_series, get_change_over_time_df, get_change_over_time_df_multiple_events, + get_data_df_from_fields, + get_one_period_change_series, ) -from . import entities as e -from urllib.parse import urlparse + def get_bounds_for_month_and_prev(month): """ @@ -51,9 +53,9 @@ def get_outbound_links_df(analytics_params, ignore_index=True): # Get the custom "outbound_link_click" event df_custom_links = get_data_df_from_fields( [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_EVENT_NAME, e.DIMENSION_CUSTOM_URL, e.DIMENSION_PAGE_PATH], + [e.DIMENSION_EVENT_NAME, e.DIMENSION_CUSTOM_URL, e.DIMENSION_PAGE_PATH], dimension_filter=f"eventName=={e.EVENT_CUSTOM_CLICK['id']}", - **analytics_params, + **analytics_params, ).groupby( [e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_CUSTOM_URL["alias"]] ).sum().reset_index() @@ -94,7 +96,7 @@ def get_outbound_links_df(analytics_params, ignore_index=True): "complete_url": e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], e.METRIC_EVENT_COUNT["alias"]: e.SYNTHETIC_METRIC_CLICKS["alias"], "hostname": e.SYNTHETIC_DIMENSION_CLICKED_HOSTNAME["alias"], - } + } )[[ *dimension_aliases_to_keep, *metric_aliases_to_keep ]].copy() @@ -120,12 +122,12 @@ def get_outbound_links_change(analytics_params, start_current, end_current, star Metrics: SYNTHETIC_METRIC_CLICKS, METRIC_TOTAL_USERS """ return get_one_period_change_df( - get_outbound_links_df, + get_outbound_links_df, [e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS], - analytics_params, - start_current, - end_current, - start_previous, + analytics_params, + start_current, + end_current, + start_previous, end_previous, sort_results=[e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS] ) @@ -166,12 +168,12 @@ def get_page_views_change(analytics_params, start_current, end_current, start_pr Metrics: METRIC_PAGE_VIEWS, METRIC_TOTAL_USERS """ return get_one_period_change_df( - get_page_views_df, + get_page_views_df, [e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS], - analytics_params, - start_current, - end_current, - start_previous, + analytics_params, + start_current, + end_current, + start_previous, end_previous, sort_results=[e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS] ) @@ -194,7 +196,7 @@ def get_one_period_change_df(df_function, change_metrics, analytics_params, star :return: a DataFrame with the change between two periods for the given metrics, renamed to match titles Columns are dimension aliases (as strings), metric aliases (as ints), and metric change aliases (as floats) """ - + analytics_params_current = { **analytics_params, "start_date": start_current, @@ -301,7 +303,7 @@ def get_landing_page_change(analytics_params, start_current, end_current, start_ end_previous, sort_results=[e.METRIC_SESSIONS] ) - + def get_index_table_download_df(analytics_params, ignore_index=True): """ Get a DataFrame with firect file downloads from the Analytics API. @@ -554,7 +556,7 @@ def get_event_count_over_time_df(analytics_params, events, additional_data_path= Dimensions: DIMENSION_YEAR_MONTH (as a datetime) Metrics: METRIC_ACTIVE_USERS, METRIC_PAGE_VIEWS """ - + return get_change_over_time_df_multiple_events( e.METRIC_EVENT_COUNT, events, @@ -562,4 +564,4 @@ def get_event_count_over_time_df(analytics_params, events, additional_data_path= additional_data_path=additional_data_path, additional_data_behavior=additional_data_behavior, **analytics_params - ) \ No newline at end of file + ) diff --git a/analytics/analytics_package/analytics/static_site/__init__.py b/analytics/analytics_package/analytics/static_site/__init__.py index 786a236ef..d14b56521 100644 --- a/analytics/analytics_package/analytics/static_site/__init__.py +++ b/analytics/analytics_package/analytics/static_site/__init__.py @@ -1,5 +1,5 @@ from .charts import make_event_charts from .generator import generate_site -from .resolve import fetch_entity_title_map, enrich_detail_records +from .resolve import enrich_detail_records, fetch_entity_title_map __all__ = ["generate_site", "fetch_entity_title_map", "enrich_detail_records", "make_event_charts"] diff --git a/analytics/analytics_package/analytics/static_site/export.py b/analytics/analytics_package/analytics/static_site/export.py index 3920743fa..a3922e9af 100644 --- a/analytics/analytics_package/analytics/static_site/export.py +++ b/analytics/analytics_package/analytics/static_site/export.py @@ -9,9 +9,9 @@ from pandas.api.types import is_object_dtype, is_string_dtype from ..entities import ( - DIMENSION_PAGE_PATH, DIMENSION_FILTER_NAME, DIMENSION_FILTER_VALUE, + DIMENSION_PAGE_PATH, METRIC_EVENT_COUNT, METRIC_PAGE_VIEWS, SYNTHETIC_DIMENSION_CLICKED_LINK, diff --git a/analytics/analytics_package/analytics/static_site/fetch.py b/analytics/analytics_package/analytics/static_site/fetch.py index dfb7aa283..f4c76d396 100644 --- a/analytics/analytics_package/analytics/static_site/fetch.py +++ b/analytics/analytics_package/analytics/static_site/fetch.py @@ -2,22 +2,22 @@ import re from datetime import date -from urllib.parse import urlparse, parse_qs +from urllib.parse import parse_qs, urlparse from .. import report_elements as elements -from ..api import parse_filter_expressions from .._report_utils import get_data_df_from_fields +from ..api import parse_filter_expressions from ..entities import ( + ADDITIONAL_DATA_BEHAVIOR, + DIMENSION_CUSTOM_URL, + DIMENSION_ENTITY_NAME, + DIMENSION_EVENT_NAME, + DIMENSION_PAGE_PATH, + DIMENSION_PAGE_PATH_PLUS_QUERY, DIMENSION_YEAR_MONTH, METRIC_EVENT_COUNT, METRIC_PAGE_VIEWS, METRIC_SESSIONS, - DIMENSION_EVENT_NAME, - DIMENSION_PAGE_PATH, - DIMENSION_PAGE_PATH_PLUS_QUERY, - DIMENSION_CUSTOM_URL, - DIMENSION_ENTITY_NAME, - ADDITIONAL_DATA_BEHAVIOR, ) METRIC_ENGAGEMENT_RATE = { @@ -301,7 +301,9 @@ def _monthly_counts_from_df(df, start_date, end_date, page_path_regex=None): grouped[month_col] = grouped[month_col].apply( lambda m: f"{m[:4]}-{m[4:]}" if len(m) == 6 and "-" not in m else m ) - counts_by_month = dict(zip(grouped[month_col], grouped[count_col].astype(int))) + counts_by_month = dict( + zip(grouped[month_col], grouped[count_col].astype(int), strict=True) + ) return [{"month": m, "count": counts_by_month.get(m, 0)} for m in all_months] diff --git a/analytics/analytics_package/analytics/static_site/generator.py b/analytics/analytics_package/analytics/static_site/generator.py index eab09a44c..e8e9e7fee 100644 --- a/analytics/analytics_package/analytics/static_site/generator.py +++ b/analytics/analytics_package/analytics/static_site/generator.py @@ -3,8 +3,8 @@ import os import shutil -from .fetch import fetch_data from .export import export_data +from .fetch import fetch_data TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "template") diff --git a/analytics/anvil-catalog/generate_static_site.py b/analytics/anvil-catalog/generate_static_site.py index 3a055fa5c..16786ea30 100644 --- a/analytics/anvil-catalog/generate_static_site.py +++ b/analytics/anvil-catalog/generate_static_site.py @@ -5,7 +5,14 @@ import analytics.api as ga from analytics.static_site import generate_site -from constants import CURRENT_MONTH, ANVIL_CATALOG_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, HISTORIC_UA_DATA_PATH +from constants import ( + ANALYTICS_START, + ANVIL_CATALOG_ID, + CURRENT_MONTH, + HISTORIC_UA_DATA_PATH, + OAUTH_PORT, + SECRET_NAME, +) os.environ.setdefault(SECRET_NAME, "../../.credentials/anvil_ga4_credentials.json") diff --git a/analytics/anvil-explorer/generate_static_site.py b/analytics/anvil-explorer/generate_static_site.py index 47b3a731f..1843818de 100644 --- a/analytics/anvil-explorer/generate_static_site.py +++ b/analytics/anvil-explorer/generate_static_site.py @@ -4,8 +4,20 @@ import os import analytics.api as ga -from analytics.static_site import generate_site, fetch_entity_title_map, enrich_detail_records, make_event_charts -from constants import CURRENT_MONTH, ANVIL_EXPLORER_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, EXCLUDE_BOT_TRAFFIC_DATES +from analytics.static_site import ( + enrich_detail_records, + fetch_entity_title_map, + generate_site, + make_event_charts, +) +from constants import ( + ANALYTICS_START, + ANVIL_EXPLORER_ID, + CURRENT_MONTH, + EXCLUDE_BOT_TRAFFIC_DATES, + OAUTH_PORT, + SECRET_NAME, +) ANVIL_DATASETS_API_URL = "https://service.explore.anvilproject.org/index/datasets" diff --git a/analytics/hca-explorer/generate_static_site.py b/analytics/hca-explorer/generate_static_site.py index 1ec6dfe83..641ab2b71 100644 --- a/analytics/hca-explorer/generate_static_site.py +++ b/analytics/hca-explorer/generate_static_site.py @@ -4,8 +4,20 @@ import os import analytics.api as ga -from analytics.static_site import generate_site, fetch_entity_title_map, enrich_detail_records, make_event_charts -from constants import CURRENT_MONTH, HCA_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, HCA_BROWSER_ONLY_FILTER +from analytics.static_site import ( + enrich_detail_records, + fetch_entity_title_map, + generate_site, + make_event_charts, +) +from constants import ( + ANALYTICS_START, + CURRENT_MONTH, + HCA_BROWSER_ONLY_FILTER, + HCA_ID, + OAUTH_PORT, + SECRET_NAME, +) AZUL_PROJECTS_URL = "https://service.azul.data.humancellatlas.org/index/projects" diff --git a/analytics/lungmap/generate_static_site.py b/analytics/lungmap/generate_static_site.py index 61a8f467a..54b5be41c 100644 --- a/analytics/lungmap/generate_static_site.py +++ b/analytics/lungmap/generate_static_site.py @@ -4,8 +4,20 @@ import os import analytics.api as ga -from analytics.static_site import generate_site, fetch_entity_title_map, enrich_detail_records, make_event_charts -from constants import CURRENT_MONTH, LUNGMAP_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, HISTORIC_UA_DATA_PATH +from analytics.static_site import ( + enrich_detail_records, + fetch_entity_title_map, + generate_site, + make_event_charts, +) +from constants import ( + ANALYTICS_START, + CURRENT_MONTH, + HISTORIC_UA_DATA_PATH, + LUNGMAP_ID, + OAUTH_PORT, + SECRET_NAME, +) AZUL_PROJECTS_URL = "https://service.azul.data.humancellatlas.org/index/projects" From 5bf0d14069a6cf773c1bc7e504de4063c0165986 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:52:53 -0700 Subject: [PATCH 03/16] style: apply ruff format to the analytics package (#4934) Whitespace and wrapping only. Notably converts api.py from tab to space indentation -- it was the only tab-indented file in the tree. --- .../analytics/_report_utils.py | 121 +++- analytics/analytics_package/analytics/api.py | 591 ++++++++++-------- .../analytics_package/analytics/entities.py | 17 +- .../analytics/report_elements.py | 278 +++++--- .../analytics/static_site/__init__.py | 7 +- .../analytics/static_site/charts.py | 38 +- .../analytics/static_site/export.py | 43 +- .../analytics/static_site/fetch.py | 155 +++-- analytics/anvil-explorer/constants.py | 2 +- .../anvil-explorer/generate_static_site.py | 46 +- analytics/hca-explorer/constants.py | 10 +- .../hca-explorer/generate_static_site.py | 42 +- analytics/lungmap/constants.py | 2 +- analytics/lungmap/generate_static_site.py | 46 +- 14 files changed, 944 insertions(+), 454 deletions(-) diff --git a/analytics/analytics_package/analytics/_report_utils.py b/analytics/analytics_package/analytics/_report_utils.py index eef30e9eb..9eb0c1fbf 100644 --- a/analytics/analytics_package/analytics/_report_utils.py +++ b/analytics/analytics_package/analytics/_report_utils.py @@ -7,7 +7,15 @@ from .entities import ADDITIONAL_DATA_BEHAVIOR -def get_data_df(metrics, dimensions, percentage_metrics=None, percentage_suffix="_percentage", num_keep_dimensions=None, df_processor=None, **other_params): +def get_data_df( + metrics, + dimensions, + percentage_metrics=None, + percentage_suffix="_percentage", + num_keep_dimensions=None, + df_processor=None, + **other_params, +): if metrics is None: df = pd.DataFrame() else: @@ -27,16 +35,22 @@ def get_data_df(metrics, dimensions, percentage_metrics=None, percentage_suffix= if percentage_metrics: for metric in percentage_metrics: - df.insert(list(df.columns).index(metric) + 1, metric + percentage_suffix, df[metric] / df[metric].sum() * 100) + df.insert( + list(df.columns).index(metric) + 1, + metric + percentage_suffix, + df[metric] / df[metric].sum() * 100, + ) if df_processor: df = df_processor(df) return df + def strings_to_lists(*vals): return [[v] if isinstance(v, str) else v for v in vals] + def get_df_over_time(xlabels, metrics, dimensions, df_filter=None, **other_params): xlabels, metrics = strings_to_lists(xlabels, metrics) @@ -45,11 +59,13 @@ def get_df_over_time(xlabels, metrics, dimensions, df_filter=None, **other_param # Convert date to datetime object df.index = pd.to_datetime(df.index) - if (df_filter is not None): + if df_filter is not None: df = df_filter(df) # Rename for display - df.rename(columns={name: xlabels[i] for i, name in enumerate(df.columns)}, inplace=True) + df.rename( + columns={name: xlabels[i] for i, name in enumerate(df.columns)}, inplace=True + ) return df @@ -61,27 +77,38 @@ def get_data_df_from_fields(metrics, dimensions, **other_params): :param metrics: the metrics to get :param dimensions: the dimensions to get :param other_params: any other parameters to be passed to the get_data_df function, including service params - :return: a DataFrame with the data from the Analytics API. - The DF has an arbitrary RangeIndex, - string columns containing dimensions with names equal to the dimension alias value, + :return: a DataFrame with the data from the Analytics API. + The DF has an arbitrary RangeIndex, + string columns containing dimensions with names equal to the dimension alias value, and int columns containing metrics with names equal to the metric alias value. """ df = get_data_df( [metric["id"] for metric in metrics], [dimension["id"] for dimension in dimensions], - **other_params + **other_params, ) - return df.reset_index().rename(columns=get_rename_dict(dimensions+metrics)).copy() + return df.reset_index().rename(columns=get_rename_dict(dimensions + metrics)).copy() def get_rename_dict(dimensions): """Get a dictionary to rename the columns of a DataFrame.""" return dict( - zip([dimension["id"] for dimension in dimensions], [dimension["alias"] for dimension in dimensions], strict=True) + zip( + [dimension["id"] for dimension in dimensions], + [dimension["alias"] for dimension in dimensions], + strict=True, + ) ) -def get_one_period_change_series(series_current, series_previous, start_current, end_current, start_previous, end_previous): +def get_one_period_change_series( + series_current, + series_previous, + start_current, + end_current, + start_previous, + end_previous, +): """ Get the percent change between two serieses, accounting for different numbers of days in the month. :param series_current: the series representing the current month @@ -96,18 +123,40 @@ def get_one_period_change_series(series_current, series_previous, start_current, assert series_current.index.names == series_previous.index.names # Reindex both serieses to have the same index combined_index = series_current.index.union(series_previous.index) - current_length = float((dt.datetime.fromisoformat(end_current) - dt.datetime.fromisoformat(start_current)).days + 1) - previous_length = float((dt.datetime.fromisoformat(end_previous) - dt.datetime.fromisoformat(start_previous)).days + 1) + current_length = float( + ( + dt.datetime.fromisoformat(end_current) + - dt.datetime.fromisoformat(start_current) + ).days + + 1 + ) + previous_length = float( + ( + dt.datetime.fromisoformat(end_previous) + - dt.datetime.fromisoformat(start_previous) + ).days + + 1 + ) assert current_length != 0 and previous_length != 0 series_current_reindexed = series_current.reindex(combined_index).fillna(0) # Adjust the values from the prior series to account for the different number of days in the month - series_previous_reindexed = (series_previous.reindex(combined_index) * current_length / previous_length) - change = ((series_current_reindexed / series_previous_reindexed) - 1).replace({np.inf: np.nan}) + series_previous_reindexed = ( + series_previous.reindex(combined_index) * current_length / previous_length + ) + change = ((series_current_reindexed / series_previous_reindexed) - 1).replace( + {np.inf: np.nan} + ) return change def get_change_over_time_df( - metrics, time_dimension, include_changes=True, additional_data_path=None, additional_data_behavior=None, strftime_format="%Y-%m", **other_params + metrics, + time_dimension, + include_changes=True, + additional_data_path=None, + additional_data_behavior=None, + strftime_format="%Y-%m", + **other_params, ): """ Get a DataFrame with the change over time for the given metrics, renamed to match metric_titles @@ -126,8 +175,10 @@ def get_change_over_time_df( [metric["id"] for metric in metrics], time_dimension["id"], sort_results=[time_dimension["id"]], - df_processor=(lambda df: df.set_index(df.index + "01").sort_index(ascending=False)), - **other_params + df_processor=( + lambda df: df.set_index(df.index + "01").sort_index(ascending=False) + ), + **other_params, ).rename({time_dimension["id"]: time_dimension["alias"]}) df_combined = pd.DataFrame() @@ -139,23 +190,28 @@ def get_change_over_time_df( df_combined = df_api.add(df_saved.astype(int), fill_value=0)[::-1] elif additional_data_behavior == ADDITIONAL_DATA_BEHAVIOR.REPLACE: df_combined = pd.concat([df_saved, df_api], ignore_index=False) - df_combined = df_combined.loc[~df_combined.index.duplicated(keep="first")].sort_index(ascending=False) + df_combined = df_combined.loc[ + ~df_combined.index.duplicated(keep="first") + ].sort_index(ascending=False) else: df_combined = df_api if include_changes: - df_combined[ - [metric["change_alias"] for metric in metrics] - ] = df_combined[ - [metric["alias"] for metric in metrics] - ].pct_change(periods=-1).replace({np.inf: np.nan}) + df_combined[[metric["change_alias"] for metric in metrics]] = ( + df_combined[[metric["alias"] for metric in metrics]] + .pct_change(periods=-1) + .replace({np.inf: np.nan}) + ) if strftime_format is not None: df_combined.index = pd.to_datetime(df_combined.index).strftime(strftime_format) return df_combined.reset_index(names=time_dimension["alias"]) -def get_change_over_time_df_multiple_events(metric, events, time_dimension, **change_over_time_args): + +def get_change_over_time_df_multiple_events( + metric, events, time_dimension, **change_over_time_args +): """ Get a DataFrame with the change over time for the given metrics, renamed to match metric_titles :param metrics: the metrics to be displayed @@ -172,11 +228,16 @@ def get_change_over_time_df_multiple_events(metric, events, time_dimension, **ch [metric], time_dimension, **change_over_time_args, - dimension_filter=f"eventName=={event['id']}" - ).rename( - columns={metric["alias"]: event["alias"], metric["change_alias"]: event["change_alias"]} - ).set_index(time_dimension["alias"]) + dimension_filter=f"eventName=={event['id']}", + ) + .rename( + columns={ + metric["alias"]: event["alias"], + metric["change_alias"]: event["change_alias"], + } + ) + .set_index(time_dimension["alias"]) for event in events ], - axis=1 + axis=1, ) diff --git a/analytics/analytics_package/analytics/api.py b/analytics/analytics_package/analytics/api.py index 5ae1e3b4a..5637bf712 100644 --- a/analytics/analytics_package/analytics/api.py +++ b/analytics/analytics_package/analytics/api.py @@ -6,206 +6,295 @@ from googleapiclient.discovery import build ga_service_params = ( - ['https://www.googleapis.com/auth/analytics.readonly'], - 'analytics', 'v3', - {}, - lambda service, params: service.data().ga().get(**params).execute() + ["https://www.googleapis.com/auth/analytics.readonly"], + "analytics", + "v3", + {}, + lambda service, params: service.data().ga().get(**params).execute(), ) yt_service_params = ( - ['https://www.googleapis.com/auth/yt-analytics.readonly'], - 'youtubeAnalytics', 'v2', - { - 'start_date': 'startDate', - 'end_date': 'endDate', - 'start_index': 'startIndex', - 'max_results': 'maxResults', - 'segment': None - }, - lambda service, params: service.reports().query(**params).execute() + ["https://www.googleapis.com/auth/yt-analytics.readonly"], + "youtubeAnalytics", + "v2", + { + "start_date": "startDate", + "end_date": "endDate", + "start_index": "startIndex", + "max_results": "maxResults", + "segment": None, + }, + lambda service, params: service.reports().query(**params).execute(), ) drive_service_params = ( - ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets"], - "drive", "v3", - {}, + [ + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/spreadsheets", + ], + "drive", + "v3", + {}, ) sheets_service_params = ( - ["https://www.googleapis.com/auth/spreadsheets"], - "sheets", "v4", - {} + ["https://www.googleapis.com/auth/spreadsheets"], + "sheets", + "v4", + {}, ) next_port = None default_service_system = None -def authenticate(secret_name, first_service_params=ga_service_params, *other_service_params, port=None): - service_param_sets = (first_service_params,) + other_service_params - all_scopes = {scope for service_params in service_param_sets for scope in service_params[0]} +def authenticate( + secret_name, + first_service_params=ga_service_params, + *other_service_params, + port=None, +): + service_param_sets = (first_service_params,) + other_service_params - ANALYTICS_REPORTING_CLIENT_SECRET_PATH=os.getenv(secret_name) + all_scopes = { + scope for service_params in service_param_sets for scope in service_params[0] + } - flow = InstalledAppFlow.from_client_secrets_file(ANALYTICS_REPORTING_CLIENT_SECRET_PATH, - scopes=all_scopes) + ANALYTICS_REPORTING_CLIENT_SECRET_PATH = os.getenv(secret_name) - global next_port + flow = InstalledAppFlow.from_client_secrets_file( + ANALYTICS_REPORTING_CLIENT_SECRET_PATH, scopes=all_scopes + ) - if port is None: - if next_port is None: - port = 8082 - else: - port = next_port - next_port = port + 1 - elif next_port is None: - next_port = port + 1 + global next_port - credentials = flow.run_local_server(port=port) + if port is None: + if next_port is None: + port = 8082 + else: + port = next_port + next_port = port + 1 + elif next_port is None: + next_port = port + 1 - built_systems = [build_service_system(service_params, credentials) for service_params in service_param_sets] + credentials = flow.run_local_server(port=port) - return built_systems if len(built_systems) > 1 else built_systems[0] + built_systems = [ + build_service_system(service_params, credentials) + for service_params in service_param_sets + ] -def build_service_system(service_params, credentials): - if len(service_params) == 4: - service_name, service_version, param_subs_or_alt_api = service_params[1:] - query_func = None - else: - service_name, service_version, param_subs_or_alt_api, query_func = service_params[1:] - - # Build the service object. - service = build(service_name, service_version, credentials=credentials) - - service_system = (service, query_func, param_subs_or_alt_api, credentials) - - global default_service_system - if default_service_system is None: - default_service_system = service_system - - return service_system - - -def get_metrics_by_dimensions(metrics, dimensions, service_system=None, sort_results=None, **other_params): - if service_system is None: - service_system = default_service_system - - service, query_func, param_subs_or_alt_api = service_system[:3] - - metrics = normalize_id_list(metrics) - dimensions = normalize_id_list(dimensions) - sort_results = normalize_id_list(sort_results) - - if query_func is None: - return param_subs_or_alt_api(service, metrics, dimensions, sort_results=sort_results, **other_params) - - return get_metrics_by_dimensions_v3_style(service, query_func, param_subs_or_alt_api, metrics, dimensions, sort_results=sort_results, **other_params) - - -def get_metrics_by_dimensions_v3_style(service, query_func, param_subs, metrics, dimensions, property, start_date, end_date, sort_results, filters=None, segment=None, property_prefix='ga:', max_results=1000, **other_params): - metrics = join_id_list(metrics) - dimensions = join_id_list(dimensions) - sort_results = join_id_list(sort_results) - - # Dimensions and Metrics... - # Dimensions are atrributes, Metrics are quantitative measurements. e.g. city is a Dimension - # Sessions is a metric. - #https://support.google.com/analytics/answer/1033861?hl=en#site-search-attribution&zippy=%2Cin-this-article - - # Required other params: ids, start_date, end_date - # Other notable ones: filters, segment - - params = build_params({ - 'ids': property_prefix + property, - 'dimensions': dimensions, - 'metrics': metrics, - 'start_date': start_date, - 'end_date': end_date, - 'filters': filters, - 'segment': segment, - 'start_index': 1, - 'max_results': max_results, - 'sort': sort_results - }, param_subs) - - start_index_key = param_subs.get('start_index', 'start_index') - max_results_key = param_subs.get('max_results', 'max_results') + return built_systems if len(built_systems) > 1 else built_systems[0] - results = [] - has_more = True - while has_more: - result = query_func(service, params) - has_more = ('rows' in result) and (len(result['rows']) > 0) - if has_more or len(results) == 0: - results.append(result) - params[start_index_key] += params[max_results_key] - - df = results_to_df(results) - - return df - - -def get_metrics_by_dimensions_v4_style(service, metrics, dimensions, property, start_date, end_date, sort_results, metric_filter=None, dimension_filter=None, base_metric_filter=None, base_dimension_filter=None, property_prefix="properties/", max_results=1000, **other_params): - property = property_prefix + property - - params = { - "dateRanges": [{"startDate": start_date, "endDate": end_date}], - "metrics": [{"name": metric} for metric in metrics], - "dimensions": [{"name": dimension} for dimension in dimensions], - "metricFilter": parse_filter_expressions([base_metric_filter, metric_filter], True), - "dimensionFilter": parse_filter_expressions([base_dimension_filter, dimension_filter], False), - "orderBys": [({"dimension": {"dimensionName": field}} if field in dimensions else {"metric": {"metricName": field}}) for field in sort_results], - "limit": max_results - } - - offset = 0 - results = [] - rows_left = None - has_more = True - - while has_more: - result = service.properties().runReport(property=property, body=params).execute() - if rows_left is None: - rows_left = result.get("rowCount", 0) - page_row_count = len(result["rows"]) if "rows" in result else 0 - has_more = page_row_count > 0 - if has_more: - results.append(result) - rows_left -= page_row_count - if rows_left <= 0: - has_more = False - else: - offset += max_results - params["offset"] = offset - - df = v4_results_to_df(results, dimensions, metrics) +def build_service_system(service_params, credentials): + if len(service_params) == 4: + service_name, service_version, param_subs_or_alt_api = service_params[1:] + query_func = None + else: + service_name, service_version, param_subs_or_alt_api, query_func = ( + service_params[1:] + ) + + # Build the service object. + service = build(service_name, service_version, credentials=credentials) + + service_system = (service, query_func, param_subs_or_alt_api, credentials) + + global default_service_system + if default_service_system is None: + default_service_system = service_system + + return service_system + + +def get_metrics_by_dimensions( + metrics, dimensions, service_system=None, sort_results=None, **other_params +): + if service_system is None: + service_system = default_service_system + + service, query_func, param_subs_or_alt_api = service_system[:3] + + metrics = normalize_id_list(metrics) + dimensions = normalize_id_list(dimensions) + sort_results = normalize_id_list(sort_results) + + if query_func is None: + return param_subs_or_alt_api( + service, metrics, dimensions, sort_results=sort_results, **other_params + ) + + return get_metrics_by_dimensions_v3_style( + service, + query_func, + param_subs_or_alt_api, + metrics, + dimensions, + sort_results=sort_results, + **other_params, + ) + + +def get_metrics_by_dimensions_v3_style( + service, + query_func, + param_subs, + metrics, + dimensions, + property, + start_date, + end_date, + sort_results, + filters=None, + segment=None, + property_prefix="ga:", + max_results=1000, + **other_params, +): + metrics = join_id_list(metrics) + dimensions = join_id_list(dimensions) + sort_results = join_id_list(sort_results) + + # Dimensions and Metrics... + # Dimensions are atrributes, Metrics are quantitative measurements. e.g. city is a Dimension + # Sessions is a metric. + # https://support.google.com/analytics/answer/1033861?hl=en#site-search-attribution&zippy=%2Cin-this-article + + # Required other params: ids, start_date, end_date + # Other notable ones: filters, segment + + params = build_params( + { + "ids": property_prefix + property, + "dimensions": dimensions, + "metrics": metrics, + "start_date": start_date, + "end_date": end_date, + "filters": filters, + "segment": segment, + "start_index": 1, + "max_results": max_results, + "sort": sort_results, + }, + param_subs, + ) + + start_index_key = param_subs.get("start_index", "start_index") + max_results_key = param_subs.get("max_results", "max_results") + + results = [] + has_more = True + + while has_more: + result = query_func(service, params) + has_more = ("rows" in result) and (len(result["rows"]) > 0) + if has_more or len(results) == 0: + results.append(result) + params[start_index_key] += params[max_results_key] + + df = results_to_df(results) + + return df + + +def get_metrics_by_dimensions_v4_style( + service, + metrics, + dimensions, + property, + start_date, + end_date, + sort_results, + metric_filter=None, + dimension_filter=None, + base_metric_filter=None, + base_dimension_filter=None, + property_prefix="properties/", + max_results=1000, + **other_params, +): + property = property_prefix + property + + params = { + "dateRanges": [{"startDate": start_date, "endDate": end_date}], + "metrics": [{"name": metric} for metric in metrics], + "dimensions": [{"name": dimension} for dimension in dimensions], + "metricFilter": parse_filter_expressions( + [base_metric_filter, metric_filter], True + ), + "dimensionFilter": parse_filter_expressions( + [base_dimension_filter, dimension_filter], False + ), + "orderBys": [ + ( + {"dimension": {"dimensionName": field}} + if field in dimensions + else {"metric": {"metricName": field}} + ) + for field in sort_results + ], + "limit": max_results, + } + + offset = 0 + results = [] + rows_left = None + has_more = True + + while has_more: + result = ( + service.properties().runReport(property=property, body=params).execute() + ) + if rows_left is None: + rows_left = result.get("rowCount", 0) + page_row_count = len(result["rows"]) if "rows" in result else 0 + has_more = page_row_count > 0 + if has_more: + results.append(result) + rows_left -= page_row_count + if rows_left <= 0: + has_more = False + else: + offset += max_results + params["offset"] = offset + + df = v4_results_to_df(results, dimensions, metrics) + + return df - return df def v4_results_to_df(results, dimensions, metrics): - if (len(results) == 0): - return pd.DataFrame(columns=dimensions + metrics) + if len(results) == 0: + return pd.DataFrame(columns=dimensions + metrics) - df = pd.DataFrame() - for result in results: - # Collect column names - column_names = [header["name"] for header in result.get("dimensionHeaders", [])] + [header["name"] for header in result.get("metricHeaders", [])] + df = pd.DataFrame() + for result in results: + # Collect column names + column_names = [ + header["name"] for header in result.get("dimensionHeaders", []) + ] + [header["name"] for header in result.get("metricHeaders", [])] - # Get data - if "rows" in result: - data = [[cell["value"] for cell in row.get("dimensionValues", [])] + [cell["value"] for cell in row.get("metricValues", [])] for row in result["rows"]] - else: - data = None + # Get data + if "rows" in result: + data = [ + [cell["value"] for cell in row.get("dimensionValues", [])] + + [cell["value"] for cell in row.get("metricValues", [])] + for row in result["rows"] + ] + else: + data = None - # Crete the dataframe - df = pd.concat([df, pd.DataFrame(data, columns = column_names)]) + # Crete the dataframe + df = pd.concat([df, pd.DataFrame(data, columns=column_names)]) + + return df - return df ga4_service_params = ( - ['https://www.googleapis.com/auth/analytics.readonly'], - 'analyticsdata', 'v1beta', - get_metrics_by_dimensions_v4_style + ["https://www.googleapis.com/auth/analytics.readonly"], + "analyticsdata", + "v1beta", + get_metrics_by_dimensions_v4_style, ) filter_match_re = re.compile(r"^(\w+)(?:(==|>|<|>=|<=|=@|=~)|(!=|!@|!~))(.*)$") @@ -213,102 +302,114 @@ def v4_results_to_df(results, dimensions, metrics): filter_and_re = re.compile(r"((?:[^\\;]|\\(?:\\\\)*.)+)(?:;|$)") filter_or_re = re.compile(r"((?:[^\\,]|\\(?:\\\\)*.)+)(?:,|$)") filter_op_names = { - "==": "EQUAL", - "!=": "EQUAL", - ">": "GREATER_THAN", - "<": "LESS_THAN", - ">=": "GREATER_THAN_OR_EQUAL", - "<=": "LESS_THAN_OR_EQUAL", - "=@": "CONTAINS", - "!@": "CONTAINS", - "=~": "PARTIAL_REGEXP", - "!~": "PARTIAL_REGEXP" + "==": "EQUAL", + "!=": "EQUAL", + ">": "GREATER_THAN", + "<": "LESS_THAN", + ">=": "GREATER_THAN_OR_EQUAL", + "<=": "LESS_THAN_OR_EQUAL", + "=@": "CONTAINS", + "!@": "CONTAINS", + "=~": "PARTIAL_REGEXP", + "!~": "PARTIAL_REGEXP", } + def parse_filter_expression(text, is_metric): - if not isinstance(text, str): - return text - - def unescape(value): - return filter_escape_re.sub(r"\1", value) - - def parse_match(text): - field_name, plain_op, inverted_op, value = filter_match_re.match(text).groups() - op_name = filter_op_names[plain_op or inverted_op] - if is_metric: - plain_expression = { - "filter": { - "fieldName": field_name, - "numericFilter": { - "operation": op_name, - "value": { - "int64Value": value - } - } - } - } - else: - plain_expression = { - "filter": { - "fieldName": field_name, - "stringFilter": { - "matchType": "EXACT" if op_name == "EQUAL" else op_name, - "value": unescape(value), - "caseSensitive": True - } - } - } - return plain_expression if plain_op else {"notExpression": plain_expression} - - def parse_or(text): - or_terms = [parse_match(t) for t in filter_or_re.findall(text)] - return or_terms[0] if len(or_terms) == 1 else {"orGroup": {"expressions": or_terms}} - - and_terms = [parse_or(t) for t in filter_and_re.findall(text)] - return and_terms[0] if len(and_terms) == 1 else {"andGroup": {"expressions": and_terms}} + if not isinstance(text, str): + return text + + def unescape(value): + return filter_escape_re.sub(r"\1", value) + + def parse_match(text): + field_name, plain_op, inverted_op, value = filter_match_re.match(text).groups() + op_name = filter_op_names[plain_op or inverted_op] + if is_metric: + plain_expression = { + "filter": { + "fieldName": field_name, + "numericFilter": { + "operation": op_name, + "value": {"int64Value": value}, + }, + } + } + else: + plain_expression = { + "filter": { + "fieldName": field_name, + "stringFilter": { + "matchType": "EXACT" if op_name == "EQUAL" else op_name, + "value": unescape(value), + "caseSensitive": True, + }, + } + } + return plain_expression if plain_op else {"notExpression": plain_expression} + + def parse_or(text): + or_terms = [parse_match(t) for t in filter_or_re.findall(text)] + return ( + or_terms[0] + if len(or_terms) == 1 + else {"orGroup": {"expressions": or_terms}} + ) + + and_terms = [parse_or(t) for t in filter_and_re.findall(text)] + return ( + and_terms[0] + if len(and_terms) == 1 + else {"andGroup": {"expressions": and_terms}} + ) + def parse_filter_expressions(filters, is_metric): - result = None - for filter in filters: - parsed = parse_filter_expression(filter, is_metric) - if parsed: - result = parsed if result is None else {"andGroup": {"expressions": [result, parsed]}} - return result + result = None + for filter in filters: + parsed = parse_filter_expression(filter, is_metric) + if parsed: + result = ( + parsed + if result is None + else {"andGroup": {"expressions": [result, parsed]}} + ) + return result def normalize_id_list(ids): - return (ids.split(",") if isinstance(ids, str) else ids) if ids else [] + return (ids.split(",") if isinstance(ids, str) else ids) if ids else [] + def join_id_list(ids): - return ",".join(ids) if len(ids) > 0 else None + return ",".join(ids) if len(ids) > 0 else None def build_params(source, subs): - result = {} + result = {} - for key, value in source.items(): - if key in subs: - if subs[key] is not None: - result[subs[key]] = value - else: - result[key] = value + for key, value in source.items(): + if key in subs: + if subs[key] is not None: + result[subs[key]] = value + else: + result[key] = value - return result + return result def results_to_df(results): - df = pd.DataFrame() - for result in results: - # Collect column nmes - column_names = [] - for header in result.get('columnHeaders'): - column_names.append(header.get('name')) - - # Get data - data = result.get('rows') + df = pd.DataFrame() + for result in results: + # Collect column nmes + column_names = [] + for header in result.get("columnHeaders"): + column_names.append(header.get("name")) - # Crete the dataframe - df = pd.concat([df, pd.DataFrame(data, columns = column_names)]) + # Get data + data = result.get("rows") - return df + # Crete the dataframe + df = pd.concat([df, pd.DataFrame(data, columns=column_names)]) + return df diff --git a/analytics/analytics_package/analytics/entities.py b/analytics/analytics_package/analytics/entities.py index 3167b1de0..3341ada7e 100644 --- a/analytics/analytics_package/analytics/entities.py +++ b/analytics/analytics_package/analytics/entities.py @@ -45,21 +45,15 @@ # Event Names # The builtin outbound link click event. Stores the clicked URL in DIMENSION_BUILTIN_URL # Triggers under some circumstances where custom click does not, but does not include url fragments in any dimensions -EVENT_BUILTIN_CLICK = { - "id": "click", - "alias": "Builtin Outbound Link Click" -} +EVENT_BUILTIN_CLICK = {"id": "click", "alias": "Builtin Outbound Link Click"} # The custom outbound link click event. Stores the clicked URL DIMENSION_CUSTOM_URL # Includes url fragments, sometimes has a slightly different count to the built in click event EVENT_CUSTOM_CLICK = { "id": "outbound_link_clicked", - "alias": "Custom Outbound Link Click" + "alias": "Custom Outbound Link Click", } # The builtin page view event. -EVENT_PAGE_VIEW = { - "id": "page_view", - "alias": "Page View" -} +EVENT_PAGE_VIEW = {"id": "page_view", "alias": "Page View"} EVENT_INDEX_BULK_DOWNLOAD_SELECTED = { "id": "index_bulk_download_selected", "alias": "Bulk Download Selected", @@ -212,7 +206,8 @@ "alias": "Outbound Link", } + # Used as arguments in get_change_over_time_df class ADDITIONAL_DATA_BEHAVIOR(Enum): - ADD = "add" # Sum the cached data with the api data - REPLACE = "replace"# Replace the api data with the cached data + ADD = "add" # Sum the cached data with the api data + REPLACE = "replace" # Replace the api data with the cached data diff --git a/analytics/analytics_package/analytics/report_elements.py b/analytics/analytics_package/analytics/report_elements.py index 9e521b6ab..83e716f08 100644 --- a/analytics/analytics_package/analytics/report_elements.py +++ b/analytics/analytics_package/analytics/report_elements.py @@ -26,9 +26,10 @@ def get_bounds_for_month_and_prev(month): "start_current": start_current.strftime("%Y-%m-%d"), "end_current": end_current.strftime("%Y-%m-%d"), "start_previous": start_previous.strftime("%Y-%m-%d"), - "end_previous": end_previous.strftime("%Y-%m-%d") + "end_previous": end_previous.strftime("%Y-%m-%d"), } + def get_outbound_links_df(analytics_params, ignore_index=True): """ Get a DataFrame with outbound links from the Analytics API. Merges the builtin and custom events for outbound links. @@ -42,44 +43,62 @@ def get_outbound_links_df(analytics_params, ignore_index=True): """ assert "dimension_filter" not in analytics_params # Get the builtin "Click" event - df_builtin_links =get_data_df_from_fields( - [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_PAGE_PATH, e.DIMENSION_BUILTIN_URL, e.DIMENSION_EVENT_NAME], - dimension_filter=f"eventName=={e.EVENT_BUILTIN_CLICK['id']}", - **analytics_params, - ).groupby( - [e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_BUILTIN_URL["alias"]] - ).sum().reset_index() + df_builtin_links = ( + get_data_df_from_fields( + [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], + [e.DIMENSION_PAGE_PATH, e.DIMENSION_BUILTIN_URL, e.DIMENSION_EVENT_NAME], + dimension_filter=f"eventName=={e.EVENT_BUILTIN_CLICK['id']}", + **analytics_params, + ) + .groupby([e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_BUILTIN_URL["alias"]]) + .sum() + .reset_index() + ) # Get the custom "outbound_link_click" event - df_custom_links = get_data_df_from_fields( - [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_EVENT_NAME, e.DIMENSION_CUSTOM_URL, e.DIMENSION_PAGE_PATH], - dimension_filter=f"eventName=={e.EVENT_CUSTOM_CLICK['id']}", - **analytics_params, - ).groupby( - [e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_CUSTOM_URL["alias"]] - ).sum().reset_index() + df_custom_links = ( + get_data_df_from_fields( + [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], + [e.DIMENSION_EVENT_NAME, e.DIMENSION_CUSTOM_URL, e.DIMENSION_PAGE_PATH], + dimension_filter=f"eventName=={e.EVENT_CUSTOM_CLICK['id']}", + **analytics_params, + ) + .groupby([e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_CUSTOM_URL["alias"]]) + .sum() + .reset_index() + ) # Concatenate the two dataframes, avoiding duplicates # Keep the link from the builtin event, unless the link contains a #fragment, in which case keep the link from the custom event df_builtin_links["builtin"] = True - df_builtin_links["truncated_url"] = df_builtin_links[e.DIMENSION_BUILTIN_URL["alias"]] - df_custom_links["truncated_url"] = df_custom_links[e.DIMENSION_CUSTOM_URL["alias"]].str.replace(r"#.*", "", regex=True) - df_outbound_links_fragments = df_custom_links.loc[df_custom_links[e.DIMENSION_CUSTOM_URL["alias"]].str.contains("#")].copy() + df_builtin_links["truncated_url"] = df_builtin_links[ + e.DIMENSION_BUILTIN_URL["alias"] + ] + df_custom_links["truncated_url"] = df_custom_links[ + e.DIMENSION_CUSTOM_URL["alias"] + ].str.replace(r"#.*", "", regex=True) + df_outbound_links_fragments = df_custom_links.loc[ + df_custom_links[e.DIMENSION_CUSTOM_URL["alias"]].str.contains("#") + ].copy() df_outbound_links_fragments["is_fragment"] = True df_all_links = pd.concat( [df_builtin_links, df_outbound_links_fragments], ignore_index=True ) # Use the builtin link, unless the link is not in the custom links, in which case use the custom link df_all_links = df_all_links.loc[ - ~(df_all_links["truncated_url"].isin(df_outbound_links_fragments["truncated_url"]) & df_all_links["builtin"]) + ~( + df_all_links["truncated_url"].isin( + df_outbound_links_fragments["truncated_url"] + ) + & df_all_links["builtin"] + ) ].sort_values(e.METRIC_EVENT_COUNT["alias"], ascending=False) df_all_links["is_fragment"] = df_all_links["is_fragment"].fillna(False).astype(bool) # Use the builtin link, unless the link is a fragment, in which case use the custom link - df_all_links["complete_url"] = df_all_links[e.DIMENSION_BUILTIN_URL["alias"]].where( - ~df_all_links["is_fragment"], - df_all_links[e.DIMENSION_CUSTOM_URL["alias"]] + df_all_links["complete_url"] = df_all_links[e.DIMENSION_BUILTIN_URL["alias"]].where( + ~df_all_links["is_fragment"], df_all_links[e.DIMENSION_CUSTOM_URL["alias"]] + ) + df_all_links["hostname"] = df_all_links["complete_url"].map( + lambda x: urlparse(x).hostname ) - df_all_links["hostname"] = df_all_links["complete_url"].map(lambda x: urlparse(x).hostname) dimension_aliases_to_keep = [ e.DIMENSION_PAGE_PATH["alias"], e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], @@ -89,24 +108,34 @@ def get_outbound_links_df(analytics_params, ignore_index=True): e.SYNTHETIC_METRIC_CLICKS["alias"], e.METRIC_TOTAL_USERS["alias"], ] - df_all_links = df_all_links.drop( - columns=[e.DIMENSION_BUILTIN_URL["alias"], e.DIMENSION_CUSTOM_URL["alias"], "builtin", "is_fragment"] - ).rename( - columns={ - "complete_url": e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], - e.METRIC_EVENT_COUNT["alias"]: e.SYNTHETIC_METRIC_CLICKS["alias"], - "hostname": e.SYNTHETIC_DIMENSION_CLICKED_HOSTNAME["alias"], - } - )[[ - *dimension_aliases_to_keep, *metric_aliases_to_keep - ]].copy() + df_all_links = ( + df_all_links.drop( + columns=[ + e.DIMENSION_BUILTIN_URL["alias"], + e.DIMENSION_CUSTOM_URL["alias"], + "builtin", + "is_fragment", + ] + ) + .rename( + columns={ + "complete_url": e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], + e.METRIC_EVENT_COUNT["alias"]: e.SYNTHETIC_METRIC_CLICKS["alias"], + "hostname": e.SYNTHETIC_DIMENSION_CLICKED_HOSTNAME["alias"], + } + )[[*dimension_aliases_to_keep, *metric_aliases_to_keep]] + .copy() + ) if not ignore_index: return df_all_links.set_index(dimension_aliases_to_keep) else: return df_all_links.reset_index(drop=True) -def get_outbound_links_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_outbound_links_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with outbound links from the Analytics API and a comparison for the prior period @@ -115,7 +144,7 @@ def get_outbound_links_change(analytics_params, start_current, end_current, star :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the outbound links from the Analytics API. + :return: a DataFrame with the outbound links from the Analytics API. By default, dimensions and metrics both form columns. Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_PAGE_PATH, SYNTHETIC_DIMENSION_CLICKED_HOSTNAME, SYNTHETIC_DIMENSION_CLICKED_LINK @@ -129,9 +158,10 @@ def get_outbound_links_change(analytics_params, start_current, end_current, star end_current, start_previous, end_previous, - sort_results=[e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS] + sort_results=[e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS], ) + def get_page_views_df(analytics_params, ignore_index=False): """ Get a DataFrame with page views from the Analytics API @@ -148,12 +178,21 @@ def get_page_views_df(analytics_params, ignore_index=False): [e.DIMENSION_PAGE_PATH, e.DIMENSION_EVENT_NAME], **analytics_params, dimension_filter=f"eventName=={e.EVENT_PAGE_VIEW['id']}", - )[[e.DIMENSION_PAGE_PATH["alias"], e.METRIC_PAGE_VIEWS["alias"], e.METRIC_TOTAL_USERS["alias"]]].copy() + )[ + [ + e.DIMENSION_PAGE_PATH["alias"], + e.METRIC_PAGE_VIEWS["alias"], + e.METRIC_TOTAL_USERS["alias"], + ] + ].copy() if not ignore_index: df_response = df_response.set_index(e.DIMENSION_PAGE_PATH["alias"]) return df_response -def get_page_views_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_page_views_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with page views from the Analytics API and a comparison for the prior month @@ -175,10 +214,22 @@ def get_page_views_change(analytics_params, start_current, end_current, start_pr end_current, start_previous, end_previous, - sort_results=[e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS], ) -def get_one_period_change_df(df_function, change_metrics, analytics_params, start_current, end_current, start_previous, end_previous, sort_results=None, sort_ascending=False, ignore_index=False): + +def get_one_period_change_df( + df_function, + change_metrics, + analytics_params, + start_current, + end_current, + start_previous, + end_previous, + sort_results=None, + sort_ascending=False, + ignore_index=False, +): """ Get a DataFrame with the change between two periods for the given metrics, renamed to match titles :param df_function: a function that returns a dataframe, with numerical columns matching the aliases of change_metrics @@ -208,45 +259,49 @@ def get_one_period_change_df(df_function, change_metrics, analytics_params, star "end_date": end_previous, } - df_current = df_function( - analytics_params_current, - ignore_index=False - ) - df_previous = df_function( - analytics_params_previous, - ignore_index=False - ) + df_current = df_function(analytics_params_current, ignore_index=False) + df_previous = df_function(analytics_params_previous, ignore_index=False) df_changes = pd.concat( [ get_one_period_change_series( - df_current[metric["alias"]], df_previous[metric["alias"]], start_current, end_current, start_previous, end_previous - ) for metric in change_metrics + df_current[metric["alias"]], + df_previous[metric["alias"]], + start_current, + end_current, + start_previous, + end_previous, + ) + for metric in change_metrics ], axis=1, ).rename( columns={metric["alias"]: metric["change_alias"] for metric in change_metrics} ) df_current_with_changes = pd.concat( - [df_current.reindex(df_changes.index).fillna(0), df_changes], - axis=1 + [df_current.reindex(df_changes.index).fillna(0), df_changes], axis=1 ) if sort_results: df_current_with_changes = df_current_with_changes.sort_values( - [metric["alias"] for metric in sort_results], ascending=sort_ascending, kind="stable" + [metric["alias"] for metric in sort_results], + ascending=sort_ascending, + kind="stable", ) if ignore_index: return df_current_with_changes else: return df_current_with_changes.reset_index() -def get_page_views_over_time_df(analytics_params, additional_data_path=None, additional_data_behavior=None): + +def get_page_views_over_time_df( + analytics_params, additional_data_path=None, additional_data_behavior=None +): """ Get a DataFrame with pageviews and total active users over time from the Analytics API. :param analytics_params: the parameters for the Analytics API, including service params, start dates, and end dates :param additional_data_path: the path to a JSON file with additional data to be added to the DataFrame, defaults to None :param additional_data_behavior: the behavior to use when adding the additional data, as an instance of ADDITIONAL_DATA_BEHAVIOR, defaults to None - :return: a DataFrame with the pageviews and total active users over time from the Analytics API. + :return: a DataFrame with the pageviews and total active users over time from the Analytics API. Columns are the dimension aliases, metrics (as ints), and change metrics (as floats) Dimensions: DIMENSION_YEAR_MONTH (as a datetime) Metrics: METRIC_ACTIVE_USERS, METRIC_PAGE_VIEWS @@ -256,9 +311,10 @@ def get_page_views_over_time_df(analytics_params, additional_data_path=None, add e.DIMENSION_YEAR_MONTH, additional_data_path=additional_data_path, additional_data_behavior=additional_data_behavior, - **analytics_params + **analytics_params, ) + def get_landing_page_df(analytics_params, ignore_index=True): """ Get a DataFrame with landing pages from the Analytics API. @@ -279,7 +335,10 @@ def get_landing_page_df(analytics_params, ignore_index=True): df_response = df_response.set_index(e.DIMENSION_LANDING_PAGE["alias"]) return df_response -def get_landing_page_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_landing_page_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with landing pages from the Analytics API and a comparison for the prior month :param analytics_params: the parameters for the Analytics API, including authentication and property ids @@ -287,7 +346,7 @@ def get_landing_page_change(analytics_params, start_current, end_current, start_ :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_LANDING_PAGE @@ -301,9 +360,10 @@ def get_landing_page_change(analytics_params, start_current, end_current, start_ end_current, start_previous, end_previous, - sort_results=[e.METRIC_SESSIONS] + sort_results=[e.METRIC_SESSIONS], ) + def get_index_table_download_df(analytics_params, ignore_index=True): """ Get a DataFrame with firect file downloads from the Analytics API. @@ -318,15 +378,28 @@ def get_index_table_download_df(analytics_params, ignore_index=True): assert "dimension_filter" not in analytics_params df_response = get_data_df_from_fields( [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_ENTITY_NAME, e.DIMENSION_RELATED_ENTITY_ID, e.DIMENSION_RELATED_ENTITY_NAME], + [ + e.DIMENSION_ENTITY_NAME, + e.DIMENSION_RELATED_ENTITY_ID, + e.DIMENSION_RELATED_ENTITY_NAME, + ], **analytics_params, dimension_filter=f"eventName=={e.EVENT_FILE_DOWNLOADED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME["alias"], e.DIMENSION_RELATED_ENTITY_ID["alias"], e.DIMENSION_RELATED_ENTITY_NAME["alias"]]) + df_response = df_response.set_index( + [ + e.DIMENSION_ENTITY_NAME["alias"], + e.DIMENSION_RELATED_ENTITY_ID["alias"], + e.DIMENSION_RELATED_ENTITY_NAME["alias"], + ] + ) return df_response -def get_index_table_download_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_table_download_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with firect file downloads from the Analytics API and a comparison for the prior month :param analytics_params: the parameters for the Analytics API, including authentication and property ids @@ -334,7 +407,7 @@ def get_index_table_download_change(analytics_params, start_current, end_current :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME, DIMENSION_RELATED_ENTITY_ID, DIMENSION_RELATED_ENTITY_NAME @@ -348,9 +421,10 @@ def get_index_table_download_change(analytics_params, start_current, end_current end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) + def get_index_entity_selected_df(analytics_params, ignore_index=True): """ Get a DataFrame with index tab selections from the Analytics API. @@ -373,7 +447,10 @@ def get_index_entity_selected_df(analytics_params, ignore_index=True): df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME_TAB["alias"]]) return df_response -def get_index_entity_selected_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_entity_selected_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index tab selections from the Analytics API and a comparison for the prior month :param analytics_params: the parameters for the Analytics API, including authentication and property ids @@ -381,7 +458,7 @@ def get_index_entity_selected_change(analytics_params, start_current, end_curren :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME_TAB @@ -395,9 +472,10 @@ def get_index_entity_selected_change(analytics_params, start_current, end_curren end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) + def get_index_entity_table_sorted_df(analytics_params, ignore_index=True): """ Get a DataFrame with index table sortings from the Analytics API. @@ -412,15 +490,28 @@ def get_index_entity_table_sorted_df(analytics_params, ignore_index=True): assert "dimension_filter" not in analytics_params df_response = get_data_df_from_fields( [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_ENTITY_NAME_TAB, e.DIMENSION_COLUMN_NAME, e.DIMENSION_SORT_DIRECTION], + [ + e.DIMENSION_ENTITY_NAME_TAB, + e.DIMENSION_COLUMN_NAME, + e.DIMENSION_SORT_DIRECTION, + ], **analytics_params, dimension_filter=f"eventName=={e.EVENT_ENTITY_TABLE_SORTED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME_TAB["alias"], e.DIMENSION_COLUMN_NAME["alias"], e.DIMENSION_SORT_DIRECTION["alias"]]) + df_response = df_response.set_index( + [ + e.DIMENSION_ENTITY_NAME_TAB["alias"], + e.DIMENSION_COLUMN_NAME["alias"], + e.DIMENSION_SORT_DIRECTION["alias"], + ] + ) return df_response -def get_index_entity_table_sorted_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_entity_table_sorted_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index table sortings from the Analytics API and a comparison for the prior month @@ -443,9 +534,10 @@ def get_index_entity_table_sorted_change(analytics_params, start_current, end_cu end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) + def get_index_entity_table_paginated_df(analytics_params, ignore_index=True): """ Get a DataFrame with index table paginations from the Analytics API. @@ -465,10 +557,18 @@ def get_index_entity_table_paginated_df(analytics_params, ignore_index=True): dimension_filter=f"eventName=={e.EVENT_ENTITY_TABLE_PAGINATED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME_TAB["alias"], e.DIMENSION_PAGINATION_DIRECTION["alias"]]) + df_response = df_response.set_index( + [ + e.DIMENSION_ENTITY_NAME_TAB["alias"], + e.DIMENSION_PAGINATION_DIRECTION["alias"], + ] + ) return df_response -def get_index_entity_table_paginated_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_entity_table_paginated_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index table paginations from the Analytics API and a comparison for the prior month @@ -477,7 +577,7 @@ def get_index_entity_table_paginated_change(analytics_params, start_current, end :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME_TAB, DIMENSION_PAGINATION_DIRECTION @@ -492,9 +592,10 @@ def get_index_entity_table_paginated_change(analytics_params, start_current, end start_previous, end_previous, sort_results=[e.DIMENSION_ENTITY_NAME_TAB, e.DIMENSION_PAGINATION_DIRECTION], - sort_ascending=True + sort_ascending=True, ) + def get_index_filter_selected_df(analytics_params, ignore_index=True): """ Get a DataFrame with index filter selections from the Analytics API. @@ -514,10 +615,15 @@ def get_index_filter_selected_df(analytics_params, ignore_index=True): dimension_filter=f"eventName=={e.EVENT_FILTER_SELECTED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_FILTER_NAME["alias"], e.DIMENSION_FILTER_VALUE["alias"]]) + df_response = df_response.set_index( + [e.DIMENSION_FILTER_NAME["alias"], e.DIMENSION_FILTER_VALUE["alias"]] + ) return df_response -def get_index_filter_selected_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_filter_selected_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index filter selections from the Analytics API and a comparison for the prior month @@ -526,7 +632,7 @@ def get_index_filter_selected_change(analytics_params, start_current, end_curren :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME_TAB, DIMENSION_FILTER_NAME, DIMENSION_FILTER_VALUE @@ -540,18 +646,20 @@ def get_index_filter_selected_change(analytics_params, start_current, end_curren end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) -def get_event_count_over_time_df(analytics_params, events, additional_data_path=None, additional_data_behavior=None): +def get_event_count_over_time_df( + analytics_params, events, additional_data_path=None, additional_data_behavior=None +): """ Get a DataFrame with pageviews and total active users over time from the Analytics API. :param analytics_params: the parameters for the Analytics API, including service params, start dates, and end dates :param additional_data_path: the path to a JSON file with additional data to be added to the DataFrame, defaults to None :param additional_data_behavior: the behavior to use when adding the additional data, as an instance of ADDITIONAL_DATA_BEHAVIOR, defaults to None - :return: a DataFrame with the pageviews and total active users over time from the Analytics API. + :return: a DataFrame with the pageviews and total active users over time from the Analytics API. Columns are the dimension aliases, metrics (as ints), and change metrics (as floats) Dimensions: DIMENSION_YEAR_MONTH (as a datetime) Metrics: METRIC_ACTIVE_USERS, METRIC_PAGE_VIEWS @@ -563,5 +671,5 @@ def get_event_count_over_time_df(analytics_params, events, additional_data_path= e.DIMENSION_YEAR_MONTH, additional_data_path=additional_data_path, additional_data_behavior=additional_data_behavior, - **analytics_params + **analytics_params, ) diff --git a/analytics/analytics_package/analytics/static_site/__init__.py b/analytics/analytics_package/analytics/static_site/__init__.py index d14b56521..34efb2690 100644 --- a/analytics/analytics_package/analytics/static_site/__init__.py +++ b/analytics/analytics_package/analytics/static_site/__init__.py @@ -2,4 +2,9 @@ from .generator import generate_site from .resolve import enrich_detail_records, fetch_entity_title_map -__all__ = ["generate_site", "fetch_entity_title_map", "enrich_detail_records", "make_event_charts"] +__all__ = [ + "generate_site", + "fetch_entity_title_map", + "enrich_detail_records", + "make_event_charts", +] diff --git a/analytics/analytics_package/analytics/static_site/charts.py b/analytics/analytics_package/analytics/static_site/charts.py index 95b6c5fd3..983df6c46 100644 --- a/analytics/analytics_package/analytics/static_site/charts.py +++ b/analytics/analytics_package/analytics/static_site/charts.py @@ -19,22 +19,48 @@ def make_event_charts(entity_label, entity_path, chart_start): { "title": "Export to Terra", "series": [ - {"label": f"Single {entity_label}", "event_key": "dataset_analyze_in_terra_requested", "event_name": "dataset_analyze_in_terra_requested"}, - {"label": f"Cross-{entity_label}", "event_key": "index_analyze_in_terra_requested", "event_name": "index_analyze_in_terra_requested"}, + { + "label": f"Single {entity_label}", + "event_key": "dataset_analyze_in_terra_requested", + "event_name": "dataset_analyze_in_terra_requested", + }, + { + "label": f"Cross-{entity_label}", + "event_key": "index_analyze_in_terra_requested", + "event_name": "index_analyze_in_terra_requested", + }, ], }, { "title": "curl Command", "series": [ - {"label": f"Single {entity_label}", "event_key": "dataset_bulk_download_requested", "event_name": "bulk_download_requested", "page_path_regex": rf"^/{path_segment}/[0-9a-f-]+"}, - {"label": f"Cross-{entity_label}", "event_key": "index_bulk_download_requested", "event_name": "bulk_download_requested", "page_path_regex": rf"^(?!/{path_segment}/[0-9a-f-]+)"}, + { + "label": f"Single {entity_label}", + "event_key": "dataset_bulk_download_requested", + "event_name": "bulk_download_requested", + "page_path_regex": rf"^/{path_segment}/[0-9a-f-]+", + }, + { + "label": f"Cross-{entity_label}", + "event_key": "index_bulk_download_requested", + "event_name": "bulk_download_requested", + "page_path_regex": rf"^(?!/{path_segment}/[0-9a-f-]+)", + }, ], }, { "title": "File Manifest", "series": [ - {"label": f"Single {entity_label}", "event_key": "dataset_file_manifest_requested", "event_name": "dataset_file_manifest_requested"}, - {"label": f"Cross-{entity_label}", "event_key": "index_file_manifest_requested", "event_name": "index_file_manifest_requested"}, + { + "label": f"Single {entity_label}", + "event_key": "dataset_file_manifest_requested", + "event_name": "dataset_file_manifest_requested", + }, + { + "label": f"Cross-{entity_label}", + "event_key": "index_file_manifest_requested", + "event_name": "index_file_manifest_requested", + }, ], }, ], diff --git a/analytics/analytics_package/analytics/static_site/export.py b/analytics/analytics_package/analytics/static_site/export.py index a3922e9af..1edb37e52 100644 --- a/analytics/analytics_package/analytics/static_site/export.py +++ b/analytics/analytics_package/analytics/static_site/export.py @@ -44,7 +44,11 @@ def export_df_as_json(df, col_map, change_col, filename, output_dir): for col in output_names: col_dtype = export[col].dtype - if not (col == "change" or is_object_dtype(col_dtype) or is_string_dtype(col_dtype)): + if not ( + col == "change" + or is_object_dtype(col_dtype) + or is_string_dtype(col_dtype) + ): export[col] = export[col].fillna(0).astype(int) records = export.to_dict(orient="records") @@ -57,7 +61,15 @@ def export_df_as_json(df, col_map, change_col, filename, output_dir): print(f" Wrote {filename} ({len(records)} records)") -def export_data(data, config, current_month, analytics_start, custom_events, output_dir, event_charts=None): +def export_data( + data, + config, + current_month, + analytics_start, + custom_events, + output_dir, + event_charts=None, +): """Export all analytics data to JSON files. Args: @@ -101,7 +113,10 @@ def export_data(data, config, current_month, analytics_start, custom_events, out print("Exporting outbound links data...") export_df_as_json( data["outbound"], - {SYNTHETIC_DIMENSION_CLICKED_LINK["alias"]: "link", SYNTHETIC_METRIC_CLICKS["alias"]: "clicks"}, + { + SYNTHETIC_DIMENSION_CLICKED_LINK["alias"]: "link", + SYNTHETIC_METRIC_CLICKS["alias"]: "clicks", + }, SYNTHETIC_METRIC_CLICKS["change_alias"], "outbound_links.json", output_dir, @@ -110,7 +125,11 @@ def export_data(data, config, current_month, analytics_start, custom_events, out print("Exporting filter selections data...") export_df_as_json( data.get("filter_selected"), - {DIMENSION_FILTER_NAME["alias"]: "filterName", DIMENSION_FILTER_VALUE["alias"]: "filterValue", METRIC_EVENT_COUNT["alias"]: "count"}, + { + DIMENSION_FILTER_NAME["alias"]: "filterName", + DIMENSION_FILTER_VALUE["alias"]: "filterValue", + METRIC_EVENT_COUNT["alias"]: "count", + }, METRIC_EVENT_COUNT["change_alias"], "filter_selected.json", output_dir, @@ -142,7 +161,9 @@ def export_data(data, config, current_month, analytics_start, custom_events, out search_queries = data.get("search_queries", {"total": 0, "queries": []}) with open(os.path.join(output_dir, "search_queries.json"), "w") as f: json.dump(search_queries, f, indent=2) - print(f" Wrote search_queries.json ({len(search_queries.get('queries', []))} queries)") + print( + f" Wrote search_queries.json ({len(search_queries.get('queries', []))} queries)" + ) # Custom events print("Exporting custom events data...") @@ -188,11 +209,13 @@ def export_data(data, config, current_month, analytics_start, custom_events, out for series in chart.get("series", []): key = series["event_key"] monthly_counts = data.get(f"event_chart_{key}", []) - chart_data["series"].append({ - "label": series["label"], - "event_key": key, - "data": monthly_counts, - }) + chart_data["series"].append( + { + "label": series["label"], + "event_key": key, + "data": monthly_counts, + } + ) chart_output["charts"].append(chart_data) with open(os.path.join(output_dir, "event_charts.json"), "w") as f: diff --git a/analytics/analytics_package/analytics/static_site/fetch.py b/analytics/analytics_package/analytics/static_site/fetch.py index f4c76d396..9480e61de 100644 --- a/analytics/analytics_package/analytics/static_site/fetch.py +++ b/analytics/analytics_package/analytics/static_site/fetch.py @@ -29,15 +29,15 @@ # broken markdown links, asset requests, etc.). SUSPICIOUS_PAGE_PATH_RE = re.compile( r"(" - r"^/?\].*" # broken markdown links e.g. /](https://...) - r"|.*https?://.*" # concatenated URLs e.g. /overview/securityhttps://... - r"|^/[^/]*@[^/]*" # email-as-path e.g. /help@lists... - r"|^//.*" # double-slash probes e.g. //checkout/ - r"|^/[^/]*\.[^/]+$" # file extensions at root e.g. /robots.txt, /favicon-32x32.png - r"|^/feed$" # RSS probes - r"|^/\).*" # broken parens e.g. /), /). - r"|^/[^/]*\).*" # broken parens e.g. /events) - r"|^/docs(-\w+)?/" # CMS probes e.g. /docs/, /docs-EN/ + r"^/?\].*" # broken markdown links e.g. /](https://...) + r"|.*https?://.*" # concatenated URLs e.g. /overview/securityhttps://... + r"|^/[^/]*@[^/]*" # email-as-path e.g. /help@lists... + r"|^//.*" # double-slash probes e.g. //checkout/ + r"|^/[^/]*\.[^/]+$" # file extensions at root e.g. /robots.txt, /favicon-32x32.png + r"|^/feed$" # RSS probes + r"|^/\).*" # broken parens e.g. /), /). + r"|^/[^/]*\).*" # broken parens e.g. /events) + r"|^/docs(-\w+)?/" # CMS probes e.g. /docs/, /docs-EN/ r")" ) @@ -71,12 +71,16 @@ def _count_events(event_name, params, page_path_regex=None, click_url_regex=None df = df[df[DIMENSION_PAGE_PATH["alias"]].str.match(page_path_regex, na=False)] if click_url_regex: - df = df[df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False)] + df = df[ + df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False) + ] return int(df[METRIC_EVENT_COUNT["alias"]].sum()) -def get_custom_event_change(event_name, params_current, params_prior, page_path_regex=None, click_url_regex=None): +def get_custom_event_change( + event_name, params_current, params_prior, page_path_regex=None, click_url_regex=None +): """Fetch a custom event count with month-over-month change. Args: @@ -89,8 +93,12 @@ def get_custom_event_change(event_name, params_current, params_prior, page_path_ Returns: Dict with "current", "prior", and "change" keys. """ - current_count = _count_events(event_name, params_current, page_path_regex, click_url_regex) - prior_count = _count_events(event_name, params_prior, page_path_regex, click_url_regex) + current_count = _count_events( + event_name, params_current, page_path_regex, click_url_regex + ) + prior_count = _count_events( + event_name, params_prior, page_path_regex, click_url_regex + ) change = None if prior_count > 0: @@ -99,7 +107,9 @@ def get_custom_event_change(event_name, params_current, params_prior, page_path_ return {"current": current_count, "prior": prior_count, "change": change} -def get_event_detail_table(event_name, params, page_path_regex=None, click_url_regex=None): +def get_event_detail_table( + event_name, params, page_path_regex=None, click_url_regex=None +): """Fetch event details broken down by page path and entity name. Args: @@ -130,7 +140,9 @@ def get_event_detail_table(event_name, params, page_path_regex=None, click_url_r df = df[df[DIMENSION_PAGE_PATH["alias"]].str.match(page_path_regex, na=False)] if click_url_regex: - df = df[df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False)] + df = df[ + df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False) + ] if len(df) == 0: return [] @@ -186,17 +198,23 @@ def get_access_requests(params, url_patterns): return [] url_col = DIMENSION_CUSTOM_URL["alias"] - mask = df[url_col].str.contains("|".join(re.escape(p) for p in url_patterns), case=False, na=False) + mask = df[url_col].str.contains( + "|".join(re.escape(p) for p in url_patterns), case=False, na=False + ) df = df[mask] if len(df) == 0: return [] - result = df[[DIMENSION_PAGE_PATH["alias"], url_col, METRIC_EVENT_COUNT["alias"]]].copy() + result = df[ + [DIMENSION_PAGE_PATH["alias"], url_col, METRIC_EVENT_COUNT["alias"]] + ].copy() result.columns = ["page_path", "click_url", "count"] result["count"] = result["count"].astype(int) # Normalize page paths to entity base path (e.g., /projects/UUID/sub-page -> /projects/UUID). - result["page_path"] = result["page_path"].str.replace(_ENTITY_PATH_RE, r"\1", regex=True) + result["page_path"] = result["page_path"].str.replace( + _ENTITY_PATH_RE, r"\1", regex=True + ) result = result.groupby(["page_path", "click_url"], as_index=False)["count"].sum() result = result.sort_values("count", ascending=False) return result.to_dict(orient="records") @@ -261,6 +279,7 @@ def get_search_queries(params, search_path="/search"): def _generate_month_range(start_date, end_date): """Generate a list of YYYY-MM strings covering the given date range.""" from datetime import datetime + start = datetime.strptime(start_date[:7], "%Y-%m") end = datetime.strptime(end_date[:7], "%Y-%m") months = [] @@ -369,7 +388,9 @@ def fetch_data( if exclude_dates: for d in exclude_dates: if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", d): - raise ValueError(f"exclude_dates entries must be YYYY-MM-DD, got: {d!r}") + raise ValueError( + f"exclude_dates entries must be YYYY-MM-DD, got: {d!r}" + ) date.fromisoformat(d) base_dimension_filter = parse_filter_expressions( [ @@ -397,16 +418,37 @@ def fetch_data( } if base_dimension_filter: params["base_dimension_filter"] = base_dimension_filter - params_all_time = {**params, "start_date": analytics_start, "end_date": end_date_current} - params_prior = {**params, "start_date": start_date_prior, "end_date": end_date_prior} + params_all_time = { + **params, + "start_date": analytics_start, + "end_date": end_date_current, + } + params_prior = { + **params, + "start_date": start_date_prior, + "end_date": end_date_prior, + } print("Fetching monthly traffic data...") - historic_kwargs = {"additional_data_path": historic_data_path, "additional_data_behavior": ADDITIONAL_DATA_BEHAVIOR.ADD} if historic_data_path else {} - df_monthly_traffic = elements.get_page_views_over_time_df(params_all_time, **historic_kwargs) + historic_kwargs = ( + { + "additional_data_path": historic_data_path, + "additional_data_behavior": ADDITIONAL_DATA_BEHAVIOR.ADD, + } + if historic_data_path + else {} + ) + df_monthly_traffic = elements.get_page_views_over_time_df( + params_all_time, **historic_kwargs + ) print("Fetching pageviews data...") df_pageviews = elements.get_page_views_change( - params, start_date_current, end_date_current, start_date_prior, end_date_prior, + params, + start_date_current, + end_date_current, + start_date_prior, + end_date_prior, ) if exclude_pages and df_pageviews is not None and len(df_pageviews) > 0: @@ -416,7 +458,9 @@ def fetch_data( if df_pageviews is not None and len(df_pageviews) > 0: page_col = DIMENSION_PAGE_PATH["alias"] - suspicious_mask = df_pageviews[page_col].str.match(SUSPICIOUS_PAGE_PATH_RE, na=False) + suspicious_mask = df_pageviews[page_col].str.match( + SUSPICIOUS_PAGE_PATH_RE, na=False + ) n_suspicious = suspicious_mask.sum() if n_suspicious > 0: df_pageviews = df_pageviews[~suspicious_mask] @@ -424,13 +468,21 @@ def fetch_data( print("Fetching outbound links data...") df_outbound = elements.get_outbound_links_change( - params, start_date_current, end_date_current, start_date_prior, end_date_prior, + params, + start_date_current, + end_date_current, + start_date_prior, + end_date_prior, ) print("Fetching filter selections data...") try: df_filter_selected = elements.get_index_filter_selected_change( - params, start_date_current, end_date_current, start_date_prior, end_date_prior, + params, + start_date_current, + end_date_current, + start_date_prior, + end_date_prior, ) except Exception as e: print(f" Skipped (not available for this property): {e}") @@ -438,15 +490,35 @@ def fetch_data( print("Fetching sessions and engagement data...") df_sessions_current = get_data_df_from_fields( - [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], [], **params, + [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], + [], + **params, ) df_sessions_prior = get_data_df_from_fields( - [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], [], **params_prior, + [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], + [], + **params_prior, + ) + sessions_current = ( + int(df_sessions_current[METRIC_SESSIONS["alias"]].sum()) + if len(df_sessions_current) > 0 + else 0 + ) + sessions_prior = ( + int(df_sessions_prior[METRIC_SESSIONS["alias"]].sum()) + if len(df_sessions_prior) > 0 + else 0 + ) + engagement_current = ( + float(df_sessions_current[METRIC_ENGAGEMENT_RATE["alias"]].mean()) + if len(df_sessions_current) > 0 + else 0 + ) + engagement_prior = ( + float(df_sessions_prior[METRIC_ENGAGEMENT_RATE["alias"]].mean()) + if len(df_sessions_prior) > 0 + else 0 ) - sessions_current = int(df_sessions_current[METRIC_SESSIONS["alias"]].sum()) if len(df_sessions_current) > 0 else 0 - sessions_prior = int(df_sessions_prior[METRIC_SESSIONS["alias"]].sum()) if len(df_sessions_prior) > 0 else 0 - engagement_current = float(df_sessions_current[METRIC_ENGAGEMENT_RATE["alias"]].mean()) if len(df_sessions_current) > 0 else 0 - engagement_prior = float(df_sessions_prior[METRIC_ENGAGEMENT_RATE["alias"]].mean()) if len(df_sessions_prior) > 0 else 0 print("Fetching file downloads data...") try: @@ -504,21 +576,28 @@ def fetch_data( key = event_key(event) print(f"Fetching {event['label']} data...") data[f"event_{key}"] = get_custom_event_change( - event["event_name"], params, params_prior, + event["event_name"], + params, + params_prior, page_path_regex=event.get("page_path_regex"), click_url_regex=event.get("click_url_regex"), ) if event.get("detail_table"): print(f"Fetching {event['label']} detail table...") data[f"event_{key}_detail"] = get_event_detail_table( - event["event_name"], params, + event["event_name"], + params, page_path_regex=event.get("page_path_regex"), click_url_regex=event.get("click_url_regex"), ) if event_charts: chart_start = event_charts.get("chart_start", analytics_start) - params_chart = {**params, "start_date": chart_start, "end_date": end_date_current} + params_chart = { + **params, + "start_date": chart_start, + "end_date": end_date_current, + } data["event_chart_config"] = event_charts # Group series by event_name to avoid duplicate API calls @@ -534,7 +613,9 @@ def fetch_data( df = _fetch_event_monthly_df(event_name, params_chart, needs_page_path) for series in series_list: data[f"event_chart_{series['event_key']}"] = _monthly_counts_from_df( - df, chart_start, end_date_current, + df, + chart_start, + end_date_current, page_path_regex=series.get("page_path_regex"), ) diff --git a/analytics/anvil-explorer/constants.py b/analytics/anvil-explorer/constants.py index 0a558f55b..dfa2338c9 100644 --- a/analytics/anvil-explorer/constants.py +++ b/analytics/anvil-explorer/constants.py @@ -6,7 +6,7 @@ # Dates of known synthetic/bot traffic (headless-browser burst, likely a load test) # to exclude from reports; see DataBiosphere/data-browser#4907. EXCLUDE_BOT_TRAFFIC_DATES = ["2025-02-10", "2025-02-11"] -SECRET_NAME = 'ANVIL_ANALYTICS_REPORTING_CLIENT_SECRET_PATH' +SECRET_NAME = "ANVIL_ANALYTICS_REPORTING_CLIENT_SECRET_PATH" ANALYTICS_START = "2024-01-01" OAUTH_PORT = 8082 diff --git a/analytics/anvil-explorer/generate_static_site.py b/analytics/anvil-explorer/generate_static_site.py index 1843818de..6faba7cca 100644 --- a/analytics/anvil-explorer/generate_static_site.py +++ b/analytics/anvil-explorer/generate_static_site.py @@ -31,7 +31,9 @@ def resolve_dataset_titles(data): - title_map = fetch_entity_title_map(ANVIL_DATASETS_API_URL, "datasets", "title", page_size=1000) + title_map = fetch_entity_title_map( + ANVIL_DATASETS_API_URL, "datasets", "title", page_size=1000 + ) count = enrich_detail_records(data, title_map, r"/datasets/([0-9a-f-]+)") print(f" Enriched {count} records with dataset titles") @@ -50,21 +52,47 @@ def resolve_dataset_titles(data): "summary_stats": [ { "label": "Cohort Export Requests", - "event_keys": ["index_bulk_download_requested", "index_file_manifest_requested", "index_analyze_in_terra_requested"], + "event_keys": [ + "index_bulk_download_requested", + "index_file_manifest_requested", + "index_analyze_in_terra_requested", + ], }, { "label": "Dataset Export Requests", - "event_keys": ["dataset_bulk_download_requested", "dataset_file_manifest_requested", "dataset_analyze_in_terra_requested"], + "event_keys": [ + "dataset_bulk_download_requested", + "dataset_file_manifest_requested", + "dataset_analyze_in_terra_requested", + ], }, ], "file_downloads_position": 3, "event_counts": [ - {"label": "Export to Terra\n(Single Dataset)", "event_key": "dataset_analyze_in_terra_requested"}, - {"label": "Export to Terra\n(Cross-Dataset)", "event_key": "index_analyze_in_terra_requested"}, - {"label": "curl Command\n(Single Dataset)", "event_key": "dataset_bulk_download_requested"}, - {"label": "curl Command\n(Cross-Dataset)", "event_key": "index_bulk_download_requested"}, - {"label": "File Manifest\n(Single Dataset)", "event_key": "dataset_file_manifest_requested"}, - {"label": "File Manifest\n(Cross-Dataset)", "event_key": "index_file_manifest_requested"}, + { + "label": "Export to Terra\n(Single Dataset)", + "event_key": "dataset_analyze_in_terra_requested", + }, + { + "label": "Export to Terra\n(Cross-Dataset)", + "event_key": "index_analyze_in_terra_requested", + }, + { + "label": "curl Command\n(Single Dataset)", + "event_key": "dataset_bulk_download_requested", + }, + { + "label": "curl Command\n(Cross-Dataset)", + "event_key": "index_bulk_download_requested", + }, + { + "label": "File Manifest\n(Single Dataset)", + "event_key": "dataset_file_manifest_requested", + }, + { + "label": "File Manifest\n(Cross-Dataset)", + "event_key": "index_file_manifest_requested", + }, ], }, property_id=ANVIL_EXPLORER_ID, diff --git a/analytics/hca-explorer/constants.py b/analytics/hca-explorer/constants.py index daabae2dd..18887f758 100644 --- a/analytics/hca-explorer/constants.py +++ b/analytics/hca-explorer/constants.py @@ -4,7 +4,15 @@ HCA_ID = "361323030" # Filter to exclude the Data Explorer -HCA_BROWSER_ONLY_FILTER = {"filter": {"fieldName": "hostName", "stringFilter": {"matchType": "EXACT", "value": "explore.data.humancellatlas.org"}}} +HCA_BROWSER_ONLY_FILTER = { + "filter": { + "fieldName": "hostName", + "stringFilter": { + "matchType": "EXACT", + "value": "explore.data.humancellatlas.org", + }, + } +} SECRET_NAME = "HCA_ANALYTICS_REPORTING_CLIENT_SECRET_PATH" ANALYTICS_START = "2024-02-01" diff --git a/analytics/hca-explorer/generate_static_site.py b/analytics/hca-explorer/generate_static_site.py index 641ab2b71..58dc76e42 100644 --- a/analytics/hca-explorer/generate_static_site.py +++ b/analytics/hca-explorer/generate_static_site.py @@ -50,21 +50,47 @@ def resolve_project_titles(data): "summary_stats": [ { "label": "Cohort Export Requests", - "event_keys": ["index_bulk_download_requested", "index_file_manifest_requested", "index_analyze_in_terra_requested"], + "event_keys": [ + "index_bulk_download_requested", + "index_file_manifest_requested", + "index_analyze_in_terra_requested", + ], }, { "label": "Project Export Requests", - "event_keys": ["dataset_bulk_download_requested", "dataset_file_manifest_requested", "dataset_analyze_in_terra_requested"], + "event_keys": [ + "dataset_bulk_download_requested", + "dataset_file_manifest_requested", + "dataset_analyze_in_terra_requested", + ], }, ], "file_downloads_position": 3, "event_counts": [ - {"label": "Export to Terra\n(Single Project)", "event_key": "dataset_analyze_in_terra_requested"}, - {"label": "Export to Terra\n(Cross-Project)", "event_key": "index_analyze_in_terra_requested"}, - {"label": "curl Command\n(Single Project)", "event_key": "dataset_bulk_download_requested"}, - {"label": "curl Command\n(Cross-Project)", "event_key": "index_bulk_download_requested"}, - {"label": "File Manifest\n(Single Project)", "event_key": "dataset_file_manifest_requested"}, - {"label": "File Manifest\n(Cross-Project)", "event_key": "index_file_manifest_requested"}, + { + "label": "Export to Terra\n(Single Project)", + "event_key": "dataset_analyze_in_terra_requested", + }, + { + "label": "Export to Terra\n(Cross-Project)", + "event_key": "index_analyze_in_terra_requested", + }, + { + "label": "curl Command\n(Single Project)", + "event_key": "dataset_bulk_download_requested", + }, + { + "label": "curl Command\n(Cross-Project)", + "event_key": "index_bulk_download_requested", + }, + { + "label": "File Manifest\n(Single Project)", + "event_key": "dataset_file_manifest_requested", + }, + { + "label": "File Manifest\n(Cross-Project)", + "event_key": "index_file_manifest_requested", + }, ], }, property_id=HCA_ID, diff --git a/analytics/lungmap/constants.py b/analytics/lungmap/constants.py index 1ff20d9d7..777c8c612 100644 --- a/analytics/lungmap/constants.py +++ b/analytics/lungmap/constants.py @@ -4,7 +4,7 @@ HISTORIC_UA_DATA_PATH = "users_over_time_history.json" LUNGMAP_ID = "362871218" -SECRET_NAME = 'ANALYTICS_REPORTING_CLIENT_SECRET_PATH' +SECRET_NAME = "ANALYTICS_REPORTING_CLIENT_SECRET_PATH" ANALYTICS_START = "2023-07-01" OAUTH_PORT = 8082 diff --git a/analytics/lungmap/generate_static_site.py b/analytics/lungmap/generate_static_site.py index 54b5be41c..5bac5c780 100644 --- a/analytics/lungmap/generate_static_site.py +++ b/analytics/lungmap/generate_static_site.py @@ -31,7 +31,9 @@ def resolve_project_titles(data): - title_map = fetch_entity_title_map(AZUL_PROJECTS_URL, "projects", "projectTitle", catalog="lm10") + title_map = fetch_entity_title_map( + AZUL_PROJECTS_URL, "projects", "projectTitle", catalog="lm10" + ) count = enrich_detail_records(data, title_map, r"/projects/([0-9a-f-]+)") print(f" Enriched {count} records with project titles") @@ -50,21 +52,47 @@ def resolve_project_titles(data): "summary_stats": [ { "label": "Cohort Export Requests", - "event_keys": ["index_bulk_download_requested", "index_file_manifest_requested", "index_analyze_in_terra_requested"], + "event_keys": [ + "index_bulk_download_requested", + "index_file_manifest_requested", + "index_analyze_in_terra_requested", + ], }, { "label": "Project Export Requests", - "event_keys": ["dataset_bulk_download_requested", "dataset_file_manifest_requested", "dataset_analyze_in_terra_requested"], + "event_keys": [ + "dataset_bulk_download_requested", + "dataset_file_manifest_requested", + "dataset_analyze_in_terra_requested", + ], }, ], "file_downloads_position": 3, "event_counts": [ - {"label": "Export to Terra\n(Single Project)", "event_key": "dataset_analyze_in_terra_requested"}, - {"label": "Export to Terra\n(Cross-Project)", "event_key": "index_analyze_in_terra_requested"}, - {"label": "curl Command\n(Single Project)", "event_key": "dataset_bulk_download_requested"}, - {"label": "curl Command\n(Cross-Project)", "event_key": "index_bulk_download_requested"}, - {"label": "File Manifest\n(Single Project)", "event_key": "dataset_file_manifest_requested"}, - {"label": "File Manifest\n(Cross-Project)", "event_key": "index_file_manifest_requested"}, + { + "label": "Export to Terra\n(Single Project)", + "event_key": "dataset_analyze_in_terra_requested", + }, + { + "label": "Export to Terra\n(Cross-Project)", + "event_key": "index_analyze_in_terra_requested", + }, + { + "label": "curl Command\n(Single Project)", + "event_key": "dataset_bulk_download_requested", + }, + { + "label": "curl Command\n(Cross-Project)", + "event_key": "index_bulk_download_requested", + }, + { + "label": "File Manifest\n(Single Project)", + "event_key": "dataset_file_manifest_requested", + }, + { + "label": "File Manifest\n(Cross-Project)", + "event_key": "index_file_manifest_requested", + }, ], }, property_id=LUNGMAP_ID, From ab88ab02b6c6f543cd2274376a67a669ab0b0e0f Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:59:27 -0700 Subject: [PATCH 04/16] ci: run ruff against the analytics package (#4934) Adds an analytics job to run-checks.yml that syncs the uv environment against the lockfile, runs ruff check and ruff format --check, and smoke-imports the package to prove the pyproject packaging installs. --- .github/workflows/run-checks.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml index db866b2d2..c63eb95a6 100644 --- a/.github/workflows/run-checks.yml +++ b/.github/workflows/run-checks.yml @@ -17,3 +17,19 @@ jobs: cd files npm ci npx tsc --noEmit + + analytics: + runs-on: ubuntu-latest + defaults: + run: + working-directory: analytics + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + - run: | + uv sync --locked + uv run ruff check . + uv run ruff format --check . + uv run python -c "import analytics.static_site" From 1cf052f1aefdb6bd82dd24b0b61a3bc1dd501379 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:59:48 -0700 Subject: [PATCH 05/16] docs: update analytics setup instructions for uv (#4934) Replaces the venv + pip + requirements.txt instructions with uv sync, and documents the ruff commands. --- analytics/readme.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/analytics/readme.md b/analytics/readme.md index b1ce27dd5..b3a5923f4 100644 --- a/analytics/readme.md +++ b/analytics/readme.md @@ -1,14 +1,26 @@ ## Installing the environment -- Use Python 3.12.4. -- Run `python -m venv ./venv` to create a new environment under `./venv`. -- Run `source ./venv/bin/activate` to activate the environment. -- Run `pip install -r ./requirements.txt` to install requirements. +Dependencies are managed with [uv](https://docs.astral.sh/uv/), and pinned in `uv.lock`. -## Deactivating/reactivating +- Install uv, e.g. with `brew install uv` or `curl -LsSf https://astral.sh/uv/install.sh | sh`. +- From this folder, run `uv sync`. This creates `./.venv`, provisions the Python version given in + `.python-version`, and installs the `analytics_package` in editable mode along with its dependencies. -- To deactivate the environment, run `deactivate`. -- To activate the environment again, run `source ./venv/bin/activate`. +There's no need to activate the environment: prefixing a command with `uv run` runs it in the +environment, re-syncing first if the lockfile has changed. If you'd rather activate it anyway, run +`source ../.venv/bin/activate` from an app subfolder (or `source ./.venv/bin/activate` from here), and +`deactivate` to exit. + +To change the package's dependencies, edit `analytics_package/pyproject.toml` and run `uv lock` to +update `uv.lock`. + +## Linting and formatting + +Linting and formatting are handled by [ruff](https://docs.astral.sh/ruff/), configured in +`pyproject.toml` and enforced by the `analytics` job in `run-checks.yml`. From this folder: + +- `uv run ruff check .` to lint, or `uv run ruff check --fix .` to apply the automatic fixes. +- `uv run ruff format .` to format, or `uv run ruff format --check .` to check without writing. ## Generating reports for the static site @@ -21,7 +33,7 @@ Each app-specific analytics subfolder (e.g. `anvil-explorer`, `hca-explorer`, `l - `.credentials/hca_ga4_credentials.json`, for HCA Data Explorer and LungMAP. - Alternatively: A path set in the environment variable specified by the `SECRET_NAME` variable in the folder's `constants.py`. - Update the `CURRENT_MONTH` variable in the folder's `constants.py` to the month you wish to generate the report for. -- From within the folder, run `python generate_static_site.py`. +- From within the folder, run `uv run python generate_static_site.py`. A browser window will open for Google OAuth. After authenticating, the script fetches data from GA4 and writes the site to the configured folder. With the exception of `anvil-catalog` (which is a retired app and writes to `anvil-catalog/site`), this will be a subfolder of `gh-pages` under the repository root. From b3940a32230c8a2fa33df14e9f70d71bf8e6675d Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:16:24 -0700 Subject: [PATCH 06/16] chore: add npm scripts to run ruff (#4934) --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index ef813e578..6cd707763 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "start": "npx serve out", "lint": "eslint .", "check-format": "prettier --check .", + "lint:python": "cd analytics && uv run ruff check", + "format:python": "cd analytics && uv run ruff format", "prepare": "husky", "test": "jest --watch", "test:e2e": "playwright test", From a2c8904c5bcd8c019bdf87a9b9f62efa89e47d82 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:52:39 -0700 Subject: [PATCH 07/16] chore: remove obsolete venv folder from gitignore (#4934) --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index c261d674f..37aa1304a 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,6 @@ _templates __pycache__/ *.egg-info/ /analytics/**/site/ -/analytics/venv /analytics/.venv .ruff_cache/ From 3de99f18d574a1252f2f2c130f4c8f896aca91c3 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:19:41 -0700 Subject: [PATCH 08/16] ci: run ruff checks before installing all dependencies for import check (#4934) --- .github/workflows/run-checks.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml index c63eb95a6..408827fe1 100644 --- a/.github/workflows/run-checks.yml +++ b/.github/workflows/run-checks.yml @@ -29,7 +29,7 @@ jobs: with: enable-cache: true - run: | + uv run --locked --only-group dev ruff check . + uv run --locked --only-group dev ruff format --check . uv sync --locked - uv run ruff check . - uv run ruff format --check . - uv run python -c "import analytics.static_site" + uv run --no-sync python -c "import analytics.static_site" From 0f4390d254f7e44e64732c7fa230781c60a1c9e7 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:23:06 -0700 Subject: [PATCH 09/16] refactor: simplify dict construction previously done via zip (#4934) --- analytics/analytics_package/analytics/_report_utils.py | 10 ++-------- .../analytics_package/analytics/static_site/fetch.py | 4 +--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/analytics/analytics_package/analytics/_report_utils.py b/analytics/analytics_package/analytics/_report_utils.py index 9eb0c1fbf..3b49feff4 100644 --- a/analytics/analytics_package/analytics/_report_utils.py +++ b/analytics/analytics_package/analytics/_report_utils.py @@ -90,15 +90,9 @@ def get_data_df_from_fields(metrics, dimensions, **other_params): return df.reset_index().rename(columns=get_rename_dict(dimensions + metrics)).copy() -def get_rename_dict(dimensions): +def get_rename_dict(fields): """Get a dictionary to rename the columns of a DataFrame.""" - return dict( - zip( - [dimension["id"] for dimension in dimensions], - [dimension["alias"] for dimension in dimensions], - strict=True, - ) - ) + return {field["id"]: field["alias"] for field in fields} def get_one_period_change_series( diff --git a/analytics/analytics_package/analytics/static_site/fetch.py b/analytics/analytics_package/analytics/static_site/fetch.py index 9480e61de..a5fddaac1 100644 --- a/analytics/analytics_package/analytics/static_site/fetch.py +++ b/analytics/analytics_package/analytics/static_site/fetch.py @@ -320,9 +320,7 @@ def _monthly_counts_from_df(df, start_date, end_date, page_path_regex=None): grouped[month_col] = grouped[month_col].apply( lambda m: f"{m[:4]}-{m[4:]}" if len(m) == 6 and "-" not in m else m ) - counts_by_month = dict( - zip(grouped[month_col], grouped[count_col].astype(int), strict=True) - ) + counts_by_month = grouped.set_index(month_col)[count_col].astype(int).to_dict() return [{"month": m, "count": counts_by_month.get(m, 0)} for m in all_months] From 4c1e4913a1e530a16fe27c156038d48ef12d29eb Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:27:17 -0700 Subject: [PATCH 10/16] chore: make non-package status explicit and remove unnecessary ruff excludes (#4934) --- analytics/pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml index 6127dd474..8b1ee0ccf 100644 --- a/analytics/pyproject.toml +++ b/analytics/pyproject.toml @@ -5,6 +5,9 @@ description = "Environment for generating the static analytics site reports." requires-python = ">=3.12" dependencies = ["analytics"] +[tool.uv] +package = false + [tool.uv.workspace] members = ["analytics_package"] @@ -14,8 +17,5 @@ analytics = { workspace = true } [dependency-groups] dev = ["ruff==0.16.3"] -[tool.ruff] -extend-exclude = ["venv", ".venv"] - [tool.ruff.lint] select = ["E4", "E7", "E9", "F", "I", "W", "B"] From 8410fdd127e123f155e3897af8ce24e75b41e86b Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:31:03 -0700 Subject: [PATCH 11/16] chore: refine uv/ruff commands and add `check-format:python` script (#4934) --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6cd707763..0165f48f0 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "start": "npx serve out", "lint": "eslint .", "check-format": "prettier --check .", - "lint:python": "cd analytics && uv run ruff check", - "format:python": "cd analytics && uv run ruff format", + "lint:python": "uv run --directory analytics ruff check .", + "check-format:python": "uv run --directory analytics ruff format --check .", + "format:python": "uv run --directory analytics ruff format .", "prepare": "husky", "test": "jest --watch", "test:e2e": "playwright test", From f66798d5370cc91738c8ba5ac01a46d482668c8c Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:26:47 -0700 Subject: [PATCH 12/16] docs: note npm scripts in readme (#4934) --- analytics/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/analytics/readme.md b/analytics/readme.md index b3a5923f4..6da08f639 100644 --- a/analytics/readme.md +++ b/analytics/readme.md @@ -22,6 +22,8 @@ Linting and formatting are handled by [ruff](https://docs.astral.sh/ruff/), conf - `uv run ruff check .` to lint, or `uv run ruff check --fix .` to apply the automatic fixes. - `uv run ruff format .` to format, or `uv run ruff format --check .` to check without writing. +The NPM scripts `lint:python`, `format:python`, and `check-format:python` can also be used as shortcuts. + ## Generating reports for the static site Each app-specific analytics subfolder (e.g. `anvil-explorer`, `hca-explorer`, `lungmap`) contains a script that can be run to generate a report for the static site. To run these scripts: From 2e8470705ee9110b231481c1f3fafe4b6a773830 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:34:54 -0700 Subject: [PATCH 13/16] revert: "chore: remove obsolete venv folder from gitignore (#4934)" This reverts commit 5052ad5b5131db515f979c486bb9b3a5a4ca66e1. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 37aa1304a..c261d674f 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ _templates __pycache__/ *.egg-info/ /analytics/**/site/ +/analytics/venv /analytics/.venv .ruff_cache/ From 89e9ae18fc9e2627077bcc7cdeafabdd803789fe Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:42:28 -0700 Subject: [PATCH 14/16] chore: add additional flags to ruff npm scripts and focus on npm scripts in readme (#4934) --- analytics/readme.md | 11 +++++------ package.json | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/analytics/readme.md b/analytics/readme.md index 6da08f639..b80ed06c7 100644 --- a/analytics/readme.md +++ b/analytics/readme.md @@ -16,13 +16,12 @@ update `uv.lock`. ## Linting and formatting -Linting and formatting are handled by [ruff](https://docs.astral.sh/ruff/), configured in -`pyproject.toml` and enforced by the `analytics` job in `run-checks.yml`. From this folder: +Linting and formatting are handled by [Ruff](https://docs.astral.sh/ruff/), configured in +`pyproject.toml` and enforced by the `analytics` job in `run-checks.yml`. NPM scripts can be used +as shortcuts to run Ruff: -- `uv run ruff check .` to lint, or `uv run ruff check --fix .` to apply the automatic fixes. -- `uv run ruff format .` to format, or `uv run ruff format --check .` to check without writing. - -The NPM scripts `lint:python`, `format:python`, and `check-format:python` can also be used as shortcuts. +- `npm run lint:python` to lint, or `npm run lint:python -- --fix` to apply the automatic fixes. +- `npm run format:python` to format, or `npm run check-format:python` to check without writing. ## Generating reports for the static site diff --git a/package.json b/package.json index 0165f48f0..f66b2e7a8 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "start": "npx serve out", "lint": "eslint .", "check-format": "prettier --check .", - "lint:python": "uv run --directory analytics ruff check .", - "check-format:python": "uv run --directory analytics ruff format --check .", - "format:python": "uv run --directory analytics ruff format .", + "lint:python": "uv run --directory analytics --only-dev --locked ruff check .", + "check-format:python": "uv run --directory analytics --only-dev --locked ruff format --check .", + "format:python": "uv run --directory analytics --only-dev --locked ruff format .", "prepare": "husky", "test": "jest --watch", "test:e2e": "playwright test", From 50cf7c0242a2d4e2ac16bdd07a293ed5dcb58893 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:44:41 -0700 Subject: [PATCH 15/16] docs: add note about order of checks to workflow (#4934) --- .github/workflows/run-checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml index 408827fe1..29acf089c 100644 --- a/.github/workflows/run-checks.yml +++ b/.github/workflows/run-checks.yml @@ -29,6 +29,7 @@ jobs: with: enable-cache: true - run: | + # Run Ruff checks before installing all dependencies uv run --locked --only-group dev ruff check . uv run --locked --only-group dev ruff format --check . uv sync --locked From 0efac16921f37c3542eb56cdddd81cb7449561d8 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:12:33 -0700 Subject: [PATCH 16/16] chore: run ruff checks in precommit hook if uv is installed (#4934) --- .husky/pre-commit | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.husky/pre-commit b/.husky/pre-commit index d9d8a4ccd..d8441b320 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -21,3 +21,21 @@ npx tsc --noEmit || false; ) echo 'TypeScript compile succeeded!' + +# Check Python, only if uv is installed +if command -v uv >/dev/null 2>&1; then + # Run Ruff lint before format check + npm run lint:python || + ( + echo '🤔 Ruff Check Failed. Make the required changes listed above, add changes and try to commit again.' + false; + ) + + npm run check-format:python || + ( + echo '🤔 Ruff Format Check Failed. Run npm run format:python and try commit again.'; + false; + ) +else + echo 'uv not installed, skipping Python checks' +fi