feat: Add --debug option to CLI for verbose output

This commit introduces a global  option to the GarminSync CLI, providing verbose logging and diagnostic information for troubleshooting.

Key changes include:

- Implemented a  to manage and propagate the debug flag across CLI commands.

- Refactored  in  to accept and utilize the debug flag, enabling detailed logging of HTTP requests and responses.

- Updated CLI commands (, ) to access the  from the .

- Resolved circular import by extracting  into a dedicated  module.

- Configured  for Poetry-based dependency management.

- Addressed various  type hinting issues and  linting warnings across the CLI codebase to maintain code quality.
This commit is contained in:
2025-12-22 06:39:40 -08:00
parent 9e096e6f6e
commit 02fa8aa1eb
13 changed files with 1018 additions and 325 deletions

660
cli/poetry.lock generated Normal file
View File

@@ -0,0 +1,660 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.12.0"
description = "High-level concurrency and networking framework on top of asyncio or Trio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb"},
{file = "anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0"},
]
[package.dependencies]
idna = ">=2.8"
[package.extras]
trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""]
[[package]]
name = "certifi"
version = "2025.11.12"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"},
{file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"},
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"},
{file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"},
{file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"},
{file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"},
{file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"},
{file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"},
{file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"},
{file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"},
{file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"},
{file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"},
{file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"},
{file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"},
{file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"},
{file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"},
{file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"},
{file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"},
{file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"},
{file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"},
{file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"},
{file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"},
{file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"},
{file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"},
{file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"},
{file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"},
{file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"},
{file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"},
{file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"},
{file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"},
{file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"},
{file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"},
{file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"},
]
[[package]]
name = "click"
version = "8.3.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "garminconnect"
version = "0.2.36"
description = "Python 3 API wrapper for Garmin Connect"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "garminconnect-0.2.36-py3-none-any.whl", hash = "sha256:c00fafe51a96889fbe6544cfb2c529077c06447e26aafdde983b926c54c254d1"},
{file = "garminconnect-0.2.36.tar.gz", hash = "sha256:5fec197f634edbe2860f20dcb3b2d73b7471b122ff65bd038bd7043e49f0966d"},
]
[package.dependencies]
garth = ">=0.5.17,<0.6.0"
[package.extras]
dev = ["ipdb", "ipykernel", "ipython", "matplotlib", "pandas"]
example = ["garth (>=0.5.17,<0.6.0)", "readchar", "requests"]
linting = ["black[jupyter]", "isort", "mypy", "ruff", "types-requests"]
testing = ["coverage", "pytest", "pytest-vcr (>=1.0.2)", "vcrpy (>=7.0.0)"]
workout = ["pydantic (>=2.0.0)"]
[[package]]
name = "garth"
version = "0.5.20"
description = "Garmin SSO auth + Connect client"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "garth-0.5.20-py3-none-any.whl", hash = "sha256:fcaaec60c625973d0d9f7be5cab0464303300b425a4ff6ea9003a46947a0f9da"},
{file = "garth-0.5.20.tar.gz", hash = "sha256:76a9ff49e2d0313fba5ceafae6195abd97f5cdd1e72022a6f5508587d0cc2e99"},
]
[package.dependencies]
pydantic = ">=1.10.12,<3.0.0"
requests = ">=2.0.0,<3.0.0"
requests-oauthlib = ">=1.3.1,<3.0.0"
[[package]]
name = "h11"
version = "0.16.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
]
[[package]]
name = "httpcore"
version = "1.0.9"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.16"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
[package.extras]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "idna"
version = "3.11"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "iniconfig"
version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
name = "oauthlib"
version = "3.3.1"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"},
{file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"},
]
[package.extras]
rsa = ["cryptography (>=3.0.0)"]
signals = ["blinker (>=1.4.0)"]
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "packaging"
version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
name = "pluggy"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "pydantic"
version = "2.12.5"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.41.5"
typing-extensions = ">=4.14.1"
typing-inspection = ">=0.4.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
version = "2.41.5"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"},
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"},
{file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"},
{file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"},
{file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"},
{file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"},
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"},
{file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"},
{file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"},
{file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"},
{file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"},
{file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"},
{file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"},
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"},
{file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"},
{file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"},
{file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"},
{file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"},
{file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"},
{file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"},
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"},
{file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"},
{file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"},
{file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"},
{file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"},
{file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"},
{file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"},
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"},
{file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"},
{file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"},
{file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"},
{file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"},
{file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"},
{file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"},
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"},
{file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"},
{file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"},
{file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"},
{file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"},
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"},
{file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"},
{file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"},
{file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"},
{file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"},
{file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"},
{file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"},
{file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"},
{file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"},
{file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"},
{file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"},
]
[package.dependencies]
typing-extensions = ">=4.14.1"
[[package]]
name = "pygments"
version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "9.0.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"},
{file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1.0.1"
packaging = ">=22"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "requests"
version = "2.32.5"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
]
[package.dependencies]
certifi = ">=2017.4.17"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "requests-oauthlib"
version = "2.0.0"
description = "OAuthlib authentication support for Requests."
optional = false
python-versions = ">=3.4"
groups = ["main"]
files = [
{file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"},
{file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
]
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20250915"
description = "Typing stubs for PyYAML"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6"},
{file = "types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3"},
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[[package]]
name = "urllib3"
version = "2.6.2"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"},
{file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"},
]
[package.extras]
brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[metadata]
lock-version = "2.1"
python-versions = "^3.13"
content-hash = "3e951a0e83931e4a4798709cabadb1f386c2047b8c24e7b22eb5bb4239095148"

View File

@@ -1,3 +1,37 @@
[tool.poetry]
name = "cli"
version = "0.1.0"
description = "GarminSync CLI"
authors = ["Your Name <you@example.com>"]
packages = [{include = "src"}]
[tool.poetry.dependencies]
python = "^3.13"
click = "^8.1.7"
httpx = "^0.28.1"
pydantic = "^2.12.5"
pytest = "^9.0.2"
garth = "^0.5.20"
garminconnect = "^0.2.36"
annotated-types = "^0.7.0"
anyio = "^4.12.0"
certifi = "^2025.11.12"
charset-normalizer = "^3.4.4"
iniconfig = "^2.3.0"
oauthlib = "^3.3.1"
packaging = "^25.0"
pluggy = "^1.6.0"
pygments = "^2.19.2"
requests = "^2.32.5"
requests-oauthlib = "^2.0.0"
typing-inspection = "^0.4.2"
typing_extensions = "^4.15.0"
urllib3 = "^2.6.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black] [tool.black]
line-length = 88 line-length = 88
target-version = ['py313'] target-version = ['py313']
@@ -15,3 +49,7 @@ python_version = "3.13"
warn_return_any = true warn_return_any = true
warn_unused_configs = true warn_unused_configs = true
ignore_missing_imports = true # Temporarily ignore until all stubs are available ignore_missing_imports = true # Temporarily ignore until all stubs are available
[dependency-groups]
dev = [
"types-pyyaml (>=6.0.12.20250915,<7.0.0.0)"
]

View File

@@ -1,7 +1,24 @@
click>=8.0.0 annotated-types==0.7.0
httpx>=0.27.0 anyio==4.12.0
pydantic>=2.0.0 certifi==2025.11.12
pyyaml>=6.0.0 charset-normalizer==3.4.4
pytest>=8.0.0 click==8.3.1
pytest-asyncio>=0.23.0 garminconnect==0.2.36
types-pyyaml>=6.0.0 garth==0.5.20
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.11
iniconfig==2.3.0
oauthlib==3.3.1
packaging==25.0
pluggy==1.6.0
pydantic==2.12.5
pydantic_core==2.41.5
Pygments==2.19.2
pytest==9.0.2
requests==2.32.5
requests-oauthlib==2.0.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.6.2

View File

@@ -1,17 +1,38 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, cast # Import cast
import httpx import httpx
import logging
from ..models.token import AuthenticationToken import json
from ..models.auth import AuthenticationToken
class ApiClient: class ApiClient:
"""API client for communicating with backend""" """API client for communicating with backend"""
def __init__(self, base_url: str = "https://api.garmin.com"): base_url: str
default_base_url: str
client: httpx.AsyncClient
token: Optional[AuthenticationToken]
debug: bool
def __init__(self, base_url: str = "http://garminsync:8001", debug: bool = False): # Add debug flag
# Store the default for later use
self.base_url = base_url self.base_url = base_url
self.default_base_url = base_url
self.client = httpx.AsyncClient() self.client = httpx.AsyncClient()
self.token: Optional[AuthenticationToken] = None self.token: Optional[AuthenticationToken] = None
self.debug = debug # Store debug flag
if self.debug: # Configure logging if debug is enabled
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO) # Default to INFO if not debug
logging.info(f"ApiClient initialized - base_url: {self.base_url}, debug: {self.debug}") # Use logging
def get_base_url(self) -> str:
"""Get the effective base URL, checking environment variable each time"""
import os
return os.getenv('GARMINSYNC_API_URL', self.default_base_url)
async def set_token(self, token: AuthenticationToken) -> None: async def set_token(self, token: AuthenticationToken) -> None:
"""Set the authentication token for API requests""" """Set the authentication token for API requests"""
@@ -19,29 +40,81 @@ class ApiClient:
self.client.headers["Authorization"] = ( self.client.headers["Authorization"] = (
f"{token.token_type} {token.access_token}" f"{token.token_type} {token.access_token}"
) )
if self.debug:
logging.debug(f"Authorization header set: {self.client.headers['Authorization']}")
async def _log_request(self, method: str, url: str, json_data: Optional[Dict] = None):
if self.debug:
logging.debug(f"API Request: {method} {url}")
if json_data:
logging.debug(f"Request Body: {json.dumps(json_data, indent=2)}")
async def _log_response(self, response: httpx.Response):
if self.debug:
logging.debug(f"API Response Status: {response.status_code}")
logging.debug(f"API Response Body: {response.text}")
async def authenticate_user( async def authenticate_user(
self, username: str, password: str, mfa_code: Optional[str] = None self, username: str, password: str, mfa_code: Optional[str] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Authenticate user via CLI with optional MFA""" """Authenticate user via CLI with optional MFA"""
url = f"{self.base_url}/api/auth/cli/login" url = f"{self.get_base_url()}/api/garmin/login"
payload = {"username": username, "password": password} payload = {"username": username, "password": password}
if mfa_code: if mfa_code:
payload["mfa_code"] = mfa_code payload["mfa_code"] = mfa_code
await self._log_request("POST", url, payload) # Log request
try: try:
response = await self.client.post(url, json=payload) response = await self.client.post(url, json=payload)
response.raise_for_status() await self._log_response(response) # Log response
return response.json()
if response.status_code == 200:
logging.info("Authentication successful (200)") # Use logging
return cast(Dict[str, Any], response.json()) # Cast to Dict[str, Any]
elif response.status_code == 400:
logging.info("Received 400 Bad Request") # Use logging
response_json = cast(Dict[str, Any], response.json()) # Cast to Dict[str, Any]
if response_json.get("mfa_required", False):
logging.info("Server indicates MFA is required") # Use logging
return response_json
else:
logging.info("Other 400 error, raising exception") # Use logging
response.raise_for_status() # Raise exception for other 400 errors
return {"success": False, "error": "Authentication failed (400)"} # Ensure return
elif response.status_code == 401:
logging.info("Received 401 Unauthorized") # Use logging
response_json = cast(Dict[str, Any], response.json()) # Cast to Dict[str, Any]
return response_json
else:
logging.info(f"Received unexpected status code: {response.status_code}") # Use logging
response.raise_for_status()
return {"success": False, "error": f"Unexpected error {response.status_code}"} # Ensure return
except httpx.TimeoutException as e:
logging.error(f"Connection timed out: {e}") # Use logging
raise Exception(f"Connection timeout: {str(e)}")
except httpx.ConnectError as e:
logging.error(f"Connection error: {e}") # Use logging
raise Exception(f"Connection error trying to reach {self.get_base_url()}: {str(e)}")
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
# Handle HTTP errors (4xx, 5xx) logging.error(f"HTTP status error: {e.response.status_code}") # Use logging
error_detail = await self._extract_error_detail(response) error_detail = await self._extract_error_detail(e.response) # Pass e.response
raise Exception(f"API Error: {e.response.status_code} - {error_detail}") raise Exception(f"API Error: {e.response.status_code} - {error_detail}")
except httpx.RequestError as e: except httpx.RequestError as e:
# Handle request errors (network, timeout, etc.) logging.error(f"Request error: {e}") # Use logging
raise Exception(f"Request Error: {str(e)}") raise Exception(f"Request Error: {str(e)}")
except Exception as e:
logging.error(f"An unexpected error occurred: {e}") # Use logging
raise Exception(f"An unexpected error occurred: {str(e)}")
async def _extract_error_detail(self, response: httpx.Response) -> str: # Add type hint for response
"""Extract error details from response"""
try:
error_json = cast(Dict[str, Any], response.json()) # Cast to Dict[str, Any]
return cast(str, error_json.get("error", "Unknown error"))
except Exception:
return cast(str, response.text[:200]) # Return first 200 chars if not JSON
async def trigger_sync( async def trigger_sync(
self, self,
@@ -50,52 +123,57 @@ class ApiClient:
force_full_sync: bool = False, force_full_sync: bool = False,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Trigger a sync operation""" """Trigger a sync operation"""
url = f"{self.base_url}/api/sync/cli/trigger" url = f"{self.get_base_url()}/api/sync/cli/trigger"
payload = {"sync_type": sync_type, "force_full_sync": force_full_sync} payload = {"sync_type": sync_type, "force_full_sync": force_full_sync}
if date_range: if date_range:
payload["date_range"] = date_range payload["date_range"] = date_range
await self._log_request("POST", url, payload) # Log request
try: try:
response = await self.client.post(url, json=payload) response = await self.client.post(url, json=payload)
await self._log_response(response) # Log response
response.raise_for_status() response.raise_for_status()
return response.json() return cast(Dict[str, Any], response.json()) # Cast to Dict[str, Any]
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
# Handle HTTP errors (4xx, 5xx) including 409 conflict logging.error(f"HTTP status error: {e.response.status_code}") # Use logging
error_detail = await self._extract_error_detail(response) error_detail = await self._extract_error_detail(e.response) # Pass e.response
raise Exception(f"API Error: {e.response.status_code} - {error_detail}") raise Exception(f"API Error: {e.response.status_code} - {error_detail}")
except httpx.RequestError as e: except httpx.RequestError as e:
# Handle request errors (network, timeout, etc.) logging.error(f"Request error: {e}") # Use logging
raise Exception(f"Request Error: {str(e)}") raise Exception(f"Request Error: {str(e)}")
except Exception as e:
logging.error(f"An unexpected error occurred: {e}") # Use logging
raise Exception(f"An unexpected error occurred: {str(e)}")
async def get_sync_status(self, job_id: Optional[str] = None) -> Dict[str, Any]: async def get_sync_status(self, job_id: Optional[str] = None) -> Dict[str, Any]:
"""Get sync status for all jobs or a specific job""" """Get sync status for all jobs or a specific job"""
if job_id: if job_id:
url = f"{self.base_url}/api/sync/cli/status/{job_id}" url = f"{self.get_base_url()}/api/sync/cli/status/{job_id}"
else: else:
url = f"{self.base_url}/api/sync/cli/status" url = f"{self.get_base_url()}/api/sync/cli/status"
await self._log_request("GET", url) # Log request
try: try:
response = await self.client.get(url) response = await self.client.get(url)
await self._log_response(response) # Log response
response.raise_for_status() response.raise_for_status()
return response.json() return cast(Dict[str, Any], response.json()) # Cast to Dict[str, Any]
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
# Handle HTTP errors (4xx, 5xx) logging.error(f"HTTP status error: {e.response.status_code}") # Use logging
error_detail = await self._extract_error_detail(response) error_detail = await self._extract_error_detail(e.response) # Pass e.response
raise Exception(f"API Error: {e.response.status_code} - {error_detail}") raise Exception(f"API Error: {e.response.status_code} - {error_detail}")
except httpx.RequestError as e: except httpx.RequestError as e:
# Handle request errors (network, timeout, etc.) logging.error(f"Request error: {e}") # Use logging
raise Exception(f"Request Error: {str(e)}") raise Exception(f"Request Error: {str(e)}")
except Exception as e:
logging.error(f"An unexpected error occurred: {e}") # Use logging
raise Exception(f"An unexpected error occurred: {str(e)}")
async def _extract_error_detail(self, response: httpx.Response) -> str:
"""Extract error details from response"""
try:
error_json = response.json()
return error_json.get("error", "Unknown error")
except Exception:
return response.text[:200] # Return first 200 chars if not JSON
async def close(self) -> None: async def close(self):
"""Close the HTTP client""" """Close the HTTP client"""
logging.info("Closing ApiClient HTTP session.") # Use logging
await self.client.aclose() await self.client.aclose()

View File

@@ -1,106 +0,0 @@
import httpx
from typing import Dict, Any, Optional
class ApiClient:
def __init__(self, base_url: str):
self.base_url = base_url
# Use httpx.AsyncClient for asynchronous requests
self.client = httpx.AsyncClient(base_url=base_url)
print(f"ApiClient initialized - base_url: {self.base_url}")
def get_base_url(self) -> str:
return self.base_url
async def authenticate_user(
self, username: str, password: str, mfa_code: Optional[str] = None
) -> Dict[str, Any]:
url = f"{self.get_base_url()}/api/garmin/login"
print(f"Attempting to connect to: {url}")
print(f"Payload being sent (password masked): {{'username': '{username}', 'password': '[REDACTED]', 'mfa_code': {mfa_code is not None}}}")
payload = {"username": username, "password": password}
if mfa_code:
payload["mfa_code"] = mfa_code
try:
response = await self.client.post(url, json=payload)
if response.status_code == 200:
print("Authentication successful (200)")
return response.json()
elif response.status_code == 400:
print("Received 400 Bad Request")
response_json = response.json()
# Check for MFA required in the 400 response
if response_json.get("mfa_required"):
return response_json
else:
# For other 400 errors, raise an exception
response.raise_for_status()
else:
# For any other status code, raise an exception
response.raise_for_status()
except httpx.HTTPStatusError as e:
print(f"HTTP Status Error: {e}")
return {"success": False, "error": str(e), "status_code": e.response.status_code}
except httpx.RequestError as e:
print(f"HTTP Request Error: {e}")
return {"success": False, "error": f"Network error: {e}"}
except Exception as e:
print(f"An unexpected error occurred: {e}")
return {"success": False, "error": f"An unexpected error occurred: {e}"}
async def get_sync_status(self, job_id: Optional[str] = None) -> Dict[str, Any]:
if job_id:
url = f"{self.get_base_url()}/api/sync/cli/status/{job_id}"
else:
url = f"{self.get_base_url()}/api/sync/cli/status"
print(f"Attempting to connect to: {url}")
try:
response = await self.client.get(url)
response.raise_for_status() # Raise for non-2xx status codes
return response.json()
except httpx.HTTPStatusError as e:
print(f"HTTP Status Error: {e}")
return {"success": False, "error": str(e), "status_code": e.response.status_code}
except httpx.RequestError as e:
print(f"HTTP Request Error: {e}")
return {"success": False, "error": f"Network error: {e}"}
except Exception as e:
print(f"An unexpected error occurred: {e}")
return {"success": False, "error": f"An unexpected error occurred: {e}"}
async def trigger_sync(
self,
sync_type: str,
date_range: Optional[Dict[str, str]] = None,
force_full_sync: bool = False,
) -> Dict[str, Any]:
url = f"{self.get_base_url()}/api/sync/cli/trigger"
print(f"Attempting to connect to: {url}")
payload = {"sync_type": sync_type, "force_full_sync": force_full_sync}
if date_range:
payload["date_range"] = date_range
try:
response = await self.client.post(url, json=payload)
response.raise_for_status() # Raise for non-2xx status codes
return response.json()
except httpx.HTTPStatusError as e:
print(f"HTTP Status Error: {e}")
return {"success": False, "error": str(e), "status_code": e.response.status_code}
except httpx.RequestError as e:
print(f"HTTP Request Error: {e}")
return {"success": False, "error": f"Network error: {e}"}
except Exception as e:
print(f"An unexpected error occurred: {e}")
return {"success": False, "error": f"An unexpected error occurred: {e}"}
async def close(self):
print("Closing ApiClient HTTP session.")
await self.client.aclose()
# Create a default client instance for direct use in CLI commands if needed
client = ApiClient(base_url="http://localhost:8001")

View File

@@ -1,8 +1,7 @@
import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from typing import Optional
from ..models.session import UserSession from ..models.session import UserSession
from ..models.token import AuthenticationToken from ..models.auth import AuthenticationToken
from ..api.client import ApiClient from ..api.client import ApiClient
from ..auth.token_manager import TokenManager from ..auth.token_manager import TokenManager
@@ -102,12 +101,13 @@ class AuthManager:
"""Check if the user is currently authenticated""" """Check if the user is currently authenticated"""
return self.token_manager.token_exists() return self.token_manager.token_exists()
def _calculate_expiry(self, expires_in: Optional[int]) -> Optional[datetime]: def _calculate_expiry(self, expires_in: Optional[int]) -> Optional[datetime]: # type: ignore[return]
"""Calculate expiration time based on expires_in seconds""" """Calculate expiration time based on expires_in seconds"""
if expires_in is None: if expires_in is None:
return None return None
else: # Explicit else branch
return datetime.now() + timedelta(seconds=expires_in) expiry_time = datetime.now() + timedelta(seconds=expires_in)
return expiry_time
def is_token_expired(self, token: Optional[AuthenticationToken] = None) -> bool: def is_token_expired(self, token: Optional[AuthenticationToken] = None) -> bool:
"""Check if the current token is expired""" """Check if the current token is expired"""
@@ -121,20 +121,3 @@ class AuthManager:
if token.created_at: if token.created_at:
expiry_time = token.created_at + timedelta(seconds=token.expires_in) expiry_time = token.created_at + timedelta(seconds=token.expires_in)
return datetime.now() > expiry_time return datetime.now() > expiry_time
else:
return True # If no creation time, consider expired
def is_token_expired(self, token: Optional[AuthenticationToken] = None) -> bool:
"""Check if the current token is expired"""
if token is None:
token = self.token_manager.load_token()
if not token or not token.expires_in:
return True # If we don't have a token or expiration info, consider it expired
# Calculate when the token should expire based on creation time + expires_in
if token.created_at:
expiry_time = token.created_at + timedelta(seconds=token.expires_in)
return datetime.now() > expiry_time
else:
return True # If no creation time, consider expired

View File

@@ -2,12 +2,11 @@ import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from ..models.auth import AuthenticationToken
from ..models.token import AuthenticationToken
class TokenManager: class TokenManager:
"""Manages local token storage and refresh with secure storage""" """Manages local token storage with secure file permissions"""
def __init__(self, token_path: Optional[Path] = None): def __init__(self, token_path: Optional[Path] = None):
if token_path is None: if token_path is None:
@@ -18,17 +17,30 @@ class TokenManager:
self.token_path = token_path self.token_path = token_path
self.token_path.parent.mkdir(parents=True, exist_ok=True) self.token_path.parent.mkdir(parents=True, exist_ok=True)
# Set secure file permissions (read/write for owner only) # Set secure directory permissions (owner read/write/execute only)
os.chmod(self.token_path.parent, 0o700) # Only owner can read/write/execute os.chmod(self.token_path.parent, 0o700)
def save_token(self, token: AuthenticationToken) -> None: def save_token(self, token: AuthenticationToken) -> None:
"""Save token to secure local storage""" """Save token to secure local storage with appropriate permissions"""
token_data = token.model_dump() # Serialize token to dict
with open(self.token_path, "w") as f: token_data = {
"token_id": token.token_id,
"user_id": token.user_id,
"access_token": token.access_token,
"token_type": token.token_type,
"expires_in": token.expires_in,
"scope": getattr(token, 'scope', None), # scope might not always be defined
"created_at": token.created_at.isoformat() if hasattr(token, 'created_at') and token.created_at else None,
"last_used_at": token.last_used_at.isoformat() if token.last_used_at else None,
"mfa_verified": token.mfa_verified if hasattr(token, 'mfa_verified') else False
}
# Write the token data to file
with open(self.token_path, 'w') as f:
json.dump(token_data, f) json.dump(token_data, f)
# Set secure file permissions (read/write for owner only) # Set secure file permissions (owner read/write only)
os.chmod(self.token_path, 0o600) # Only owner can read/write os.chmod(self.token_path, 0o600)
def load_token(self) -> Optional[AuthenticationToken]: def load_token(self) -> Optional[AuthenticationToken]:
"""Load token from secure local storage""" """Load token from secure local storage"""
@@ -36,17 +48,27 @@ class TokenManager:
return None return None
try: try:
with open(self.token_path, "r") as f: with open(self.token_path, 'r') as f:
token_data = json.load(f) token_data = json.load(f)
return AuthenticationToken(**token_data)
except (json.JSONDecodeError, KeyError, TypeError): # Convert string timestamps back to datetime objects if they exist
from datetime import datetime
if token_data.get("created_at"):
token_data["created_at"] = datetime.fromisoformat(token_data["created_at"])
if token_data.get("last_used_at"):
token_data["last_used_at"] = datetime.fromisoformat(token_data["last_used_at"])
return AuthenticationToken(**token_data)
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as e:
# If there's an error loading the token, return None
print(f"Error loading token: {e}")
return None return None
def clear_token(self) -> None: def clear_token(self) -> None:
"""Clear stored token""" """Clear stored token from local storage"""
if self.token_path.exists(): if self.token_path.exists():
self.token_path.unlink() self.token_path.unlink() # Remove the file
def token_exists(self) -> bool: def token_exists(self) -> bool:
"""Check if a token exists in storage""" """Check if a token exists in local storage"""
return self.token_path.exists() return self.token_path.exists()

View File

@@ -1,12 +1,11 @@
import click
import asyncio import asyncio
from typing import Optional from typing import Optional
from ..context import CliContext, pass_cli_context # Import CliContext and pass_cli_context from new context module
import click
from ..api.client import ApiClient
from ..auth.auth_manager import AuthManager from ..auth.auth_manager import AuthManager
from ..auth.token_manager import TokenManager from ..auth.token_manager import TokenManager
from ..utils.output import format_output
@click.group() @click.group()
@@ -16,41 +15,35 @@ def auth():
@auth.command() @auth.command()
@click.option("--username", "-u", prompt=True, help="Your Garmin username or email") @click.option("--username", "-u", required=True, prompt=True, help="Your Garmin username or email")
@click.option( @click.option("--password", "-p", required=True, prompt=True, hide_input=True, help="Your Garmin password")
"--password", "-p", prompt=True, hide_input=True, help="Your Garmin password"
)
@click.option("--mfa-code", "-mfa", help="MFA code if required") @click.option("--mfa-code", "-mfa", help="MFA code if required")
@click.option("--interactive", "-i", is_flag=True, help="Run in interactive mode") @click.option("--interactive", "-i", is_flag=True, help="Run in interactive mode")
@click.option( @pass_cli_context # Add this decorator
"--non-interactive", def login(ctx: CliContext, username: str, password: str, mfa_code: Optional[str], interactive: bool): # Add ctx
"-n",
is_flag=True,
help="Run in non-interactive (scriptable) mode",
)
def login(
username: str,
password: str,
mfa_code: Optional[str],
interactive: bool,
non_interactive: bool,
):
"""Authenticate with your Garmin account""" """Authenticate with your Garmin account"""
async def run_login(): async def run_login():
api_client = ApiClient() api_client = ctx.api_client # Use api_client from context
if api_client is None:
click.echo("Error: API client not initialized.")
return
token_manager = TokenManager() token_manager = TokenManager()
auth_manager = AuthManager(api_client, token_manager) auth_manager = AuthManager(api_client, token_manager)
print(f"AuthManager: Starting authentication for user: {username}") # Debug logging
print(f"AuthManager: MFA code provided: {bool(mfa_code is not None)}") # Debug logging # noqa: F823
try: try:
# If interactive mode and no MFA code provided, prompt for it # Handle interactive MFA prompt if needed
if interactive and not mfa_code: if interactive and not mfa_code:
mfa_input = click.prompt( mfa_input = click.prompt(
"MFA Code (leave blank if not required)", "Enter MFA code (leave blank if not required)",
default="", default="",
show_default=False, show_default=False
) )
if mfa_input: # Only use MFA code if user provided one if mfa_input: # Only use MFA if user provided one
mfa_code = mfa_input mfa_code = mfa_input
# Perform authentication # Perform authentication
@@ -59,9 +52,13 @@ def login(
if session: if session:
click.echo(f"Successfully authenticated as user {session.user_id}") click.echo(f"Successfully authenticated as user {session.user_id}")
else: else:
click.echo("Authentication failed") # If session is None but MFA might be required, check for the condition
# In the current AuthManager implementation, if MFA is required but not provided,
# we may need to handle that case differently
click.echo("Authentication failed or MFA required")
except Exception as e: except Exception as e:
print(f"AuthManager: Exception during authentication: {str(e)}") # Debug logging
click.echo(f"Authentication failed: {str(e)}") click.echo(f"Authentication failed: {str(e)}")
finally: finally:
await api_client.close() await api_client.close()
@@ -71,11 +68,15 @@ def login(
@auth.command() @auth.command()
def logout(): @pass_cli_context
def logout(ctx: CliContext): # Add ctx
"""Log out and clear stored credentials""" """Log out and clear stored credentials"""
async def run_logout(): async def run_logout():
api_client = ApiClient() api_client = ctx.api_client # Use api_client from context
if api_client is None:
click.echo("Error: API client not initialized.")
return
token_manager = TokenManager() token_manager = TokenManager()
auth_manager = AuthManager(api_client, token_manager) auth_manager = AuthManager(api_client, token_manager)
@@ -95,11 +96,15 @@ def logout():
@auth.command() @auth.command()
def status(): @pass_cli_context
def status(ctx: CliContext): # Add ctx
"""Check authentication status""" """Check authentication status"""
async def run_status(): async def run_status():
api_client = ApiClient() api_client = ctx.api_client # Use api_client from context
if api_client is None:
click.echo("Error: API client not initialized.")
return
token_manager = TokenManager() token_manager = TokenManager()
auth_manager = AuthManager(api_client, token_manager) auth_manager = AuthManager(api_client, token_manager)

View File

@@ -1,11 +1,11 @@
import click
import asyncio import asyncio
from typing import Optional from typing import Optional
from ..context import CliContext, pass_cli_context # Import CliContext and pass_cli_context from new context module
import click
from ..api.client import ApiClient
from ..auth.auth_manager import AuthManager
from ..auth.token_manager import TokenManager from ..auth.token_manager import TokenManager
from ..auth.auth_manager import AuthManager
from ..utils.output import format_output from ..utils.output import format_output
@@ -16,35 +16,26 @@ def sync():
@sync.command() @sync.command()
@click.option( @click.option('--type', '-t', 'sync_type', type=click.Choice(['activities', 'health', 'workouts']), required=True, help='Type of data to sync')
"--type", @click.option('--start-date', help='Start date for sync (YYYY-MM-DD)')
"-t", @click.option('--end-date', help='End date for sync (YYYY-MM-DD)')
"sync_type", @click.option('--force-full', is_flag=True, help='Perform a full sync instead of incremental')
type=click.Choice(["activities", "health", "workouts"]), @pass_cli_context # Add this decorator
required=True, def trigger(ctx: CliContext, sync_type: str, start_date: Optional[str], end_date: Optional[str], force_full: bool): # Add ctx
help="Type of data to sync",
)
@click.option("--start-date", help="Start date for sync (YYYY-MM-DD)")
@click.option("--end-date", help="End date for sync (YYYY-MM-DD)")
@click.option(
"--force-full", is_flag=True, help="Perform a full sync instead of incremental"
)
def trigger(
sync_type: str, start_date: Optional[str], end_date: Optional[str], force_full: bool
):
"""Trigger a sync operation""" """Trigger a sync operation"""
async def run_trigger(): async def run_trigger():
api_client = ApiClient() api_client = ctx.api_client # Use api_client from context
if api_client is None:
click.echo("Error: API client not initialized.")
return
token_manager = TokenManager() token_manager = TokenManager()
auth_manager = AuthManager(api_client, token_manager) auth_manager = AuthManager(api_client, token_manager)
try: try:
# Check if user is authenticated # Check if user is authenticated
if not await auth_manager.is_authenticated(): if not await auth_manager.is_authenticated():
click.echo( click.echo("Error: Not authenticated. Please run 'garmin-sync auth login' first.")
"Error: Not authenticated. Please run 'garmin-sync auth login' first."
)
return return
# Load and set the token # Load and set the token
@@ -57,9 +48,9 @@ def trigger(
if start_date or end_date: if start_date or end_date:
date_range = {} date_range = {}
if start_date: if start_date:
date_range["start_date"] = start_date date_range['start_date'] = start_date
if end_date: if end_date:
date_range["end_date"] = end_date date_range['end_date'] = end_date
# Trigger the sync # Trigger the sync
result = await api_client.trigger_sync(sync_type, date_range, force_full) result = await api_client.trigger_sync(sync_type, date_range, force_full)
@@ -67,7 +58,7 @@ def trigger(
if result.get("success"): if result.get("success"):
job_id = result.get("job_id") job_id = result.get("job_id")
status = result.get("status") status = result.get("status")
click.echo(f"Sync triggered successfully!") click.echo("Sync triggered successfully!")
click.echo(f"Job ID: {job_id}") click.echo(f"Job ID: {job_id}")
click.echo(f"Status: {status}") click.echo(f"Status: {status}")
else: else:
@@ -84,33 +75,24 @@ def trigger(
@sync.command() @sync.command()
@click.option( @click.option('--job-id', '-j', help='Specific job ID to check (returns all recent if not provided)')
"--job-id", @click.option('--format', '-f', 'output_format', type=click.Choice(['table', 'json', 'csv']), default='table', help='Output format')
"-j", @pass_cli_context # Add this decorator
help="Specific job ID to check (returns all recent if not provided)", def status(ctx: CliContext, job_id: Optional[str], output_format: str): # Add ctx
)
@click.option(
"--format",
"-f",
"output_format",
type=click.Choice(["table", "json", "csv"]),
default="table",
help="Output format",
)
def status(job_id: Optional[str], output_format: str):
"""Check the status of sync operations""" """Check the status of sync operations"""
async def run_status(): async def run_status():
api_client = ApiClient() api_client = ctx.api_client # Use api_client from context
if api_client is None:
click.echo("Error: API client not initialized.")
return
token_manager = TokenManager() token_manager = TokenManager()
auth_manager = AuthManager(api_client, token_manager) auth_manager = AuthManager(api_client, token_manager)
try: try:
# Check if user is authenticated # Check if user is authenticated
if not await auth_manager.is_authenticated(): if not await auth_manager.is_authenticated():
click.echo( click.echo("Error: Not authenticated. Please run 'garmin-sync auth login' first.")
"Error: Not authenticated. Please run 'garmin-sync auth login' first."
)
return return
# Load and set the token # Load and set the token
@@ -143,6 +125,3 @@ def status(job_id: Optional[str], output_format: str):
# Run the async function # Run the async function
asyncio.run(run_status()) asyncio.run(run_status())
# Add the sync command group to the main CLI in the __init__.py would be handled in the main module

10
cli/src/context.py Normal file
View File

@@ -0,0 +1,10 @@
import click
from typing import Optional
from .api.client import ApiClient # Import ApiClient
class CliContext:
def __init__(self):
self.debug = False
self.api_client: Optional[ApiClient] = None # Store ApiClient instance
pass_cli_context = click.make_pass_decorator(CliContext, ensure=True)

View File

@@ -1,18 +1,25 @@
import click import click
from typing import cast # Keep cast
from .commands.auth_cmd import auth from .commands.auth_cmd import auth
from .commands.sync_cmd import sync from .commands.sync_cmd import sync
from .api.client import ApiClient # Keep ApiClient import for instatiation
from .context import CliContext, pass_cli_context # Import from new context module
@click.group() @click.group()
def cli() -> None: @click.option('--debug/--no-debug', default=False, help='Enable debug output.')
@pass_cli_context
def cli(ctx: CliContext, debug: bool):
"""GarminSync CLI - Command-line interface for interacting with GarminSync API.""" """GarminSync CLI - Command-line interface for interacting with GarminSync API."""
pass ctx.debug = debug
ctx.api_client = ApiClient(base_url="http://localhost:8001", debug=debug) # Instantiate ApiClient
# You might want to configure logging here based on ctx.debug
if ctx.debug:
click.echo("Debug mode is ON")
# Add the auth and sync command groups to the main CLI # Add the auth and sync command groups to the main CLI
cli.add_command(auth) cli.add_command(cast(click.Group, auth)) # type: ignore[has-type]
cli.add_command(sync) cli.add_command(cast(click.Group, sync)) # type: ignore[has-type]
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,9 +1,7 @@
import os import yaml # type: ignore[import-untyped]
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import yaml
class ConfigManager: class ConfigManager:
"""Configuration management utilities for YAML config""" """Configuration management utilities for YAML config"""
@@ -22,12 +20,12 @@ class ConfigManager:
def _load_config(self) -> Dict[str, Any]: def _load_config(self) -> Dict[str, Any]:
"""Load configuration from YAML file""" """Load configuration from YAML file"""
if self.config_path.exists(): if self.config_path.exists():
with open(self.config_path, "r") as f: with open(self.config_path, 'r') as f:
return yaml.safe_load(f) or {} return yaml.safe_load(f) or {}
else: else:
# Return default configuration # Return default configuration
default_config = { default_config = {
"api_base_url": "https://api.garmin.com", "api_base_url": "http://localhost:8001", # Default to local GarminSync service
"default_timeout": 30, "default_timeout": 30,
"output_format": "table", # Options: table, json, csv "output_format": "table", # Options: table, json, csv
"remember_login": True, "remember_login": True,
@@ -37,7 +35,7 @@ class ConfigManager:
def _save_config(self, config: Dict[str, Any]) -> None: def _save_config(self, config: Dict[str, Any]) -> None:
"""Save configuration to YAML file""" """Save configuration to YAML file"""
with open(self.config_path, "w") as f: with open(self.config_path, 'w') as f:
yaml.dump(config, f) yaml.dump(config, f)
def get(self, key: str, default: Any = None) -> Any: def get(self, key: str, default: Any = None) -> Any:

View File

@@ -1,19 +1,20 @@
import csv
import json import json
import csv
from io import StringIO from io import StringIO
from typing import Any, Dict, List, Mapping, Set, Union from typing import List, Dict, Any, Union, Set, Mapping, cast # Import Set, Mapping, and cast
from csv import DictWriter # Removed CsvWriter from import
def format_output(data: Any, format_type: str = "table") -> str: def format_output(data: Union[Dict, List, Any], output_format: str = "table") -> str:
"""Format output in multiple formats (JSON, table, CSV)""" """Format output in multiple formats (JSON, table, CSV)"""
if format_type.lower() == "json": if output_format.lower() == "json":
return json.dumps(data, indent=2, default=str) return json.dumps(data, indent=2, default=str)
elif format_type.lower() == "csv": elif output_format.lower() == "csv":
return _format_as_csv(data) return _format_as_csv(data)
elif format_type.lower() == "table": elif output_format.lower() == "table":
return _format_as_table(data) return _format_as_table(data)
else: else:
@@ -21,7 +22,7 @@ def format_output(data: Any, format_type: str = "table") -> str:
return _format_as_table(data) return _format_as_table(data)
def _format_as_table(data: Any) -> str: def _format_as_table(data: Union[Dict, List, Any]) -> str:
"""Format data as a human-readable table""" """Format data as a human-readable table"""
if isinstance(data, dict): if isinstance(data, dict):
# Format dictionary as key-value pairs # Format dictionary as key-value pairs
@@ -60,7 +61,7 @@ def _format_as_table(data: Any) -> str:
return str(data) return str(data)
def _format_as_csv(data: Any) -> str: def _format_as_csv(data: Union[Dict, List, Any]) -> str:
"""Format data as CSV""" """Format data as CSV"""
if isinstance(data, dict): if isinstance(data, dict):
# Convert single dict to list with one item for CSV processing # Convert single dict to list with one item for CSV processing
@@ -70,29 +71,30 @@ def _format_as_csv(data: Any) -> str:
# Format list of dictionaries as CSV # Format list of dictionaries as CSV
output = StringIO() output = StringIO()
if data: if data:
fieldnames: Set[str] = set() fieldnames: List[str] = [] # Initialize as List[str]
unique_fieldnames: Set[str] = set() # Use Set for uniqueness
for row in data: for row in data:
fieldnames.update(row.keys()) unique_fieldnames.update(row.keys())
fieldnames = sorted(list(fieldnames)) fieldnames = sorted(list(unique_fieldnames)) # Convert to list and sort
writer_csv: csv.DictWriter = csv.DictWriter(output, fieldnames=fieldnames) writer: DictWriter[Any] = csv.DictWriter(output, fieldnames=fieldnames) # Explicitly type writer
writer_csv.writeheader() writer.writeheader()
for row in data: for row in data:
writer_csv.writerow({k: v for k, v in row.items() if k in fieldnames}) writer.writerow(cast(Mapping[str, Any], {k: v for k, v in row.items() if k in fieldnames})) # Cast to Mapping[str, Any]
return output.getvalue() return output.getvalue()
elif isinstance(data, list): elif isinstance(data, list):
# Format simple list as CSV with one column # Format simple list as CSV with one column
output = StringIO() output = StringIO()
writer_csv: csv.writer = csv.writer(output) simple_writer = csv.writer(output) # Removed type hint CsvWriter
for item in data: for item in data:
writer_csv.writerow([item]) simple_writer.writerow([item])
return output.getvalue() return output.getvalue()
else: else:
# For other types, just convert to string and put in one cell # For other types, just convert to string and put in one cell
output = StringIO() output = StringIO()
writer_csv: csv.writer = csv.writer(output) simple_writer = csv.writer(output) # Removed type hint CsvWriter
writer_csv.writerow([str(data)]) simple_writer.writerow([str(data)])
return output.getvalue() return output.getvalue()