diff --git a/.gitignore b/.gitignore index 3a8816c..688e6d6 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.vscode # Spyder project settings .spyderproject diff --git a/README.md b/README.md index 0563730..d15e419 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ # token-status-list + +This is an implementation of [Token Status List Draft 6][spec] and [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/). + +[spec]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-06 + + +## Features + +- Support for 1, 2, 4, and 8 bits. +- Compression as required by the Specification (ZLIB at level 9) +- Formatting, signing, and verifying Status Lists as either JWT or CWT + - A `TokenSigner` and `TokenVerifier` protocol is defined so the user can Bring Their Own Crypto implementation + - Alternatively, methods for preparing payloads and assembling payload and signature bytes into the final token is also supported. +- Two Index Allocation strategies, Linear and Random + - Linear strategy will allocate indices serially + - Random strategy will allocate indices pseudo-randomly (as the list fills, speed is favored over randomness) + - Allocators contain state that must be persisted along side the status list itself + - IssuerStatusList and Allocators are serializeable so the user can persist them to the backend of their choice +- Basic example using Nginx web server as an issuer to simulate fetching and verifying a Status List + - Run using `docker-compose up -d && pytest tests/test_web_server.py` + - Scripts that are issued, as well as other information about issuer are in `tests/test_web_server` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..23a12f3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +services: + issuer: + build: + context: . + dockerfile: tests/test_web_server/Dockerfile + ports: + - "3001:80" + restart: unless-stopped diff --git a/pdm.lock b/pdm.lock index e946bd8..66013e6 100644 --- a/pdm.lock +++ b/pdm.lock @@ -3,47 +3,260 @@ [metadata] groups = ["default", "cbor", "dev"] -strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.4.1" -content_hash = "sha256:eca250aff70362a1ed25521f5d19c5711f42fcd498edfa466c59292777bd2a4b" +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:5da16897b67a2231330f355e20459589c85476e650aad31710d8ec144969cea1" + +[[metadata.targets]] +requires_python = ">=3.10" + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +requires_python = ">=3.8" +summary = "Happy Eyeballs for asyncio" +groups = ["default"] +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.11" +requires_python = ">=3.9" +summary = "Async http client/server framework (asyncio)" +groups = ["default"] +dependencies = [ + "aiohappyeyeballs>=2.3.0", + "aiosignal>=1.1.2", + "async-timeout<6.0,>=4.0; python_version < \"3.11\"", + "attrs>=17.3.0", + "frozenlist>=1.1.1", + "multidict<7.0,>=4.5", + "propcache>=0.2.0", + "yarl<2.0,>=1.17.0", +] +files = [ + {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, + {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, + {file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"}, + {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"}, + {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"}, + {file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"}, + {file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"}, + {file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"}, + {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"}, + {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"}, + {file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"}, + {file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"}, + {file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"}, + {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"}, + {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"}, + {file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"}, + {file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"}, + {file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"}, + {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"}, + {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"}, + {file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"}, + {file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"}, + {file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"}, +] + +[[package]] +name = "aiosignal" +version = "1.3.2" +requires_python = ">=3.9" +summary = "aiosignal: a list of registered asynchronous callbacks" +groups = ["default"] +dependencies = [ + "frozenlist>=1.1.0", +] +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +requires_python = ">=3.8" +summary = "Timeout context manager for asyncio programs" +groups = ["default"] +marker = "python_version < \"3.11\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "24.3.0" +requires_python = ">=3.8" +summary = "Classes Without Boilerplate" +groups = ["default"] +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +requires_python = ">=3.7" +summary = "Extensible memoizing collections and decorators" +groups = ["default"] +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] [[package]] name = "cbor2" -version = "5.6.4" +version = "5.6.5" requires_python = ">=3.8" summary = "CBOR (de)serializer with extensive tag support" groups = ["cbor"] files = [ - {file = "cbor2-5.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c40c68779a363f47a11ded7b189ba16767391d5eae27fac289e7f62b730ae1fc"}, - {file = "cbor2-5.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0625c8d3c487e509458459de99bf052f62eb5d773cc9fc141c6a6ea9367726d"}, - {file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7137622204168c3a57882f15dd09b5135bda2bcb1cf8b56b58d26b5150dfca"}, - {file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3545e1e62ec48944b81da2c0e0a736ca98b9e4653c2365cae2f10ae871e9113"}, - {file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6749913cd00a24eba17406a0bfc872044036c30a37eb2fcde7acfd975317e8a"}, - {file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57db966ab08443ee54b6f154f72021a41bfecd4ba897fe108728183ad8784a2a"}, - {file = "cbor2-5.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:380e0c7f4db574dcd86e6eee1b0041863b0aae7efd449d49b0b784cf9a481b9b"}, - {file = "cbor2-5.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c763d50a1714e0356b90ad39194fc8ef319356b89fb001667a2e836bfde88e3"}, - {file = "cbor2-5.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58a7ac8861857a9f9b0de320a4808a2a5f68a2599b4c14863e2748d5a4686c99"}, - {file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d715b2f101730335e84a25fe0893e2b6adf049d6d44da123bf243b8c875ffd8"}, - {file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f53a67600038cb9668720b309fdfafa8c16d1a02570b96d2144d58d66774318"}, - {file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f898bab20c4f42dca3688c673ff97c2f719b1811090430173c94452603fbcf13"}, - {file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e5d50fb9f47d295c1b7f55592111350424283aff4cc88766c656aad0300f11f"}, - {file = "cbor2-5.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7f9d867dcd814ab8383ad132eb4063e2b69f6a9f688797b7a8ca34a4eadb3944"}, - {file = "cbor2-5.6.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e0860ca88edf8aaec5461ce0e498eb5318f1bcc70d93f90091b7a1f1d351a167"}, - {file = "cbor2-5.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c38a0ed495a63a8bef6400158746a9cb03c36f89aeed699be7ffebf82720bf86"}, - {file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8d8c2f208c223a61bed48dfd0661694b891e423094ed30bac2ed75032142aa"}, - {file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cd2ce6136e1985da989e5ba572521023a320dcefad5d1fff57fba261de80ca"}, - {file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7facce04aed2bf69ef43bdffb725446fe243594c2451921e89cc305bede16f02"}, - {file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f9c8ee0d89411e5e039a4f3419befe8b43c0dd8746eedc979e73f4c06fe0ef97"}, - {file = "cbor2-5.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:9b45d554daa540e2f29f1747df9f08f8d98ade65a67b1911791bc193d33a5923"}, - {file = "cbor2-5.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41c43abffe217dce70ae51c7086530687670a0995dfc90cc35f32f2cf4d86392"}, - {file = "cbor2-5.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:227a7e68ba378fe53741ed892b5b03fe472b5bd23ef26230a71964accebf50a2"}, - {file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13521b7c9a0551fcc812d36afd03fc554fa4e1b193659bb5d4d521889aa81154"}, - {file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4816d290535d20c7b7e2663b76da5b0deb4237b90275c202c26343d8852b8a"}, - {file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1e98d370106821335efcc8fbe4136ea26b4747bf29ca0e66512b6c4f6f5cc59f"}, - {file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:68743a18e16167ff37654a29321f64f0441801dba68359c82dc48173cc6c87e1"}, - {file = "cbor2-5.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:7ba5e9c6ed17526d266a1116c045c0941f710860c5f2495758df2e0d848c1b6d"}, - {file = "cbor2-5.6.4-py3-none-any.whl", hash = "sha256:fe411c4bf464f5976605103ebcd0f60b893ac3e4c7c8d8bc8f4a0cb456e33c60"}, - {file = "cbor2-5.6.4.tar.gz", hash = "sha256:1c533c50dde86bef1c6950602054a0ffa3c376e8b0e20c7b8f5b108793f6983e"}, + {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, + {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, + {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"}, + {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"}, + {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"}, + {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"}, + {file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"}, + {file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"}, + {file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"}, + {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"}, + {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"}, + {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"}, + {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"}, + {file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"}, + {file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"}, + {file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"}, + {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"}, + {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"}, + {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"}, + {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"}, + {file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"}, + {file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"}, + {file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"}, + {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"}, + {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"}, + {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"}, + {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"}, + {file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"}, + {file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"}, + {file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +marker = "platform_python_implementation != \"PyPy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [[package]] @@ -57,12 +270,83 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.4.0" +requires_python = ">=3.7.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default"] +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + [[package]] name = "colorama" version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." -groups = ["dev"] +groups = ["default", "dev"] marker = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, @@ -176,6 +460,47 @@ files = [ {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] +[[package]] +name = "cryptography" +version = "44.0.0" +requires_python = "!=3.9.0,!=3.9.1,>=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["default"] +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -191,7 +516,7 @@ name = "exceptiongroup" version = "1.2.1" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" -groups = ["dev"] +groups = ["default", "dev"] marker = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, @@ -209,6 +534,93 @@ files = [ {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] +[[package]] +name = "frozenlist" +version = "1.5.0" +requires_python = ">=3.8" +summary = "A list-like structure which implements collections.abc.MutableSequence" +groups = ["default"] +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "google-auth" +version = "2.36.0" +requires_python = ">=3.7" +summary = "Google Authentication Library" +groups = ["default"] +dependencies = [ + "cachetools<6.0,>=2.0.0", + "pyasn1-modules>=0.2.1", + "rsa<5,>=3.1.4", +] +files = [ + {file = "google_auth-2.36.0-py2.py3-none-any.whl", hash = "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb"}, + {file = "google_auth-2.36.0.tar.gz", hash = "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1"}, +] + [[package]] name = "identify" version = "2.5.36" @@ -220,17 +632,102 @@ files = [ {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, ] +[[package]] +name = "idna" +version = "3.10" +requires_python = ">=3.6" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + [[package]] name = "iniconfig" version = "2.0.0" requires_python = ">=3.7" summary = "brain-dead simple config-ini parsing" -groups = ["dev"] +groups = ["default", "dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "multidict" +version = "6.1.0" +requires_python = ">=3.8" +summary = "multidict implementation" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.1.0; python_version < \"3.11\"", +] +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -242,12 +739,22 @@ files = [ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[[package]] +name = "overloading" +version = "0.5.0" +summary = "Function overloading for Python 3" +groups = ["default"] +files = [ + {file = "overloading-0.5.0-py3-none-any.whl", hash = "sha256:c28d2a227cfb6bdefcfe0ded055bc620a5c784a52ecadc441f8d6c281b8bb1c1"}, + {file = "overloading-0.5.0.tar.gz", hash = "sha256:493f0f67211244ed6bf2acf9f3ac61fb38e8aa87834c4f0f84d8943512066588"}, +] + [[package]] name = "packaging" version = "24.1" requires_python = ">=3.8" summary = "Core utilities for Python packages" -groups = ["dev"] +groups = ["default", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -269,7 +776,7 @@ name = "pluggy" version = "1.5.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" -groups = ["dev"] +groups = ["default", "dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -293,12 +800,124 @@ files = [ {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] +[[package]] +name = "propcache" +version = "0.2.1" +requires_python = ">=3.9" +summary = "Accelerated property cache" +groups = ["default"] +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +requires_python = ">=3.8" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +groups = ["default"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +requires_python = ">=3.8" +summary = "A collection of ASN.1-based protocols modules" +groups = ["default"] +dependencies = [ + "pyasn1<0.7.0,>=0.4.6", +] +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +marker = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pytest" version = "8.2.2" requires_python = ">=3.8" summary = "pytest: simple powerful testing with Python" -groups = ["dev"] +groups = ["default", "dev"] dependencies = [ "colorama; sys_platform == \"win32\"", "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", @@ -312,6 +931,20 @@ files = [ {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] +[[package]] +name = "pytest-asyncio" +version = "0.25.2" +requires_python = ">=3.9" +summary = "Pytest support for asyncio" +groups = ["default"] +dependencies = [ + "pytest<9,>=8.2", +] +files = [ + {file = "pytest_asyncio-0.25.2-py3-none-any.whl", hash = "sha256:0d0bb693f7b99da304a0634afc0a4b19e49d5e0de2d670f38dc4bfa5727c5075"}, + {file = "pytest_asyncio-0.25.2.tar.gz", hash = "sha256:3f8ef9a98f45948ea91a0ed3dc4268b5326c0e7bce73892acc654df4262ad45f"}, +] + [[package]] name = "pytest-cov" version = "5.0.0" @@ -368,6 +1001,37 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "requests" +version = "2.32.3" +requires_python = ">=3.8" +summary = "Python HTTP for Humans." +groups = ["default"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[[package]] +name = "rsa" +version = "4.9" +requires_python = ">=3.6,<4" +summary = "Pure-Python RSA implementation" +groups = ["default"] +dependencies = [ + "pyasn1>=0.1.3", +] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + [[package]] name = "ruff" version = "0.5.0" @@ -400,13 +1064,36 @@ name = "tomli" version = "2.0.1" requires_python = ">=3.7" summary = "A lil' TOML parser" -groups = ["dev"] +groups = ["default", "dev"] marker = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.12.2" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +marker = "python_version < \"3.11\"" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +requires_python = ">=3.8" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + [[package]] name = "virtualenv" version = "20.26.3" @@ -416,9 +1103,90 @@ groups = ["dev"] dependencies = [ "distlib<1,>=0.3.7", "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", "platformdirs<5,>=3.9.1", ] files = [ {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] + +[[package]] +name = "yarl" +version = "1.18.3" +requires_python = ">=3.9" +summary = "Yet another URL library" +groups = ["default"] +dependencies = [ + "idna>=2.0", + "multidict>=4.0", + "propcache>=0.2.0", +] +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] diff --git a/pyproject.toml b/pyproject.toml index 8e539e4..3b0a999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Implementation of IETF Token Status List" authors = [ {name = "Daniel Bluhm", email = "dbluhm@pm.me"}, ] -dependencies = [] +dependencies = ["google-auth>=2.36.0", "requests>=2.32.3", "cryptography>=44.0.0", "overloading>=0.5.0", "aiohttp>=3.11.11", "pytest-asyncio>=0.25.2"] requires-python = ">=3.10" readme = "README.md" license = {text = "Apache-2.0"} diff --git a/src/bit_array.py b/src/bit_array.py new file mode 100644 index 0000000..15deba1 --- /dev/null +++ b/src/bit_array.py @@ -0,0 +1,416 @@ +"""Token Status List. + +Python implementation of Token Status List. + +This implementation is based on draft 6, found here: +https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-06 +""" + +import base64 +import json +from random import sample +from secrets import choice, randbelow +from time import time +from typing import ( + Any, + Callable, + Generic, + List, + Literal, + Optional, + Protocol, + Tuple, + TypeVar, + Union, + cast, +) +import zlib + + +def b64url_decode(value: bytes) -> bytes: + """Return the base64 url encoded value, without padding.""" + padding_needed = 4 - (len(value) % 4) + if padding_needed != 4: + value += b"=" * padding_needed + + return base64.urlsafe_b64decode(value) + + +def b64url_encode(value: bytes) -> bytes: + """Return the decoded base64 url encoded value, without padding.""" + return base64.urlsafe_b64encode(value).rstrip(b"=") + + +def dict_to_b64(value: dict) -> bytes: + """Transform a dictionary into base64url encoded json dump of dictionary.""" + return b64url_encode(json.dumps(value, separators=(",", ":")).encode()) + + +VALID = 0x00 +INVALID = 0x01 +SUSPENDED = 0x02 + +Bits = Union[Literal[1, 2, 4, 8], int] +Bit = Literal[1] +Crumb = Literal[2] +Nibble = Literal[4] +Byte = Literal[8] +StatusTypes = Union[Literal[0x00, 0x01, 0x02], int] + + +N = TypeVar("N", bound=Bits) + + +class BitArray(Generic[N]): + """Variable size bit array.""" + + SHIFT_BY = {1: 3, 2: 2, 4: 1, 8: 0} + # Number of elements that fit in a byte for a number of bits + PER_BYTE = {1: 8, 2: 4, 4: 2, 8: 1} + MASK = {1: 0b1, 2: 0b11, 4: 0b1111, 8: 0b11111111} + MAX = {1: 1, 2: 3, 4: 15, 8: 255} + + def __init__( + self, + bits: N, + lst: bytes, + ): + """Initialize the list.""" + if bits not in (1, 2, 4, 8): + raise ValueError("Invalid bits value, must be one of: 1, 2, 4, 8") + + self.bits = bits + self.per_byte = self.PER_BYTE[bits] + self.shift = self.SHIFT_BY[bits] + self.mask = self.MASK[bits] + self.max = self.MAX[bits] + + # len * indexes per byte + self.size = len(lst) << self.shift + self.lst = bytearray(lst) + + @classmethod + def of_size(cls, bits: Bits, size: int) -> "BitArray": + """Create empty list of a given size.""" + per_byte = cls.PER_BYTE[bits] + if size < 1: + raise ValueError("size must be greater than 1") + # size mod per_byte + if size & (per_byte - 1) != 0: + raise ValueError(f"size must be multiple of {per_byte}") + + length = size >> cls.SHIFT_BY[bits] + return cls(bits, bytearray(length)) + + @classmethod + def with_at_least(cls, bits: Bits, size: int): + """Create an empty list large enough to accommodate at least the given size.""" + # Determine minimum number of bytes to fit size + # This is essentially a fast ceil(n / 2^x) + length = (size + cls.PER_BYTE[bits] - 1) >> cls.SHIFT_BY[bits] + return cls(bits, bytearray(length)) + + def __getitem__(self, index: int): + """Retrieve the status of an index.""" + if isinstance(index, slice): + raise ValueError("Slices are not supported on BitArray") + + return self.get(index) + + def __setitem__(self, index: int, status: StatusTypes): + """Set the status of an index.""" + return self.set(index, status) + + def __len__(self): + """Return size of array.""" + return self.size + + def get(self, index: int): + """Retrieve the status of an index.""" + if index >= self.size: + raise IndexError("Index is out of bounds") + + if index < 0: + raise IndexError("Index is out of bounds") + + # index / indexes per byte + byte_idx = index >> self.shift + # index mod indexes per byte * bits + # Determines the number of shifts to move relevant bits all the way right + bit_idx = (index & (self.per_byte - 1)) * self.bits + # Shift relevant bits all the way right and mask out irrelevant bits + return self.mask & (self.lst[byte_idx] >> bit_idx) + + def set(self, index: int, status: int): + """Set the status of an index.""" + if status > self.max: + raise ValueError(f"status {status} too large for list with bits {self.bits}") + if index >= self.size: + raise ValueError("Invalid index; out of range") + + # index / indexes per byte + byte_idx = index >> self.shift + # index mod indexes per byte * bits + # Determines the number of shifts to move relevant bits all the way right + bit_idx = (index & (self.per_byte - 1)) * self.bits + byte = self.lst[byte_idx] + + # Shift status to relevant position + status <<= bit_idx + # Create mask to clear bits getting reset + # (0 where the bits will be, 1 everywhere else) + clear_mask = ~(self.mask << bit_idx) + # Reset bits to zero + byte &= clear_mask + # Set status bits + self.lst[byte_idx] = byte | status + + def compressed(self) -> bytes: + """Return compressed list.""" + return zlib.compress(self.lst, level=9) + + def to_b64(self) -> str: + """Return list as compressed b64url encoded str.""" + return b64url_encode(self.compressed()).decode() + + @classmethod + def from_b64(cls, bits: N, value: str) -> "BitArray": + """Return list from compressed b64url encoded str.""" + return cls(bits, zlib.decompress(b64url_decode(value.encode()))) + + def dump(self) -> dict: + """Return json serializable representation of BitArray.""" + return {"bits": self.bits, "lst": self.to_b64()} + + @classmethod + def load(cls, value: dict) -> "BitArray": + """Deserialize dict into BitArray.""" + bits = value.get("bits") + if not bits: + raise ValueError("bits missing from issuer status list dictionary") + + if not isinstance(bits, int): + raise TypeError("bits must be int") + + if bits not in (1, 2, 4, 8): + raise ValueError("bits must be 1, 2, 4, or 8") + + lst = value.get("lst") + if not lst: + raise ValueError("status_list missing from status list dictionary") + + if not isinstance(lst, str): + raise TypeError("status_list must be str") + + return cls.from_b64(cast(N, bits), lst) + + +class NoMoreIndices(Exception): + """Raised when no more indices are available.""" + + +class IndexAllocator(Protocol): + """Protocol defining interface for tracking allocated indices.""" + + def take(self) -> int: + """Return next index and mark as allocated.""" + ... + + def take_n(self, n: int) -> List[int]: + """Return next n indices and mark as allocated.""" + ... + + def dump(self) -> dict: + """Return serializable representation of allocated indices and metadata.""" + ... + + @classmethod + def load(cls, value: dict) -> "IndexAllocator": + """Deseiralize a representation of allocated indices and metadata.""" + ... + + +class LinearIndexAllocator(IndexAllocator): + """Linearly allocate indices.""" + + def __init__(self, size: int, start: int = 0): + """Initialize the allocator.""" + self.size = size + self.next = start + + def take(self) -> int: + """Return next index and mark as allocated.""" + if self.next >= self.size: + raise NoMoreIndices("All indices are allocated") + + allocated = self.next + self.next += 1 + return allocated + + def take_n(self, n: int) -> List[int]: + """Return next n indices and mark as allocated. + + This may return fewer than n indices if the list is nearly consumed. + """ + if self.next >= self.size: + raise NoMoreIndices("All indices are allocated") + + if self.next + n >= self.size: + n = self.size - self.next + allocated = list(range(self.next, self.next + n)) + self.next += n + return allocated + + def dump(self) -> dict: + """Return serializable representation of allocated indices and metadata.""" + return { + "type": "linear", + "next": self.next, + "size": self.size, + } + + @classmethod + def load(cls, value: dict) -> "LinearIndexAllocator": + """Deseiralize a representation of allocated indices and metadata.""" + typ = value.get("type") + if typ != "linear": + raise ValueError(f"type incorrect for {cls.__name__}") + + next = value.get("next") + if not isinstance(next, int): + raise TypeError(f"Invalid type for next: {type(next)}") + + size = value.get("size") + if not isinstance(size, int): + raise TypeError(f"Invalid type for size: {type(size)}") + + return cls(size, next) + + +class RandomIndexAllocator(IndexAllocator): + """Randomly allocate indices.""" + + def __init__(self, allocated: BitArray[Bit], num_allocated: Optional[int] = None): + """Initialize allocator.""" + self.allocated = allocated + if num_allocated is not None: + self.num_allocated = num_allocated + else: + self.num_allocated = 0 + for chunk in allocated.lst: + self.num_allocated += chunk.bit_count() + + def linear_scan(self, start: int, stop: int, select: Callable[[int], bool]): + """Scan a small space and return all indices matching condition.""" + return [i for i in range(start, stop) if select(i)] + + def scan_and_rand(self): + """Linear scan and random shuffle and select.""" + byte_idx = choice( + self.linear_scan( + 0, len(self.allocated.lst), lambda i: self.allocated.lst[i] < 255 + ) + ) + start = byte_idx << 3 + end = start + 8 + index = choice(self.linear_scan(start, end, lambda i: self.allocated[i] == 0)) + self.num_allocated += 1 + self.allocated[index] = 1 + return index + + def scan_and_rand_n(self, n: int): + """Take n.""" + available_bytes = self.linear_scan( + 0, len(self.allocated.lst), lambda i: self.allocated.lst[i] < 255 + ) + available_indices = [ + index + for byte_idx in available_bytes + for index in self.linear_scan( + byte_idx << 3, (byte_idx << 3) + 8, lambda i: self.allocated[i] == 0 + ) + ] + return sample(available_indices, n) + + def _rand_settle(self, max: int, settled: Callable[[int], bool]): + """Randomly select a point and 'roll down hill' until settled condition met.""" + direction = choice((-1, 1)) + index = randbelow(max) + start = index + count = 0 + while True: + count += 1 + if settled(index): + return index + index += direction + if index < 0 or index >= max: + index = start + direction = -direction + + def rand_and_settle(self): + """Use rand_settle to randomly select an index.""" + byte_idx = self._rand_settle( + len(self.allocated.lst), lambda index: self.allocated.lst[index] < 255 + ) + start = byte_idx << 3 + end = start + 8 + index = choice(self.linear_scan(start, end, lambda i: self.allocated[i] == 0)) + self.allocated[index] = 1 + self.num_allocated += 1 + return index + + def rand_and_settle_n(self, n: int): + """Take n.""" + return [self.rand_and_settle() for _ in range(n)] + + def take(self) -> int: + """Return next index and mark as allocated.""" + remaining = self.num_allocated - self.allocated.size + if remaining == 0: + raise NoMoreIndices("All Indices are allocated.") + + return self.rand_and_settle() + + def take_n(self, n: int) -> List[int]: + """Return next n indices and mark as allocated. + + This may return fewer than n indices if n is greater than the number of + indices remaining. + """ + remaining = self.num_allocated - self.allocated.size + if remaining == 0: + raise NoMoreIndices("All Indices are allocated.") + + if self.num_allocated + n >= self.allocated.size: + n = self.allocated.size - self.num_allocated + + if n / remaining > 0.4: + return self.scan_and_rand_n(n) + + return self.rand_and_settle_n(n) + + def dump(self) -> dict: + """Return serializable representation of allocated indices and metadata.""" + return { + "type": "random", + "allocated": self.allocated.to_b64(), + "num_allocated": self.num_allocated, + } + + @classmethod + def load(cls, value: dict) -> "IndexAllocator": + """Deseiralize a representation of allocated indices and metadata.""" + typ = value.get("type") + if typ != "random": + raise ValueError(f"type incorrect for {cls.__name__}") + + allocated = value.get("allocated") + if not isinstance(allocated, str): + raise TypeError(f"Invalid type for next: {type(allocated)}") + + num_allocated = value.get("num_allocated") + if not isinstance(num_allocated, int): + raise TypeError(f"Invalid type for num_allocated: {type(num_allocated)}") + + return cls(BitArray.from_b64(1, allocated), num_allocated) + diff --git a/src/bitstring_status_list/issuer.py b/src/bitstring_status_list/issuer.py new file mode 100644 index 0000000..750c093 --- /dev/null +++ b/src/bitstring_status_list/issuer.py @@ -0,0 +1,217 @@ +from src.issuer import Issuer +from src.bit_array import BitArray, IndexAllocator, N, LinearIndexAllocator, RandomIndexAllocator, Bits, dict_to_b64 + +from typing import ( + List, + Literal, + Optional, + Tuple, + Protocol, + Union, +) + +MIN_LIST_LENGTH = 131072 + +class EnvelopingTokenSigner(Protocol): + """Protocol defining the signing callable for enveloping proofs.""" + + def __call__(self, payload: bytes) -> bytes: + """Sign the payload returning bytes of the signature.""" + ... + +class EmbeddingTokenSigner(Protocol): + """Protocol defining the signing callable for embedding proofs.""" + + def __call__(self, payload: bytes) -> dict: + """Sign the payload returning signature in dict form to inject into payload.""" + ... + +class StatusListLengthError(Exception): + """Raised when the status list is insufficiently long.""" + +class BitstringStatusListIssuer(Issuer): + """Bitstring Status List Issuer.""" + def __init__( + self, + status_list: BitArray[N], + allocator: IndexAllocator, + min_list_length: int = MIN_LIST_LENGTH, + ): + super().__init__( + status_list=status_list, + allocator=allocator + ) + self.min_list_length = min_list_length + + if len(self.status_list) < self.min_list_length: + raise StatusListLengthError(f"Bitstring status list must be at least {self.min_list_length} bits \ + long, but was {len(self.status_list)} bits long instead.") + + @classmethod + def load(cls, value: dict) -> "BitstringStatusListIssuer": + """Parse issuer status list from dictionary.""" + allocator = value.get("allocator") + if not allocator: + raise ValueError("allocator missing from issuer status list dictionary") + + if not isinstance(allocator, dict): + raise TypeError("allocator must be dict") + + if allocator.get("type") == "linear": + allocator = LinearIndexAllocator.load(allocator) + elif allocator.get("type") == "random": + allocator = RandomIndexAllocator.load(allocator) + else: + raise ValueError(f"Invalid allocator: {allocator}") + + status_list = value.get("status_list") + if not status_list: + raise ValueError("status_list missing from status list dictionary") + + if not isinstance(status_list, dict): + raise TypeError("status_list must be dict") + + parsed_status_list = BitArray.load(status_list) + return cls(parsed_status_list, allocator) + + @classmethod + def new(cls, size: int, bits: Bits = 1, strategy: Literal["linear", "random"] = "random", min_list_length: int = MIN_LIST_LENGTH) -> "BitstringStatusListIssuer": + """Return a new Issuer.""" + if size < min_list_length: + raise StatusListLengthError(f"Bitstring status list must be at least {min_list_length} bits \ + long, but was {size} bits long instead.") + + if strategy == "linear": + allocator = LinearIndexAllocator(size) + elif strategy == "random": + allocator = RandomIndexAllocator( + BitArray.with_at_least(1, size), num_allocated=0 + ) + else: + raise ValueError(f"Invalid strategy: {strategy}") + + status_list = BitArray.with_at_least(bits, size) + return cls(status_list, allocator) + + def generate_jwt( + self, + alg: Optional[str], + kid: Optional[str], + status_purpose = Union[str, List[str]], + id: Optional[str] = None, + type: Optional[List[str]] = None, + validFrom: Optional[str] = None, + validUntil: Optional[str] = None, + ttl: Optional[int] = None, + issuer: Optional[str] = None, + status_messages: Optional[list] = None, + status_size: Optional[Bits] = None, + ) -> Tuple[dict, dict]: + if status_purpose == "message": + assert status_size is not None + if status_size > 1: + assert status_messages is not None + + headers = { + "kid": kid, + "alg": alg, + } + + payload = { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + ], + + **({"id": id} if id else {}), + "type": (type.append("BitstringStatusListCredential") + if "BitstringStatusListCredential" not in type + else type) + if type + else ["BitstringStatusListCredential"], + + **({"issuer": issuer} if issuer else {}), + **({"validFrom": validFrom} if validFrom else {}), + **({"validUntil": validUntil} if validUntil else {}), + + "credentialSubject": { + **({"id": id} if id else {}), + "type": "BitstringStatusList", + "statusPurpose": status_purpose, + "encodedList": self.status_list.to_b64(), + **({"ttl": ttl} if ttl else {}), + **({"statusMessages": status_messages} if status_messages else {}), + **({"statusSize": status_size} if status_size else {}), + } + } + + return headers, payload + + def sign_jwt_enveloping( + self, + signer: EnvelopingTokenSigner, + alg: str, + kid: str, + status_purpose = str | List[str], + id: Optional[str] = None, + type: Optional[List[str]] = None, + validFrom: Optional[str] = None, + validUntil: Optional[str] = None, + ttl: Optional[int] = None, + issuer: Optional[str] = None, + status_messages: Optional[list] = None, + status_size: Optional[Bits] = None, + ) -> bytes: + headers, payload = self.generate_jwt( + alg=alg, + kid=kid, + status_purpose=status_purpose, + id=id, + type=type, + validFrom=validFrom, + validUntil=validUntil, + ttl=ttl, + issuer=issuer, + status_messages=status_messages, + status_size=status_size, + ) + + enc_headers = dict_to_b64(headers) + enc_payload = dict_to_b64(payload) + enc_to_sign = enc_headers + b"." + enc_payload + + signature = signer(enc_to_sign) + return enc_to_sign + b"." + signature + + def sign_jwt_embedding( + self, + signer: EmbeddingTokenSigner, + status_purpose = str | List[str], + id: Optional[str] = None, + type: Optional[List[str]] = None, + validFrom: Optional[str] = None, + validUntil: Optional[str] = None, + ttl: Optional[int] = None, + issuer: Optional[str] = None, + status_messages: Optional[list] = None, + status_size: Optional[Bits] = None, + ): + headers, payload = self.generate_jwt( + alg=None, + kid=None, + status_purpose=status_purpose, + id=id, + type=type, + validFrom=validFrom, + validUntil=validUntil, + ttl=ttl, + issuer=issuer, + status_messages=status_messages, + status_size=status_size, + ) + + unsigned_payload_bytes = dict_to_b64(payload) + payload["proof"] = signer(unsigned_payload_bytes) + + return dict_to_b64(payload) + + \ No newline at end of file diff --git a/src/bitstring_status_list/verifier.py b/src/bitstring_status_list/verifier.py new file mode 100644 index 0000000..e1ec102 --- /dev/null +++ b/src/bitstring_status_list/verifier.py @@ -0,0 +1,274 @@ +from typing import ( + Optional, + Protocol, +) + +from aiohttp import ClientSession +import json + +from src.bit_array import BitArray, b64url_decode, dict_to_b64 +from src.bitstring_status_list.issuer import MIN_LIST_LENGTH, StatusListLengthError + +class EnvelopingTokenVerifier(Protocol): + """Protocol defining the verifying callable for enveloping signatures.""" + + def __call__(self, payload: bytes, signature: bytes) -> bool: + """Verify the signature of the payload. Returns true if the signature is valid.""" + ... + +class EmbeddingTokenVerifier(Protocol): + """Protocol defining the verifying callable for embedding signatures.""" + + def __call__(self, payload: bytes, signature: dict) -> bool: + """Verify the signature of the payload. Returns true if the signature is valid.""" + ... + +class StatusRetrievalError(Exception): + """Raised if dereference of URL fails. See Bitstring Status List Spec S. 3.2""" + +class StatusVerificationError(Exception): + """Raised if proofs fail or if format is invalid. See Bitstring Status List Spec S. 3.2""" + +class BitstringStatusListVerifier(): + def __init__( + self, + credential_status: dict, + + headers: Optional[dict], + payload: dict, + + bit_array: BitArray, + ): + self.credential_status = credential_status + if not all(key in credential_status.keys() for key in ["id", "type", "statusPurpose", "statusListIndex", "statusListCredential"]): + raise StatusVerificationError(f"Invalid credential_status: {credential_status}. \ + credential status is expected to have keys: \ + [id, type, statusPurpose, statusListIndex, statusListCredential]") + + self.headers = headers + self.payload = payload + + self._bit_array = bit_array + + @classmethod + async def retrieve_list( + cls, + credential_status: dict, + verifier: EnvelopingTokenVerifier | EmbeddingTokenVerifier, + headers: dict | None = None, + min_list_length: int = MIN_LIST_LENGTH, + ) -> "BitstringStatusListVerifier": + """ + Establish connection, parse and verify response, and create instance of + BitstringStatusListVerifier to access it. + + Args: + credential_status: REQUIRED. The credentialStatus field of a verifiable credential as + specified in S. 2.1. + + verifier: REQUIRED. A callable that verifies the signature of a payload, equivalent to + signer in sign_jwt() in issuer.py. + + headers: OPTIONAL. Additional headers for the HTTP request. + + min_list_length: OPTIONAL. The minimum list length, recommended to be 131,072 (see S. 6.1) + + Returns: + An instance of BitstringStatusListVerifier which has been verified for correctness and + integrity. + """ + + headers = headers or {} + + if not all(key in credential_status.keys() for key in ["id", "type", "statusPurpose", "statusListIndex", "statusListCredential"]): + raise StatusVerificationError(f"Invalid credential_status: {credential_status}. \ + credential status is expected to have keys: \ + [id, type, statusPurpose, statusListIndex, statusListCredential]") + + async with ClientSession() as session: + async with session.get(credential_status["statusListCredential"], headers=headers) as resp: + if not 200 <= resp.status < 300: + raise StatusRetrievalError(f"Unable to retrieve token at {credential_status["statusListCredential"]}") + + token = await resp.read() + + return cls.from_jwt(token, credential_status, verifier, min_list_length) + + @classmethod + def from_jwt( + cls, + token: bytes | str, + credential_status: dict, + verifier: EnvelopingTokenVerifier | EmbeddingTokenVerifier, + min_list_length: int = MIN_LIST_LENGTH, + ) -> "BitstringStatusListVerifier": + """ + Takes a status-list response and a verifier, and ensures that the response matches the + required format, verifying the signature using verifier. + + Will assign the headers and payload fields in the class if the format is valid and the + signature is correct, and raise an exception if not. + + Args: + token: REQUIRED. A base64-encoded status_list response, acquired (eg.) from + establish_connection(). + + credential_status: REQUIRED. The credentialStatus field of a verifiable credential as + specified in S. 2.1. + + verifier: REQUIRED. A callable that verifies the signature of a payload. Must match + the proof format of the token (embedded or enveloping) + + min_list_length: OPTIONAL. The minimum list length, recommended to be 131,072 (see S. 6.1) + + Returns: + An instance of BitstringStatusListVerifier which has been verified for correctness and + integrity. + """ + # Check that message is in valid JWT format + if isinstance(token, str): + token = token.encode() + + if not token.startswith(b"ey"): + raise ValueError("JWT requested but token is not a JWT") + + headers = None + if b"." in token: + # Enveloping proof + + # Check that message is in valid JWT format + headers_bytes, payload_bytes, signature = token.split(b".", maxsplit=3) + assert headers_bytes and payload_bytes and signature + + # Verify signature. verifier must be of type EnvelopingTokenVerifier + if not verifier(headers_bytes + b"." + payload_bytes, signature): + raise StatusVerificationError("Invalid signature on payload.") + + # Extract data + headers = json.loads(b64url_decode(headers_bytes)) + payload = json.loads(b64url_decode(payload_bytes)) + else: + # Embedding proof + + # Extract data + payload = json.loads(b64url_decode(token)) + + # Verify signature + unsigned_payload = {key: payload[key] for key in payload if key != "proof"} + if not verifier(dict_to_b64(unsigned_payload), payload["proof"]): + raise StatusVerificationError("Invalid signature on payload") + + # Check values of status list against provided credential + credential_subject = payload["credentialSubject"] + if credential_subject["statusPurpose"] != credential_status["statusPurpose"]: + raise StatusVerificationError( + f"statusPurpose in credential is {credential_status["statusPurpose"]}, while \ + statusPurpose in status list is {credential_subject["statusPurpose"]}" + ) + + # If statusPurpose = message, ensure that a statusMessage list exists in the credential + bits = credential_status.get("statusSize") + if bits is not None and bits > 1 and credential_status.get("statusMessage") is None: + raise StatusVerificationError("For statusSize > 1, a message must exist.") + + if credential_status["statusPurpose"] == "message" and credential_status.get("statusMessage") is None: + raise StatusVerificationError("If statusPurpose is `message`, a statusMessage field must \ + be included which provides the message associated with each bit.") + + # Cache returned status list as BitArray + bit_array = BitArray.from_b64(1 if bits is None else bits, credential_subject["encodedList"]) + if bit_array.size < min_list_length: + raise StatusListLengthError(f"Bitstring status list must be at least {min_list_length} \ + bits long, but was {bit_array.size} bits long instead.") + + return cls( + credential_status=credential_status, + headers=headers, + payload=payload, + bit_array=bit_array, + ) + + def get_status(self, idx: Optional[int] = None): + """ + Returns the status of an object from the status_list in payload. + + Args: + index: OPTIONAL. The index of the token's status in the list, along with relevant metadata + as specified in S. 3.2. If none is provided, the index used will be the index found in + self.credential_status. + + Returns: + The status of the requested token along with relevant metadata. + """ + + if idx is None: + idx = int(self.credential_status["statusListIndex"]) + + status = self._bit_array[idx] + + return_dict = { + "status": status, + "valid": not bool(status), + } + + # If purpose == message, extract the relevant message and add it to the return_dict, as + # described in S. 3.2 Part 14. + purpose = self.credential_status.get("statusPurpose") + if purpose is None or purpose != "message": + return return_dict + + # if purpose == "message" + try: + for message in self.credential_status["statusMessage"]: + if int(message["status"], 16) == status: + return_dict["message"] = message["message"] + return return_dict + + except KeyError as k: + raise StatusVerificationError("statusMessage is malformed or not present") from k + + raise StatusVerificationError(f"Status {status} not found in message list: {self.credential_status["statusMessage"]}") + + def serialize_verifier(self) -> dict: + """ + Utility function: serialize a BitstringStatusListVerifier for storing. + + Returns: + A dictionary with headers and payload, as well as relevant metadata. + """ + + return { + "credential_status": self.credential_status, + **({"headers": self.headers} if self.headers else {}), + "payload": self.payload, + } + + + @classmethod + def deserialize_verifier(cls, seralized_verifier: dict) -> "BitstringStatusListVerifier": + """ + Utility function: deserializes a seralized BitstringStatusListVerifier, which must be in the + same format as the return type of seralize_verifier. Returns a BitstringStatusListVerifier type + with fields populated and the status list stored as a BitArray. + + This function DOES NOT check for correctness or integrity. + + Args: + serialized_verifier: REQUIRED. Serialized verifier type which must be in the same format + as BitstringStatusListVerifier.serialize_verifier. + + Returns: + A BitstringStatusListVerifier instance with relevant fields populated. + """ + + bits = seralized_verifier["credential_status"].get("statusSize") + return cls( + credential_status=seralized_verifier["credential_status"], + headers=seralized_verifier.get("headers"), + payload=seralized_verifier["payload"], + bit_array=BitArray.from_b64( + bits=1 if bits is None else bits, + value=seralized_verifier["payload"]["credentialSubject"]["encodedList"] + ) + ) + \ No newline at end of file diff --git a/src/issuer.py b/src/issuer.py new file mode 100644 index 0000000..64cdf7c --- /dev/null +++ b/src/issuer.py @@ -0,0 +1,50 @@ +from bit_array import * + + + +class Issuer(Generic[N]): + """ Base class for a generic issuer. """ + + def __init__( + self, + status_list: BitArray[N], + allocator: IndexAllocator, + ): + """Initialize issuer status list.""" + self.allocator = allocator + self.status_list = status_list + + def __getitem__(self, index: int): + """Retrieve the status of an index.""" + return self.status_list.get(index) + + def __setitem__(self, index: int, status: StatusTypes): + """Set the status of an index.""" + current = self.status_list.get(index) + if current == 0x01 and status != 0x01: + raise ValueError("Cannot change status of index previously set to invalid") + + return self.status_list.set(index, status) + + def __len__(self): + """Return size of array.""" + return len(self.status_list.lst) + + def take(self) -> int: + """Return the next index to use.""" + return self.allocator.take() + + def take_n(self, n: int) -> List[int]: + """Return the next n indices to use.""" + return self.allocator.take_n(n) + + def dump(self) -> dict: + """Return serializable representation of issuer status list. + + This is an internal representation of the list, including the index selection + strategy and the list of taken indices. + """ + return { + "allocator": self.allocator.dump(), + "status_list": self.status_list.dump(), + } diff --git a/src/token_status_list/issuer.py b/src/token_status_list/issuer.py new file mode 100644 index 0000000..c56a7c9 --- /dev/null +++ b/src/token_status_list/issuer.py @@ -0,0 +1,375 @@ +from time import time +from typing import ( + Any, + Literal, + Optional, + Tuple, + Union, +) + +from bit_array import * +from issuer import * + +# COSE Headers +ALG = 1 +KID = 4 +TYP = 16 # TBD + +# CWT Claims +ISS = 1 +SUB = 2 +AUD = 3 +EXP = 4 +NBF = 5 +IAT = 6 +CTI = 7 + +# Status List Claims +STATUS_LIST = 65533 +TTL = 65534 +STATUS = 65535 + +KNOWN_ALGS_TO_CWT_ALG = { + "ES256": -7, + "ES384": -35, + "ES512": -36, + "EdDSA": -8, +} + +CWTKnownAlgs = Literal["ES256", "ES384", "ES512", "EdDSA"] + +class TokenSigner(Protocol): + """Protocol defining the signing callable.""" + + def __call__(self, payload: bytes) -> bytes: + """Sign the payload returning bytes of the signature.""" + ... + +class TokenStatusListIssuer(Issuer): + """Token Status List Issuer.""" + @classmethod + def load(cls, value: dict) -> "TokenStatusListIssuer": + """Parse issuer status list from dictionary.""" + allocator = value.get("allocator") + if not allocator: + raise ValueError("allocator missing from issuer status list dictionary") + + if not isinstance(allocator, dict): + raise TypeError("allocator must be dict") + + if allocator.get("type") == "linear": + allocator = LinearIndexAllocator.load(allocator) + elif allocator.get("type") == "random": + allocator = RandomIndexAllocator.load(allocator) + else: + raise ValueError(f"Invalid allocator: {allocator}") + + status_list = value.get("status_list") + if not status_list: + raise ValueError("status_list missing from status list dictionary") + + if not isinstance(status_list, dict): + raise TypeError("status_list must be dict") + + parsed_status_list = BitArray.load(status_list) + return cls(parsed_status_list, allocator) + + @classmethod + def new(cls, bits: Bits, size: int, strategy: Literal["linear", "random"] = "random") -> "TokenStatusListIssuer": + """Return a new Issuer.""" + if strategy == "linear": + allocator = LinearIndexAllocator(size) + elif strategy == "random": + allocator = RandomIndexAllocator( + BitArray.with_at_least(1, size), num_allocated=0 + ) + else: + raise ValueError(f"Invalid strategy: {strategy}") + + status_list = BitArray.with_at_least(bits, size) + return cls(status_list, allocator) + + def sign_jwt_payload( + self, + *, + alg: str, + kid: str, + iss: str, + sub: str, + iat: Optional[int] = None, + exp: Optional[int] = None, + ttl: Optional[int] = None, + **additional_claims: Any, + ) -> bytes: + """Create payload of Status List Token in JWT format for signing. + + Signing is NOT performed by this function; only the payload to the signature is + prepared. The caller is responsible for producing a signature. + + Args: + alg: REQUIRED. The algorithm to be used to sign the payload. + + kid: REQUIRED. The kid used to sign the payload. + + iss: REQUIRED when also present in the Referenced Token. The iss (issuer) + claim MUST specify a unique string identifier for the entity that issued + the Status List Token. In the absence of an application profile specifying + otherwise, compliant applications MUST compare issuer values using the + Simple String Comparison method defined in Section 6.2.1 of [RFC3986]. + The value MUST be equal to that of the iss claim contained within the + Referenced Token. + + sub: REQUIRED. The sub (subject) claim MUST specify a unique string identifier + for the Status List Token. The value MUST be equal to that of the uri + claim contained in the status_list claim of the Referenced Token. + + iat: OPTIONAL. The iat (issued at) claim MUST specify the time at which the + Status List Token was issued. If not provided, `now` is used. + + exp: OPTIONAL. The exp (expiration time) claim, if present, MUST specify the + time at which the Status List Token is considered expired by its issuer. + + ttl: OPTIONAL. The ttl (time to live) claim, if present, MUST specify the + maximum amount of time, in seconds, that the Status List Token can be + cached by a consumer before a fresh copy SHOULD be retrieved. The value + of the claim MUST be a positive number. + + additional_claims: OPTIONAL. Additional claims to include in the token. + + Returns: + JWT payload ready for signing. + """ + headers = { + "typ": "statuslist+jwt", + "alg": alg, + "kid": kid, + } + payload = { + "iss": iss, + "sub": sub, + "iat": iat or int(time()), + "status_list": self.status_list.dump(), + **additional_claims, + } + if exp is not None: + payload["exp"] = exp + + if ttl is not None: + payload["ttl"] = ttl + + enc_headers = dict_to_b64(headers).decode() + enc_payload = dict_to_b64(payload).decode() + return f"{enc_headers}.{enc_payload}".encode() + + def signed_jwt_token(self, signed_payload: bytes, signature: bytes) -> str: + """Finish creating a signed token. + + Args: + signed_payload: The value returned from `sign_payload`. + signature: The signature over the signed_payload in bytes. + + Returns: + Finished Status List Token. + """ + return f"{signed_payload.decode()}.{b64url_encode(signature).decode()}" + + def sign_jwt( + self, + signer: TokenSigner, + *, + alg: str, + kid: str, + iss: str, + sub: str, + iat: Optional[int] = None, + exp: Optional[int] = None, + ttl: Optional[int] = None, + **additional_claims: Any, + ) -> str: + """Sign status list to produce a token. + + Args: + signer: REQUIRED. A callable that returns a signature over the payload. + + alg: REQUIRED. The algorithm to be used to sign the payload. + + kid: REQUIRED. The kid used to sign the payload. + + iss: REQUIRED when also present in the Referenced Token. The iss (issuer) + claim MUST specify a unique string identifier for the entity that issued + the Status List Token. In the absence of an application profile specifying + otherwise, compliant applications MUST compare issuer values using the + Simple String Comparison method defined in Section 6.2.1 of [RFC3986]. + The value MUST be equal to that of the iss claim contained within the + Referenced Token. + + sub: REQUIRED. The sub (subject) claim MUST specify a unique string identifier + for the Status List Token. The value MUST be equal to that of the uri + claim contained in the status_list claim of the Referenced Token. + + iat: OPTIONAL. The iat (issued at) claim MUST specify the time at which the + Status List Token was issued. If not provided, `now` is used. + + exp: OPTIONAL. The exp (expiration time) claim, if present, MUST specify the + time at which the Status List Token is considered expired by its issuer. + + ttl: OPTIONAL. The ttl (time to live) claim, if present, MUST specify the + maximum amount of time, in seconds, that the Status List Token can be + cached by a consumer before a fresh copy SHOULD be retrieved. The value + of the claim MUST be a positive number. + + additional_claims: OPTIONAL. Additional claims to include in the token. + + Returns: + Signed JWT of Status List. + """ + payload = self.sign_jwt_payload( + alg=alg, + kid=kid, + iss=iss, + sub=sub, + iat=iat, + exp=exp, + ttl=ttl, + **additional_claims, + ) + signature = signer(payload) + return self.signed_jwt_token(payload, signature) + + def sign_cwt_payload( + self, + *, + alg: Union[CWTKnownAlgs, str], + iss: str, + sub: str, + iat: Optional[int] = None, + exp: Optional[int] = None, + ttl: Optional[int] = None, + **additional_claims: Any, + ) -> Tuple[bytes, bytes]: + """Prepare a CWT Format payload of the status list for signing. + + Signing is NOT performed by this function; only the payload to the signature is + prepared. The caller is responsible for producing a signature. + + Args: + alg: REQUIRED. The algorithm to be used to sign the payload. + + kid: REQUIRED. The kid used to sign the payload. + + iss: REQUIRED when also present in the Referenced Token. The iss (issuer) + claim MUST specify a unique string identifier for the entity that issued + the Status List Token. In the absence of an application profile specifying + otherwise, compliant applications MUST compare issuer values using the + Simple String Comparison method defined in Section 6.2.1 of [RFC3986]. + The value MUST be equal to that of the iss claim contained within the + Referenced Token. + + sub: REQUIRED. The sub (subject) claim MUST specify a unique string identifier + for the Status List Token. The value MUST be equal to that of the uri + claim contained in the status_list claim of the Referenced Token. + + iat: OPTIONAL. The iat (issued at) claim MUST specify the time at which the + Status List Token was issued. If not provided, `now` is used. + + exp: OPTIONAL. The exp (expiration time) claim, if present, MUST specify the + time at which the Status List Token is considered expired by its issuer. + + additional_claims: OPTIONAL. Additional claims to include in the token. + + Returns: + Tuple of encoded_protected_headers and encoded_payload + """ + try: + import cbor2 + except ImportError as err: + raise ImportError("cbor extra required to use this function") from err + + cwt_alg = KNOWN_ALGS_TO_CWT_ALG.get(alg) + if not cwt_alg: + raise ValueError(f"Unknown alg {alg}") + + protected = {ALG: cwt_alg, TYP: "statuslist+cwt"} + payload = { + SUB: sub, + ISS: iss, + IAT: iat or int(time()), + **({EXP: exp} if exp else {}), + **({TTL: ttl} if ttl else {}), + STATUS_LIST: cbor2.dumps( + {"bits": self.status_list.bits, "lst": self.status_list.compressed()} + ), + **additional_claims, + } + + return cbor2.dumps(protected), cbor2.dumps(payload) + + def signed_cwt_token( + self, + kid: str, + encoded_protected_headers: bytes, + encoded_payload: bytes, + signature: bytes, + ) -> bytes: + """Return a CWT its parts.""" + try: + import cbor2 + except ImportError as err: + raise ImportError("cbor extra required to use this function") from err + + unprotected = {KID: kid} + cose_sign1 = [encoded_protected_headers, unprotected, encoded_payload, signature] + tagged = cbor2.CBORTag(18, cose_sign1) + return cbor2.dumps(tagged) + + def sign_cwt( + self, + signer: TokenSigner, + *, + alg: str, + kid: Any, + iss: str, + sub: str, + iat: Optional[int] = None, + exp: Optional[int] = None, + ttl: Optional[int] = None, + **additional_claims: Any, + ) -> bytes: + """Sign status list to produce a CWT token. + + Args: + signer: REQUIRED. A callable that returns a signature over the payload. + + alg: REQUIRED. The algorithm to be used to sign the payload. + + kid: REQUIRED. The kid used to sign the payload. + + iss: REQUIRED when also present in the Referenced Token. The iss (issuer) + claim MUST specify a unique string identifier for the entity that issued + the Status List Token. In the absence of an application profile specifying + otherwise, compliant applications MUST compare issuer values using the + Simple String Comparison method defined in Section 6.2.1 of [RFC3986]. + The value MUST be equal to that of the iss claim contained within the + Referenced Token. + + sub: REQUIRED. The sub (subject) claim MUST specify a unique string identifier + for the Status List Token. The value MUST be equal to that of the uri + claim contained in the status_list claim of the Referenced Token. + + iat: OPTIONAL. The iat (issued at) claim MUST specify the time at which the + Status List Token was issued. If not provided, `now` is used. + + exp: OPTIONAL. The exp (expiration time) claim, if present, MUST specify the + time at which the Status List Token is considered expired by its issuer. + + additional_claims: OPTIONAL. Additional claims to include in the token. + + Returns: + Signed JWT of Status List. + """ + headers, payload = self.sign_cwt_payload( + alg=alg, iss=iss, sub=sub, iat=iat, exp=exp, ttl=ttl, **additional_claims + ) + signature = signer(headers + payload) + token = self.signed_cwt_token(kid, headers, payload, signature) + return token diff --git a/src/token_status_list/verifier.py b/src/token_status_list/verifier.py new file mode 100644 index 0000000..b64652e --- /dev/null +++ b/src/token_status_list/verifier.py @@ -0,0 +1,329 @@ +import json +from time import time +from typing import ( + Literal, + Optional, + Protocol, +) + +from aiohttp import ClientSession + +from bit_array import * +from token_status_list.issuer import ALG, KID, TYP, ISS, SUB, AUD, EXP, NBF, IAT, CTI, STATUS_LIST, TTL, STATUS + +class TokenVerifier(Protocol): + """Protocol defining the verifying callable.""" + + def __call__(self, payload: bytes, signature: bytes) -> bool: + """Verify the signature of the payload. Returns true if the signature is valid.""" + ... + +class SignatureError(Exception): + """ Raised when signature is invalid. """ + +class TokenStatusListVerifier(): + def __init__( + self, + encoding: Literal["CWT", "JWT"], + status_list_uri: str, + + payload: dict, + bit_array: BitArray, + + headers: Optional[dict] = None, + protected_headers: Optional[dict] = None, + unprotected_headers: Optional[dict] = None, + ): + self.encoding = encoding + self.status_list_uri = status_list_uri + + self.headers = headers + self.protected_headers = protected_headers + self.unprotected_headers = unprotected_headers + + if headers is None and (protected_headers is None or unprotected_headers is None): + raise ValueError("Headers must be included.") + + self.payload = payload + self._bit_array = bit_array + + @classmethod + async def retrieve_list( + cls, + status_list_uri: str, + verifier: TokenVerifier, + encoding: Literal["JWT", "CWT"] = "JWT", + headers: dict | None = None, + ) -> "TokenStatusListVerifier": + """ + Establish connection, parse and verify response, and create instance of + TokenStatusListVerifier to access it. + + Args: + status_list_uri: REQUIRED. The uri where the status list can be accessed via HTTP request. + + verifier: REQUIRED. A callable that verifies the signature of a payload, equivalent to + signer in sign_jwt() in issuer.py. + + encoding: OPTIONAL. Either JWT or CWT. Default is JWT. + + headers: OPTIONAL. Additional headers for the HTTP request that is sent to status_list_uri. + + Returns: + An instance of TokenStatusListVerifier which has been verified for correctness and + integrity. + """ + + headers = headers or {} + headers.update({"Accept": f"application/statuslist+{encoding.lower()}"}) + + async with ClientSession() as session: + async with session.get(status_list_uri, headers=headers) as resp: + # Quick method to raise exception on status outside of 200 range + # TODO: consider using a more semantically rich exception + resp.raise_for_status() + token = await resp.read() + + if encoding == "JWT": + return cls.from_jwt(token, status_list_uri, verifier) + if encoding == "CWT": + return cls.from_cwt(token, status_list_uri, verifier) + + @classmethod + def from_jwt( + cls, + token: bytes | str, + status_list_uri: str, + verifier: TokenVerifier, + ) -> "TokenStatusListVerifier": + """ + Takes a status-list response and a verifier, and ensures that the response matches the + required format, verifying the signature using verifier. + + Will assign the headers and payload fields in the class if the format is valid and the + signature is correct, and raise an exception if not. + + Args: + token: REQUIRED. A base64-encoded status_list response, acquired (eg.) from + retrieve_list(). + + status_list_uri: REQUIRED. The uri used to access the status list. + + verifier: REQUIRED. A callable that verifies the signature of a payload, equivalent to + signer in sign_jwt() in issuer.py. + + Returns: + An instance of TokenStatusListVerifier which has been verified for correctness and + integrity. + """ + + # Check that message is in valid JWT format + if isinstance(token, str): + token = token.encode() + + if not token.startswith(b"ey"): + raise ValueError("JWT requested but token is not a JWT") + + headers_bytes, payload_bytes, signature = token.split(b".") + assert headers_bytes and payload_bytes and signature + + # Verify signature + if not verifier(headers_bytes + b"." + payload_bytes, b64url_decode(signature)): + raise SignatureError("Invalid signature on payload.") + + # Extract data + headers: dict = json.loads(b64url_decode(headers_bytes)) + payload: dict = json.loads(b64url_decode(payload_bytes)) + + # Ensure that correct format has been received. + if headers.get("typ") != "statuslist+jwt": + raise TypeError(f"Incorrect format: expected JWT but instead was {headers.get("typ")}") + + # Check correctness of format: ensure existence of status_list, sub, and iat fields + if (payload.get("status_list") is None) or (payload.get("sub") is None) or (payload.get("iat") is None): + raise ValueError(f"Incorrect format: expected fields status_list, sub, and iat in \ + payload, but got {payload} instead.") + + status_list = payload["status_list"] + if (status_list.get("bits") is None) or (status_list.get("lst") is None): + raise ValueError(f"Incorrect format: expected status_list to have a `bits` and `lst` \ + field, but got {status_list} instead.") + + # Check that token is still valid + if "exp" in payload.keys() and payload["exp"] < int(time()): + raise ValueError(f"Token is expired: exp = {payload["exp"]}.") + + # Check issuer uri, if applicable + if status_list_uri != payload["sub"]: + raise ValueError(f"Expected URI {status_list_uri} but instead got {payload["sub"]}") + + return cls( + encoding="JWT", + status_list_uri=status_list_uri, + headers=headers, + payload=payload, + bit_array=BitArray.load(payload["status_list"]), + ) + + @classmethod + def from_cwt( + cls, + token: bytes, + status_list_uri: str, + verifier: TokenVerifier, + ) -> "TokenStatusListVerifier": + """ + Takes a status-list response and a verifier, and ensures that the response matches the + required format, verifying the signature using verifier. + + Will assign the (un)protected headers and payload fields in the class if the format is valid + and the signature is correct, and raise an exception if not. + + Args: + token: REQUIRED. A base64-encoded status_list response, acquired (eg.) from + retrieve_list(). + + status_list_uri: REQUIRED. The uri used to access the status list. + + verifier: REQUIRED. A callable that verifies the signature of a payload, equivalent to + signer in sign_jwt() in issuer.py. + + Returns: + An instance of TokenStatusListVerifier with the relevant fields (headers, payload) + populated. + """ + + try: + import cbor2 + except ImportError as err: + raise ImportError("cbor extra required to use this function") from err + + # Ensure that the format is correct + if token.startswith(b"ey"): + raise ValueError("CWT request but got JWT") + + # Extract data + obj = cbor2.loads(token) + if obj.tag != 18: + raise ValueError(f"Incorrect format: expected tag to be 18 but was {obj.tag} instead.") + + encoded_protected_headers, unprotected_headers, encoded_payload, signature = obj.value + protected_headers: dict = cbor2.loads(encoded_protected_headers) + payload: dict = cbor2.loads(encoded_payload) + + if payload.get(STATUS_LIST) is None: + raise ValueError(f"Incorrect format: unable to find status list tag (65533)") + + status_list = cbor2.loads(payload[STATUS_LIST]) + + # Check signature + if not verifier(encoded_protected_headers + encoded_payload, signature): + raise SignatureError("Invalid signature on payload.") + + # Ensure that the correct format has been received + if protected_headers.get(TYP) != "statuslist+cwt": + raise TypeError(f"Incorrect format: expected CWT but instead was {protected_headers.get(TYP)}.") + + # Check correctness of format: ensure existence of status_list, sub, and iat fields + if status_list.get("bits") is None or status_list.get("lst") is None: + raise ValueError(f"Incorrect format: unble to find bits and lst fields in status list.") + + status_list["lst"] = b64url_encode(status_list["lst"]).decode() # return status_list in b64 encoding + payload[STATUS_LIST] = status_list # put the status_list in human readable form + + if payload.get(SUB) is None or payload.get(IAT) is None: + raise ValueError(f"Incorrect format: sub (2) and iat (6) fields not found.") + + # Check that the token is still valid + if EXP in payload.keys() and payload[EXP] < int(time()): + raise ValueError(f"Token is expired: exp = {payload[EXP]}.") + + # Check status_list_uri + if status_list_uri != payload[SUB]: + raise ValueError(f"Expected URI {status_list_uri} but instead got {payload[SUB]}") + + return cls( + encoding="CWT", + status_list_uri=status_list_uri, + payload=payload, + protected_headers=protected_headers, + unprotected_headers=unprotected_headers, + bit_array=BitArray.load(payload[STATUS_LIST]) + ) + + def get_status(self, idx: int) -> int: + """ + Returns the status of an object from the status_list in payload. + + Args: + index: REQUIRED. The index of the token's status in the list. + + Returns: + The status of the requested token. + """ + + return self._bit_array[idx] + + + def serialize_verifier(self) -> dict: + """ + Utility function: serialize a TokenStatusListVerifier for storing. + + Returns: + A dictionary with headers and payload, as well as relevant metadata. + """ + return_dict = {} + + return_dict["encoding"] = self.encoding + return_dict["status_list_uri"] = self.status_list_uri + return_dict["payload"] = self.payload + + if self.encoding == "JWT": + return_dict["headers"] = self.headers + elif self.encoding == "CWT": + return_dict["protected_headers"] = self.protected_headers + return_dict["unprotected_headers"] = self.unprotected_headers + else: + raise ValueError(f"Invalid encoding: was {self.encoding} but needs to be JWT or CWT") + + return return_dict + + + @classmethod + def deserialize_verifier(cls, seralized_verifier: dict) -> "TokenStatusListVerifier": + """ + Utility function: deserializes a seralized TokenStatusListVerifier, which must be in the + same format as the return type of seralize_verifier. Returns a TokenStatusListVerifier type + with fields populated and the status list stored as a BitArray. + + This function DOES NOT check for correctness or integrity. + + Args: + serialized_verifier: REQUIRED. Serialized verifier type which must be in the same format + as TokenStatusListVerifier.serialize_verifier. + + Returns: + A TokenStatusListVerifier instance with relevant fields populated. + """ + + if seralized_verifier["encoding"] == "JWT": + return cls( + encoding="JWT", + status_list_uri=seralized_verifier["status_list_uri"], + payload=seralized_verifier["payload"], + headers=seralized_verifier["headers"], + bit_array=BitArray.load(seralized_verifier["payload"]["status_list"]) + ) + + elif seralized_verifier["encoding"]: + return cls( + encoding="CWT", + status_list_uri=seralized_verifier["status_list_uri"], + payload=seralized_verifier["payload"], + unprotected_headers=seralized_verifier["unprotected_headers"], + protected_headers=seralized_verifier["protected_headers"], + bit_array=BitArray.load(seralized_verifier["payload"]["status_list"]) + ) + + raise ValueError(f"Invalid encoding: was {seralized_verifier["encoding"]} but needs to be JWT or CWT") + \ No newline at end of file diff --git a/tests/test_alloctor.py b/tests/test_alloctor.py index 065e7ee..9567764 100644 --- a/tests/test_alloctor.py +++ b/tests/test_alloctor.py @@ -5,7 +5,7 @@ from itertools import product from tests import MemoryTracer, Timer -from token_status_list import BitArray, RandomIndexAllocator +from bit_array import BitArray, RandomIndexAllocator SIZE = 10000 diff --git a/tests/test_bit_array.py b/tests/test_bit_array.py index 75215af..f34dfe2 100644 --- a/tests/test_bit_array.py +++ b/tests/test_bit_array.py @@ -4,7 +4,7 @@ from secrets import randbelow from tests import Timer -from token_status_list import SUSPENDED, BitArray, VALID, b64url_decode +from bit_array import SUSPENDED, BitArray, VALID, b64url_decode def test_get_1_bits(): diff --git a/tests/test_bitstring_status_list.py b/tests/test_bitstring_status_list.py new file mode 100644 index 0000000..1d9861f --- /dev/null +++ b/tests/test_bitstring_status_list.py @@ -0,0 +1,294 @@ +import pytest + +from google.auth.crypt.es256 import ES256Signer, ES256Verifier +from cryptography.hazmat.primitives.asymmetric import ec + +from src.bit_array import b64url_encode, b64url_decode +from src.bitstring_status_list.issuer import BitstringStatusListIssuer, EmbeddingTokenSigner, EnvelopingTokenSigner, MIN_LIST_LENGTH +from src.bitstring_status_list.verifier import BitstringStatusListVerifier, EmbeddingTokenVerifier, EnvelopingTokenVerifier + +@pytest.fixture +def status(): + status = BitstringStatusListIssuer.new(MIN_LIST_LENGTH) + for idx in status.take_n(50): + status[idx] = 1 + yield status + +# Integrity +def trivial_enveloping_signer(payload: bytes) -> bytes: + return b"signed" + +def trivial_enveloping_verifier(payload: bytes, signature: bytes) -> bool: + """ Trivial verifier: always says that the signature is valid. """ + return True + +def trivial_embedding_signer(payload: bytes) -> dict: + return {"value": "signed"} + +def trivial_embedding_verifier(payload: bytes, signature: dict) -> bool: + """ Trivial verifier: always says that the signature is valid. """ + return True + + +# More advanced verification +ES256_KEY = ec.generate_private_key(ec.SECT233K1()) + +@pytest.fixture +def es256_enveloping_signer(): + signer = ES256Signer(ES256_KEY) + def sign(payload: bytes) -> bytes: + return signer.sign(payload) + yield sign + +@pytest.fixture +def es256_enveloping_verifier(): + verifier = ES256Verifier(ES256_KEY.public_key()) + def verify(payload: bytes, signature: bytes) -> bool: + return verifier.verify(payload, signature) + yield verify + +@pytest.fixture +def es256_embedding_signer(): + signer = ES256Signer(ES256_KEY) + def sign(payload: bytes) -> dict: + return {"proofValue": b64url_encode(signer.sign(payload)).decode()} + yield sign + +@pytest.fixture +def es256_embedding_verifier(): + verifier = ES256Verifier(ES256_KEY.public_key()) + def verify(payload: bytes, signature: dict) -> bool: + return verifier.verify(payload, b64url_decode(signature["proofValue"].encode())) + yield verify + +def test_verify_jwt_basic_enveloping(status: BitstringStatusListIssuer): + encoded_jwt = status.sign_jwt_enveloping( + signer=trivial_enveloping_signer, + alg="ES256", + kid="12", + status_purpose="revocation", + ) + + credential_status = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "https://example.com/credentials/status/3" + } + + verifier = BitstringStatusListVerifier.from_jwt( + token=encoded_jwt, + credential_status=credential_status, + verifier=trivial_embedding_verifier, + ) + + assert verifier.headers == {"alg": "ES256", "kid": "12"} + assert verifier.payload == { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + ], + + "type": ["BitstringStatusListCredential"], + + "credentialSubject": { + "type": "BitstringStatusList", + "statusPurpose": "revocation", + "encodedList": status.status_list.to_b64(), + } + } + + for i in range(status.status_list.size): + assert verifier.get_status(i) == { + "status": status[i], + "valid": not bool(status[i]) + } + +def test_verify_jwt_basic_embedding(status: BitstringStatusListIssuer): + encoded_jwt = status.sign_jwt_embedding( + signer=trivial_embedding_signer, + status_purpose="revocation", + ) + + credential_status = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "https://example.com/credentials/status/3" + } + + verifier = BitstringStatusListVerifier.from_jwt( + token=encoded_jwt, + credential_status=credential_status, + verifier=trivial_embedding_verifier, + ) + + assert verifier.payload == { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + ], + + "type": ["BitstringStatusListCredential"], + + "credentialSubject": { + "type": "BitstringStatusList", + "statusPurpose": "revocation", + "encodedList": status.status_list.to_b64(), + }, + + "proof": { + "value": "signed", + }, + } + + for i in range(status.status_list.size): + assert verifier.get_status(i) == { + "status": status[i], + "valid": not bool(status[i]) + } + +def test_status_message(): + # Create a bitstring status list with a variety of different statuses + bitstring = BitstringStatusListIssuer.new(MIN_LIST_LENGTH, bits=2) + for idx in bitstring.take_n(50): + bitstring[idx] = 1 + for idx in bitstring.take_n(50): + bitstring[idx] = 2 + for idx in bitstring.take_n(50): + bitstring[idx] = 3 + + status_messages = [ + {"status":"0x0", "message":"0"}, + {"status":"0x1", "message":"1"}, + {"status":"0x2", "message":"2"}, + {"status":"0x3", "message":"3"}, + ] + + encoded_jwt = bitstring.sign_jwt_enveloping( + signer=trivial_enveloping_signer, + alg="ES256", + kid="12", + status_purpose="message", + status_messages=status_messages, + status_size=2, + ) + + credential_status = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "message", + "statusListIndex": "0", + "statusListCredential": "https://example.com/credentials/status/3", + "statusMessage": status_messages, + "statusSize": 2, + } + + verifier = BitstringStatusListVerifier.from_jwt( + token=encoded_jwt, + credential_status=credential_status, + verifier=trivial_enveloping_verifier, + ) + + for i in range(bitstring.status_list.size): + assert verifier.get_status(i) == { + "status": bitstring[i], + "valid": not bool(bitstring[i]), + "message": str(bitstring[i]), + } + +def test_verify_es256_enveloping( + status: BitstringStatusListIssuer, + es256_enveloping_signer: EnvelopingTokenSigner, + es256_enveloping_verifier: EnvelopingTokenVerifier, +): + encoded_jwt = status.sign_jwt_enveloping( + signer=es256_enveloping_signer, + alg="ES256", + kid="12", + status_purpose="revocation", + ) + + credential_status = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "https://example.com/credentials/status/3" + } + + verifier = BitstringStatusListVerifier.from_jwt( + token=encoded_jwt, + credential_status=credential_status, + verifier=es256_enveloping_verifier, + ) + + for i in range(status.status_list.size): + assert verifier.get_status(i) == { + "status": status[i], + "valid": not bool(status[i]) + } + +def test_verify_es256_embedding( + status: BitstringStatusListIssuer, + es256_embedding_signer: EmbeddingTokenSigner, + es256_embedding_verifier: EmbeddingTokenVerifier, +): + encoded_jwt = status.sign_jwt_embedding( + signer=es256_embedding_signer, + status_purpose="revocation", + ) + + credential_status = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "https://example.com/credentials/status/3" + } + + verifier = BitstringStatusListVerifier.from_jwt( + token=encoded_jwt, + credential_status=credential_status, + verifier=es256_embedding_verifier, + ) + + for i in range(status.status_list.size): + assert verifier.get_status(i) == { + "status": status[i], + "valid": not bool(status[i]) + } + +def test_serialization(status: BitstringStatusListIssuer): + encoded_jwt = status.sign_jwt_enveloping( + signer=trivial_enveloping_signer, + alg="ES256", + kid="12", + status_purpose="revocation", + ) + + credential_status = { + "id": "https://example.com/credentials/status/3#94567", + "type": "BitstringStatusListEntry", + "statusPurpose": "revocation", + "statusListIndex": "0", + "statusListCredential": "https://example.com/credentials/status/3" + } + + verifier = BitstringStatusListVerifier.from_jwt( + token=encoded_jwt, + credential_status=credential_status, + verifier=trivial_embedding_verifier, + ) + + serialized_verifier = verifier.serialize_verifier() + unserialized_verifier = BitstringStatusListVerifier.deserialize_verifier(serialized_verifier) + + assert verifier.credential_status == unserialized_verifier.credential_status + + assert verifier.headers == unserialized_verifier.headers + assert verifier.payload == unserialized_verifier.payload + + # Check that values match + for i in range(status.status_list.size): + assert verifier.get_status(i) == unserialized_verifier.get_status(i) diff --git a/tests/test_issuer_status_list.py b/tests/test_token_status_list_issuer.py similarity index 78% rename from tests/test_issuer_status_list.py rename to tests/test_token_status_list_issuer.py index d7b716a..fc5d794 100644 --- a/tests/test_issuer_status_list.py +++ b/tests/test_token_status_list_issuer.py @@ -2,20 +2,21 @@ import json import pytest -from token_status_list import IssuerStatusList, BitArray, b64url_decode +from src.bit_array import BitArray, b64url_decode +from src.token_status_list.issuer import TokenStatusListIssuer @pytest.fixture def status(): lst = BitArray(1, b"\xb9\xa3") - status = IssuerStatusList.new(1, 16) + status = TokenStatusListIssuer.new(1, 16) status.status_list = lst yield status -def test_serde(status: IssuerStatusList): +def test_serde(status: TokenStatusListIssuer): expected = status - actual = IssuerStatusList.load(expected.dump()) + actual = TokenStatusListIssuer.load(expected.dump()) assert len(expected.status_list.lst) == 2 assert len(actual.status_list.lst) == 2 assert expected.status_list.lst == actual.status_list.lst @@ -23,7 +24,7 @@ def test_serde(status: IssuerStatusList): assert expected.status_list.size == actual.status_list.size -def test_sign_jwt_payload(status: IssuerStatusList): +def test_sign_jwt_payload(status: TokenStatusListIssuer): payload = status.sign_jwt_payload( alg="ES256", kid="12", @@ -45,7 +46,7 @@ def test_sign_jwt_payload(status: IssuerStatusList): } -def test_sign_cwt_payload(status: IssuerStatusList): +def test_sign_cwt_payload(status: TokenStatusListIssuer): token = status.sign_cwt( lambda payload: b"10", kid=b"12", diff --git a/tests/test_token_status_list_verifier.py b/tests/test_token_status_list_verifier.py new file mode 100644 index 0000000..35a477a --- /dev/null +++ b/tests/test_token_status_list_verifier.py @@ -0,0 +1,232 @@ +"""Test VerifierStatusList.""" + +import pytest +from time import time + +from google.auth.crypt.es256 import ES256Signer, ES256Verifier +from cryptography.hazmat.primitives.asymmetric import ec + +from src.bit_array import BitArray +from src.token_status_list.issuer import TokenStatusListIssuer, ALG, KID, TYP, ISS, SUB, EXP, IAT, STATUS_LIST, KNOWN_ALGS_TO_CWT_ALG +from src.token_status_list.verifier import TokenStatusListVerifier + +@pytest.fixture +def status(): + lst = BitArray(1, b"\xb9\xa3") + status = TokenStatusListIssuer.new(1, 16) + status.status_list = lst + yield status + +def trivial_signer(payload: bytes) -> bytes: + return b"signed" + +def trivial_verifier(payload: bytes, signature: bytes) -> bool: + """ Trivial verifier: always says that the signature is valid. """ + return True + +# More advanced verification +ES256_KEY = ec.generate_private_key(ec.SECT233K1()) + +@pytest.fixture +def es256_signer(): + signer = ES256Signer(ES256_KEY) + def sign(payload: bytes) -> bytes: + return signer.sign(payload) + yield sign + +@pytest.fixture +def es256_verifier(): + verifier = ES256Verifier(ES256_KEY.public_key()) + def verify(payload: bytes, signature: bytes) -> bool: + return verifier.verify(payload, signature) + yield verify + +def test_verify_jwt_basic(status: TokenStatusListIssuer): + iat = int(time()) + exp = int(time() + 10000) + + payload = status.sign_jwt( + signer=trivial_signer, + alg="ES256", + kid="12", + iss="https://example.com", + sub="https://example.com/statuslists/1", + iat=iat, + exp=exp, + ) + + # Check that token is correctly verified + verifier = TokenStatusListVerifier.from_jwt( + payload.encode(), + "https://example.com/statuslists/1", + trivial_verifier, + ) + + # Check that headers and payload are as expected + assert verifier.encoding == "JWT" + assert verifier.headers == {"alg": "ES256", "kid": "12", "typ": "statuslist+jwt"} + assert verifier.payload == { + "exp": exp, + "iat": iat, + "iss": "https://example.com", + "status_list": {"bits": 1, "lst": "eNrbuRgAAhcBXQ"}, + "sub": "https://example.com/statuslists/1", + } + + # Check that statuses match + for i in range(status.status_list.size): + assert status[i] == verifier.get_status(i) + +def test_verify_jwt_expired(status: TokenStatusListIssuer): + payload = status.sign_jwt( + signer=trivial_signer, + alg="ES256", + kid="12", + iss="https://example.com", + sub="https://example.com/statuslists/1", + iat=10, + exp=20, + ) + + try: + _ = TokenStatusListVerifier.from_jwt( + payload.encode(), + "https://example.com/statuslists/1", + trivial_verifier, + ) + raise ValueError("Token should be expired.") + except ValueError: + return + +def test_verify_jwt_es256(status: TokenStatusListIssuer, es256_signer, es256_verifier): + payload = status.sign_jwt( + signer=es256_signer, + alg="ES256", + kid="12", + iss="https://example.com", + sub="https://example.com/statuslists/1", + iat=int(time()), + exp=int(time() + 10000), + ) + + # Check that token is correctly verified using ES256 + verifier = TokenStatusListVerifier.from_jwt( + payload.encode(), + "https://example.com/statuslists/1", + es256_verifier, + ) + + # Check that values match + for i in range(status.status_list.size): + assert status[i] == verifier.get_status(i) + +def test_verify_cwt_basic(status: TokenStatusListIssuer): + try: + import cbor2 + except ImportError as err: + raise ImportError("cbor extra required to use this function") from err + + iat = int(time()) + exp = int(time()) + 10000 + token = status.sign_cwt( + lambda payload: b"10", + kid=b"12", + alg="ES256", + iss="https://example.com", + sub="https://example.com/statuslists/1", + iat=iat, + exp=exp, + ) + + verifier = TokenStatusListVerifier.from_cwt( + token, + "https://example.com/statuslists/1", + trivial_verifier, + ) + + # Check that headers and payload are as expected + assert verifier.encoding == "CWT" + assert verifier.protected_headers == {ALG: KNOWN_ALGS_TO_CWT_ALG["ES256"], TYP: "statuslist+cwt"} + assert verifier.unprotected_headers == {KID: b"12"} + assert verifier.payload == { + EXP: exp, + IAT: iat, + ISS: "https://example.com", + STATUS_LIST: {"bits": 1, "lst": "eNrbuRgAAhcBXQ"}, + SUB: "https://example.com/statuslists/1", + } + + # Check that values match + for i in range(status.status_list.size): + assert status[i] == verifier.get_status(i) + +def test_verify_cwt_expired(status: TokenStatusListIssuer): + token = status.sign_cwt( + signer=trivial_signer, + alg="ES256", + kid="12", + iss="https://example.com", + sub="https://example.com/statuslists/1", + iat=10, + exp=20, + ) + + try: + _ = TokenStatusListVerifier.from_jwt( + token, + "https://example.com/statuslists/1", + trivial_verifier, + ) + raise ValueError("Token should be expired.") + except ValueError: + return + +def test_verify_cwt_es256(status: TokenStatusListIssuer, es256_signer, es256_verifier): + token = status.sign_cwt( + signer=es256_signer, + alg="ES256", + kid="12", + iss="https://example.com", + sub="https://example.com/statuslists/1", + iat=int(time()), + exp=int(time() + 10000), + ) + + # Check that token is correctly verified using ES256 + verifier = TokenStatusListVerifier.from_cwt( + token, + "https://example.com/statuslists/1", + es256_verifier, + ) + + # Check that values match + for i in range(status.status_list.size): + assert status[i] == verifier.get_status(i) + +def test_serialization(status: TokenStatusListIssuer): + payload = status.sign_jwt( + signer=trivial_signer, + alg="ES256", + kid="12", + iss="https://example.com", + sub="https://example.com/statuslists/1", + ) + + verifier = TokenStatusListVerifier.from_jwt( + payload, + "https://example.com/statuslists/1", + trivial_verifier + ) + + serialized_verifier = verifier.serialize_verifier() + unserialized_verifier = TokenStatusListVerifier.deserialize_verifier(serialized_verifier) + + assert verifier.encoding == unserialized_verifier.encoding + assert verifier.status_list_uri == unserialized_verifier.status_list_uri + + assert verifier.headers == unserialized_verifier.headers + assert verifier.payload == unserialized_verifier.payload + + # Check that values match + for i in range(status.status_list.size): + assert status[i] == verifier.get_status(i) == unserialized_verifier.get_status(i) diff --git a/tests/test_token_status_list_webserver.py b/tests/test_token_status_list_webserver.py new file mode 100644 index 0000000..0e57c01 --- /dev/null +++ b/tests/test_token_status_list_webserver.py @@ -0,0 +1,84 @@ +import pytest +import asyncio + +pytest_plugins = ('pytest_asyncio',) + +from google.auth.crypt.es256 import ES256Verifier +from cryptography.hazmat.primitives.asymmetric import ec +import requests as r + +from src.token_status_list.verifier import TokenStatusListVerifier +from src.token_status_list.issuer import TokenStatusListIssuer +from src.token_status_list.issuer import ALG, KID, TYP, ISS, SUB, EXP, IAT, STATUS_LIST, KNOWN_ALGS_TO_CWT_ALG +from src.bit_array import BitArray + +ISSUER = "http://localhost:3001" + +@pytest.fixture +def status(): + lst = BitArray(1, b"\xb9\xa3") + status = TokenStatusListIssuer.new(1, 16) + status.status_list = lst + yield status + +@pytest.fixture +def es256_verifier(): + public_key_bytes = r.get(ISSUER + "/public_key").content + public_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECT233K1(), public_key_bytes) + yield ES256Verifier(public_key).verify + + +EXPECTED_IAT = 1734650332 +EXPECTED_EXP = 1744650332 + +@pytest.mark.asyncio +async def test_jwt_verify(status, es256_verifier): + verifier = await TokenStatusListVerifier.retrieve_list( + encoding="JWT", + status_list_uri=ISSUER + "/jwt_example", + verifier=es256_verifier + ) + + # Check that headers and payload are as expected + assert verifier.encoding == "JWT" + assert verifier.headers == {"alg": "ES256", "kid": "12", "typ": "statuslist+jwt"} + assert verifier.payload == { + "iat": EXPECTED_IAT, + "exp": EXPECTED_EXP, + "iss": ISSUER, + "status_list": {"bits": 1, "lst": "eNrbuRgAAhcBXQ"}, + "sub": ISSUER + "/jwt_example", + } + + # Check that statuses match + for i in range(len(status)): + assert status[i] == verifier.get_status(i) + +@pytest.mark.asyncio +async def test_cwt_verify(status, es256_verifier): + try: + import cbor2 + except ImportError as err: + raise ImportError("cbor extra required to use this function") from err + + verifier = await TokenStatusListVerifier.retrieve_list( + encoding="CWT", + status_list_uri=ISSUER + "/cwt_example", + verifier=es256_verifier, + ) + + # Check that headers and payload are as expected + assert verifier.encoding == "CWT" + assert verifier.protected_headers == {ALG: KNOWN_ALGS_TO_CWT_ALG["ES256"], TYP: "statuslist+cwt"} + assert verifier.unprotected_headers == {KID: "12"} + assert verifier.payload == { + EXP: EXPECTED_EXP, + IAT: EXPECTED_IAT, + ISS: ISSUER, + STATUS_LIST: {"bits": 1, "lst": "eNrbuRgAAhcBXQ"}, + SUB: ISSUER + "/cwt_example", + } + + # Check that values match + for i in range(len(status)): + assert status[i] == verifier.get_status(i) diff --git a/tests/test_web_server/Dockerfile b/tests/test_web_server/Dockerfile new file mode 100644 index 0000000..c983518 --- /dev/null +++ b/tests/test_web_server/Dockerfile @@ -0,0 +1,21 @@ +FROM alpine + +RUN apk update &&\ + apk add python3 curl nginx + +COPY tests/test_web_server/index.html /var/www/html/index.html +COPY tests/test_web_server/nginx.conf /etc/nginx + +EXPOSE 80 + +RUN curl -sSL https://pdm-project.org/install-pdm.py | python3 - + +WORKDIR /usr/src/app +COPY pdm.lock pyproject.toml tests/test_web_server/main.py README.md ./ +COPY src ./src + +# cbor2 is an optional dependency, but we use it for this test +RUN /root/.local/bin/pdm add cbor2 +RUN /root/.local/bin/pdm run main.py + +CMD ["nginx", "-g", "daemon off;"] diff --git a/tests/test_web_server/index.html b/tests/test_web_server/index.html new file mode 100644 index 0000000..e3e9aac --- /dev/null +++ b/tests/test_web_server/index.html @@ -0,0 +1,11 @@ + + +
+ + + +