From 26e797052f215bd2dd6572cace797fb7662dccf2 Mon Sep 17 00:00:00 2001 From: fegloff Date: Wed, 4 Dec 2024 10:45:32 -0500 Subject: [PATCH 01/11] add jwt logic + test script --- Pipfile | 4 + Pipfile.lock | 1865 ++++++++++++++++++++++++------------ apis/__init__.py | 5 +- apis/auth/__init__.py | 2 + apis/auth/auth_helper.py | 112 +++ apis/auth/auth_resource.py | 151 +++ config.py | 3 + main.py | 73 +- models/__init__.py | 1 + models/auth.py | 27 + test/test_api.py | 95 ++ 11 files changed, 1699 insertions(+), 639 deletions(-) create mode 100644 apis/auth/__init__.py create mode 100644 apis/auth/auth_helper.py create mode 100644 apis/auth/auth_resource.py create mode 100644 models/auth.py create mode 100644 test/test_api.py diff --git a/Pipfile b/Pipfile index d56a804..7bd3fe7 100644 --- a/Pipfile +++ b/Pipfile @@ -28,6 +28,10 @@ google-generativeai = "==0.5.0" yfinance = "==0.2.38" flask-httpauth = "==4.8.0" lumaai = "==1.0.2" +flask-jwt-extended = "*" +eth-account = "*" +web3 = "*" +werkzeug = "==2.3.7" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 769cf30..8306498 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f9bd6e6fc3bbd99ee4abb11548b162c455752df56d710ed151628b9ec186520e" + "sha256": "2f104a67ac35e9a400e2329d8e8d7eb806b70991e898e9e44020f21b1607d064" }, "pipfile-spec": 6, "requires": { @@ -27,100 +27,85 @@ }, "aiohttp": { "hashes": [ - "sha256:10c7932337285a6bfa3a5fe1fd4da90b66ebfd9d0cbd1544402e1202eb9a8c3e", - "sha256:177126e971782769b34933e94fddd1089cef0fe6b82fee8a885e539f5b0f0c6a", - "sha256:1ce46dfb49cfbf9e92818be4b761d4042230b1f0e05ffec0aad15b3eb162b905", - "sha256:1e7a6af57091056a79a35104d6ec29d98ec7f1fb7270ad9c6fff871b678d1ff8", - "sha256:21a72f4a9c69a8567a0aca12042f12bba25d3139fd5dd8eeb9931f4d9e8599cd", - "sha256:21c1925541ca84f7b5e0df361c0a813a7d6a56d3b0030ebd4b220b8d232015f9", - "sha256:21f8225f7dc187018e8433c9326be01477fb2810721e048b33ac49091b19fb4a", - "sha256:22cdeb684d8552490dd2697a5138c4ecb46f844892df437aaf94f7eea99af879", - "sha256:270e653b5a4b557476a1ed40e6b6ce82f331aab669620d7c95c658ef976c9c5e", - "sha256:2df786c96c57cd6b87156ba4c5f166af7b88f3fc05f9d592252fdc83d8615a3c", - "sha256:32710d6b3b6c09c60c794d84ca887a3a2890131c0b02b3cefdcc6709a2260a7c", - "sha256:33a68011a38020ed4ff41ae0dbf4a96a202562ecf2024bdd8f65385f1d07f6ef", - "sha256:365783e1b7c40b59ed4ce2b5a7491bae48f41cd2c30d52647a5b1ee8604c68ad", - "sha256:3a95d2686bc4794d66bd8de654e41b5339fab542b2bca9238aa63ed5f4f2ce82", - "sha256:3b2036479b6b94afaaca7d07b8a68dc0e67b0caf5f6293bb6a5a1825f5923000", - "sha256:3c7f270f4ca92760f98a42c45a58674fff488e23b144ec80b1cc6fa2effed377", - "sha256:3f6d47e392c27206701565c8df4cac6ebed28fdf6dcaea5b1eea7a4631d8e6db", - "sha256:40d2d719c3c36a7a65ed26400e2b45b2d9ed7edf498f4df38b2ae130f25a0d01", - "sha256:4618f0d2bf523043866a9ff8458900d8eb0a6d4018f251dae98e5f1fb699f3a8", - "sha256:471a8c47344b9cc309558b3fcc469bd2c12b49322b4b31eb386c4a2b2d44e44a", - "sha256:4954e6b06dd0be97e1a5751fc606be1f9edbdc553c5d9b57d72406a8fbd17f9d", - "sha256:497a7d20caea8855c5429db3cdb829385467217d7feb86952a6107e033e031b9", - "sha256:4b91f4f62ad39a8a42d511d66269b46cb2fb7dea9564c21ab6c56a642d28bff5", - "sha256:4dbf252ac19860e0ab56cd480d2805498f47c5a2d04f5995d8d8a6effd04b48c", - "sha256:4e10b04542d27e21538e670156e88766543692a0a883f243ba8fad9ddea82e53", - "sha256:5284997e3d88d0dfb874c43e51ae8f4a6f4ca5b90dcf22995035187253d430db", - "sha256:57359785f27394a8bcab0da6dcd46706d087dfebf59a8d0ad2e64a4bc2f6f94f", - "sha256:597128cb7bc5f068181b49a732961f46cb89f85686206289d6ccb5e27cb5fbe2", - "sha256:5aa1a073514cf59c81ad49a4ed9b5d72b2433638cd53160fd2f3a9cfa94718db", - "sha256:680dbcff5adc7f696ccf8bf671d38366a1f620b5616a1d333d0cb33956065395", - "sha256:6984dda9d79064361ab58d03f6c1e793ea845c6cfa89ffe1a7b9bb400dfd56bd", - "sha256:69de056022e7abf69cb9fec795515973cc3eeaff51e3ea8d72a77aa933a91c52", - "sha256:6c7efa6616a95e3bd73b8a69691012d2ef1f95f9ea0189e42f338fae080c2fc6", - "sha256:6d1ad868624f6cea77341ef2877ad4e71f7116834a6cd7ec36ec5c32f94ee6ae", - "sha256:713dff3f87ceec3bde4f3f484861464e722cf7533f9fa6b824ec82bb5a9010a7", - "sha256:71462f8eeca477cbc0c9700a9464e3f75f59068aed5e9d4a521a103692da72dc", - "sha256:7c38cfd355fd86c39b2d54651bd6ed7d63d4fe3b5553f364bae3306e2445f847", - "sha256:8296edd99d0dd9d0eb8b9e25b3b3506eef55c1854e9cc230f0b3f885f680410b", - "sha256:85431c9131a9a0f65260dc7a65c800ca5eae78c4c9931618f18c8e0933a0e0c1", - "sha256:85e4d7bd05d18e4b348441e7584c681eff646e3bf38f68b2626807f3add21aa2", - "sha256:8885ca09d3a9317219c0831276bfe26984b17b2c37b7bf70dd478d17092a4772", - "sha256:8960fabc20bfe4fafb941067cda8e23c8c17c98c121aa31c7bf0cdab11b07842", - "sha256:9443d9ebc5167ce1fbb552faf2d666fb22ef5716a8750be67efd140a7733738c", - "sha256:9721554bfa9e15f6e462da304374c2f1baede3cb06008c36c47fa37ea32f1dc4", - "sha256:98a4eb60e27033dee9593814ca320ee8c199489fbc6b2699d0f710584db7feb7", - "sha256:98fae99d5c2146f254b7806001498e6f9ffb0e330de55a35e72feb7cb2fa399b", - "sha256:9a281cba03bdaa341c70b7551b2256a88d45eead149f48b75a96d41128c240b3", - "sha256:a087c84b4992160ffef7afd98ef24177c8bd4ad61c53607145a8377457385100", - "sha256:a1ba7bc139592339ddeb62c06486d0fa0f4ca61216e14137a40d626c81faf10c", - "sha256:a3081246bab4d419697ee45e555cef5cd1def7ac193dff6f50be761d2e44f194", - "sha256:a72f89aea712c619b2ca32c6f4335c77125ede27530ad9705f4f349357833695", - "sha256:a78ba86d5a08207d1d1ad10b97aed6ea48b374b3f6831d02d0b06545ac0f181e", - "sha256:a961ee6f2cdd1a2be4735333ab284691180d40bad48f97bb598841bfcbfb94ec", - "sha256:ab1546fc8e00676febc81c548a876c7bde32f881b8334b77f84719ab2c7d28dc", - "sha256:ab2d6523575fc98896c80f49ac99e849c0b0e69cc80bf864eed6af2ae728a52b", - "sha256:aff048793d05e1ce05b62e49dccf81fe52719a13f4861530706619506224992b", - "sha256:b1a012677b8e0a39e181e218de47d6741c5922202e3b0b65e412e2ce47c39337", - "sha256:b667e2a03407d79a76c618dc30cedebd48f082d85880d0c9c4ec2faa3e10f43e", - "sha256:b91557ee0893da52794b25660d4f57bb519bcad8b7df301acd3898f7197c5d81", - "sha256:badb51d851358cd7535b647bb67af4854b64f3c85f0d089c737f75504d5910ec", - "sha256:c36074b26f3263879ba8e4dbd33db2b79874a3392f403a70b772701363148b9f", - "sha256:c4916070e12ae140110aa598031876c1bf8676a36a750716ea0aa5bd694aa2e7", - "sha256:c6769d71bfb1ed60321363a9bc05e94dcf05e38295ef41d46ac08919e5b00d19", - "sha256:c887019dbcb4af58a091a45ccf376fffe800b5531b45c1efccda4bedf87747ea", - "sha256:cd9716ef0224fe0d0336997eb242f40619f9f8c5c57e66b525a1ebf9f1d8cebe", - "sha256:ceacea31f8a55cdba02bc72c93eb2e1b77160e91f8abd605969c168502fd71eb", - "sha256:d088ca05381fd409793571d8e34eca06daf41c8c50a05aeed358d2d340c7af81", - "sha256:d3a79200a9d5e621c4623081ddb25380b713c8cf5233cd11c1aabad990bb9381", - "sha256:d82404a0e7b10e0d7f022cf44031b78af8a4f99bd01561ac68f7c24772fed021", - "sha256:d95ae4420669c871667aad92ba8cce6251d61d79c1a38504621094143f94a8b4", - "sha256:da57af0c54a302b7c655fa1ccd5b1817a53739afa39924ef1816e7b7c8a07ccb", - "sha256:ddb9b9764cfb4459acf01c02d2a59d3e5066b06a846a364fd1749aa168efa2be", - "sha256:de23085cf90911600ace512e909114385026b16324fa203cc74c81f21fd3276a", - "sha256:e1f0f7b27171b2956a27bd8f899751d0866ddabdd05cbddf3520f945130a908c", - "sha256:e32148b4a745e70a255a1d44b5664de1f2e24fcefb98a75b60c83b9e260ddb5b", - "sha256:e45fdfcb2d5bcad83373e4808825b7512953146d147488114575780640665027", - "sha256:e56bb7e31c4bc79956b866163170bc89fd619e0581ce813330d4ea46921a4881", - "sha256:e860985f30f3a015979e63e7ba1a391526cdac1b22b7b332579df7867848e255", - "sha256:ee3587506898d4a404b33bd19689286ccf226c3d44d7a73670c8498cd688e42c", - "sha256:ee97c4e54f457c366e1f76fbbf3e8effee9de57dae671084a161c00f481106ce", - "sha256:ef9b484604af05ca745b6108ca1aaa22ae1919037ae4f93aaf9a37ba42e0b835", - "sha256:f21e8f2abed9a44afc3d15bba22e0dfc71e5fa859bea916e42354c16102b036f", - "sha256:f23a6c1d09de5de89a33c9e9b229106cb70dcfdd55e81a3a3580eaadaa32bc92", - "sha256:f5d5d5401744dda50b943d8764508d0e60cc2d3305ac1e6420935861a9d544bc", - "sha256:f78e2a78432c537ae876a93013b7bc0027ba5b93ad7b3463624c4b6906489332", - "sha256:f8179855a4e4f3b931cb1764ec87673d3fbdcca2af496c8d30567d7b034a13db", - "sha256:fc0e7f91705445d79beafba9bb3057dd50830e40fe5417017a76a214af54e122", - "sha256:fe285a697c851734285369614443451462ce78aac2b77db23567507484b1dc6f", - "sha256:fe3d79d6af839ffa46fdc5d2cf34295390894471e9875050eafa584cb781508d", - "sha256:fecd55e7418fabd297fd836e65cbd6371aa4035a264998a091bbf13f94d9c44d", - "sha256:ffef3d763e4c8fc97e740da5b4d0f080b78630a3914f4e772a122bbfa608c1db" + "sha256:018f1b04883a12e77e7fc161934c0f298865d3a484aea536a6a2ca8d909f0ba0", + "sha256:01a8aca4af3da85cea5c90141d23f4b0eee3cbecfd33b029a45a80f28c66c668", + "sha256:04b0cc74d5a882c9dacaeeccc1444f0233212b6f5be8bc90833feef1e1ce14b9", + "sha256:0de6466b9d742b4ee56fe1b2440706e225eb48c77c63152b1584864a236e7a50", + "sha256:12724f3a211fa243570e601f65a8831372caf1a149d2f1859f68479f07efec3d", + "sha256:12e4d45847a174f77b2b9919719203769f220058f642b08504cf8b1cf185dacf", + "sha256:17829f37c0d31d89aa6b8b010475a10233774771f9b6dc2cc352ea4f8ce95d9a", + "sha256:1a17f6a230f81eb53282503823f59d61dff14fb2a93847bf0399dc8e87817307", + "sha256:1cf03d27885f8c5ebf3993a220cc84fc66375e1e6e812731f51aab2b2748f4a6", + "sha256:1fbf41a6bbc319a7816ae0f0177c265b62f2a59ad301a0e49b395746eb2a9884", + "sha256:2257bdd5cf54a4039a4337162cd8048f05a724380a2283df34620f55d4e29341", + "sha256:24054fce8c6d6f33a3e35d1c603ef1b91bbcba73e3f04a22b4f2f27dac59b347", + "sha256:241a6ca732d2766836d62c58c49ca7a93d08251daef0c1e3c850df1d1ca0cbc4", + "sha256:28c7af3e50e5903d21d7b935aceed901cc2475463bc16ddd5587653548661fdb", + "sha256:351849aca2c6f814575c1a485c01c17a4240413f960df1bf9f5deb0003c61a53", + "sha256:3ce18f703b7298e7f7633efd6a90138d99a3f9a656cb52c1201e76cb5d79cf08", + "sha256:3d1c9c15d3999107cbb9b2d76ca6172e6710a12fda22434ee8bd3f432b7b17e8", + "sha256:3dd3e7e7c9ef3e7214f014f1ae260892286647b3cf7c7f1b644a568fd410f8ca", + "sha256:43bfd25113c1e98aec6c70e26d5f4331efbf4aa9037ba9ad88f090853bf64d7f", + "sha256:43dd89a6194f6ab02a3fe36b09e42e2df19c211fc2050ce37374d96f39604997", + "sha256:481f10a1a45c5f4c4a578bbd74cff22eb64460a6549819242a87a80788461fba", + "sha256:4ba8d043fed7ffa117024d7ba66fdea011c0e7602327c6d73cacaea38abe4491", + "sha256:4bb7493c3e3a36d3012b8564bd0e2783259ddd7ef3a81a74f0dbfa000fce48b7", + "sha256:4c1a6309005acc4b2bcc577ba3b9169fea52638709ffacbd071f3503264620da", + "sha256:4dda726f89bfa5c465ba45b76515135a3ece0088dfa2da49b8bb278f3bdeea12", + "sha256:53c921b58fdc6485d6b2603e0132bb01cd59b8f0620ffc0907f525e0ba071687", + "sha256:5578cf40440eafcb054cf859964bc120ab52ebe0e0562d2b898126d868749629", + "sha256:59ee1925b5a5efdf6c4e7be51deee93984d0ac14a6897bd521b498b9916f1544", + "sha256:670847ee6aeb3a569cd7cdfbe0c3bec1d44828bbfbe78c5d305f7f804870ef9e", + "sha256:78c657ece7a73b976905ab9ec8be9ef2df12ed8984c24598a1791c58ce3b4ce4", + "sha256:7a9318da4b4ada9a67c1dd84d1c0834123081e746bee311a16bb449f363d965e", + "sha256:7b2f8107a3c329789f3c00b2daad0e35f548d0a55cda6291579136622099a46e", + "sha256:7ea4490360b605804bea8173d2d086b6c379d6bb22ac434de605a9cbce006e7d", + "sha256:8360c7cc620abb320e1b8d603c39095101391a82b1d0be05fb2225471c9c5c52", + "sha256:875f7100ce0e74af51d4139495eec4025affa1a605280f23990b6434b81df1bd", + "sha256:8bedb1f6cb919af3b6353921c71281b1491f948ca64408871465d889b4ee1b66", + "sha256:8d20cfe63a1c135d26bde8c1d0ea46fd1200884afbc523466d2f1cf517d1fe33", + "sha256:9202f184cc0582b1db15056f2225ab4c1e3dac4d9ade50dd0613ac3c46352ac2", + "sha256:9acfc7f652b31853eed3b92095b0acf06fd5597eeea42e939bd23a17137679d5", + "sha256:9d18a8b44ec8502a7fde91446cd9c9b95ce7c49f1eacc1fb2358b8907d4369fd", + "sha256:9e67531370a3b07e49b280c1f8c2df67985c790ad2834d1b288a2f13cd341c5f", + "sha256:9ee6a4cdcbf54b8083dc9723cdf5f41f722c00db40ccf9ec2616e27869151129", + "sha256:a7d9a606355655617fee25dd7e54d3af50804d002f1fd3118dd6312d26692d70", + "sha256:aa3705a8d14de39898da0fbad920b2a37b7547c3afd2a18b9b81f0223b7d0f68", + "sha256:b7215bf2b53bc6cb35808149980c2ae80a4ae4e273890ac85459c014d5aa60ac", + "sha256:badda65ac99555791eed75e234afb94686ed2317670c68bff8a4498acdaee935", + "sha256:bf0e6cce113596377cadda4e3ac5fb89f095bd492226e46d91b4baef1dd16f60", + "sha256:c171fc35d3174bbf4787381716564042a4cbc008824d8195eede3d9b938e29a8", + "sha256:c1f6490dd1862af5aae6cfcf2a274bffa9a5b32a8f5acb519a7ecf5a99a88866", + "sha256:c25b74a811dba37c7ea6a14d99eb9402d89c8d739d50748a75f3cf994cf19c43", + "sha256:c6095aaf852c34f42e1bd0cf0dc32d1e4b48a90bfb5054abdbb9d64b36acadcb", + "sha256:c63f898f683d1379b9be5afc3dd139e20b30b0b1e0bf69a3fc3681f364cf1629", + "sha256:cd8d62cab363dfe713067027a5adb4907515861f1e4ce63e7be810b83668b847", + "sha256:ce91a24aac80de6be8512fb1c4838a9881aa713f44f4e91dd7bb3b34061b497d", + "sha256:cea52d11e02123f125f9055dfe0ccf1c3857225fb879e4a944fae12989e2aef2", + "sha256:cf4efa2d01f697a7dbd0509891a286a4af0d86902fc594e20e3b1712c28c0106", + "sha256:d2fa6fc7cc865d26ff42480ac9b52b8c9b7da30a10a6442a9cdf429de840e949", + "sha256:d329300fb23e14ed1f8c6d688dfd867d1dcc3b1d7cd49b7f8c5b44e797ce0932", + "sha256:d6177077a31b1aecfc3c9070bd2f11419dbb4a70f30f4c65b124714f525c2e48", + "sha256:db37248535d1ae40735d15bdf26ad43be19e3d93ab3f3dad8507eb0f85bb8124", + "sha256:db70a47987e34494b451a334605bee57a126fe8d290511349e86810b4be53b01", + "sha256:dcefcf2915a2dbdbce37e2fc1622129a1918abfe3d06721ce9f6cdac9b6d2eaa", + "sha256:dda3ed0a7869d2fa16aa41f9961ade73aa2c2e3b2fcb0a352524e7b744881889", + "sha256:e0bf378db07df0a713a1e32381a1b277e62ad106d0dbe17b5479e76ec706d720", + "sha256:e13a05db87d3b241c186d0936808d0e4e12decc267c617d54e9c643807e968b6", + "sha256:e143b0ef9cb1a2b4f74f56d4fbe50caa7c2bb93390aff52f9398d21d89bc73ea", + "sha256:e22d1721c978a6494adc824e0916f9d187fa57baeda34b55140315fa2f740184", + "sha256:e5522ee72f95661e79db691310290c4618b86dff2d9b90baedf343fd7a08bf79", + "sha256:e993676c71288618eb07e20622572b1250d8713e7e00ab3aabae28cb70f3640d", + "sha256:ee9afa1b0d2293c46954f47f33e150798ad68b78925e3710044e0d67a9487791", + "sha256:f1ac5462582d6561c1c1708853a9faf612ff4e5ea5e679e99be36143d6eabd8e", + "sha256:f5022504adab881e2d801a88b748ea63f2a9d130e0b2c430824682a96f6534be", + "sha256:f5b973cce96793725ef63eb449adfb74f99c043c718acb76e0d2a447ae369962", + "sha256:f7c58a240260822dc07f6ae32a0293dd5bccd618bb2d0f36d51c5dbd526f89c0", + "sha256:fc6da202068e0a268e298d7cd09b6e9f3997736cd9b060e2750963754552a0a9", + "sha256:fdadc3f6a32d6eca45f9a900a254757fd7855dfb2d8f8dcf0e88f0fae3ff8eb1" ], - "markers": "python_version >= '3.8'", - "version": "==3.10.8" + "markers": "python_version >= '3.9'", + "version": "==3.11.7" }, "aiosignal": { "hashes": [ @@ -174,7 +159,7 @@ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_version < '3.11'", + "markers": "python_version >= '3.7'", "version": "==4.0.3" }, "attrs": { @@ -234,13 +219,155 @@ "markers": "python_full_version >= '3.6.0'", "version": "==4.12.3" }, + "bitarray": { + "hashes": [ + "sha256:000df24c183011b5d27c23d79970f49b6762e5bb5aacd25da9c3e9695c693222", + "sha256:0027b8f3bb2bba914c79115e96a59b9924aafa1a578223a7c4f0a7242d349842", + "sha256:00f9a88c56e373009ac3c73c55205cfbd9683fbd247e2f9a64bae3da78795252", + "sha256:041c889e69c847b8a96346650e50f728b747ae176889199c49a3f31ae1de0e23", + "sha256:0879f839ec8f079fa60c3255966c2e1aa7196699a234d4e5b7898fbc321901b5", + "sha256:0b555006a7dea53f6bebc616a4d0249cecbf8f1fadf77860120a2e5dbdc2f167", + "sha256:0b655c3110e315219e266b2732609fddb0857bc69593de29f3c2ba74b7d3f51a", + "sha256:0cecaf2981c9cd2054547f651537b4f4939f9fe225d3fc2b77324b597c124e40", + "sha256:0e104f9399144fab6a892d379ba1bb4275e56272eb465059beef52a77b4e5ce6", + "sha256:0ef5c787c8263c082a73219a69eb60a500e157a4ac69d1b8515ad836b0e71fb4", + "sha256:12f19ede03e685c5c588ab5ed63167999295ffab5e1126c5fe97d12c0718c18f", + "sha256:1414a7102a3c4986f241480544f5c99f5d32258fb9b85c9c04e84e48c490ab35", + "sha256:147542299f458bdb177f798726e5f7d39ab8491de4182c3c6d9885ed275a3c2b", + "sha256:150b7b29c36d9f1a24779aea723fdfc73d1c1c161dc0ea14990da27d4e947092", + "sha256:153d7c416a70951dcfa73487af05d2f49c632e95602f1620cd9a651fa2033695", + "sha256:184972c96e1c7e691be60c3792ca1a51dd22b7f25d96ebea502fe3c9b554f25d", + "sha256:18abdce7ab5d2104437c39670821cba0b32fdb9b2da9e6d17a4ff295362bd9dc", + "sha256:2055206ed653bee0b56628f6a4d248d53e5660228d355bbec0014bdfa27050ae", + "sha256:20f30373f0af9cb583e4122348cefde93c82865dbcbccc4997108b3d575ece84", + "sha256:22b00f65193fafb13aa644e16012c8b49e7d5cbb6bb72825105ff89aadaa01e3", + "sha256:251cd5bd47f542893b2b61860eded54f34920ea47fd5bff038d85e7a2f7ae99b", + "sha256:2855cc01ee370f7e6e3ec97eebe44b1453c83fb35080313145e2c8c3c5243afb", + "sha256:2ac67b658fa5426503e9581a3fb44a26a3b346c1abd17105735f07db572195b3", + "sha256:2d9fe3ee51afeb909b68f97e14c6539ace3f4faa99b21012e610bbe7315c388d", + "sha256:2da91ab3633c66999c2a352f0ca9ae064f553e5fc0eca231d28e7e305b83e942", + "sha256:2dad7ba2af80f9ec1dd988c3aca7992408ec0d0b4c215b65d353d95ab0070b10", + "sha256:34fc13da3518f14825b239374734fce93c1a9299ed7b558c3ec1d659ec7e4c70", + "sha256:369b6d457af94af901d632c7e625ca6caf0a7484110fc91c6290ce26bc4f1478", + "sha256:37be5482b9df3105bad00fdf7dc65244e449b130867c3879c9db1db7d72e508b", + "sha256:3963b80a68aedcd722a9978d261ae53cb9bb6a8129cc29790f0f10ce5aca287a", + "sha256:39b38a3d45dac39d528c87b700b81dfd5e8dc8e9e1a102503336310ef837c3fd", + "sha256:3cd565253889940b4ec4768d24f101d9fe111cad4606fdb203ea16f9797cf9ed", + "sha256:3d47bc4ff9b0e1624d613563c6fa7b80aebe7863c56c3df5ab238bb7134e8755", + "sha256:3fa5d8e4b28388b337face6ce4029be73585651a44866901513df44be9a491ab", + "sha256:42bf1b222c698b467097f58b9f59dc850dfa694dde4e08237407a6a103757aa3", + "sha256:43b6c7c4f4a7b80e86e24a76f4c6b9b67d03229ea16d7d403520616535c32196", + "sha256:44c3e78b60070389b824d5a654afa1c893df723153c81904088d4922c3cfb6ac", + "sha256:4683bff52f5a0fd523fb5d3138161ef87611e63968e1fcb6cf4b0c6a86970fe0", + "sha256:47ccf9887bd595d4a0536f2310f0dcf89e17ab83b8befa7dc8727b8017120fda", + "sha256:4800c91a14656789d2e67d9513359e23e8a534c8ee1482bb9b517a4cfc845200", + "sha256:4817d73d995bd2b977d9cde6050be8d407791cf1f84c8047fa0bea88c1b815bc", + "sha256:4839d3b64af51e4b8bb4a602563b98b9faeb34fd6c00ed23d7834e40a9d080fc", + "sha256:4ac2027ca650a7302864ed2528220d6cc6921501b383e9917afc7a2424a1e36d", + "sha256:4cb5702dd667f4bb10fed056ffdc4ddaae8193a52cd74cb2cdb54e71f4ef2dd1", + "sha256:53e002ac1073ac70e323a7a4bfa9ab95e7e1a85c79160799e265563f342b1557", + "sha256:545d36332de81e4742a845a80df89530ff193213a50b4cbef937ed5a44c0e5e5", + "sha256:572a61fba7e3a710a8324771322fba8488d134034d349dcd036a7aef74723a80", + "sha256:57d5ef854f8ec434f2ffd9ddcefc25a10848393fe2976e2be2c8c773cf5fef42", + "sha256:5ddbf71a97ad1d6252e6e93d2d703b624d0a5b77c153b12f9ea87d83e1250e0c", + "sha256:5fa4b4d9fa90124b33b251ef74e44e737021f253dc7a9174e1b39f097451f7ca", + "sha256:628f93e9c2c23930bd1cfe21c634d6c84ec30f45f23e69aefe1fcd262186d7bb", + "sha256:648e7ce794928e8d11343b5da8ecc5b910af75a82ea1a4264d5d0a55c3785faa", + "sha256:656db7bdf1d81ec3b57b3cad7ec7276765964bcfd0eb81c5d1331f385298169c", + "sha256:666e44b0458bb2894b64264a29f2cc7b5b2cbcc4c5e9cedfe1fdbde37a8e329a", + "sha256:66a33a537e781eac3a352397ce6b07eedf3a8380ef4a804f8844f3f45e335544", + "sha256:66d6134b7bb737b88f1d16478ad0927c571387f6054f4afa5557825a4c1b78e2", + "sha256:67a0b56dd02f2713f6f52cacb3f251afd67c94c5f0748026d307d87a81a8e15c", + "sha256:6c33129b49196aa7965ac0f16fcde7b6ad8614b606caf01669a0277cef1afe1d", + "sha256:6d2a2ce73f9897268f58857ad6893a1a6680c5a6b28f79d21c7d33285a5ae646", + "sha256:71ad0139c95c9acf4fb62e203b428f9906157b15eecf3f30dc10b55919225896", + "sha256:7814c9924a0b30ecd401f02f082d8697fc5a5be3f8d407efa6e34531ff3c306a", + "sha256:787db8da5e9e29be712f7a6bce153c7bc8697ccc2c38633e347bb9c82475d5c9", + "sha256:7cb885c043000924554fe2124d13084c8fdae03aec52c4086915cd4cb87fe8be", + "sha256:7cd021ada988e73d649289cee00428b75564c46d55fbdcb0e3402e504b0ae5ea", + "sha256:7e51e7f8289bf6bb631e1ef2a8f5e9ca287985ff518fe666abbdfdb6a848cb26", + "sha256:7e9eee03f187cef1e54a4545124109ee0afc84398628b4b32ebb4852b4a66393", + "sha256:7edb83089acbf2c86c8002b96599071931dc4ea5e1513e08306f6f7df879a48b", + "sha256:7f1c24be7519f16a47b7e2ad1a1ef73023d34d8cbe1a3a59b185fc14baabb132", + "sha256:8330912be6cb8e2fbfe8eb69f82dee139d605730cadf8d50882103af9ac83bb4", + "sha256:8a9eb510cde3fa78c2e302bece510bf5ed494ec40e6b082dec753d6e22d5d1b1", + "sha256:8c9733d2ff9b7838ac04bf1048baea153174753e6a47312be14c83c6a395424b", + "sha256:904c1d5e3bd24f0c0d37a582d2461312033c91436a6a4f3bdeeceb4bea4a899d", + "sha256:928b8b6dfcd015e1a81334cfdac02815da2a2407854492a80cf8a3a922b04052", + "sha256:9502c2230d59a4ace2fddfd770dad8e8b414cbd99517e7e56c55c20997c28b8d", + "sha256:96cf0898f8060b2d3ae491762ae871b071212ded97ff9e1e3a5229e9fefe544c", + "sha256:98a4070ddafabddaee70b2aa7cc6286cf73c37984169ab03af1782da2351059a", + "sha256:9929051feeaf8d948cc0b1c9ce57748079a941a1a15c89f6014edf18adaade84", + "sha256:996d1b83eb904589f40974538223eaed1ab0f62be8a5105c280b9bd849e685c4", + "sha256:9c6e52005e91803eb4e08c0a08a481fb55ddce97f926bae1f6fa61b3396b5b61", + "sha256:9e3727ab63dfb6bde00b281934e2212bb7529ea3006c0031a556a84d2268bea5", + "sha256:a0255bd05ec7165e512c115423a5255a3f301417973d20a80fc5bfc3f3640bcb", + "sha256:a2083dc20f0d828a7cdf7a16b20dae56aab0f43dc4f347a3b3039f6577992b03", + "sha256:a3c36b2fcfebe15ad1c10a90c1d52a42bebe960adcbce340fef867203028fbe7", + "sha256:a4f49ac31734fe654a68e2515c0da7f5bbdf2d52755ba09a42ac406f1f08c9d0", + "sha256:a667ea05ba1ea81b722682276dbef1d36990f8908cf51e570099fd505a89f931", + "sha256:a754c1464e7b946b1cac7300c582c6fba7d66e535cd1dab76d998ad285ac5a37", + "sha256:a817ad70c1aff217530576b4f037dd9b539eb2926603354fcac605d824082ad1", + "sha256:aa54c7e1da8cf4be0aab941ea284ec64033ede5d6de3fd47d75e77cafe986e9d", + "sha256:ab37da66a8736ad5a75a58034180e92c41e864da0152b84e71fcc253a2f69cd4", + "sha256:ac06dd72ee1e1b6e312504d06f75220b5894af1fb58f0c20643698f5122aea76", + "sha256:aca0a9cd376beaccd9f504961de83e776dd209c2de5a4c78dc87a78edf61839b", + "sha256:acc07211a59e2f245e9a06f28fa374d094fb0e71cf5366eef52abbb826ddc81e", + "sha256:aef404d5400d95c6ec86664df9924bde667c8865f8e33c9b7bd79823d53b3e5d", + "sha256:b1047999f1797c3ea7b7c85261649249c243308dcf3632840d076d18fa72f142", + "sha256:b7d09ef06ba57bea646144c29764bf6b870fb3c5558ca098191e07b6a1d40bf7", + "sha256:bcf0150ae0bcc4aa97bdfcb231b37bad1a59083c1b5012643b266012bf420e68", + "sha256:bcf524a087b143ba736aebbb054bb399d49e77cf7c04ed24c728e411adc82bfa", + "sha256:beeb79e476d19b91fd6a3439853e4e5ba1b3b475920fa40d62bde719c8af786f", + "sha256:bf90aba4cff9e72e24ecdefe33bad608f147a23fa5c97790a5bab0e72fe62b6d", + "sha256:c23286abba0cb509733c6ce8f4013cd951672c332b2e184dbefbd7331cd234c8", + "sha256:c2945e0390d1329c585c584c6b6d78be017d9c6a1288f9c92006fe907f69cc28", + "sha256:c756a92cf1c1abf01e56a4cc40cb89f0ff9147f2a0be5b557ec436a23ff464d8", + "sha256:c9e9fef0754867d88e948ce8351c9fd7e507d8514e0f242fd67c907b9cdf98b3", + "sha256:ca79f02a98cbda1472449d440592a2fe2ad96fe55515a0447fa8864a38017cf8", + "sha256:cb7302dbcfcb676f0b66f15891f091d0233c4fc23e1d4b9dc9b9e958156e347f", + "sha256:cb98d5b6eac4b2cf2a5a69f60a9c499844b8bea207059e9fc45c752436e6bb49", + "sha256:cc83ea003dd75e9ade3291ef0585577dd5524aec0c8c99305c0aaa2a7570d6db", + "sha256:ce249ed981f428a8b61538ca82d3875847733d579dd40084ab8246549160f8a4", + "sha256:cf0cc2e91dd38122dec2e6541efa99aafb0a62e118179218181eff720b4b8153", + "sha256:d1a199e6d7c3bad5ba9d0e4dc00dde70ee7d111c9dfc521247fa646ef59fa57e", + "sha256:d1d5abf1d6d910599ac16afdd9a0ed3e24f3b46af57f3070cf2792f236f36e0b", + "sha256:d3f761184b93092077c7f6b7dad7bd4e671c1620404a76620da7872ceb576a94", + "sha256:d756bfeb62ca4fe65d2af7a39249d442c05070c047d03729ad6cd4c2e9b0f0bd", + "sha256:d8c36ddc1923bcc4c11b9994c54eaae25034812a42400b7b8a86fe6d242166a2", + "sha256:dbe1084935b942fab206e609fa1ed3f46ad1f2612fb4833e177e9b2a5e006c96", + "sha256:dc1937a0ff2671797d35243db4b596329842480d125a65e9fe964bcffaf16dfc", + "sha256:dfea514e665af278b2e1d4deb542de1cd4f77413bee83dd15ae16175976ea8d5", + "sha256:e008b7b4ce6c7f7a54b250c45c28d4243cc2a3bbfd5298fa7dac92afda229842", + "sha256:e0e7f24a0b01e6e6a0191c50b06ca8edfdec1988d9d2b264d669d2487f4f4680", + "sha256:e15c94d79810c5ab90ddf4d943f71f14332890417be896ca253f21fa3d78d2b1", + "sha256:e56ba8be5f17dee0ffa6d6ce85251e062ded2faa3cbd2558659c671e6c3bf96d", + "sha256:e89ea59a3ed86a6eb150d016ed28b1bedf892802d0ed32b5659d3199440f3ced", + "sha256:e91d46d12781a14ccb8b284566b14933de4e3b29f8bc5e1c17de7a2001ad3b5b", + "sha256:ea40e98d751ed4b255db4a88fe8fb743374183f78470b9e9305aab186bf28ede", + "sha256:eb27c01b747649afd7e1c342961680893df6d8d81f832a6f04d8c8e03a8a54cc", + "sha256:ec5b0f2d13da53e0975ac15ecbe8badb463bdb0bebaa09457f4df3320421915c", + "sha256:ee040ad3b7dfa05e459713099f16373c1f2a6f68b43cb0575a66718e7a5daef4", + "sha256:f12cc7c7638074918cdcc7491aff897df921b092ffd877227892d2686e98f876", + "sha256:f536fc4d1a683025f9caef0bebeafd60384054579ffe0825bb9bd8c59f8c55b8", + "sha256:f71f24b58e75a889b9915e3197865302467f13e7390efdea5b6afc7424b3a2ea", + "sha256:f75fc0198c955d840b836059bd43e0993edbf119923029ca60c4fc017cefa54a", + "sha256:f785af6b7cb07a9b1e5db0dea9ef9e3e8bb3d74874a0a61303eab9c16acc1999", + "sha256:fbb645477595ce2a0fbb678d1cfd08d3b896e5d56196d40fb9e114eeab9382b3", + "sha256:fcef31b062f756ba7eebcd7890c5d5de84b9d64ee877325257bcc9782288564a", + "sha256:fe606e728842389943a939258809dc5db2de831b1d2e0118515059e87f7bbc1a", + "sha256:fef4e3b3f2084b4dae3e5316b44cda72587dcc81f68b4eb2dbda1b8d15261b61", + "sha256:ffd94b4803811c738e504a4b499fb2f848b2f7412d71e6b517508217c1d7929d" + ], + "version": "==3.0.0" + }, "blinker": { "hashes": [ - "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", - "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83" + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" ], - "markers": "python_version >= '3.8'", - "version": "==1.8.2" + "markers": "python_version >= '3.9'", + "version": "==1.9.0" }, "cachelib": { "hashes": [ @@ -341,99 +468,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "chroma-hnswlib": { "hashes": [ @@ -474,6 +616,105 @@ "markers": "python_version >= '3.7'", "version": "==0.4.13" }, + "ckzg": { + "hashes": [ + "sha256:0518933ff3b9550f9dd60d833cdb74e8e97cc1cc58f0560b706916606dfd47d0", + "sha256:0b249914aeaf05cabc71c5c3797e3d6c126cb2c64192b7eb6755ef6aa5ab2f11", + "sha256:1644369af9900a9f109d417d6760693edf134118f3100d0c68f56667de775b80", + "sha256:174f0c356df644d6e349ce03b7284d83dbec859e11ca5d1b1b3bace8b8fbc65d", + "sha256:19c86c8102200484074afac06b3946b457ba9915636de187f63854522be2e3bd", + "sha256:1ac0bca0795990076cde1930ecec307379b5303e34367c6e6e8a16bdba5a7ba5", + "sha256:1d0cf3dccd72376bff10e1833641cc9d642f34f60ca63972626d9dfcfdc8e77f", + "sha256:1d6deb2c822122bdd32b555fa3b9216c86a355f24a2cc6a46b9b5743b412b60c", + "sha256:1dd2aec2c61e8cc2ec815900f6768c6fe74b8fd29810e79b57c4150c6db32fb6", + "sha256:1fd9fb690c88919f30c9f3ab7cc46a7ecd734d5ff4c9ccea383c119b9b7cc4da", + "sha256:24fda2637598a467e7b11ff664805ee7fdf4f6c7b0c043d6d0a6ccb69b5681ee", + "sha256:260608a22e2f2cadcd31f4495832d45d6460438c38faba9761b92df885a99d88", + "sha256:261414121091042d29f28fc319d7c9a7f950f91f8bf54c010b581ee6a0499473", + "sha256:269f82b992facbd20461310cf5784551c77d11017b7d4b85d741d70359be6794", + "sha256:285cf3121b8a8c5609c5b706314f68d2ba2784ab02c5bb7487c6ae1714ecb27f", + "sha256:2eb50c53efdb9c34f762bd0c8006cf79bc92a9daf47aa6b541e496988484124f", + "sha256:2f53fba88febac17e82a96eb83dc38ecf4b28abcdd15c0246534c358bd3b26c4", + "sha256:2f927bc41c2551b0ef0056a649a7ebed29d9665680a10795f4cee5002c69ddb7", + "sha256:30e375cd45142e56b5dbfdec05ce4deb2368d7f7dedfc7408ba37d5639af05ff", + "sha256:30f08c984286853271d4adae219e9ba87275a15047dbaa262ab8dd6c01be97b0", + "sha256:31d1b141d41fa51aeac9440c936b812e885aef5719adfbd3a27550d8dc433997", + "sha256:33ca40ef30129e2347bff3c95ad093403a0d5703476705ab92c92fbffe89bd5a", + "sha256:369cf1aeaf336c31f2050a7f54ae21cf46f4b2db23ebb013fff621144ab361bb", + "sha256:3dbc9580eccecbd485f22e48f6044c48cbe6d838a7b7514cce179c085c65a960", + "sha256:449c4fe38017351eca362106420eeb2d28d50b7e54aa8668b3af29a8ab780132", + "sha256:4508a089e53330866d3360000d76483400eeab5f8057b8e1f3e344ce2cc0097b", + "sha256:4516d86647ee4e8ea9470f4adf68fbebb6dc1bdedff7d9592c2504fe53145908", + "sha256:4595db84ce63c227e4448de0f7b39d3043e3477d78394ff651708c37fee6c486", + "sha256:4876313614ea01f9a0039b5ca2c754340ba40aa8405f8756912d90ae55718011", + "sha256:4a12a1d8ef8f475d9f0af9a538e1674057e007806cb1204bb269ea00d9f8c1e5", + "sha256:4b4442667058db791325fe231f22e4fc7aaa3495d535d75af5595bc5f4f86036", + "sha256:4c3c9aa9d4477ad52f3561b717e776c1a8a442d9d8b06600c7d8a2857d1ecf05", + "sha256:4cc4bb5f62417a58065deeaf124e178cb1787ef3228e6032600d1e0a2775765b", + "sha256:4e0ebc55253addaa24dd2cd871bbe3b8f57855f32b5f74e70bf2cb76b6f7da54", + "sha256:4fa1ea4888417e1f109fd5e57965788fb7f53b674329b937a65604a3c1ca1d03", + "sha256:50f6f2fbceba9ece3fbc1d2613a246f4e6ec4d787f542859e70c358928c0e4a1", + "sha256:524e1e66edd2be2c38b660824aa7b5d4525b41b30ac029d80738a8eee491aeb5", + "sha256:564abf27878f129781e1df4d33b1c4e264e5b25f89c1bdf95b7d6256e4bceb6c", + "sha256:5747d7926873e3af0f6af5fca666feb0097d06cab525950e2664a6fbcb90165d", + "sha256:583a0b6b531a16974676439b23e7defb3dfe9732f18d13d2316152019c538af1", + "sha256:5b6ec738350771dbf5974fb70cc8bbb20a4df784af770f7e655922adc08a2171", + "sha256:60a58e4d8cb91bad669ca111b7ccdd05c32de6787fdb571bb599625b043ad75b", + "sha256:62c5adc381637affa7e1df465c57750b356a761b8a3164c3106589b02532b9c9", + "sha256:633e143385622d7a43fcb5c4f400ec5ec15df0b1c74ab7d6449a41a7abed24ad", + "sha256:68e0a9cde35f11e80b4e560d22990f2f29dd200a95d3141acde137cb6c883f9a", + "sha256:6f85e5802fea5b77f52fc3a14c8dec18a3f2b7c7070c811a4608940834f563cc", + "sha256:700b989c2f7089edc8fac6dfbd1b4677e85b966216ebedee8eb5e7894765c188", + "sha256:70406b10acf68469ac62110047044a6c1a998f5d5fcd6e27cb3ec2d5760d0490", + "sha256:75484ffb78aaebaeb3a30f1194a9143b904312b0f365fc4101e58e1bf5f89f66", + "sha256:770809c7e93087470cc524724419b0f85590edb033c7c73ba94aef70b36ca18b", + "sha256:7960cc62f959403293fb53a3c2404778369ae7cefc6d7f202e5e00567cf98c4b", + "sha256:8086d23a41020ede312843bda7ea4ee0c9831265379027904106f99f2f8ed469", + "sha256:828cecee16ec576dcf4386beac4eedfd058fd32ee90827f2282e7156a53600be", + "sha256:895d67cfd43130652e1ae39b90465b392d9a72c7c7e6f250eaf14689bfda6351", + "sha256:8f5f29518b0a4555d8f2a28559209bd1d4080547aa629ff9ee51799346573b3f", + "sha256:8f917a7bf363a3735db30559e1ed63cf1ccf414234433ba687fa72c007abd756", + "sha256:8fdec3ff96399acba9baeef9e1b0b5258c08f73245780e6c69f7b73def5e8d0a", + "sha256:91866fc58a29b4829201efd9ffadfac3ffeca6359254a54a360ff6a189c34bf5", + "sha256:926507c569727bb4c851a1eea702c5e902267de96e06ce2d685019f973f72968", + "sha256:9747d92883199d4f8f3a3d7018134745fddcf692dfe67115434e4b32609ea785", + "sha256:979841be50f2782b447762db38e9bc927ae251f6ca86c54a26561a52068ee779", + "sha256:9b4b669fc77edeb16adc182efc32b3737b36f741a2e33a170d40619e8b171a94", + "sha256:9c1869671140ae7e698520b678b594ebd26fb59ef476711403541597d7d32c01", + "sha256:9dd350d97554c161dc5b8c7b32c2dc8e659632c374f60e2669fb3c9b5b294827", + "sha256:a038e26baf650e1c733dcaa066ec948e75556b0c485e8c790c9a758875c71a93", + "sha256:a04bf0b32f04f5ea5e4b8518e292d3321bc05596fde95f9c3b4f504e5e4bc780", + "sha256:a12e96f20dce35e5222f898a5c8355054ef7c5ee038eeb97dbb694640b57577b", + "sha256:a33f71e382020f2bc4ead2bd6881a9bd3811d929f272da239ac01ad615a00802", + "sha256:a9632ef17285dbdd3fcd9780f599c266da736d9b2897decc4ea02ba8690bdf72", + "sha256:abdee71958b214730a8341b16bdd413d0fab1b1a2504fbdb7b0ef2aeee9f9d22", + "sha256:ad6eb83f343fea6dd9a13fd1bce87b9cd26abeeb72f0674a62d26e40fe0b8aca", + "sha256:b2cf58fb9e165da97f0ffe9f4a6efb73992645fac8e0fa223a6cc7ec486a434a", + "sha256:b2f72bc861b8bee9bac3314c58586d1ab2d23530f932a8f0a8562c8a4a6a45f9", + "sha256:b7f9ba6d215f8981c5545f952aac84875bd564a63da02fb22a3d1321662ecdc0", + "sha256:bc2da29bb970d3f5de04fb60797dbb4490c010ffc683cbc6016349dd6fa60d14", + "sha256:bd437ec1dfb4f5609979328b5f465a74307f45d46d24234868c67d44da96903b", + "sha256:be8e0d5015e7755af4ddaab9ae1a4084f72c84b2cbb53628f4366aeed46cc380", + "sha256:c0a2146f122d489ac7e67ae0c0743f8d0db1718e6aeed8f05717340594fe07dd", + "sha256:c3fa0f4398fa67fb71f0a2b34a652cc89e6e0e6af1340b0dc771db1a5f3e089c", + "sha256:d25d006899d76bb8c9d3e8b27981dd6b66a78f9826e33c1bf981af6577a69a19", + "sha256:d721bcd492294c70eca39da0b0a433c29b6a571dbac2f7084bab06334904af06", + "sha256:dde2391d025b5033ef0eeacf62b11ecfe446aea25682b5f547a907766ad0a8cb", + "sha256:decb97f4a17c7338b2130dcc4b045df4cc0e7785ece872c764b554c7c73a99ff", + "sha256:e1015f99c50215098751b07d7e459ba9a2790d3692ca81552eed29996128e90d", + "sha256:e31b59b8124148d5e21f7e41b35532d7af98260c44a77c3917958adece84296d", + "sha256:e7b015f5615bcb82fa0d935481a209fc1dcd9308fb52fb1a7e5400108df67a94", + "sha256:ea27baabe5b22b92901c428768eacf93b992ac7681f93768ab24818ad26ccfed", + "sha256:ed35508dac059b2c0a7994383bc7a92eaf35d0b9ce790016819e2619e0f4b8a9", + "sha256:eec7724fa8dc4ae95757efe4a87e7b2d4b880cb348c72ce7355fc0c4f64bc298", + "sha256:f11933c007c3df02446a81957ac6e2488058b969e2eff5357c98ab569a0c7999", + "sha256:f865a0297aabeeb638187a46f7df445763360417b9df4dea60560d512c2cda09", + "sha256:fab8859d9420f6f7df4e094ee3639bc49d18c8dab0df81bee825e2363dd67a09", + "sha256:fabc3bd41b306d1c7025d561c3281a007c2aca8ceaf998582dc3894904d9c73e", + "sha256:fafb9ac36b3398f8091d40773d9a450e5f74883dad8ca4ee22d472e7a231ef4d" + ], + "version": "==2.0.1" + }, "click": { "hashes": [ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", @@ -532,6 +773,98 @@ "markers": "python_version >= '3.7'", "version": "==43.0.1" }, + "cytoolz": { + "hashes": [ + "sha256:035c8bb4706dcf93a89fb35feadff67e9301935bf6bb864cd2366923b69d9a29", + "sha256:04a84778f48ebddb26948971dc60948907c876ba33b13f9cbb014fe65b341fc2", + "sha256:05a871688df749b982839239fcd3f8ec3b3b4853775d575ff9cd335fa7c75035", + "sha256:05df5ff1cdd198fb57e7368623662578c950be0b14883cadfb9ee4098415e1e5", + "sha256:06d09e9569cfdfc5c082806d4b4582db8023a3ce034097008622bcbac7236f38", + "sha256:0983eee73df86e54bb4a79fcc4996aa8b8368fdbf43897f02f9c3bf39c4dc4fb", + "sha256:0cace092dfda174eed09ed871793beb5b65633963bcda5b1632c73a5aceea1ce", + "sha256:0ce8a2a85c0741c1b19b16e6782c4a5abc54c3caecda66793447112ab2fa9884", + "sha256:0d603f5e2b1072166745ecdd81384a75757a96a704a5642231eb51969f919d5f", + "sha256:0f16907fdc724c55b16776bdb7e629deae81d500fe48cfc3861231753b271355", + "sha256:10e3986066dc379e30e225b230754d9f5996aa8d84c2accc69c473c21d261e46", + "sha256:11d48b8521ef5fe92e099f4fc00717b5d0789c3c90d5d84031b6d3b17dee1700", + "sha256:122ef2425bd3c0419e6e5260d0b18cd25cf74de589cd0184e4a63b24a4641e2e", + "sha256:16576f1bb143ee2cb9f719fcc4b845879fb121f9075c7c5e8a5ff4854bd02fc6", + "sha256:20194dd02954c00c1f0755e636be75a20781f91a4ac9270c7f747e82d3c7f5a5", + "sha256:27c684799708bdc7ee7acfaf464836e1b4dec0996815c1d5efd6a92a4356a562", + "sha256:287d6d7f475882c2ddcbedf8da9a9b37d85b77690779a2d1cdceb5ae3998d52e", + "sha256:28bb88e1e2f7d6d4b8e0890b06d292c568984d717de3e8381f2ca1dd12af6470", + "sha256:364c2fda148def38003b2c86e8adde1d2aab12411dd50872c244a815262e2fda", + "sha256:388cd07ee9a9e504c735a0a933e53c98586a1c301a64af81f7aa7ff40c747520", + "sha256:389ec328bb535f09e71dfe658bf0041f17194ca4cedaacd39bafe7893497a819", + "sha256:3b319a7f0fed5db07d189db4046162ebc183c108df3562a65ba6ebe862d1f634", + "sha256:3faa25a1840b984315e8b3ae517312375f4273ffc9a2f035f548b7f916884f37", + "sha256:44ab57cfc922b15d94899f980d76759ef9e0256912dfab70bf2561bea9cd5b19", + "sha256:45d346620abc8c83ae634136e700432ad6202faffcc24c5ab70b87392dcda8a1", + "sha256:478af5ecc066da093d7660b23d0b465a7f44179739937afbded8af00af412eb6", + "sha256:49375aad431d76650f94877afb92f09f58b6ff9055079ef4f2cd55313f5a1b39", + "sha256:4c45106171c824a61e755355520b646cb35a1987b34bbf5789443823ee137f63", + "sha256:509ed3799c47e4ada14f63e41e8f540ac6e2dab97d5d7298934e6abb9d3830ec", + "sha256:51dfda3983fcc59075c534ce54ca041bb3c80e827ada5d4f25ff7b4049777f94", + "sha256:576a4f1fc73d8836b10458b583f915849da6e4f7914f4ecb623ad95c2508cad5", + "sha256:6433f03910c5e5345d82d6299457c26bf33821224ebb837c6b09d9cdbc414a6c", + "sha256:643a593ec272ef7429099e1182a22f64ec2696c00d295d2a5be390db1b7ff176", + "sha256:658dd85deb375ff7af990a674e5c9058cef1c9d1f5dc89bc87b77be499348144", + "sha256:69a7e5e98fd446079b8b8ec5987aec9a31ec3570a6f494baefa6800b783eaf22", + "sha256:6c371b3114d38ee717780b239179e88d5d358fe759a00dcf07691b8922bbc762", + "sha256:6ce38e2e42cbae30446190c59b92a8a9029e1806fd79eaf88f48b0fe33003893", + "sha256:6dbe5fe3b835859fc559eb59bf2775b5a108f7f2cfab0966f3202859d787d8fd", + "sha256:781fce70a277b20fd95dc66811d1a97bb07b611ceea9bda8b7dd3c6a4b05d59a", + "sha256:7a562c25338eb24d419d1e80a7ae12133844ce6fdeb4ab54459daf250088a1b2", + "sha256:7d56569dfe67a39ce74ffff0dc12cf0a3d1aae709667a303fe8f2dd5fd004fdf", + "sha256:7df2dfd679f0517a96ced1cdd22f5c6c6aeeed28d928a82a02bf4c3fd6fd7ac4", + "sha256:7e53cfcce87e05b7f0ae2fb2b3e5820048cd0bb7b701e92bd8f75c9fbb7c9ae9", + "sha256:810a6a168b8c5ecb412fbae3dd6f7ed6c6253a63caf4174ee9794ebd29b2224f", + "sha256:85c9c8c4465ed1b2c8d67003809aec9627b129cb531d2f6cf0bbfe39952e7e4d", + "sha256:86fb208bfb7420e1d0d20065d661310e4a8a6884851d4044f47d37ed4cd7410e", + "sha256:8819f1f97ebe36efcaf4b550e21677c46ac8a41bed482cf66845f377dd20700d", + "sha256:89a554a9ba112403232a54e15e46ff218b33020f3f45c4baf6520ab198b7ad93", + "sha256:8c51452c938e610f57551aa96e34924169c9100c0448bac88c2fb395cbd3538c", + "sha256:90b343b2f3b3e77c3832ba19b0b17e95412a5b2e715b05c23a55ba525d1fca49", + "sha256:921082fff09ff6e40c12c87b49be044492b2d6bb01d47783995813b76680c7b2", + "sha256:9502bd9e37779cc9893cbab515a474c2ab6af61ed22ac2f7e16033db18fcaa85", + "sha256:9715d1ff5576919d10b68f17241375f6a1eec8961c25b78a83e6ef1487053f39", + "sha256:9770e1b09748ad0d751853d994991e2592a9f8c464a87014365f80dac2e83faa", + "sha256:98a96c54aa55ed9c7cdb23c2f0df39a7b4ee518ac54888480b5bdb5ef69c7ef0", + "sha256:99f39dcc46416dca3eb23664b73187b77fb52cd8ba2ddd8020a292d8f449db67", + "sha256:9af793b1738e4191d15a92e1793f1ffea9f6461022c7b2442f3cb1ea0a4f758a", + "sha256:9b2e945617325242687189966335e785dc0fae316f4c1825baacf56e5a97e65f", + "sha256:9ce25f02b910630f6dc2540dd1e26c9326027ddde6c59f8cab07c56acc70714c", + "sha256:a09cdfb21dfb38aa04df43e7546a41f673377eb5485da88ceb784e327ec7603b", + "sha256:a32f1356f3b64dda883583383966948604ac69ca0b7fbcf5f28856e5f9133b4e", + "sha256:a99e7e29274e293f4ffe20e07f76c2ac753a78f1b40c1828dfc54b2981b2f6c4", + "sha256:acfb8780c04d29423d14aaab74cd1b7b4beaba32f676e7ace02c9acfbf532aba", + "sha256:b1707b6c3a91676ac83a28a231a14b337dbb4436b937e6b3e4fd44209852a48b", + "sha256:becce4b13e110b5ac6b23753dcd0c977f4fdccffa31898296e13fd1109e517e3", + "sha256:c0d56b3721977806dcf1a68b0ecd56feb382fdb0f632af1a9fc5ab9b662b32c6", + "sha256:c507a3e0a45c41d66b43f96797290d75d1e7a8549aa03a4a6b8854fdf3f7b8d8", + "sha256:c64658e1209517ce4b54c1c9269a508b289d8d55fc742760e4b8579eacf09a33", + "sha256:ca526905a014a38cc23ae78635dc51d0462c5c24425b22c08beed9ff2ee03845", + "sha256:caa7ef840847a23b379e6146760e3a22f15f445656af97e55a435c592125cfa5", + "sha256:d3206c81ca3ba2d7b8fe78f2e116e3028e721148be753308e88dcbbc370bca52", + "sha256:da1f82a7828a42468ea2820a25b6e56461361390c29dcd4d68beccfa1b71066b", + "sha256:dbb2ec1177dca700f3db2127e572da20de280c214fc587b2a11c717fc421af56", + "sha256:df0c81197fc130de94c09fc6f024a6a19c98ba8fe55c17f1e45ebba2e9229079", + "sha256:dffc22fd2c91be64dbdbc462d0786f8e8ac9a275cfa1869a1084d1867d4f67e0", + "sha256:e672712d5dc3094afc6fb346dd4e9c18c1f3c69608ddb8cf3b9f8428f9c26a5c", + "sha256:ea4ac72e6b830861035c4c7999af8e55813f57c6d1913a3d93cc4a6babc27bf7", + "sha256:eb453b30182152f9917a5189b7d99046b6ce90cdf8aeb0feff4b2683e600defd", + "sha256:ecf5a887acb8f079ab1b81612b1c889bcbe6611aa7804fd2df46ed310aa5a345", + "sha256:ef0ef30c1e091d4d59d14d8108a16d50bd227be5d52a47da891da5019ac2f8e4", + "sha256:f29d8330aaf070304f7cd5cb7e73e198753624eb0aec278557cccd460c699b5b", + "sha256:f370a1f1f1afc5c1c8cc5edc1cfe0ba444263a0772af7ce094be8e734f41769d", + "sha256:f6039a9bd5bb988762458b9ca82b39e60ca5e5baae2ba93913990dcc5d19fa88", + "sha256:f65283b618b4c4df759f57bcf8483865a73f7f268e6d76886c743407c8d26c1c", + "sha256:f7a9d816af3be9725c70efe0a6e4352a45d3877751b395014b8eb2f79d7d8d9d", + "sha256:fcddbb853770dd6e270d89ea8742f0aa42c255a274b9e1620eb04e019b79785e" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.0" + }, "dataclasses-json": { "hashes": [ "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", @@ -556,6 +889,74 @@ "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==0.16" }, + "eth-abi": { + "hashes": [ + "sha256:33ddd756206e90f7ddff1330cc8cac4aa411a824fe779314a0a52abea2c8fc14", + "sha256:84cac2626a7db8b7d9ebe62b0fdca676ab1014cc7f777189e3c0cd721a4c16d8" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==5.1.0" + }, + "eth-account": { + "hashes": [ + "sha256:2e1f2de240bef3d9f3d8013656135d2a79b6be6d4e7885bce9cace4334a4a376", + "sha256:a4c109e9bad3a278243fcc028b755fb72b43e25b1e6256b3f309a44f5f7d87c3" + ], + "index": "pypi", + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==0.13.4" + }, + "eth-hash": { + "extras": [ + "pycryptodome" + ], + "hashes": [ + "sha256:b8d5a230a2b251f4a291e3164a23a14057c4a6de4b0aa4a16fa4dc9161b57e2f", + "sha256:bacdc705bfd85dadd055ecd35fd1b4f846b671add101427e089a4ca2e8db310a" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==0.7.0" + }, + "eth-keyfile": { + "hashes": [ + "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64", + "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==0.8.1" + }, + "eth-keys": { + "hashes": [ + "sha256:b396fdfe048a5bba3ef3990739aec64901eb99901c03921caa774be668b1db6e", + "sha256:ba33230f851d02c894e83989185b21d76152c49b37e35b61b1d8a6d9f1d20430" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==0.6.0" + }, + "eth-rlp": { + "hashes": [ + "sha256:6f476eb7e37d81feaba5d98aed887e467be92648778c44b19fe594aea209cde1", + "sha256:d5b408a8cd20ed496e8e66d0559560d29bc21cee482f893936a1f05d0dddc4a0" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==2.1.0" + }, + "eth-typing": { + "hashes": [ + "sha256:83debf88c9df286db43bb7374974681ebcc9f048fac81be2548dbc549a3203c0", + "sha256:f30d1af16aac598f216748a952eeb64fbcb6e73efa691d2de31148138afe96de" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==5.0.1" + }, + "eth-utils": { + "hashes": [ + "sha256:84c6314b9cf1fcd526107464bbf487e3f87097a2e753360d5ed319f7d42e3f20", + "sha256:a99f1f01b51206620904c5af47fac65abc143aebd0a76bdec860381c5a3230f8" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==5.1.0" + }, "exceptiongroup": { "hashes": [ "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", @@ -605,6 +1006,15 @@ "index": "pypi", "version": "==4.8.0" }, + "flask-jwt-extended": { + "hashes": [ + "sha256:52f35bf0985354d7fb7b876e2eb0e0b141aaff865a22ff6cc33d9a18aa987978", + "sha256:8085d6757505b6f3291a2638c84d207e8f0ad0de662d1f46aa2f77e658a0c976" + ], + "index": "pypi", + "markers": "python_version >= '3.9' and python_version < '4'", + "version": "==4.7.1" + }, "flask-restx": { "hashes": [ "sha256:62b6b6c9de65e5960cf4f8b35e1bd3eca6998838a01b2f71e2a9d4c14a4ccd14", @@ -679,86 +1089,101 @@ }, "frozenlist": { "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", + "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", + "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", + "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", + "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", + "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", + "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", + "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", + "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", + "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", + "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", + "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", + "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c", + "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", + "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", + "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", + "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", + "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", + "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10", + "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", + "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", + "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", + "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", + "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10", + "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", + "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", + "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", + "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", + "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", + "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923", + "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", + "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", + "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", + "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", + "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", + "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", + "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", + "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", + "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", + "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", + "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", + "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", + "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", + "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", + "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", + "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604", + "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", + "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", + "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", + "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", + "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", + "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", + "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", + "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", + "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3", + "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", + "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", + "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", + "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf", + "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", + "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", + "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171", + "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", + "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", + "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", + "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", + "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", + "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", + "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9", + "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", + "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723", + "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", + "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", + "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99", + "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e", + "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", + "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", + "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", + "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", + "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", + "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca", + "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", + "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", + "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", + "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", + "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307", + "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e", + "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", + "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", + "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", + "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", + "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a" ], "markers": "python_version >= '3.8'", - "version": "==1.4.1" + "version": "==1.5.0" }, "fsspec": { "hashes": [ @@ -1003,6 +1428,14 @@ "markers": "python_version >= '3.7'", "version": "==0.14.0" }, + "hexbytes": { + "hashes": [ + "sha256:515f00dddf31053db4d0d7636dd16061c1d896c3109b8e751005db4ca46bcca7", + "sha256:e64890b203a31f4a23ef11470ecfcca565beaee9198df623047df322b757471a" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==1.2.1" + }, "html5lib": { "hashes": [ "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", @@ -1370,69 +1803,70 @@ }, "markupsafe": { "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" }, "marshmallow": { "hashes": [ @@ -1773,6 +2207,13 @@ "markers": "python_version >= '3.9'", "version": "==2.2.3" }, + "parsimonious": { + "hashes": [ + "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", + "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f" + ], + "version": "==0.10.0" + }, "pdfminer.six": { "hashes": [ "sha256:1eaddd712d5b2732f8ac8486824533514f8ba12a0787b3d5fe1e686cd826532d", @@ -1794,6 +2235,110 @@ ], "version": "==3.6.6" }, + "propcache": { + "hashes": [ + "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", + "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", + "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", + "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", + "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", + "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", + "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", + "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", + "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", + "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", + "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", + "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", + "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", + "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", + "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", + "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", + "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", + "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", + "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", + "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", + "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", + "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", + "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", + "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", + "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", + "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", + "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", + "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", + "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", + "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", + "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", + "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", + "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", + "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", + "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", + "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", + "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", + "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", + "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", + "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", + "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", + "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", + "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", + "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", + "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", + "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", + "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", + "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", + "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", + "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", + "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", + "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", + "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", + "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", + "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", + "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", + "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", + "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", + "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", + "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", + "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", + "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", + "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", + "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", + "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", + "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", + "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", + "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", + "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", + "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", + "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", + "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", + "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", + "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", + "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", + "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", + "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", + "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", + "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", + "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", + "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", + "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", + "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", + "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", + "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", + "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", + "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", + "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", + "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", + "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", + "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", + "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", + "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", + "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", + "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", + "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", + "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", + "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504" + ], + "markers": "python_version >= '3.8'", + "version": "==0.2.0" + }, "proto-plus": { "hashes": [ "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", @@ -1878,108 +2423,157 @@ "markers": "python_version >= '3.8'", "version": "==2.22" }, + "pycryptodome": { + "hashes": [ + "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", + "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d", + "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", + "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", + "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", + "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a", + "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", + "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", + "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", + "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e", + "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", + "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f", + "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", + "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb", + "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e", + "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", + "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", + "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", + "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c", + "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", + "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4", + "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3", + "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819", + "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568", + "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", + "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3", + "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", + "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd", + "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", + "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b", + "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", + "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==3.21.0" + }, "pydantic": { "hashes": [ - "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", - "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12" + "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", + "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e" ], "markers": "python_version >= '3.8'", - "version": "==2.9.2" + "version": "==2.10.1" }, "pydantic-core": { "hashes": [ - "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36", - "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", - "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", - "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", - "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c", - "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", - "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29", - "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744", - "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", - "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", - "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", - "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", - "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577", - "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", - "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", - "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", - "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368", - "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", - "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", - "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2", - "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6", - "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", - "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", - "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", - "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", - "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", - "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271", - "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", - "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb", - "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13", - "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323", - "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556", - "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665", - "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef", - "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", - "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", - "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", - "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", - "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", - "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", - "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", - "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", - "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", - "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21", - "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", - "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", - "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658", - "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", - "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3", - "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb", - "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59", - "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", - "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", - "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", - "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", - "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", - "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55", - "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad", - "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a", - "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605", - "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e", - "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", - "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", - "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", - "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", - "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", - "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", - "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", - "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555", - "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", - "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6", - "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", - "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b", - "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df", - "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", - "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", - "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", - "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", - "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040", - "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12", - "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", - "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", - "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", - "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", - "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", - "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", - "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8", - "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", - "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607" + "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9", + "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b", + "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", + "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", + "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", + "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854", + "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", + "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", + "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a", + "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", + "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", + "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", + "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", + "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", + "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", + "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97", + "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", + "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", + "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", + "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4", + "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", + "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131", + "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", + "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd", + "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", + "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", + "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", + "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60", + "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", + "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", + "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", + "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", + "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2", + "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", + "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", + "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", + "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62", + "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", + "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be", + "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067", + "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", + "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f", + "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", + "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840", + "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5", + "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", + "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", + "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", + "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864", + "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e", + "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", + "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", + "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", + "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a", + "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3", + "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", + "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", + "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31", + "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", + "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", + "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", + "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36", + "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", + "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154", + "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", + "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", + "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd", + "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3", + "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", + "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78", + "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", + "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618", + "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", + "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", + "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", + "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c", + "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", + "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", + "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792", + "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", + "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9", + "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", + "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01", + "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", + "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", + "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f", + "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd", + "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", + "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab", + "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", + "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", + "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", + "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", + "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", + "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967", + "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", + "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", + "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", + "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", + "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b" ], "markers": "python_version >= '3.8'", - "version": "==2.23.4" + "version": "==2.27.1" }, "pygments": { "hashes": [ @@ -1989,6 +2583,14 @@ "markers": "python_version >= '3.8'", "version": "==2.18.0" }, + "pyjwt": { + "hashes": [ + "sha256:543b77207db656de204372350926bed5a86201c4cbff159f623f79c7bb487a15", + "sha256:7628a7eb7938959ac1b26e819a1df0fd3259505627b575e4bad6d08f76db695c" + ], + "markers": "python_version >= '3.9'", + "version": "==2.10.0" + }, "pyparsing": { "hashes": [ "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", @@ -2034,6 +2636,14 @@ ], "version": "==2024.2" }, + "pyunormalize": { + "hashes": [ + "sha256:2e1dfbb4a118154ae26f70710426a52a364b926c9191f764601f5a8cb12761f7", + "sha256:c647d95e5d1e2ea9a2f448d1d95d8518348df24eab5c3fd32d2b5c3300a49152" + ], + "markers": "python_version >= '3.6'", + "version": "==16.0.0" + }, "pyyaml": { "hashes": [ "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", @@ -2103,103 +2713,103 @@ }, "regex": { "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", - "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", - "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" + "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", + "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", + "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", + "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", + "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", + "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773", + "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", + "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", + "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", + "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", + "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", + "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", + "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", + "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", + "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", + "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", + "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", + "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", + "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", + "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", + "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", + "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", + "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", + "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", + "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b", + "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", + "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd", + "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", + "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", + "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", + "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f", + "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", + "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", + "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", + "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", + "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", + "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", + "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", + "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", + "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", + "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", + "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", + "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", + "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", + "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4", + "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", + "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", + "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", + "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", + "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", + "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", + "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc", + "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", + "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", + "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", + "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", + "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", + "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", + "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd", + "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", + "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", + "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", + "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", + "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", + "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3", + "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", + "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", + "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", + "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", + "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", + "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467", + "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", + "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001", + "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", + "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", + "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", + "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf", + "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6", + "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", + "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", + "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", + "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df", + "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", + "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5", + "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", + "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", + "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", + "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", + "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c", + "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f", + "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", + "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", + "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", + "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91" ], "markers": "python_version >= '3.8'", - "version": "==2024.9.11" + "version": "==2024.11.6" }, "requests": { "hashes": [ @@ -2217,6 +2827,14 @@ "markers": "python_full_version >= '3.8.0'", "version": "==13.9.1" }, + "rlp": { + "hashes": [ + "sha256:bcefb11013dfadf8902642337923bd0c786dc8a27cb4c21da6e154e52869ecb1", + "sha256:ff6846c3c27b97ee0492373aa074a7c3046aadd973320f4fffa7ac45564b0258" + ], + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==4.0.1" + }, "rpds-py": { "hashes": [ "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", @@ -2661,6 +3279,14 @@ "markers": "python_version >= '3.7'", "version": "==0.20.0" }, + "toolz": { + "hashes": [ + "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", + "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.0" + }, "tqdm": { "hashes": [ "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", @@ -2677,6 +3303,14 @@ "markers": "python_version >= '3.7'", "version": "==0.12.5" }, + "types-requests": { + "hashes": [ + "sha256:a2db9cb228a81da8348b49ad6db3f5519452dd20a9c1e1a868c83c5fe88fd1a9", + "sha256:cd74ce3b53c461f1228a9b783929ac73a666658f223e28ed29753771477b3bd0" + ], + "markers": "python_version >= '3.7'", + "version": "==2.31.0.6" + }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", @@ -2869,6 +3503,15 @@ ], "version": "==0.24.0" }, + "web3": { + "hashes": [ + "sha256:25df8acdcb78eb872c3299408b79e8b4fd091602de5e3d29cbd8459e8f75ff23", + "sha256:670dac222b2ec5ce72f4572d8e5d91afe79fcac03af9dabfc69da4fe9f6621df" + ], + "index": "pypi", + "markers": "python_version >= '3.8' and python_version < '4'", + "version": "==7.6.0" + }, "webencodings": { "hashes": [ "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", @@ -2965,113 +3608,105 @@ "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6" ], + "markers": "python_version >= '3.8'", "version": "==13.1" }, "werkzeug": { "hashes": [ - "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", - "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" + "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8", + "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.0.4" + "version": "==2.3.7" }, "yarl": { "hashes": [ - "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b", - "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424", - "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644", - "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465", - "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88", - "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8", - "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da", - "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154", - "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51", - "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f", - "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc", - "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d", - "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8", - "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4", - "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c", - "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc", - "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2", - "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606", - "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6", - "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c", - "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734", - "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae", - "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220", - "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e", - "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d", - "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c", - "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177", - "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da", - "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23", - "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485", - "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85", - "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05", - "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09", - "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99", - "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9", - "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e", - "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5", - "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71", - "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0", - "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8", - "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10", - "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246", - "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049", - "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d", - "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2", - "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad", - "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323", - "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7", - "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2", - "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3", - "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a", - "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851", - "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206", - "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b", - "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550", - "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f", - "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1", - "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b", - "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe", - "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74", - "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5", - "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495", - "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c", - "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813", - "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a", - "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2", - "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d", - "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57", - "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc", - "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320", - "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43", - "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e", - "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac", - "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26", - "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c", - "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2", - "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799", - "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d", - "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735", - "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419", - "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e", - "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4", - "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0", - "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c", - "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3", - "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8", - "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9", - "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224", - "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38", - "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf", - "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7", - "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093" + "sha256:01be8688fc211dc237e628fcc209dda412d35de7642453059a0553747018d075", + "sha256:039c299a0864d1f43c3e31570045635034ea7021db41bf4842693a72aca8df3a", + "sha256:074fee89caab89a97e18ef5f29060ef61ba3cae6cd77673acc54bfdd3214b7b7", + "sha256:13aaf2bdbc8c86ddce48626b15f4987f22e80d898818d735b20bd58f17292ee8", + "sha256:14408cc4d34e202caba7b5ac9cc84700e3421a9e2d1b157d744d101b061a4a88", + "sha256:1db1537e9cb846eb0ff206eac667f627794be8b71368c1ab3207ec7b6f8c5afc", + "sha256:1ece25e2251c28bab737bdf0519c88189b3dd9492dc086a1d77336d940c28ced", + "sha256:1ff116f0285b5c8b3b9a2680aeca29a858b3b9e0402fc79fd850b32c2bcb9f8b", + "sha256:205de377bd23365cd85562c9c6c33844050a93661640fda38e0567d2826b50df", + "sha256:20d95535e7d833889982bfe7cc321b7f63bf8879788fee982c76ae2b24cfb715", + "sha256:20de4a8b04de70c49698dc2390b7fd2d18d424d3b876371f9b775e2b462d4b41", + "sha256:2d90f2e4d16a5b0915ee065218b435d2ef619dd228973b1b47d262a6f7cd8fa5", + "sha256:2e6b4466714a73f5251d84b471475850954f1fa6acce4d3f404da1d55d644c34", + "sha256:309f8d27d6f93ceeeb80aa6980e883aa57895270f7f41842b92247e65d7aeddf", + "sha256:32141e13a1d5a48525e519c9197d3f4d9744d818d5c7d6547524cc9eccc8971e", + "sha256:34176bfb082add67cb2a20abd85854165540891147f88b687a5ed0dc225750a0", + "sha256:38b39b7b3e692b6c92b986b00137a3891eddb66311b229d1940dcbd4f025083c", + "sha256:3a3709450a574d61be6ac53d582496014342ea34876af8dc17cc16da32826c9a", + "sha256:3adaaf9c6b1b4fc258584f4443f24d775a2086aee82d1387e48a8b4f3d6aecf6", + "sha256:3f576ed278860df2721a5d57da3381040176ef1d07def9688a385c8330db61a1", + "sha256:42ba84e2ac26a3f252715f8ec17e6fdc0cbf95b9617c5367579fafcd7fba50eb", + "sha256:454902dc1830d935c90b5b53c863ba2a98dcde0fbaa31ca2ed1ad33b2a7171c6", + "sha256:466d31fd043ef9af822ee3f1df8fdff4e8c199a7f4012c2642006af240eade17", + "sha256:49a98ecadc5a241c9ba06de08127ee4796e1009555efd791bac514207862b43d", + "sha256:4d26f1fa9fa2167bb238f6f4b20218eb4e88dd3ef21bb8f97439fa6b5313e30d", + "sha256:52c136f348605974c9b1c878addd6b7a60e3bf2245833e370862009b86fa4689", + "sha256:536a7a8a53b75b2e98ff96edb2dfb91a26b81c4fed82782035767db5a465be46", + "sha256:576d258b21c1db4c6449b1c572c75d03f16a482eb380be8003682bdbe7db2f28", + "sha256:609ffd44fed2ed88d9b4ef62ee860cf86446cf066333ad4ce4123505b819e581", + "sha256:67b336c15e564d76869c9a21316f90edf546809a5796a083b8f57c845056bc01", + "sha256:685cc37f3f307c6a8e879986c6d85328f4c637f002e219f50e2ef66f7e062c1d", + "sha256:6a49ad0102c0f0ba839628d0bf45973c86ce7b590cdedf7540d5b1833ddc6f00", + "sha256:6fb64dd45453225f57d82c4764818d7a205ee31ce193e9f0086e493916bd4f72", + "sha256:701bb4a8f4de191c8c0cc9a1e6d5142f4df880e9d1210e333b829ca9425570ed", + "sha256:73553bbeea7d6ec88c08ad8027f4e992798f0abc459361bf06641c71972794dc", + "sha256:7520e799b1f84e095cce919bd6c23c9d49472deeef25fe1ef960b04cca51c3fc", + "sha256:7609b8462351c4836b3edce4201acb6dd46187b207c589b30a87ffd1813b48dc", + "sha256:7db9584235895a1dffca17e1c634b13870852094f6389b68dcc6338086aa7b08", + "sha256:7fa7d37f2ada0f42e0723632993ed422f2a679af0e200874d9d861720a54f53e", + "sha256:80741ec5b471fbdfb997821b2842c59660a1c930ceb42f8a84ba8ca0f25a66aa", + "sha256:8254dbfce84ee5d1e81051ee7a0f1536c108ba294c0fdb5933476398df0654f3", + "sha256:8b8d3e4e014fb4274f1c5bf61511d2199e263909fb0b8bda2a7428b0894e8dc6", + "sha256:8e1c18890091aa3cc8a77967943476b729dc2016f4cfe11e45d89b12519d4a93", + "sha256:9106025c7f261f9f5144f9aa7681d43867eed06349a7cfb297a1bc804de2f0d1", + "sha256:91b8fb9427e33f83ca2ba9501221ffaac1ecf0407f758c4d2f283c523da185ee", + "sha256:96404e8d5e1bbe36bdaa84ef89dc36f0e75939e060ca5cd45451aba01db02902", + "sha256:9b4c90c5363c6b0a54188122b61edb919c2cd1119684999d08cd5e538813a28e", + "sha256:a0509475d714df8f6d498935b3f307cd122c4ca76f7d426c7e1bb791bcd87eda", + "sha256:a173401d7821a2a81c7b47d4e7d5c4021375a1441af0c58611c1957445055056", + "sha256:a45d94075ac0647621eaaf693c8751813a3eccac455d423f473ffed38c8ac5c9", + "sha256:a5f72421246c21af6a92fbc8c13b6d4c5427dfd949049b937c3b731f2f9076bd", + "sha256:a64619a9c47c25582190af38e9eb382279ad42e1f06034f14d794670796016c0", + "sha256:a7ee6884a8848792d58b854946b685521f41d8871afa65e0d4a774954e9c9e89", + "sha256:ae38bd86eae3ba3d2ce5636cc9e23c80c9db2e9cb557e40b98153ed102b5a736", + "sha256:b026cf2c32daf48d90c0c4e406815c3f8f4cfe0c6dfccb094a9add1ff6a0e41a", + "sha256:b0a2074a37285570d54b55820687de3d2f2b9ecf1b714e482e48c9e7c0402038", + "sha256:b1a3297b9cad594e1ff0c040d2881d7d3a74124a3c73e00c3c71526a1234a9f7", + "sha256:b212452b80cae26cb767aa045b051740e464c5129b7bd739c58fbb7deb339e7b", + "sha256:b234a4a9248a9f000b7a5dfe84b8cb6210ee5120ae70eb72a4dcbdb4c528f72f", + "sha256:b4095c5019bb889aa866bf12ed4c85c0daea5aafcb7c20d1519f02a1e738f07f", + "sha256:b8e8c516dc4e1a51d86ac975b0350735007e554c962281c432eaa5822aa9765c", + "sha256:bd80ed29761490c622edde5dd70537ca8c992c2952eb62ed46984f8eff66d6e8", + "sha256:c083f6dd6951b86e484ebfc9c3524b49bcaa9c420cb4b2a78ef9f7a512bfcc85", + "sha256:c0f4808644baf0a434a3442df5e0bedf8d05208f0719cedcd499e168b23bfdc4", + "sha256:c4cb992d8090d5ae5f7afa6754d7211c578be0c45f54d3d94f7781c495d56716", + "sha256:c60e547c0a375c4bfcdd60eef82e7e0e8698bf84c239d715f5c1278a73050393", + "sha256:c73a6bbc97ba1b5a0c3c992ae93d721c395bdbb120492759b94cc1ac71bc6350", + "sha256:c893f8c1a6d48b25961e00922724732d00b39de8bb0b451307482dc87bddcd74", + "sha256:cd6ab7d6776c186f544f893b45ee0c883542b35e8a493db74665d2e594d3ca75", + "sha256:d89ae7de94631b60d468412c18290d358a9d805182373d804ec839978b120422", + "sha256:d9d4f5e471e8dc49b593a80766c2328257e405f943c56a3dc985c125732bc4cf", + "sha256:da206d1ec78438a563c5429ab808a2b23ad7bc025c8adbf08540dde202be37d5", + "sha256:dbf53db46f7cf176ee01d8d98c39381440776fcda13779d269a8ba664f69bec0", + "sha256:dd21c0128e301851de51bc607b0a6da50e82dc34e9601f4b508d08cc89ee7929", + "sha256:e2580c1d7e66e6d29d6e11855e3b1c6381971e0edd9a5066e6c14d79bc8967af", + "sha256:e3818eabaefb90adeb5e0f62f047310079d426387991106d4fbf3519eec7d90a", + "sha256:ed69af4fe2a0949b1ea1d012bf065c77b4c7822bad4737f17807af2adb15a73c", + "sha256:f172b8b2c72a13a06ea49225a9c47079549036ad1b34afa12d5491b881f5b993", + "sha256:f275ede6199d0f1ed4ea5d55a7b7573ccd40d97aee7808559e1298fe6efc8dbd", + "sha256:f7edeb1dcc7f50a2c8e08b9dc13a413903b7817e72273f00878cb70e766bdb3b", + "sha256:fa2c9cb607e0f660d48c54a63de7a9b36fef62f6b8bd50ff592ce1137e73ac7d", + "sha256:fe94d1de77c4cd8caff1bd5480e22342dbd54c93929f5943495d9c1e8abe9f42" ], - "markers": "python_version >= '3.8'", - "version": "==1.13.1" + "markers": "python_version >= '3.9'", + "version": "==1.18.0" }, "yfinance": { "hashes": [ diff --git a/apis/__init__.py b/apis/__init__.py index 8d8afdb..e32bfdd 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -6,7 +6,7 @@ from .openai_resource import api as openai_namespace from .anthropic import api as anthropic_namespace, AnthropicCompletionRes from .luma import api as lumaai_namespace - +from .auth import api as auth_resource api = Api( title='LLMs Api Hub', @@ -20,4 +20,5 @@ api.add_namespace(openai_namespace) api.add_namespace(anthropic_namespace) api.add_namespace(lumaai_namespace) -api.add_namespace(xai_namespace) \ No newline at end of file +api.add_namespace(xai_namespace) +api.add_namespace(auth_resource) \ No newline at end of file diff --git a/apis/auth/__init__.py b/apis/auth/__init__.py new file mode 100644 index 0000000..dd02ca1 --- /dev/null +++ b/apis/auth/__init__.py @@ -0,0 +1,2 @@ +from .auth_resource import api +from .auth_helper import AuthHelper diff --git a/apis/auth/auth_helper.py b/apis/auth/auth_helper.py new file mode 100644 index 0000000..b39a032 --- /dev/null +++ b/apis/auth/auth_helper.py @@ -0,0 +1,112 @@ +from flask_jwt_extended import create_access_token, create_refresh_token +from eth_account.messages import encode_defunct +from web3 import Web3 +from typing import Optional, Tuple +import logging +import uuid +from datetime import datetime, timedelta +from models.auth import SignInRequest, User, Token +from models import db +from config import config + +class AuthHelper: + def __init__(self): + self.config = config + self.w3 = Web3(Web3.HTTPProvider(config.WEB3_PROVIDER_URL)) + + def get_sign_in_request(self, address: str) -> Optional[SignInRequest]: + """Get existing sign in request for address""" + return SignInRequest.query.filter_by( + address=address.lower() + ).first() + + def create_sign_in_request(self, address: str) -> SignInRequest: + """Create new sign in request with nonce""" + # Delete any existing requests + SignInRequest.query.filter_by(address=address.lower()).delete() + + nonce = uuid.uuid4().int % 1000000 # Generate 6-digit nonce + request = SignInRequest( + address=address.lower(), + nonce=nonce + ) + db.session.add(request) + db.session.commit() + return request + + def verify_signature(self, address: str, signature: str, nonce: int) -> bool: + """Verify wallet signature matches address""" + try: + message = f"I'm signing my one-time nonce: {nonce}" + logging.debug(f"Verifying signature for message: {message}") + encoded_message = encode_defunct(text=message) + recovered_address = self.w3.eth.account.recover_message( + encoded_message, + signature=signature + ) + logging.debug(f"Recovered address: {recovered_address}") + logging.debug(f"Original address: {address}") + return recovered_address.lower() == address.lower() + except Exception as e: + logging.error(f"Signature verification failed: {str(e)}", exc_info=True) + return False + + def delete_sign_in_request(self, address: str): + """Delete sign in request after use""" + SignInRequest.query.filter_by(address=address.lower()).delete() + db.session.commit() + + def get_or_create_user(self, address: str) -> User: + """Get existing user or create new one""" + user = User.query.filter_by(address=address.lower()).first() + if not user: + username = User.generate_username(address) + # Check if username exists + while User.query.filter_by(username=username).first(): + username = address.lower() + + user = User(address=address.lower(), username=username) + db.session.add(user) + db.session.commit() + return user + + + def validate_token_jti(self, jti: str, user_id: int) -> bool: + """Validate that the JTI exists and belongs to the user""" + token = Token.query.filter_by( + jti=jti, + user_id=user_id + ).first() + return token is not None + + def generate_tokens(self, user_id: int) -> Tuple[str, str]: + """Generate access and refresh tokens""" + jti = str(uuid.uuid4()) + + # Store token record + token = Token(user_id=user_id, jti=jti) + db.session.add(token) + db.session.commit() + + # Convert user_id to string for JWT identity + payload = { + 'user_id': str(user_id), # Convert to string + 'jti': jti + } + + access_token = create_access_token( + identity=str(user_id), # Use string user_id as identity + additional_claims={'jti': jti} # Add jti as additional claim + ) + + refresh_token = create_refresh_token( + identity=str(user_id), # Use string user_id as identity + additional_claims={'jti': jti} # Add jti as additional claim + ) + + return access_token, refresh_token + + def revoke_token(self, jti: str): + """Revoke a token by deleting it""" + Token.query.filter_by(jti=jti).delete() + db.session.commit() \ No newline at end of file diff --git a/apis/auth/auth_resource.py b/apis/auth/auth_resource.py new file mode 100644 index 0000000..da6af50 --- /dev/null +++ b/apis/auth/auth_resource.py @@ -0,0 +1,151 @@ +from flask_restx import Namespace, Resource, fields +from http import HTTPStatus +from .auth_helper import AuthHelper +from flask import request, jsonify, make_response, current_app +from res import EngMsg as msg +from flask_jwt_extended import get_jwt, jwt_required, get_jwt_identity +import logging + +api = Namespace('auth', description=msg.API_NAMESPACE_LLMS_DESCRIPTION) + +token_response = api.model('TokenResponse', { + 'access_token': fields.String(required=True, description='New JWT access token'), + 'refresh_token': fields.String(required=True, description='New JWT refresh token'), + 'token_type': fields.String(required=True, description='Token type', default='Bearer') +}) + +error_response = api.model('ErrorResponse', { + 'error': fields.String(required=True, description='Error message'), + 'error_code': fields.String(required=False, description='Error code for client handling') +}) + +auth_service = AuthHelper() + +@api.route('/nonce') +class NonceHandler(Resource): + + def post(self): + try: + data = request.get_json() + address = data.get('address') + + if not address: + return jsonify({"error": "Address is required"}), 400 + + # Get or create sign in request + sign_in = auth_service.get_sign_in_request(address) + if not sign_in: + sign_in = auth_service.create_sign_in_request(address) + + return jsonify({ + "nonce": sign_in.nonce, + "address": sign_in.address + }) + + except Exception as e: + logging.error(f"Error generating nonce: {str(e)}") + return jsonify({"error": "Failed to generate nonce"}), 500 + +@api.route('/verify') +class VerifyHandler(Resource): + def post(self): + """Verify wallet signature and issue tokens""" + try: + data = request.get_json() + logging.debug(f"Received verification data: {data}") + + address = data.get('address') + signature = data.get('signature') + + if not address or not signature: + return {'error': 'Address and signature required'}, 400 + + logging.debug(f"Looking for sign in request for address: {address}") + # Get sign in request + sign_in = auth_service.get_sign_in_request(address) + if not sign_in: + return {'error': 'No sign in request found'}, 404 + + logging.debug(f"Found sign in request with nonce: {sign_in.nonce}") + # Verify signature + if not auth_service.verify_signature(address, signature, sign_in.nonce): + return {'error': 'Invalid signature'}, 401 + + logging.debug("Signature verified successfully") + # Get or create user + user = auth_service.get_or_create_user(address) + logging.debug(f"User retrieved/created: {user.id}") + + # Generate tokens + access_token, refresh_token = auth_service.generate_tokens(user.id) + logging.debug("Tokens generated successfully") + + # Delete used sign in request + auth_service.delete_sign_in_request(address) + + response_data = { + 'access_token': access_token, + 'refresh_token': refresh_token, + 'token_type': 'Bearer', + 'user': { + 'id': str(user.id), # Convert to string to ensure JSON serialization + 'address': user.address, + 'username': user.username + } + } + + logging.debug("Preparing response data") + return response_data + + except Exception as e: + logging.error(f"Authentication error: {str(e)}", exc_info=True) + return {'error': 'Authentication failed'}, 500 + +@api.route('/refresh') +class RefreshHandler(Resource): + @api.doc(security='refresh_token') + @api.response(200, 'Successfully refreshed tokens', token_response) + @api.response(401, 'Invalid or expired refresh token', error_response) + @api.response(500, 'Internal server error', error_response) + @jwt_required(refresh=True) + def post(self): + """Refresh access token using a valid refresh token""" + try: + # Get user_id from identity + user_id = get_jwt_identity() + # Get additional claims + claims = get_jwt() + jti = claims.get('jti') + + if not user_id or not jti: + return { + 'error': 'Invalid token claims', + 'error_code': 'INVALID_CLAIMS' + }, 401 + + # Validate JTI + if not auth_service.validate_token_jti(jti, int(user_id)): + return { + 'error': 'Invalid or revoked token', + 'error_code': 'INVALID_TOKEN' + }, 401 + + # Revoke the old refresh token + auth_service.revoke_token(jti) + + # Generate new tokens + access_token, refresh_token = auth_service.generate_tokens(int(user_id)) + + return { + 'access_token': access_token, + 'refresh_token': refresh_token, + 'token_type': 'Bearer' + }, 200 + + except Exception as e: + logging.error(f"Token refresh error: {str(e)}") + return { + 'error': 'Failed to refresh token', + 'error_code': 'REFRESH_FAILED' + }, 500 + diff --git a/config.py b/config.py index 9b26586..a4b6e10 100644 --- a/config.py +++ b/config.py @@ -23,4 +23,7 @@ class Config(object): TELEGRAM_API_KEY = os.environ.get('TELEGRAM_API_KEY') XAI_API_KEY = os.getenv("XAI_API_KEY") TELEGRAM_REPORT_ID = os.environ.get('TELEGRAM_REPORT_ID') # telegram user id + WEB3_PROVIDER_URL = 'https://api.harmony.one' + JWT_EXPIRATION_MINUTES = 60 + REFRESH_EXPIRATION_DAYS = 1 config = Config() diff --git a/main.py b/main.py index e70f92a..074b196 100644 --- a/main.py +++ b/main.py @@ -1,44 +1,68 @@ from flask import Flask, jsonify, request from flask_httpauth import HTTPTokenAuth +from flask_jwt_extended import JWTManager, create_access_token, get_jwt_identity, jwt_required from flask_session import Session from flask_cors import CORS from apis import api from models import db from res import CustomError +from datetime import timedelta import config as app_config import os import logging -API_KEYS = app_config.config.API_KEYS -app = Flask(__name__) auth = HTTPTokenAuth(scheme='Bearer') - -# my_key_manager.fetch_api_key_loader(lambda: API_KEYS) -# print(my_key_manager.fetch_api_key_loader( .create_api_key_loader()) -# .init_app() . fetch_api_key_loader() +app = Flask(__name__) + +app.config +API_KEYS = app_config.config.API_KEYS +app.config['JWT_SECRET_KEY'] = app_config.config.SECRET_KEY +app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) +app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30) +jwt = JWTManager(app) app.config['SECRET_KEY']=app_config.config.SECRET_KEY app.config['SESSION_PERMANENT'] = True app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///:memory:' -UPLOAD_FOLDER = 'uploads/' -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER - -# if app_config.config.ENV == 'development': -# app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///:memory:' -# else: -# path = app_config.config.CHROMA_SERVER_PATH -# os.makedirs(os.path.dirname(path), exist_ok=True) -# # f = open(os.path.join(app_config.config.CHROMA_SERVER_PATH, "app.db"), 'w') -# app.config['SQLALCHEMY_DATABASE_URI']="sqlite:///" +os.path.join(app_config.config.CHROMA_SERVER_PATH, "app.db") # chroma.sqlite3 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True) +UPLOAD_FOLDER = 'uploads/' +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + sess = Session() api.init_app(app) sess.init_app(app) db.init_app(app) +def verify_jwt_token(): + try: + jwt_identity = get_jwt_identity() + return bool(jwt_identity) + except: + return False + + +def verify_auth(): + # Check for JWT first + if verify_jwt_token(): + return True + + # Fall back to API key check + token = request.headers.get('Authorization') + if token: + token = token.split(' ')[1] + if token in API_KEYS: + return True + + # Check for session token in cookies + token = request.cookies.get('session_token') + if token and token in API_KEYS: + return True + + return False + @auth.verify_token def verify_token(token): @@ -62,9 +86,17 @@ def verify_token(token): logging.info(f'****** APP Enviroment={app_config.config.ENV} *******') @app.before_request -@auth.login_required -def can_activate(): - logging.debug('checking api key') +def auth_middleware(): + # Skip auth for specific endpoints + if request.endpoint in ['auth_nonce_handler', 'auth_verify_handler', 'auth_refresh_handler']: + return + logging.debug('checking authentication') + + if not verify_auth(): + return jsonify({"msg": "Invalid or missing authentication"}), 401 + + logging.debug('authentication successful') + @app.route('/') def index(): @@ -92,6 +124,3 @@ def handle_custom_error(error): serve(app, host="0.0.0.0", port=8080) else: app.run(debug=True) - - -# python main.py \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index fa84c73..ed55762 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -7,3 +7,4 @@ class Base(DeclarativeBase): db = SQLAlchemy(model_class=Base) from .collection_error_model import CollectionError +from .auth import Token, User, SignInRequest diff --git a/models/auth.py b/models/auth.py new file mode 100644 index 0000000..5dbae70 --- /dev/null +++ b/models/auth.py @@ -0,0 +1,27 @@ +from . import db +from datetime import datetime + +class SignInRequest(db.Model): + id = db.Column(db.Integer, primary_key=True) + address = db.Column(db.String(42), unique=True, nullable=False) + nonce = db.Column(db.Integer, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + address = db.Column(db.String(42), unique=True, nullable=False) + username = db.Column(db.String(100), unique=True, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + @staticmethod + def generate_username(address: str) -> str: + # Similar to the TypeScript implementation + return address.replace('0x', '')[:6] + +class Token(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + jti = db.Column(db.String(36), unique=True, nullable=False) # JWT ID + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + user = db.relationship('User', backref=db.backref('tokens', lazy=True)) \ No newline at end of file diff --git a/test/test_api.py b/test/test_api.py new file mode 100644 index 0000000..92a507d --- /dev/null +++ b/test/test_api.py @@ -0,0 +1,95 @@ +import requests +import json +from eth_account import Account +from eth_account.messages import encode_defunct +import os + +class APITester: + def __init__(self, base_url='http://127.0.0.1:5000'): + self.base_url = base_url + self.session = requests.Session() + + # Create a test account if you don't have a real wallet + self.account = Account.create() + print(f"Test wallet address: {self.account.address}") + + def get_nonce(self): + print('fco::: getNonce', self.account.address) + response = self.session.post( + f"{self.base_url}/auth/nonce", + json={"address": self.account.address} + ) + print("\nNonce Response:", json.dumps(response.json(), indent=2)) + return response.json()['nonce'] + + def sign_and_verify(self, nonce): + # Create the message + message = f"I'm signing my one-time nonce: {nonce}" + print(f"\nSigning message: {message}") + + # Create the signable message + message_encoded = encode_defunct(text=message) + + # Sign the message + signed_message = Account.sign_message( + message_encoded, + private_key=self.account.key + ) + + print(f"Generated signature: {signed_message.signature.hex()}") + + # Verify signature + response = self.session.post( + f"{self.base_url}/auth/verify", + json={ + "address": self.account.address, + "signature": signed_message.signature.hex() + } + ) + print("\nVerify Response:", response.text) # Print raw response text + try: + return response.json() + except Exception as e: + print(f"Error parsing response: {str(e)}") + return None + + def refresh_tokens(self, refresh_token): + response = self.session.post( + f"{self.base_url}/auth/refresh", + headers={"Authorization": f"Bearer {refresh_token}"} + ) + print("\nRefresh Response:", json.dumps(response.json(), indent=2)) + return response.json() + + def run_full_test(self): + try: + print("Starting API test flow...") + + # Get nonce + nonce = self.get_nonce() + + # Sign and verify + auth_result = self.sign_and_verify(nonce) + if 'access_token' not in auth_result: + print("Authentication failed!") + return + + # Store tokens + access_token = auth_result['access_token'] + refresh_token = auth_result['refresh_token'] + + # Wait for user input to test refresh + input("\nPress Enter to test token refresh...") + + # Refresh tokens + refresh_result = self.refresh_tokens(refresh_token) + + print('result', refresh_result) + print("\nTest completed successfully!") + + except Exception as e: + print(f"Test failed: {str(e)}") + +if __name__ == "__main__": + tester = APITester() + tester.run_full_test() \ No newline at end of file From c2799162c354da4ab4768c52337fe33bb7f7ef35 Mon Sep 17 00:00:00 2001 From: fegloff Date: Thu, 5 Dec 2024 15:58:38 -0500 Subject: [PATCH 02/11] fix jwt auth --- Pipfile | 6 ++-- apis/anthropic/__init__.py | 8 ++++- apis/auth/__init__.py | 5 ++++ apis/auth/auth_helper.py | 58 ++++++++++++++++++++++++++---------- apis/auth/auth_resource.py | 55 ++++++++++++++++++++-------------- apis/collections/__init__.py | 7 ++++- apis/luma/__init__.py | 8 ++++- app_types/__init__.py | 7 ++++- config.py | 4 +-- fly.toml | 2 +- requirements.txt | 4 +++ test/test_api.py | 3 +- 12 files changed, 117 insertions(+), 50 deletions(-) diff --git a/Pipfile b/Pipfile index 7bd3fe7..badc9d0 100644 --- a/Pipfile +++ b/Pipfile @@ -28,9 +28,9 @@ google-generativeai = "==0.5.0" yfinance = "==0.2.38" flask-httpauth = "==4.8.0" lumaai = "==1.0.2" -flask-jwt-extended = "*" -eth-account = "*" -web3 = "*" +flask-jwt-extended = "==4.7.1" +eth-account = "==0.13.4" +web3 = "==7.6.0" werkzeug = "==2.3.7" [dev-packages] diff --git a/apis/anthropic/__init__.py b/apis/anthropic/__init__.py index f0d35da..92dbece 100644 --- a/apis/anthropic/__init__.py +++ b/apis/anthropic/__init__.py @@ -1,2 +1,8 @@ from .anthropic_resource import api, AnthropicCompletionRes -from .anthropic_helper import anthropicHelper \ No newline at end of file +from .anthropic_helper import anthropicHelper + +__all__ = { + 'api', + 'AnthropicCompletionRes', + 'anthropicHelper' +} \ No newline at end of file diff --git a/apis/auth/__init__.py b/apis/auth/__init__.py index dd02ca1..8f1102a 100644 --- a/apis/auth/__init__.py +++ b/apis/auth/__init__.py @@ -1,2 +1,7 @@ from .auth_resource import api from .auth_helper import AuthHelper + +__all__ = { + 'api', + 'AuthHelper' +} \ No newline at end of file diff --git a/apis/auth/auth_helper.py b/apis/auth/auth_helper.py index b39a032..f63ffb4 100644 --- a/apis/auth/auth_helper.py +++ b/apis/auth/auth_helper.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import IntegrityError from flask_jwt_extended import create_access_token, create_refresh_token from eth_account.messages import encode_defunct from web3 import Web3 @@ -14,6 +15,38 @@ def __init__(self): self.config = config self.w3 = Web3(Web3.HTTPProvider(config.WEB3_PROVIDER_URL)) + async def get_user(self, address: str) -> Optional[User]: + """Get user by address.""" + return User.query.filter_by(address=address.lower()).first() + + async def get_user_by_username(self, username: str) -> Optional[User]: + """Get user by username.""" + return User.query.filter_by(username=username).first() + + + async def create_user(self, address: str) -> User: + """Create a new user.""" + address = address.lower() + # Generate initial username + username = User.generate_username(address) + + # If username exists, use full address as username + if await self.get_user_by_username(username): + username = address + + user = User( + address=address, + username=username + ) + + try: + db.session.add(user) + db.session.commit() + return user + except IntegrityError: + db.session.rollback() + raise ValueError("User already exists") + def get_sign_in_request(self, address: str) -> Optional[SignInRequest]: """Get existing sign in request for address""" return SignInRequest.query.filter_by( @@ -58,17 +91,16 @@ def delete_sign_in_request(self, address: str): def get_or_create_user(self, address: str) -> User: """Get existing user or create new one""" - user = User.query.filter_by(address=address.lower()).first() - if not user: - username = User.generate_username(address) - # Check if username exists - while User.query.filter_by(username=username).first(): - username = address.lower() + user = self.get_user(address) + if user: + return user - user = User(address=address.lower(), username=username) - db.session.add(user) - db.session.commit() - return user + try: + user = self.create_user(address) + return user + except Exception as e: + db.session.rollback() + raise e def validate_token_jti(self, jti: str, user_id: int) -> bool: @@ -88,12 +120,6 @@ def generate_tokens(self, user_id: int) -> Tuple[str, str]: db.session.add(token) db.session.commit() - # Convert user_id to string for JWT identity - payload = { - 'user_id': str(user_id), # Convert to string - 'jti': jti - } - access_token = create_access_token( identity=str(user_id), # Use string user_id as identity additional_claims={'jti': jti} # Add jti as additional claim diff --git a/apis/auth/auth_resource.py b/apis/auth/auth_resource.py index da6af50..b67f835 100644 --- a/apis/auth/auth_resource.py +++ b/apis/auth/auth_resource.py @@ -1,7 +1,7 @@ from flask_restx import Namespace, Resource, fields from http import HTTPStatus from .auth_helper import AuthHelper -from flask import request, jsonify, make_response, current_app +from flask import request, jsonify, make_response, current_app as app from res import EngMsg as msg from flask_jwt_extended import get_jwt, jwt_required, get_jwt_identity import logging @@ -26,25 +26,28 @@ class NonceHandler(Resource): def post(self): try: - data = request.get_json() - address = data.get('address') + data = request.get_json() + address = data.get('address') - if not address: - return jsonify({"error": "Address is required"}), 400 - - # Get or create sign in request - sign_in = auth_service.get_sign_in_request(address) - if not sign_in: - sign_in = auth_service.create_sign_in_request(address) - - return jsonify({ - "nonce": sign_in.nonce, - "address": sign_in.address - }) + if not address: + return jsonify({"error": "Address is required"}), 400 + app.logger.info(f'handling nonce request for address {address}') + # Get or create sign in request + sign_in = auth_service.get_sign_in_request(address) + if not sign_in: + sign_in = auth_service.create_sign_in_request(address) + + return jsonify({ + "nonce": sign_in.nonce, + "address": sign_in.address + }) except Exception as e: logging.error(f"Error generating nonce: {str(e)}") - return jsonify({"error": "Failed to generate nonce"}), 500 + return api.marshal({ + 'error': "Failed to generate nonce", + 'error_code': 'NONCE_FAILED' + }, error_response), HTTPStatus.INTERNAL_SERVER_ERROR @api.route('/verify') class VerifyHandler(Resource): @@ -60,13 +63,16 @@ def post(self): if not address or not signature: return {'error': 'Address and signature required'}, 400 + app.logger.info(f'handling verify request for address {address}') logging.debug(f"Looking for sign in request for address: {address}") + # Get sign in request sign_in = auth_service.get_sign_in_request(address) if not sign_in: return {'error': 'No sign in request found'}, 404 logging.debug(f"Found sign in request with nonce: {sign_in.nonce}") + # Verify signature if not auth_service.verify_signature(address, signature, sign_in.nonce): return {'error': 'Invalid signature'}, 401 @@ -99,7 +105,10 @@ def post(self): except Exception as e: logging.error(f"Authentication error: {str(e)}", exc_info=True) - return {'error': 'Authentication failed'}, 500 + return api.marshal({ + 'error': "Authentication failed", + 'error_code': 'VERIFY_FAILED' + }, error_response), HTTPStatus.INTERNAL_SERVER_ERROR @api.route('/refresh') class RefreshHandler(Resource): @@ -122,7 +131,7 @@ def post(self): 'error': 'Invalid token claims', 'error_code': 'INVALID_CLAIMS' }, 401 - + app.logger.info(f'handling refresh request') # Validate JTI if not auth_service.validate_token_jti(jti, int(user_id)): return { @@ -136,16 +145,16 @@ def post(self): # Generate new tokens access_token, refresh_token = auth_service.generate_tokens(int(user_id)) - return { + return api.marshal({ 'access_token': access_token, 'refresh_token': refresh_token, 'token_type': 'Bearer' - }, 200 - + }, token_response), HTTPStatus.OK + except Exception as e: logging.error(f"Token refresh error: {str(e)}") - return { + return api.marshal({ 'error': 'Failed to refresh token', 'error_code': 'REFRESH_FAILED' - }, 500 + }, error_response), HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/apis/collections/__init__.py b/apis/collections/__init__.py index b64da4a..4e71573 100644 --- a/apis/collections/__init__.py +++ b/apis/collections/__init__.py @@ -1,2 +1,7 @@ from .collections_resource import api -from .collections_helper import CollectionHelper \ No newline at end of file +from .collections_helper import CollectionHelper + +__all__ = { + 'api', + 'CollectionHelper' +} \ No newline at end of file diff --git a/apis/luma/__init__.py b/apis/luma/__init__.py index 9ca0116..bed9112 100644 --- a/apis/luma/__init__.py +++ b/apis/luma/__init__.py @@ -3,4 +3,10 @@ luna_client = LumaAI() from .luma_resource import api -from .luma_helper import * \ No newline at end of file +from .luma_helper import * + +__all__ = { + 'api': api, + 'luna_client': luna_client, + 'luma_helper': luma_helper, +} \ No newline at end of file diff --git a/app_types/__init__.py b/app_types/__init__.py index fc8804d..6bedad6 100644 --- a/app_types/__init__.py +++ b/app_types/__init__.py @@ -1,2 +1,7 @@ from .tools_model import ToolsBetaMessage, RunningTool -from .stock_info import StockInfo \ No newline at end of file +from .stock_info import StockInfo + +__all__ = { + 'StockInfo', + 'ToolsBetaMessage', +} \ No newline at end of file diff --git a/config.py b/config.py index a4b6e10..81ebf0b 100644 --- a/config.py +++ b/config.py @@ -24,6 +24,6 @@ class Config(object): XAI_API_KEY = os.getenv("XAI_API_KEY") TELEGRAM_REPORT_ID = os.environ.get('TELEGRAM_REPORT_ID') # telegram user id WEB3_PROVIDER_URL = 'https://api.harmony.one' - JWT_EXPIRATION_MINUTES = 60 - REFRESH_EXPIRATION_DAYS = 1 + JWT_EXPIRATION_MINUTES = 30 + REFRESH_EXPIRATION_DAYS = 7 config = Config() diff --git a/fly.toml b/fly.toml index a35f70a..80c0489 100644 --- a/fly.toml +++ b/fly.toml @@ -11,7 +11,7 @@ primary_region = "den" [http_service] internal_port = 8080 force_https = true - auto_stop_machines = true + auto_stop_machines = "stop" auto_start_machines = true min_machines_running = 1 processes = ["app"] diff --git a/requirements.txt b/requirements.txt index b4b7cc2..aaa729e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -143,3 +143,7 @@ Werkzeug==3.0.2 yarl==1.9.4 yfinance==0.2.38 zipp==3.18.1 +flask-jwt-extended==4.7.1 +eth-account==0.13.4 +web3==7.6.0 +# werkzeug = "==2.3.7" diff --git a/test/test_api.py b/test/test_api.py index 92a507d..46ee7a3 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -4,8 +4,9 @@ from eth_account.messages import encode_defunct import os +ENDPOINT = 'https://harmony-llm-api-dev.fly.dev' # 'http://127.0.0.1:5000'): class APITester: - def __init__(self, base_url='http://127.0.0.1:5000'): + def __init__(self, base_url=ENDPOINT): self.base_url = base_url self.session = requests.Session() From c25587f3f32c411b324d8b893f140a220a23dd65 Mon Sep 17 00:00:00 2001 From: fegloff Date: Mon, 9 Dec 2024 10:33:38 -0500 Subject: [PATCH 03/11] add database migrations, database configuration + upgrade authetication middlware --- Dockerfile | 8 +- Pipfile | 3 + Pipfile.lock | 215 ++++++++++++++++++++++++++--------- apis/__init__.py | 10 ++ apis/auth/__init__.py | 6 +- apis/auth/auth_helper.py | 6 +- apis/auth/auth_middleware.py | 72 ++++++++++++ apis/auth/auth_resource.py | 29 ++++- apis/openai_resource.py | 31 +++++ config.py | 1 + fly.toml | 18 +-- main.py | 154 ++++++++----------------- migrations/README | 1 + migrations/alembic.ini | 50 ++++++++ migrations/env.py | 113 ++++++++++++++++++ migrations/script.py.mako | 24 ++++ models/__init__.py | 15 +++ models/auth.py | 41 ++++++- models/enums.py | 17 +++ models/transactions.py | 33 ++++++ requirements.txt | 4 +- routes.py | 25 ++++ start.sh | 3 + test/test_api.py | 80 ++++++++++++- tools/model_prices.py | 80 +++++++++++++ 25 files changed, 854 insertions(+), 185 deletions(-) create mode 100644 apis/auth/auth_middleware.py create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 models/enums.py create mode 100644 models/transactions.py create mode 100644 routes.py create mode 100755 start.sh create mode 100644 tools/model_prices.py diff --git a/Dockerfile b/Dockerfile index 5db0d17..621be11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM python:3.10.13 # Set environment variables for configuration ENV FLASK_APP=main.py + # ENV GOOGLE_APPLICATION_CREDENTIALS=/app/res/service_account.json # Set default values for environment variables @@ -25,11 +26,14 @@ RUN pip install --no-cache-dir -r requirements.txt # anthropic package is not installed with the previous command RUN pip install --no-cache-dir anthropic RUN pip install --no-cache-dir vertexai - + +# Ensure the start script is executable +RUN chmod +x start.sh + # Expose the port on which the Flask app will run EXPOSE 8080 # Run the Flask app when the container starts # CMD ["flask", "--app", "main.py", "run", "--host=0.0.0.0"] -CMD ["python", "main.py"] +CMD ["./start.sh"] diff --git a/Pipfile b/Pipfile index badc9d0..ac6dbbb 100644 --- a/Pipfile +++ b/Pipfile @@ -32,6 +32,9 @@ flask-jwt-extended = "==4.7.1" eth-account = "==0.13.4" web3 = "==7.6.0" werkzeug = "==2.3.7" +flask-migrate = "*" +alembic = "*" +psycopg2-binary = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 8306498..17cdfcb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2f104a67ac35e9a400e2329d8e8d7eb806b70991e898e9e44020f21b1607d064" + "sha256": "c2a044a0da81955a051eaeeb326e60837cefb95f302084f2cf21b898edef296a" }, "pipfile-spec": 6, "requires": { @@ -115,6 +115,15 @@ "markers": "python_version >= '3.7'", "version": "==1.3.1" }, + "alembic": { + "hashes": [ + "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25", + "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.14.0" + }, "aniso8601": { "hashes": [ "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", @@ -1015,6 +1024,15 @@ "markers": "python_version >= '3.9' and python_version < '4'", "version": "==4.7.1" }, + "flask-migrate": { + "hashes": [ + "sha256:5c532be17e7b43a223b7500d620edae33795df27c75811ddf32560f7d48ec617", + "sha256:dff7dd25113c210b069af280ea713b883f3840c1e3455274745d7355778c8622" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==4.0.7" + }, "flask-restx": { "hashes": [ "sha256:62b6b6c9de65e5960cf4f8b35e1bd3eca6998838a01b2f71e2a9d4c14a4ccd14", @@ -1793,6 +1811,14 @@ "markers": "python_version >= '3.6'", "version": "==5.3.0" }, + "mako": { + "hashes": [ + "sha256:20405b1232e0759f0e7d87b01f6bb94fce0761747f1cb876ecf90bd512d0b639", + "sha256:d18f990ad57f800ce8e76cbfb0b74afe471c293517e9f5003ace6dad5aa72c36" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.7" + }, "markdown-it-py": { "hashes": [ "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", @@ -2364,6 +2390,80 @@ "markers": "python_version >= '3.8'", "version": "==4.25.5" }, + "psycopg2-binary": { + "hashes": [ + "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", + "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5", + "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", + "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", + "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", + "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", + "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", + "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", + "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", + "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", + "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", + "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", + "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", + "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", + "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", + "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", + "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", + "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", + "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", + "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", + "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", + "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", + "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", + "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", + "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", + "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", + "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", + "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", + "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44", + "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", + "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", + "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", + "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa", + "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", + "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", + "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", + "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", + "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", + "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", + "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", + "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", + "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", + "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", + "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3", + "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", + "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92", + "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", + "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", + "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8", + "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", + "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", + "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864", + "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", + "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", + "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", + "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", + "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", + "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", + "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", + "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4", + "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", + "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", + "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", + "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", + "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", + "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", + "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.9.10" + }, "pulsar-client": { "hashes": [ "sha256:03b4d440b2d74323784328b082872ee2f206c440b5d224d7941eb3c083ec06c6", @@ -3042,62 +3142,67 @@ "version": "==2.6" }, "sqlalchemy": { - "extras": [ - "asyncio" - ], - "hashes": [ - "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9", - "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00", - "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee", - "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6", - "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1", - "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72", - "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf", - "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8", - "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b", - "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc", - "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c", - "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1", - "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3", - "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5", - "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90", - "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec", - "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71", - "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7", - "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b", - "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468", - "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3", - "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e", - "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139", - "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff", - "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11", - "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01", - "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62", - "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d", - "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a", - "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db", - "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87", - "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e", - "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1", - "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9", - "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f", - "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0", - "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44", - "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936", - "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8", - "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea", - "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f", - "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4", - "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0", - "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c", - "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f", - "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60", - "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2", - "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9", - "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33" + "hashes": [ + "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", + "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", + "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2", + "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", + "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", + "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", + "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", + "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575", + "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", + "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", + "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", + "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", + "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", + "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971", + "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", + "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", + "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", + "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d", + "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", + "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", + "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", + "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", + "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", + "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", + "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", + "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", + "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", + "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", + "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", + "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", + "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", + "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", + "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", + "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", + "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", + "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", + "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", + "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", + "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", + "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", + "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", + "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", + "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c", + "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b", + "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", + "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", + "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", + "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", + "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", + "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", + "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", + "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", + "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb", + "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", + "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", + "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", + "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53" ], "markers": "python_version >= '3.7'", - "version": "==2.0.35" + "version": "==2.0.36" }, "starlette": { "hashes": [ diff --git a/apis/__init__.py b/apis/__init__.py index e32bfdd..f8e7831 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -8,10 +8,20 @@ from .luma import api as lumaai_namespace from .auth import api as auth_resource +authorizations = { + 'Bearer': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization' + } +} + api = Api( title='LLMs Api Hub', version='1.0', description='Large Language Models (LLM) API Hub', + authorizations=authorizations, + security='Bearer' ) api.add_namespace(vertex_namespace) diff --git a/apis/auth/__init__.py b/apis/auth/__init__.py index 8f1102a..30cdcdd 100644 --- a/apis/auth/__init__.py +++ b/apis/auth/__init__.py @@ -1,7 +1,11 @@ from .auth_resource import api from .auth_helper import AuthHelper +from .auth_middleware import require_any_auth, require_jwt, require_token __all__ = { 'api', - 'AuthHelper' + 'AuthHelper', + 'require_any_auth', + 'require_jwt', + 'require_token' } \ No newline at end of file diff --git a/apis/auth/auth_helper.py b/apis/auth/auth_helper.py index f63ffb4..f4d0aef 100644 --- a/apis/auth/auth_helper.py +++ b/apis/auth/auth_helper.py @@ -89,14 +89,14 @@ def delete_sign_in_request(self, address: str): SignInRequest.query.filter_by(address=address.lower()).delete() db.session.commit() - def get_or_create_user(self, address: str) -> User: + async def get_or_create_user(self, address: str) -> User: """Get existing user or create new one""" - user = self.get_user(address) + user = await self.get_user(address) if user: return user try: - user = self.create_user(address) + user = await self.create_user(address) return user except Exception as e: db.session.rollback() diff --git a/apis/auth/auth_middleware.py b/apis/auth/auth_middleware.py new file mode 100644 index 0000000..27a0726 --- /dev/null +++ b/apis/auth/auth_middleware.py @@ -0,0 +1,72 @@ +# auth/middleware.py +from functools import wraps +from flask import request, jsonify +from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request +import config as app_config +import logging + +def require_jwt(f): + @wraps(f) + def decorated(*args, **kwargs): + try: + jwt_identity = get_jwt_identity() + if not jwt_identity: + return jsonify({"msg": "Invalid JWT token"}), 401 + return f(*args, **kwargs) + except: + return jsonify({"msg": "JWT authentication required"}), 401 + return decorated + +def require_token(f): + @wraps(f) + def decorated(*args, **kwargs): + token = request.headers.get('Authorization') + if token: + token = token.split(' ')[1] + if token in app_config.config.API_KEYS: + return f(*args, **kwargs) + token = request.cookies.get('session_token') + if token and token in app_config.config.API_KEYS: + return f(*args, **kwargs) + return jsonify({"msg": "Invalid API token"}), 401 + return decorated + +def require_any_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + logging.info("Starting authentication check") + + if request.endpoint in ['auth_nonce_handler', 'auth_verify_handler', 'auth_refresh_handler']: + return f(*args, **kwargs) + + # Try JWT first + try: + verify_jwt_in_request() + logging.info("JWT verification successful") + return f(*args, **kwargs) + except Exception as e: + logging.info(f"JWT verification failed: {str(e)}") + pass + + # Try API token + auth_header = request.headers.get('Authorization') + if auth_header: + try: + token = auth_header.split(' ')[1] + if token in app_config.config.API_KEYS: + logging.info("API key verification successful") + return f(*args, **kwargs) + logging.info("Invalid API key") + except Exception as e: + logging.info(f"API key verification error: {str(e)}") + pass + + # Try session token + session_token = request.cookies.get('session_token') + if session_token and session_token in app_config.config.API_KEYS: + logging.info("Session token verification successful") + return f(*args, **kwargs) + + logging.info("All authentication methods failed") + return {"message": "Authentication required"}, 401 + return decorated diff --git a/apis/auth/auth_resource.py b/apis/auth/auth_resource.py index b67f835..1a2dc9b 100644 --- a/apis/auth/auth_resource.py +++ b/apis/auth/auth_resource.py @@ -1,3 +1,4 @@ +import asyncio from flask_restx import Namespace, Resource, fields from http import HTTPStatus from .auth_helper import AuthHelper @@ -78,10 +79,29 @@ def post(self): return {'error': 'Invalid signature'}, 401 logging.debug("Signature verified successfully") - # Get or create user - user = auth_service.get_or_create_user(address) - logging.debug(f"User retrieved/created: {user.id}") + # # Get or create user + # user = auth_service.get_or_create_user(address) + # logging.debug(f"User retrieved/created: {user.id}") + + # Create event loop and run async operations + async def get_user_async(): + return await auth_service.get_or_create_user(address) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + user = loop.run_until_complete(get_user_async()) + finally: + loop.close() + + logging.debug(f"User retrieved/created: {user.id}") + + + + + + # Generate tokens access_token, refresh_token = auth_service.generate_tokens(user.id) logging.debug("Tokens generated successfully") @@ -100,8 +120,9 @@ def post(self): } } - logging.debug("Preparing response data") + logging.debug("Preparing response data", response_data) return response_data + # return make_response(jsonify(response_data), 200) except Exception as e: logging.error(f"Authentication error: {str(e)}", exc_info=True) diff --git a/apis/openai_resource.py b/apis/openai_resource.py index 1d42984..90f5727 100644 --- a/apis/openai_resource.py +++ b/apis/openai_resource.py @@ -2,6 +2,7 @@ from flask_restx import Namespace, Resource from werkzeug.utils import secure_filename from openai.error import OpenAIError +from .auth import require_any_auth from res import EngMsg as msg, CustomError import openai import os @@ -68,3 +69,33 @@ def post(self): app.logger.error(f"Unexpected Error: {error_message}") raise CustomError(500, "An unexpected error occurred.") + +@api.route('/generate-image', methods=['POST']) +class GenerateImage(Resource): + @require_any_auth + def post(self): + try: + data = request.get_json() + if not data or 'prompt' not in data: + return {"error": "No prompt provided"}, 400 + + size = data.get('size', '1024x1024') + n = min(max(1, data.get('n', 1)), 10) + + response = openai.Image.create( + prompt=data['prompt'], + size=size, + n=n, + ) + + return {"images": [img['url'] for img in response['data']]}, 200 + + except OpenAIError as e: + error_message = str(e) + app.logger.error(f"OpenAI API Error: {error_message}") + telegram_report_error("openai", "NO_CHAT_ID", e.code, error_message) + raise CustomError(500, error_message) + except Exception as e: + error_message = str(e) + app.logger.error(f"Unexpected Error: {error_message}") + raise CustomError(500, "An unexpected error occurred.") \ No newline at end of file diff --git a/config.py b/config.py index 81ebf0b..d491cae 100644 --- a/config.py +++ b/config.py @@ -26,4 +26,5 @@ class Config(object): WEB3_PROVIDER_URL = 'https://api.harmony.one' JWT_EXPIRATION_MINUTES = 30 REFRESH_EXPIRATION_DAYS = 7 + DATABASE_URL= os.environ.get('DATABASE_URL') if os.environ.get('DATABASE_URL') else 'sqlite:///:memory:' config = Config() diff --git a/fly.toml b/fly.toml index 80c0489..a993c0e 100644 --- a/fly.toml +++ b/fly.toml @@ -1,9 +1,6 @@ -# fly.toml app configuration file generated for harmony-llm-api-dev on 2023-10-12T17:41:19-05:00 -# -# See https://fly.io/docs/reference/configuration/ for information about how to use this file. -# +# fly.toml app configuration file for harmony-llm-api -app = "harmony-llm-api-dev" +app = "harmony-llm-api" primary_region = "den" [build] @@ -17,5 +14,12 @@ primary_region = "den" processes = ["app"] [mounts] - source="llm_api_data_dev" - destination="/data" \ No newline at end of file + source="llm_api_data" + destination="/data" + +[env] + FLASK_APP = "main.py" + FLASK_ENV = "production" + +[deploy] + release_command = "flask db upgrade" diff --git a/main.py b/main.py index 074b196..56945ca 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ +# main.py from flask import Flask, jsonify, request +from flask_migrate import Migrate from flask_httpauth import HTTPTokenAuth -from flask_jwt_extended import JWTManager, create_access_token, get_jwt_identity, jwt_required +from flask_jwt_extended import JWTManager, get_jwt_identity from flask_session import Session from flask_cors import CORS from apis import api @@ -8,119 +10,63 @@ from res import CustomError from datetime import timedelta import config as app_config -import os import logging auth = HTTPTokenAuth(scheme='Bearer') -app = Flask(__name__) - -app.config -API_KEYS = app_config.config.API_KEYS -app.config['JWT_SECRET_KEY'] = app_config.config.SECRET_KEY -app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) -app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30) -jwt = JWTManager(app) - -app.config['SECRET_KEY']=app_config.config.SECRET_KEY -app.config['SESSION_PERMANENT'] = True -app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///:memory:' -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True) - -UPLOAD_FOLDER = 'uploads/' -app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER - -sess = Session() -api.init_app(app) -sess.init_app(app) -db.init_app(app) - -def verify_jwt_token(): - try: - jwt_identity = get_jwt_identity() - return bool(jwt_identity) - except: - return False - - -def verify_auth(): - # Check for JWT first - if verify_jwt_token(): - return True - - # Fall back to API key check - token = request.headers.get('Authorization') - if token: - token = token.split(' ')[1] - if token in API_KEYS: - return True - - # Check for session token in cookies - token = request.cookies.get('session_token') - if token and token in API_KEYS: - return True - - return False - - -@auth.verify_token -def verify_token(token): - token = request.headers.get('Authorization') - if token: - token = token.split(' ')[1] +def create_app(): + app = Flask(__name__) - # for web client calls that uses HttpOnly cookies - if not token: - token = request.cookies.get('session_token') - - if token and token in API_KEYS: - return True - - return False - -app.app_context().push() -db.create_all() - -CORS(app) -logging.info(f'****** APP Enviroment={app_config.config.ENV} *******') - -@app.before_request -def auth_middleware(): - # Skip auth for specific endpoints - if request.endpoint in ['auth_nonce_handler', 'auth_verify_handler', 'auth_refresh_handler']: - return - logging.debug('checking authentication') - - if not verify_auth(): - return jsonify({"msg": "Invalid or missing authentication"}), 401 - - logging.debug('authentication successful') - + # Configuration + app.config['JWT_SECRET_KEY'] = app_config.config.SECRET_KEY + app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) + app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30) + app.config['SECRET_KEY'] = app_config.config.SECRET_KEY + app.config['SESSION_PERMANENT'] = True + app.config['SQLALCHEMY_DATABASE_URI'] = app_config.config.DATABASE_URL + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE=True) + app.config['UPLOAD_FOLDER'] = 'uploads/' + + # Initialize extensions + db.init_app(app) + api.init_app(app) + Session().init_app(app) + jwt = JWTManager(app) + migrate = Migrate(app, db) + CORS(app) + + + @app.route('/') + def index(): + return 'received!', 200 + + @app.route('/health') + def health(): + return "I'm healthy", 200 + + @app.errorhandler(CustomError) + def handle_custom_error(error): + response = { + "error": { + "status_code": error.error_code, + "message": error.message + } + } + return jsonify(response), error.error_code -@app.route('/') -def index(): - return 'received!', 200 + # Register additional routes + from routes import register_additional_routes + register_additional_routes(app) -@app.route('/health') -def health(): - return "I'm healthy", 200 + return app -@app.errorhandler(CustomError) -def handle_custom_error(error): - response = { - "error": { - "status_code": error.error_code, - "message": error.message - } - } - return jsonify(response), error.error_code +# Create the Flask application instance +app = create_app() if __name__ == '__main__': - # from waitress import serve - # serve(app, host="0.0.0.0", port=8080) # listen='0.0.0.0:8081') # port=8080, host="0.0.0.0", if app_config.config.ENV != 'development': from waitress import serve - serve(app, host="0.0.0.0", port=8080) + serve(app, host="0.0.0.0", port=8080) else: - app.run(debug=True) + app.run(debug=True) \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/models/__init__.py b/models/__init__.py index ed55762..6a1a451 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -8,3 +8,18 @@ class Base(DeclarativeBase): from .collection_error_model import CollectionError from .auth import Token, User, SignInRequest +from .transactions import Transaction +from .enums import TransactionType, UserType, ModelType + +__all__ = { + 'db', + 'CollectionError', + 'Token', + 'User', + 'SignInRequest', + 'Transaction', + 'TransactionType', + 'UserType', + 'ModelType' +} + diff --git a/models/auth.py b/models/auth.py index 5dbae70..eb24724 100644 --- a/models/auth.py +++ b/models/auth.py @@ -1,27 +1,56 @@ -from . import db -from datetime import datetime +from datetime import datetime, timezone +from sqlalchemy import func + +from .enums import TransactionType, UserType +from .transactions import Transaction +from . import db + class SignInRequest(db.Model): id = db.Column(db.Integer, primary_key=True) address = db.Column(db.String(42), unique=True, nullable=False) nonce = db.Column(db.Integer, nullable=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) class User(db.Model): id = db.Column(db.Integer, primary_key=True) address = db.Column(db.String(42), unique=True, nullable=False) username = db.Column(db.String(100), unique=True, nullable=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + user_type = db.Column(db.Enum(UserType), nullable=False, default=UserType.WALLET) + is_active = db.Column(db.Boolean, default=True) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) @staticmethod def generate_username(address: str) -> str: # Similar to the TypeScript implementation return address.replace('0x', '')[:6] + + def get_balance(self): + """Calculate current balance from transactions""" + result = db.session.query(func.sum(Transaction.amount)).filter( + Transaction.user_id == self.id + ).scalar() or 0 + + return result class Token(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) jti = db.Column(db.String(36), unique=True, nullable=False) # JWT ID - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) - user = db.relationship('User', backref=db.backref('tokens', lazy=True)) \ No newline at end of file + user = db.relationship('User', backref=db.backref('tokens', lazy=True)) + + def get_usage_stats(self): + """Helper method to get API usage statistics""" + if self.type != TransactionType.API_USAGE: + return None + + return { + 'model': self.model_type, + 'tokens_input': self.tokens_input, + 'tokens_output': self.tokens_output, + 'cost': self.amount, + 'endpoint': self.endpoint, + 'status': self.status + } \ No newline at end of file diff --git a/models/enums.py b/models/enums.py new file mode 100644 index 0000000..fb55422 --- /dev/null +++ b/models/enums.py @@ -0,0 +1,17 @@ +from enum import Enum + +class UserType(Enum): + WALLET = 'wallet' # Prepaid users who need to deposit ONE tokens + API_KEY = 'api_key' # Free tier with API key access + +class TransactionType(Enum): + DEPOSIT = 'deposit' + WITHDRAWAL = 'withdrawal' + API_USAGE = 'api_usage' + REFUND = 'refund' + +class ModelType(Enum): + GPT_4 = 'gpt-4' + GPT_35 = 'gpt-3.5-turbo' + CLAUDE = 'claude' + GEMINI = 'gemini' \ No newline at end of file diff --git a/models/transactions.py b/models/transactions.py new file mode 100644 index 0000000..ac3245a --- /dev/null +++ b/models/transactions.py @@ -0,0 +1,33 @@ +from datetime import datetime, timezone +from enum import Enum +from . import db +from .enums import ModelType, TransactionType + +class Transaction(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + type = db.Column(db.Enum(TransactionType), nullable=False) + amount = db.Column(db.Numeric(precision=18, scale=8), nullable=False) + tx_hash = db.Column(db.String(66), unique=True, nullable=True) # For blockchain transactions + + # Fields for API usage transactions + model_type = db.Column(db.Enum(ModelType), nullable=True) # Only for API_USAGE + tokens_input = db.Column(db.Integer, nullable=True) # Only for API_USAGE + tokens_output = db.Column(db.Integer, nullable=True) # Only for API_USAGE + request_id = db.Column(db.String(36), unique=True, nullable=True) # For API request tracking + status = db.Column(db.String(20), nullable=True) # For API call status + endpoint = db.Column(db.String(100), nullable=True) # API endpoint used + error = db.Column(db.Text, nullable=True) # For failed API calls + + # Additional metadata as JSON for flexibility + transaction_metadata = db.Column(db.JSON) + + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + def __init__(self, **kwargs): + # Ensure amount is negative for usage and withdrawals + if kwargs.get('type') in [TransactionType.API_USAGE, TransactionType.WITHDRAWAL]: + kwargs['amount'] = -abs(kwargs['amount']) + super().__init__(**kwargs) + + user = db.relationship('User', backref=db.backref('transactions', lazy=True)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index aaa729e..6b238a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -146,4 +146,6 @@ zipp==3.18.1 flask-jwt-extended==4.7.1 eth-account==0.13.4 web3==7.6.0 -# werkzeug = "==2.3.7" +Flask-Migrate>=4.0.5 +alembic>=1.13.1 +psycopg2-binary>=2.9.9 diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..7c966e6 --- /dev/null +++ b/routes.py @@ -0,0 +1,25 @@ +# routes.py +from flask import jsonify +from flask_httpauth import HTTPTokenAuth + +auth = HTTPTokenAuth(scheme='Bearer') + +def register_additional_routes(app): + """ + Register additional routes beyond the core ones defined in create_app() + This keeps route registration modular and organized + """ + + @app.route('/api/v1/balance') + @auth.login_required + def get_balance(): + # Example additional route + return jsonify({"balance": 100}), 200 + + @app.route('/api/v1/transactions') + @auth.login_required + def get_transactions(): + # Example additional route + return jsonify({"transactions": []}), 200 + + # Add more routes as needed \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..2197388 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +flask db upgrade +exec python main.py \ No newline at end of file diff --git a/test/test_api.py b/test/test_api.py index 46ee7a3..d2534e4 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,10 +1,18 @@ +import sys +import os import requests import json from eth_account import Account from eth_account.messages import encode_defunct -import os +from sqlalchemy import create_engine + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import config as app_config + + +ENDPOINT = 'http://127.0.0.1:5000' #'https://harmony-llm-api-dev.fly.dev' # 'http://127.0.0.1:5000'): +DATABASE_URL = app_config.config.DATABASE_URL -ENDPOINT = 'https://harmony-llm-api-dev.fly.dev' # 'http://127.0.0.1:5000'): class APITester: def __init__(self, base_url=ENDPOINT): self.base_url = base_url @@ -13,7 +21,45 @@ def __init__(self, base_url=ENDPOINT): # Create a test account if you don't have a real wallet self.account = Account.create() print(f"Test wallet address: {self.account.address}") + + if DATABASE_URL: + try: + self.engine = create_engine(DATABASE_URL) + print("Database connection established") + except Exception as e: + print(f"Failed to connect to database: {str(e)}") + self.engine = None + else: + print("DATABASE_URL not set") + self.engine = None + + + def check_user_in_database(self, address): + """Query the database to check if user exists""" + if not self.engine: + print("Database connection not available") + return None + + try: + with self.engine.connect() as connection: + result = connection.execute( + f"SELECT * FROM users WHERE address = '{address.lower()}'" + ).fetchone() + + if result: + print("\nUser found in database:") + print(f"ID: {result[0]}") + print(f"Address: {result[1]}") + print(f"Username: {result[2]}") + return result + else: + print("\nUser not found in database") + return None + except Exception as e: + print(f"Database query failed: {str(e)}") + return None + def get_nonce(self): print('fco::: getNonce', self.account.address) response = self.session.post( @@ -62,6 +108,26 @@ def refresh_tokens(self, refresh_token): print("\nRefresh Response:", json.dumps(response.json(), indent=2)) return response.json() + + def generate_image(self, access_token, prompt, size="1024x1024", num_images=1, quality="standard", style="vivid"): + print(f"Using access token: {access_token[:10]}...") # Add this debug line + headers = {"Authorization": f"Bearer {access_token}"} + print(f"Headers: {headers}") # Add this debug line + + response = self.session.post( + f"{self.base_url}/openai/generate-image", + headers=headers, + json={ + "prompt": prompt, + "size": size, + "n": num_images, + "quality": quality, + "style": style + } + ) + print("\nDALL-E Response:", json.dumps(response.json(), indent=2)) + return response.json() + def run_full_test(self): try: print("Starting API test flow...") @@ -75,6 +141,11 @@ def run_full_test(self): print("Authentication failed!") return + # Check database for user + print("\nChecking database for user...") + user_result = self.check_user_in_database(self.account.address) + print("user result", user_result) + # Store tokens access_token = auth_result['access_token'] refresh_token = auth_result['refresh_token'] @@ -82,6 +153,11 @@ def run_full_test(self): # Wait for user input to test refresh input("\nPress Enter to test token refresh...") + # Test DALL-E image generation + print("\nTesting DALL-E image generation...") + dalle_result = self.generate_image(access_token, "kid playing with a ball") + print("DALL-E result:", dalle_result) + # Refresh tokens refresh_result = self.refresh_tokens(refresh_token) diff --git a/tools/model_prices.py b/tools/model_prices.py new file mode 100644 index 0000000..7f95698 --- /dev/null +++ b/tools/model_prices.py @@ -0,0 +1,80 @@ +# config/model_prices.py +from decimal import Decimal +from models.enums import ModelType + +MODEL_PRICES = { + ModelType.GPT_4: { + 'input_price': Decimal('0.03'), # Price per 1K input tokens + 'output_price': Decimal('0.06'), # Price per 1K output tokens + 'min_price': Decimal('0.01') # Minimum charge per request + }, + ModelType.GPT_35: { + 'input_price': Decimal('0.0015'), + 'output_price': Decimal('0.002'), + 'min_price': Decimal('0.001') + }, + ModelType.CLAUDE: { + 'input_price': Decimal('0.008'), + 'output_price': Decimal('0.024'), + 'min_price': Decimal('0.01') + }, + ModelType.GEMINI: { + 'input_price': Decimal('0.001'), + 'output_price': Decimal('0.002'), + 'min_price': Decimal('0.001') + } +} + +def calculate_cost(model_type: ModelType, input_tokens: int, output_tokens: int) -> Decimal: + """ + Calculate the cost of an API call based on token usage. + + Args: + model_type: The LLM model used + input_tokens: Number of input tokens + output_tokens: Number of output tokens + + Returns: + Decimal: Total cost in ONE tokens + """ + if model_type not in MODEL_PRICES: + raise ValueError(f"Unknown model type: {model_type}") + + prices = MODEL_PRICES[model_type] + + # Calculate costs per token type + input_cost = (Decimal(input_tokens) / 1000) * prices['input_price'] + output_cost = (Decimal(output_tokens) / 1000) * prices['output_price'] + + # Total cost + total_cost = input_cost + output_cost + + # Apply minimum charge if total is below minimum + return max(total_cost, prices['min_price']) + +# from config.model_prices import calculate_cost, MODEL_PRICES +# from models.enums import ModelType + +# When processing an API call: +# def process_api_call(user_id: int, model_type: ModelType, input_tokens: int, output_tokens: int): +# # Calculate cost +# cost = calculate_cost(model_type, input_tokens, output_tokens) + +# # Check user balance +# user = User.query.get(user_id) +# if user.get_balance() < cost: +# raise InsufficientFundsError() + +# # Create transaction for API usage +# transaction = Transaction( +# user_id=user_id, +# type=TransactionType.API_USAGE, +# amount=cost, +# metadata={ +# 'model': model_type.value, +# 'input_tokens': input_tokens, +# 'output_tokens': output_tokens +# } +# ) +# db.session.add(transaction) +# db.session.commit() \ No newline at end of file From d9b5308ab7dc1e1de205bef016c2600c1e54016c Mon Sep 17 00:00:00 2001 From: fegloff Date: Sat, 14 Dec 2024 16:53:16 -0500 Subject: [PATCH 04/11] fix jwt auth decorator and auth logic --- .gitignore | 1 + apis/anthropic/__init__.py | 4 +- apis/auth/__init__.py | 4 +- apis/auth/auth_helper.py | 23 ++- apis/auth/auth_middleware.py | 126 +++++++++----- apis/auth/auth_resource.py | 46 ++--- apis/collections/__init__.py | 4 +- apis/luma/__init__.py | 10 +- apis/openai_resource.py | 122 +++++++------ apis/vertex_resource.py | 155 +++++++++-------- app_types/__init__.py | 4 +- config.py | 3 +- main.py | 8 +- models/__init__.py | 15 +- models/llm_data.py | 57 +++++++ services/__init__.py | 9 + services/payment/__init__.py | 9 + services/payment/decorators.py | 60 +++++++ services/payment/llm_manager.py | 128 ++++++++++++++ services/payment/llm_models.py | 293 ++++++++++++++++++++++++++++++++ test/__init__.py | 11 ++ test/api_tester.py | 127 ++++++++++++++ test/auth_tester.py | 75 ++++++++ test/run_test.py | 14 ++ test/test_api.py | 172 ------------------- test/test_config.py | 2 + test/token_manager.py | 79 +++++++++ 27 files changed, 1154 insertions(+), 407 deletions(-) create mode 100644 models/llm_data.py create mode 100644 services/payment/__init__.py create mode 100644 services/payment/decorators.py create mode 100644 services/payment/llm_manager.py create mode 100644 services/payment/llm_models.py create mode 100644 test/__init__.py create mode 100644 test/api_tester.py create mode 100644 test/auth_tester.py create mode 100644 test/run_test.py delete mode 100644 test/test_api.py create mode 100644 test/test_config.py create mode 100644 test/token_manager.py diff --git a/.gitignore b/.gitignore index 1bfbb67..a070fac 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ data/ chroma/ venv/ uploads/ +test/.test_tokens.json diff --git a/apis/anthropic/__init__.py b/apis/anthropic/__init__.py index 92dbece..83adef7 100644 --- a/apis/anthropic/__init__.py +++ b/apis/anthropic/__init__.py @@ -1,8 +1,8 @@ from .anthropic_resource import api, AnthropicCompletionRes from .anthropic_helper import anthropicHelper -__all__ = { +__all__ = [ 'api', 'AnthropicCompletionRes', 'anthropicHelper' -} \ No newline at end of file +] \ No newline at end of file diff --git a/apis/auth/__init__.py b/apis/auth/__init__.py index 30cdcdd..90d4aee 100644 --- a/apis/auth/__init__.py +++ b/apis/auth/__init__.py @@ -2,10 +2,10 @@ from .auth_helper import AuthHelper from .auth_middleware import require_any_auth, require_jwt, require_token -__all__ = { +__all__ = [ 'api', 'AuthHelper', 'require_any_auth', 'require_jwt', 'require_token' -} \ No newline at end of file +] \ No newline at end of file diff --git a/apis/auth/auth_helper.py b/apis/auth/auth_helper.py index f4d0aef..41e6e62 100644 --- a/apis/auth/auth_helper.py +++ b/apis/auth/auth_helper.py @@ -23,14 +23,11 @@ async def get_user_by_username(self, username: str) -> Optional[User]: """Get user by username.""" return User.query.filter_by(username=username).first() - async def create_user(self, address: str) -> User: """Create a new user.""" address = address.lower() - # Generate initial username username = User.generate_username(address) - # If username exists, use full address as username if await self.get_user_by_username(username): username = address @@ -55,7 +52,6 @@ def get_sign_in_request(self, address: str) -> Optional[SignInRequest]: def create_sign_in_request(self, address: str) -> SignInRequest: """Create new sign in request with nonce""" - # Delete any existing requests SignInRequest.query.filter_by(address=address.lower()).delete() nonce = uuid.uuid4().int % 1000000 # Generate 6-digit nonce @@ -71,14 +67,11 @@ def verify_signature(self, address: str, signature: str, nonce: int) -> bool: """Verify wallet signature matches address""" try: message = f"I'm signing my one-time nonce: {nonce}" - logging.debug(f"Verifying signature for message: {message}") encoded_message = encode_defunct(text=message) recovered_address = self.w3.eth.account.recover_message( encoded_message, signature=signature ) - logging.debug(f"Recovered address: {recovered_address}") - logging.debug(f"Original address: {address}") return recovered_address.lower() == address.lower() except Exception as e: logging.error(f"Signature verification failed: {str(e)}", exc_info=True) @@ -101,7 +94,6 @@ async def get_or_create_user(self, address: str) -> User: except Exception as e: db.session.rollback() raise e - def validate_token_jti(self, jti: str, user_id: int) -> bool: """Validate that the JTI exists and belongs to the user""" @@ -121,13 +113,20 @@ def generate_tokens(self, user_id: int) -> Tuple[str, str]: db.session.commit() access_token = create_access_token( - identity=str(user_id), # Use string user_id as identity - additional_claims={'jti': jti} # Add jti as additional claim + identity=str(user_id), + additional_claims={ + 'jti': jti, + 'type': 'access' + }, + fresh=False ) refresh_token = create_refresh_token( - identity=str(user_id), # Use string user_id as identity - additional_claims={'jti': jti} # Add jti as additional claim + identity=str(user_id), + additional_claims={ + 'jti': jti, + 'type': 'refresh' + } ) return access_token, refresh_token diff --git a/apis/auth/auth_middleware.py b/apis/auth/auth_middleware.py index 27a0726..506f125 100644 --- a/apis/auth/auth_middleware.py +++ b/apis/auth/auth_middleware.py @@ -1,72 +1,122 @@ # auth/middleware.py from functools import wraps -from flask import request, jsonify +from flask import g, request, jsonify, current_app as app from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request +import jwt import config as app_config -import logging def require_jwt(f): @wraps(f) def decorated(*args, **kwargs): try: + verify_jwt_in_request() jwt_identity = get_jwt_identity() if not jwt_identity: return jsonify({"msg": "Invalid JWT token"}), 401 - return f(*args, **kwargs) - except: + + class UserContext: + def __init__(self, id): + self.id = id + + g.auth_type = 'jwt' + g.is_jwt_user = True + g.user = UserContext(id=int(jwt_identity)) + + try: + return f(*args, **kwargs) + except Exception as e: + app.logger.error(f"Endpoint execution error: {str(e)}") + raise + + except Exception as e: + app.logger.error(f"JWT authentication error: {str(e)}") return jsonify({"msg": "JWT authentication required"}), 401 + return decorated def require_token(f): @wraps(f) def decorated(*args, **kwargs): + auth_successful = False token = request.headers.get('Authorization') - if token: - token = token.split(' ')[1] - if token in app_config.config.API_KEYS: + + try: + if token: + token = token.split(' ')[1] + if token in app_config.config.API_KEYS: + auth_successful = True + + if not auth_successful: + token = request.cookies.get('session_token') + if token and token in app_config.config.API_KEYS: + auth_successful = True + + if not auth_successful: + return jsonify({"msg": "Invalid API token"}), 401 + + g.auth_type = 'token' + g.is_jwt_user = False + + try: return f(*args, **kwargs) - token = request.cookies.get('session_token') - if token and token in app_config.config.API_KEYS: - return f(*args, **kwargs) - return jsonify({"msg": "Invalid API token"}), 401 + except Exception as e: + app.logger.error(f"Endpoint execution error: {str(e)}") + raise + + except Exception as e: + app.logger.error(f"Token authentication error: {str(e)}") + return jsonify({"msg": "Invalid API token"}), 401 + return decorated def require_any_auth(f): @wraps(f) def decorated(*args, **kwargs): - logging.info("Starting authentication check") + # Reset auth context + g.auth_type = None + g.user = None + g.is_jwt_user = False - if request.endpoint in ['auth_nonce_handler', 'auth_verify_handler', 'auth_refresh_handler']: - return f(*args, **kwargs) + auth_successful = False + auth_header = request.headers.get('Authorization') # Try JWT first try: verify_jwt_in_request() - logging.info("JWT verification successful") + user_id = get_jwt_identity() + + class UserContext: + def __init__(self, id): + self.id = id + + g.auth_type = 'jwt' + g.is_jwt_user = True + g.user = UserContext(id=int(user_id)) + auth_successful = True + app.logger.debug(f"JWT verification successful") return f(*args, **kwargs) except Exception as e: - logging.info(f"JWT verification failed: {str(e)}") - pass + app.logger.debug(f"JWT verification failed, trying API token") + + # Try API token + if auth_header: + try: + token = auth_header.split(' ')[1] + if token in app_config.config.API_KEYS: + g.auth_type = 'token' + g.is_jwt_user = False + auth_successful = True + return f(*args, **kwargs) + except Exception as e: + app.logger.error(f"API key verification error: {str(e)}") - # Try API token - auth_header = request.headers.get('Authorization') - if auth_header: - try: - token = auth_header.split(' ')[1] - if token in app_config.config.API_KEYS: - logging.info("API key verification successful") - return f(*args, **kwargs) - logging.info("Invalid API key") - except Exception as e: - logging.info(f"API key verification error: {str(e)}") - pass - - # Try session token - session_token = request.cookies.get('session_token') - if session_token and session_token in app_config.config.API_KEYS: - logging.info("Session token verification successful") + if not auth_successful: + return {"message": "Authentication required"}, 401 + + try: return f(*args, **kwargs) - - logging.info("All authentication methods failed") - return {"message": "Authentication required"}, 401 - return decorated + except Exception as e: + app.logger.error(f"Endpoint execution error: {str(e)}") + raise + + return decorated \ No newline at end of file diff --git a/apis/auth/auth_resource.py b/apis/auth/auth_resource.py index 1a2dc9b..9d7f204 100644 --- a/apis/auth/auth_resource.py +++ b/apis/auth/auth_resource.py @@ -2,10 +2,9 @@ from flask_restx import Namespace, Resource, fields from http import HTTPStatus from .auth_helper import AuthHelper -from flask import request, jsonify, make_response, current_app as app +from flask import request, jsonify, current_app as app from res import EngMsg as msg from flask_jwt_extended import get_jwt, jwt_required, get_jwt_identity -import logging api = Namespace('auth', description=msg.API_NAMESPACE_LLMS_DESCRIPTION) @@ -24,7 +23,6 @@ @api.route('/nonce') class NonceHandler(Resource): - def post(self): try: data = request.get_json() @@ -32,7 +30,7 @@ def post(self): if not address: return jsonify({"error": "Address is required"}), 400 - app.logger.info(f'handling nonce request for address {address}') + # Get or create sign in request sign_in = auth_service.get_sign_in_request(address) if not sign_in: @@ -44,46 +42,34 @@ def post(self): }) except Exception as e: - logging.error(f"Error generating nonce: {str(e)}") + app.logger.error(f"Error generating nonce: {str(e)}") return api.marshal({ 'error': "Failed to generate nonce", 'error_code': 'NONCE_FAILED' }, error_response), HTTPStatus.INTERNAL_SERVER_ERROR -@api.route('/verify') +@api.route('/verify-token') class VerifyHandler(Resource): def post(self): """Verify wallet signature and issue tokens""" try: data = request.get_json() - logging.debug(f"Received verification data: {data}") - address = data.get('address') signature = data.get('signature') if not address or not signature: return {'error': 'Address and signature required'}, 400 - app.logger.info(f'handling verify request for address {address}') - logging.debug(f"Looking for sign in request for address: {address}") - # Get sign in request sign_in = auth_service.get_sign_in_request(address) if not sign_in: return {'error': 'No sign in request found'}, 404 - logging.debug(f"Found sign in request with nonce: {sign_in.nonce}") - # Verify signature if not auth_service.verify_signature(address, signature, sign_in.nonce): return {'error': 'Invalid signature'}, 401 - logging.debug("Signature verified successfully") - # # Get or create user - # user = auth_service.get_or_create_user(address) - # logging.debug(f"User retrieved/created: {user.id}") - - # Create event loop and run async operations + # Create event loop and run async operations async def get_user_async(): return await auth_service.get_or_create_user(address) @@ -94,17 +80,8 @@ async def get_user_async(): finally: loop.close() - - logging.debug(f"User retrieved/created: {user.id}") - - - - - - # Generate tokens access_token, refresh_token = auth_service.generate_tokens(user.id) - logging.debug("Tokens generated successfully") # Delete used sign in request auth_service.delete_sign_in_request(address) @@ -114,18 +91,16 @@ async def get_user_async(): 'refresh_token': refresh_token, 'token_type': 'Bearer', 'user': { - 'id': str(user.id), # Convert to string to ensure JSON serialization + 'id': str(user.id), 'address': user.address, 'username': user.username } } - logging.debug("Preparing response data", response_data) return response_data - # return make_response(jsonify(response_data), 200) except Exception as e: - logging.error(f"Authentication error: {str(e)}", exc_info=True) + app.logger.error(f"Authentication error: {str(e)}", exc_info=True) return api.marshal({ 'error': "Authentication failed", 'error_code': 'VERIFY_FAILED' @@ -152,7 +127,7 @@ def post(self): 'error': 'Invalid token claims', 'error_code': 'INVALID_CLAIMS' }, 401 - app.logger.info(f'handling refresh request') + # Validate JTI if not auth_service.validate_token_jti(jti, int(user_id)): return { @@ -173,9 +148,8 @@ def post(self): }, token_response), HTTPStatus.OK except Exception as e: - logging.error(f"Token refresh error: {str(e)}") + app.logger.error(f"Token refresh error: {str(e)}") return api.marshal({ 'error': 'Failed to refresh token', 'error_code': 'REFRESH_FAILED' - }, error_response), HTTPStatus.INTERNAL_SERVER_ERROR - + }, error_response), HTTPStatus.INTERNAL_SERVER_ERROR \ No newline at end of file diff --git a/apis/collections/__init__.py b/apis/collections/__init__.py index 4e71573..6e4c0fa 100644 --- a/apis/collections/__init__.py +++ b/apis/collections/__init__.py @@ -1,7 +1,7 @@ from .collections_resource import api from .collections_helper import CollectionHelper -__all__ = { +__all__ = [ 'api', 'CollectionHelper' -} \ No newline at end of file +] \ No newline at end of file diff --git a/apis/luma/__init__.py b/apis/luma/__init__.py index bed9112..854cd25 100644 --- a/apis/luma/__init__.py +++ b/apis/luma/__init__.py @@ -5,8 +5,8 @@ from .luma_resource import api from .luma_helper import * -__all__ = { - 'api': api, - 'luna_client': luna_client, - 'luma_helper': luma_helper, -} \ No newline at end of file +__all__ = [ + 'api', + 'luna_client', + 'luma_helper' +] \ No newline at end of file diff --git a/apis/openai_resource.py b/apis/openai_resource.py index 90f5727..ab03116 100644 --- a/apis/openai_resource.py +++ b/apis/openai_resource.py @@ -1,80 +1,85 @@ -from flask import request, jsonify, make_response, current_app as app +from flask import g, request, jsonify, make_response, current_app as app from flask_restx import Namespace, Resource from werkzeug.utils import secure_filename from openai.error import OpenAIError +from models.enums import ModelType from .auth import require_any_auth from res import EngMsg as msg, CustomError import openai import os - from services import telegram_report_error +from services.payment import check_balance, llm_models_manager api = Namespace('openai', description=msg.API_NAMESPACE_OPENAI_DESCRIPTION) ALLOWED_EXTENSIONS = {'wav', 'm4a', 'mp3', 'mp4'} + def get_transcription(path): - try: - audio_file = open(path, 'rb') - response = openai.Audio.transcribe( - model="whisper-1", - file=audio_file - ) - print(response) - return response.text - except Exception as e: - print('Error:', e) - return None + try: + audio_file = open(path, 'rb') + response = openai.Audio.transcribe( + model="whisper-1", + file=audio_file + ) + return response.text + except Exception as e: + app.logger.error(f"Transcription error: {str(e)}") + return None def allowed_file(filename): - return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @api.route('/upload-audio', methods=['POST']) class UploadAudioFile(Resource): - @api.doc(params={"data": msg.API_DOC_PARAMS_DATA}) - def post(self): - """ - Endpoint to handle Openai's Whisper request. - Receives an audio from the user, processes it, and returns a transcription. - """ - app.logger.info('handling whisper request') - data = request.files - try: - print(data) - if 'data' not in request.files: - return make_response(jsonify({"error": 'No file part'})), 400 - file = data['data'] - if file.filename == '': - return make_response(jsonify({"error": 'No selected file'})), 400 - if file and allowed_file(file.filename): - filename = secure_filename(file.filename) - app.logger.info(filename) - os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) - file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) - file.save(file_path) - transcription = get_transcription(file_path) - app.logger.info(f'transcription: {transcription}') - os.remove(file_path) - return f"{transcription}", 200 - else: - return make_response(jsonify({"error": 'Invalid file format'})), 400 - except OpenAIError as e: - # Handle OpenAI API errors - error_message = str(e) - app.logger.error(f"OpenAI API Error: {error_message}") - telegram_report_error("openai", "NO_CHAT_ID", e.code, error_message) - raise CustomError(500, error_message) - except Exception as e: - # Handle other unexpected errors - error_message = str(e) - app.logger.error(f"Unexpected Error: {error_message}") - raise CustomError(500, "An unexpected error occurred.") - + @api.doc(params={"data": msg.API_DOC_PARAMS_DATA}) + def post(self): + """ + Endpoint to handle Openai's Whisper request. + Receives an audio from the user, processes it, and returns a transcription. + """ + app.logger.info('handling upload-audio request') + data = request.files + try: + if 'data' not in request.files: + return make_response(jsonify({"error": 'No file part'})), 400 + + file = data['data'] + if file.filename == '': + return make_response(jsonify({"error": 'No selected file'})), 400 + + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + + transcription = get_transcription(file_path) + os.remove(file_path) + + if not transcription: + return make_response(jsonify({"error": 'Transcription failed'})), 500 + + return f"{transcription}", 200 + else: + return make_response(jsonify({"error": 'Invalid file format'})), 400 + + except OpenAIError as e: + error_message = str(e) + app.logger.error(f"OpenAI API Error: {error_message}") + telegram_report_error("openai", "NO_CHAT_ID", e.code, error_message) + raise CustomError(500, error_message) + except Exception as e: + error_message = str(e) + app.logger.error(f"Unexpected Error: {error_message}") + raise CustomError(500, "An unexpected error occurred.") @api.route('/generate-image', methods=['POST']) class GenerateImage(Resource): @require_any_auth + @check_balance def post(self): try: + app.logger.info('handling generate-image request') data = request.get_json() if not data or 'prompt' not in data: return {"error": "No prompt provided"}, 400 @@ -82,6 +87,17 @@ def post(self): size = data.get('size', '1024x1024') n = min(max(1, data.get('n', 1)), 10) + if g.is_jwt_user: + transaction = llm_models_manager.record_transaction( + user_id=g.user.id, + model_version=ModelType.DALL_E, + tokens_input=n, + tokens_output=0, + endpoint='/generate-image', + status='processing', + amount=g.estimated_cost + ) + response = openai.Image.create( prompt=data['prompt'], size=size, diff --git a/apis/vertex_resource.py b/apis/vertex_resource.py index 332f238..86e19cb 100644 --- a/apis/vertex_resource.py +++ b/apis/vertex_resource.py @@ -1,50 +1,33 @@ -from flask import request, Response, jsonify, make_response, current_app as app +import os +from flask import g, request, Response, jsonify, make_response, current_app as app from flask_restx import Namespace, Resource from vertexai.language_models import ChatModel, ChatMessage -from vertexai.preview.generative_models import GenerativeModel, Content, Part +from vertexai.preview.generative_models import GenerativeModel +from vertexai.generative_models import ResponseValidationError +from google.api_core.exceptions import GoogleAPICallError, ClientError import google.generativeai as genai from google.cloud import aiplatform -from google.generativeai.types import content_types from google.oauth2 import service_account -import google.cloud.aiplatform as aiplatform -from google.api_core.exceptions import GoogleAPICallError, ClientError -from vertexai.generative_models import ResponseValidationError -from services import telegram_report_error - -# from litellm import litellm -import openai import vertexai import json - +from .auth import require_any_auth +from services import telegram_report_error +from services.payment import llm_models_manager from res import EngMsg as msg, CustomError +from services.payment.decorators import check_balance -with open( - "res/service_account.json" -) as f: +# Initialize Google Cloud credentials and services +with open("res/service_account.json") as f: service_account_info = json.load(f) + project_id = service_account_info["project_id"] -my_credentials = service_account.Credentials.from_service_account_info( - service_account_info -) - -aiplatform.init( - credentials=my_credentials, -) - -with open("res/service_account.json", encoding="utf-8") as f: - project_json = json.load(f) - project_id = project_json["project_id"] - - -# litellm.vertex_project = project_id -# litellm.vertex_location = "us-central1" - -genai.configure(credentials=my_credentials) # (project_id=project_id, location='us-central') +my_credentials = service_account.Credentials.from_service_account_info(service_account_info) +aiplatform.init(credentials=my_credentials) +genai.configure(credentials=my_credentials) vertexai.init(project=project_id, location="us-central1") -api = Namespace('vertex', description=msg.API_NAMESPACE_VERTEX_DESCRIPTION) - +api = Namespace('vertex', description=msg.API_NAMESPACE_VERTEX_DESCRIPTION, path='/vertex') def basic_data_generator(response): for event in response: @@ -55,24 +38,21 @@ def data_generator(response, input_token_count, model: GenerativeModel): for chunk in response: if chunk is not None: try: - # Check if 'content' and 'parts' exist if hasattr(chunk, 'text'): completion += chunk.text + " " yield f"{chunk.text}" - except ValueError as e: - continue - except Exception as e: + except (ValueError, Exception): continue + try: - completionTokens = model.count_tokens(completion) + completion_tokens = model.count_tokens(completion) yield f"Input Token: {input_token_count}" - yield f"Output Tokens: {completionTokens.total_tokens}" - except Exception as e: - pass + yield f"Output Tokens: {completion_tokens.total_tokens}" + except Exception: + pass @api.route('/completions') class VertexCompletionRes(Resource): - def post(self): """ Endpoint to handle Google's Vertex/Palm2 LLMs. @@ -80,33 +60,29 @@ def post(self): """ app.logger.info('handling chat-bison request') data = request.json - if data.get('stream') == "True": - data['stream'] = True # convert to boolean try: if data.get('stream') == "True": - data['stream'] = True # convert to boolean - # pass in data to completion function, unpack data + data['stream'] = True + chat_model = ChatModel.from_pretrained("chat-bison@001") parameters = { "max_output_tokens": 800, "temperature": 0.2 } + prompt = data.get('messages')[-1] messages = data.get('messages') messages.pop() - history = [ChatMessage(item.get('content'), item.get( - 'author')) for item in messages] + history = [ChatMessage(item.get('content'), item.get('author')) for item in messages] + chat = chat_model.start_chat( max_output_tokens=800, message_history=history ) - response = chat.send_message( - f"{prompt.get('content')}", **parameters) - # if data['stream'] == True: # use generate_responses to stream responses - # return Response(data_generator(response), mimetype='text/event-stream') - - # return f"{response}", 200 # non streaming responses + response = chat.send_message(f"{prompt.get('content')}", **parameters) + return make_response(jsonify(response), 200) + except ResponseValidationError as e: telegram_report_error("vertex", "NO_CHAT_ID", "NO_CODE", str(e)) raise CustomError(e.code, e.message) @@ -117,35 +93,38 @@ def post(self): telegram_report_error("vertex", "NO_CHAT_ID", e.code, e.message) raise CustomError(e.code, e.message) except Exception as e: - # Handle other unexpected errors - error_message = str(e) - app.logger.error(f"Unexpected Error: {error_message}") + app.logger.error(f"Unexpected Error: {str(e)}") raise CustomError(500, "An unexpected error occurred.") - @api.route('/completions/gemini') class VertexGeminiCompletionRes(Resource): - + @require_any_auth + @check_balance def post(self): """ Endpoint to handle Google's Vertex/Gemini. Receives a message from the user, processes it, and returns a stream response from the model. """ + app.logger.info('handling gemini request') data = request.json try: if data.get('stream') == "True": - data['stream'] = True # convert to boolean + data['stream'] = True + model = data.get('model') system_instruction = data.get('system') max_output_tokens = data.get('max_tokens') + generation_config = genai.GenerationConfig( max_output_tokens=int(max_output_tokens), temperature=0.1, - top_p= 1.0, - top_k= 40, + top_p=1.0, + top_k=40, ) - app.logger.info(f'handling gemini request using {model}') + chat_model = genai.GenerativeModel(model, system_instruction=system_instruction) + + # Handle message format if all( isinstance(m, dict) and set(m.keys()) == {"parts", "role"} and @@ -154,30 +133,52 @@ def post(self): m["role"] in ["model", "user"] for m in data.get('messages') ): - # If the structure is the same, no mapping is necessary messages = data.get('messages') else: - # If the structure is different, perform the mapping messages = [ {"parts": {"text": m["content"]}, "role": "model" if m["role"] != "user" else "user"} for m in data.get('messages') ] + history = [] for item in messages: if isinstance(item, dict) and 'parts' in item and isinstance(item['parts'], dict) and 'text' in item['parts']: text = item['parts']['text'] role = item.get('role') if text and role: - temp_content = {'role':role, 'parts': [text]} - history.append(temp_content) - else: - print("Skipping item - Invalid format:", item) - inputTokens = chat_model.count_tokens(history) - if data['stream'] == True: # use generate_responses to stream responses - response = chat_model.generate_content(history, generation_config=generation_config, stream=True) - return Response(data_generator(response, inputTokens.total_tokens, chat_model), mimetype='text/event-stream') + history.append({'role': role, 'parts': [text]}) + + input_tokens = chat_model.count_tokens(history) - return make_response(jsonify(response), 200) + if data['stream']: + response = chat_model.generate_content(history, generation_config=generation_config, stream=True) + + if g.is_jwt_user: + llm_models_manager.record_transaction( + user_id=g.user.id, + model_version=model, + tokens_input=input_tokens.total_tokens, + tokens_output=0, + endpoint=request.path, + status='success', + amount=g.estimated_cost + ) + + return Response(data_generator(response, input_tokens.total_tokens, chat_model), mimetype='text/event-stream') + else: + response = chat_model.generate_content(history, generation_config=generation_config, stream=False) + if g.is_jwt_user: + llm_models_manager.record_transaction( + user_id=g.user.id, + model_version=model, + tokens_input=input_tokens.total_tokens, + tokens_output=0, + endpoint=request.path, + status='success', + amount=g.estimated_cost + ) + return make_response(jsonify(response), 200) + except ResponseValidationError as e: telegram_report_error("vertex", "NO_CHAT_ID", "NO_CODE", str(e)) raise CustomError(e.code, e.message) @@ -188,7 +189,5 @@ def post(self): telegram_report_error("vertex", "NO_CHAT_ID", e.code, e.message) raise CustomError(e.code, e.message) except Exception as e: - # Handle other unexpected errors - error_message = str(e) - app.logger.error(f"Unexpected Error: {error_message}") - raise CustomError(500, "An unexpected error occurred.") + app.logger.error(f"Unexpected Error: {str(e)}") + raise CustomError(500, "An unexpected error occurred.") \ No newline at end of file diff --git a/app_types/__init__.py b/app_types/__init__.py index 6bedad6..3ffe31e 100644 --- a/app_types/__init__.py +++ b/app_types/__init__.py @@ -1,7 +1,7 @@ from .tools_model import ToolsBetaMessage, RunningTool from .stock_info import StockInfo -__all__ = { +__all__ = [ 'StockInfo', 'ToolsBetaMessage', -} \ No newline at end of file +] \ No newline at end of file diff --git a/config.py b/config.py index d491cae..ebb57a2 100644 --- a/config.py +++ b/config.py @@ -6,12 +6,13 @@ def generate_new_secret_key(): key = os.urandom(24).hex() return key + class Config(object): ENV=os.environ.get('ENV') if os.environ.get('ENV') else 'production' + SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else generate_new_secret_key() DEBUG = os.environ.get('DEBUG') if os.environ.get('DEBUG') else True TESTING = os.getenv('TESTING') if os.environ.get('DEBUG') else True MAX_WORKERS = 10 - SECRET_KEY = generate_new_secret_key() CHROMADB_SERVER_URL = os.getenv('CHROMADB_SERVER_URL') CHROMA_SERVER_PATH = os.getenv('CHROMA_SERVER_PATH') if os.getenv('CHROMA_SERVER_PATH') else "/app/data/chroma" OPENAI_MODEL = os.getenv('OPENAI_MODEL') if os.getenv('OPENAI_MODEL') else "gpt-3.5-turbo" diff --git a/main.py b/main.py index 56945ca..b8314f2 100644 --- a/main.py +++ b/main.py @@ -16,11 +16,17 @@ def create_app(): app = Flask(__name__) - + print(f"Initializing app with SECRET_KEY: {app_config.config.SECRET_KEY[:10]}...") # Configuration + app.config['JWT_SECRET_KEY'] = app_config.config.SECRET_KEY app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) app.config['JWT_REFRESH_TOKEN_EXPIRES'] = timedelta(days=30) + + app.config['JWT_DECODE_ALGORITHMS'] = ['HS256'] + app.config['JWT_ENCODE_NBF'] = False # Disable "not before" claim + app.config['JWT_ERROR_MESSAGE_KEY'] = 'message' + app.config['SECRET_KEY'] = app_config.config.SECRET_KEY app.config['SESSION_PERMANENT'] = True app.config['SQLALCHEMY_DATABASE_URI'] = app_config.config.DATABASE_URL diff --git a/models/__init__.py b/models/__init__.py index 6a1a451..886c0c9 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -10,8 +10,9 @@ class Base(DeclarativeBase): from .auth import Token, User, SignInRequest from .transactions import Transaction from .enums import TransactionType, UserType, ModelType +from .llm_data import * -__all__ = { +__all__ = [ 'db', 'CollectionError', 'Token', @@ -20,6 +21,14 @@ class Base(DeclarativeBase): 'Transaction', 'TransactionType', 'UserType', - 'ModelType' -} + 'ModelType', + 'Provider', + 'Provider', + 'ChargeType', + 'ModelParameters', + 'BaseModel', + 'ChatModel', + 'ImageModel', + 'ProviderParameters' +] diff --git a/models/llm_data.py b/models/llm_data.py new file mode 100644 index 0000000..3d3fbc5 --- /dev/null +++ b/models/llm_data.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass +from typing import Dict, List, Optional, TypedDict, Literal +from enum import Enum + +class Provider(str, Enum): + VERTEX = "vertex" + CLAUDE = "claude" + OPENAI = "openai" + XAI = "xai" + LUMA = "luma" + +class ChargeType(str, Enum): + TOKEN = "TOKEN" + CHAR = "CHAR" + +@dataclass +class ModelParameters: + temperature: float + max_tokens: int + +@dataclass +class BaseModel: + provider: Provider + name: str + full_name: str + bot_name: str + version: str + commands: List[str] + prefix: Optional[List[str]] + api_spec: str + +@dataclass +class ChatModel(BaseModel): + input_price: float + output_price: float + max_context_tokens: int + charge_type: ChargeType + stream: bool + +@dataclass +class ImageModel(BaseModel): + price: Dict[str, float] + +class ProviderParameters(TypedDict): + default_parameters: ModelParameters + model_overrides: Optional[Dict[str, ModelParameters]] + + +__all__ = [ + 'Provider', + 'ChargeType', + 'ModelParameters', + 'BaseModel', + 'ChatModel', + 'ImageModel', + 'ProviderParameters' +] \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py index e9b70b2..9df1d60 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -2,3 +2,12 @@ from .web_crawling import WebCrawling from .pdf import PdfHandler from .timer_decorator import timer + +__all__ = [ + 'BotHandler', + 'send_telegram_error_message', + 'telegram_report_error', + 'WebCrawling', + 'PdfHandler', + 'timer' +] diff --git a/services/payment/__init__.py b/services/payment/__init__.py new file mode 100644 index 0000000..ec5b476 --- /dev/null +++ b/services/payment/__init__.py @@ -0,0 +1,9 @@ +from .decorators import check_balance +from .llm_manager import llm_models_manager +from .llm_models import llm_config + +__all__ = [ + 'check_balance', + 'llm_models_manager', + 'llm_config' +] \ No newline at end of file diff --git a/services/payment/decorators.py b/services/payment/decorators.py new file mode 100644 index 0000000..8cc4fe5 --- /dev/null +++ b/services/payment/decorators.py @@ -0,0 +1,60 @@ +from functools import wraps +from flask import g, request, current_app as app +from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request +from .llm_manager import llm_models_manager + +def check_balance(f): + @wraps(f) + def decorated(*args, **kwargs): + try: + verify_jwt_in_request() + user_id = get_jwt_identity() + g.is_jwt_user = True + + from models import User + user = User.query.get(int(user_id)) + + if not user: + app.logger.error(f"User not found with ID: {user_id}") + return {"msg": "User not found"}, 404 + + balance = user.get_balance() + endpoint = request.endpoint + request_data = request.get_json() if request.is_json else request.form.to_dict() + + try: + estimated_cost = llm_models_manager.estimate_request_cost(endpoint, request_data) + + if balance < estimated_cost: + return { + "msg": "Insufficient balance", + "current_balance": float(balance), + "estimated_cost": float(estimated_cost), + "additional_funds_needed": float(estimated_cost - balance) + }, 402 + + g.user = user + g.balance = balance + g.estimated_cost = estimated_cost + + try: + return f(*args, **kwargs) + except Exception as e: + app.logger.error(f"Endpoint execution error: {str(e)}") + raise + + except Exception as e: + app.logger.error(f"Error estimating cost: {str(e)}") + raise + + except Exception as e: + g.is_jwt_user = False + g.estimated_cost = 0 + + try: + return f(*args, **kwargs) + except Exception as e: + app.logger.error(f"Endpoint execution error: {str(e)}") + raise + + return decorated \ No newline at end of file diff --git a/services/payment/llm_manager.py b/services/payment/llm_manager.py new file mode 100644 index 0000000..56aa578 --- /dev/null +++ b/services/payment/llm_manager.py @@ -0,0 +1,128 @@ +from datetime import datetime, timezone +from decimal import Decimal +import uuid + +from models import TransactionType, Transaction, ModelType +from models import db +from typing import Optional, List, Dict, Union +from models.llm_data import ChatModel, ImageModel, Provider, ModelParameters +from config import Config +from .llm_models import llm_config + +class LLMModelsManager: + def __init__(self, llm_data: dict): + self.models: Dict[str, Union[ChatModel, ImageModel]] = {} + self.load_models(llm_data) + + def load_models(self, data: dict) -> None: + for model_data in data['chat_models'].values(): + model = ChatModel(**model_data) + self.add_model(model) + + for model_data in data['image_models'].values(): + model = ImageModel(**model_data) + self.add_model(model) + + def add_model(self, model: Union[ChatModel, ImageModel]) -> None: + self.models[model.version] = model + + def get_model(self, version: str) -> Optional[Union[ChatModel, ImageModel]]: + return self.models.get(version) + + def get_chat_model_price(self, model: ChatModel, input_tokens: int, + output_tokens: Optional[int] = None, in_cents: bool = True) -> float: + price = model.input_price * input_tokens + if output_tokens is not None: + price += output_tokens * model.output_price + else: + price += model.max_context_tokens * model.output_price + + if in_cents: + price *= 100 + return price / 1000 + + def get_prompt_price(self, model_version: str, input_tokens: int, + output_tokens: Optional[int] = None) -> Dict[str, float]: + model = self.get_model(model_version) + if not isinstance(model, ChatModel): + raise ValueError(f"Model {model_version} is not a chat model") + + price = self.get_chat_model_price(model, input_tokens, output_tokens) + price *= Config.PRICE_ADJUSTMENT + + return { + "price": price, + "prompt_tokens": input_tokens, + "completion_tokens": output_tokens or 0 + } + + def estimate_request_cost(self, endpoint: str, request_data: dict) -> float: + print('fco:::::::: request_data', request_data, endpoint) + return 0.9 + model_version = request_data.get('model') + if not model_version: + raise ValueError("Model version not provided in request") + + # Estimate tokens based on input text + input_text = request_data.get('prompt', '') + estimated_input_tokens = len(input_text.split()) # Simple estimation + estimated_output_tokens = None # Can be refined based on your needs + + price_info = self.get_prompt_price( + model_version, + estimated_input_tokens, + estimated_output_tokens + ) + + return price_info["price"] + + def record_transaction(self, user_id: int, model_version: str, + input_tokens: int, output_tokens: int, + endpoint: str, status: str = 'success', + error: str = None) -> Transaction: + """Record a transaction for API usage""" + model = self.get_model(model_version) + if not model: + raise ValueError(f"Invalid model version: {model_version}") + + # Calculate cost + price_info = self.get_prompt_price(model_version, input_tokens, output_tokens) + amount = Decimal(str(price_info['price'])) + + # Map provider to ModelType + model_type_mapping = { + 'openai': ModelType.GPT4 if 'gpt-4' in model_version else ModelType.GPT35, + 'claude': ModelType.CLAUDE, + 'vertex': ModelType.GEMINI + } + model_type = model_type_mapping.get(model.provider, ModelType.GPT35) + + transaction = Transaction( + user_id=user_id, + type=TransactionType.API_USAGE, + amount=-amount, # Negative amount for usage + model_type=model_type, + tokens_input=input_tokens, + tokens_output=output_tokens, + request_id=str(uuid.uuid4()), + status=status, + endpoint=endpoint, + error=error, + transaction_metadata={ + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'model_type': model_type.value, + 'model_version': model_version, + 'endpoint': endpoint, + 'estimated_cost': str(amount) + } + ) + + try: + db.session.add(transaction) + db.session.commit() + return transaction + except Exception as e: + db.session.rollback() + raise e + +llm_models_manager = LLMModelsManager(llm_data=llm_config) diff --git a/services/payment/llm_models.py b/services/payment/llm_models.py new file mode 100644 index 0000000..837d099 --- /dev/null +++ b/services/payment/llm_models.py @@ -0,0 +1,293 @@ +# config.py +from decimal import Decimal +from typing import Literal, Optional, TypedDict, Dict, List, Union +from dataclasses import dataclass + +class ModelPrice(TypedDict): + size_1024_1024: Decimal + size_1024_1792: Decimal + size_1792_1024: Decimal + +class ChatModelConfig(TypedDict): + provider: str + name: str + full_name: str + bot_name: str + version: str + commands: List[str] + prefix: Optional[List[str]] + api_spec: str + input_price: Decimal + output_price: Decimal + max_context_tokens: int + charge_type: Literal["TOKEN", "CHAR"] + stream: bool + +class ImageModelConfig(TypedDict): + provider: str + name: str + full_name: str + bot_name: str + version: str + commands: List[str] + prefix: Optional[List[str]] + api_spec: str + price: ModelPrice + +class ProviderConfig(TypedDict): + default_parameters: Dict + model_overrides: Optional[Dict[str, Dict]] + +class LLMConfig(TypedDict): + chat_models: Dict[str, ChatModelConfig] + image_models: Dict[str, ImageModelConfig] + provider_parameters: Dict[str, ProviderConfig] + +# Dictionary containing all model configurations +llm_config: LLMConfig = { + "chat_models": { + "gemini-15": { + "provider": "vertex", + "name": "gemini-15", + "full_name": "gemini-1.5-pro-latest", + "bot_name": "VertexBot", + "version": "gemini-1.5-pro-latest", + "commands": ["gemini15", "g"], + "prefix": ["g. "], + "api_spec": "https://deepmind.google/technologies/gemini/pro/", + "input_price": Decimal("0.0025"), + "output_price": Decimal("0.0075"), + "max_context_tokens": 1048576, + "charge_type": "CHAR", + "stream": True + }, + "gemini-10": { + "provider": "vertex", + "name": "gemini-10", + "full_name": "gemini-1.0-pro", + "bot_name": "VertexBot", + "version": "gemini-1.0-pro", + "commands": ["gemini", "g10"], + "prefix": ["g10. "], + "api_spec": "https://deepmind.google/technologies/gemini/pro/", + "input_price": Decimal("0.000125"), + "output_price": Decimal("0.000375"), + "max_context_tokens": 30720, + "charge_type": "CHAR", + "stream": True + }, + "claude-35-sonnet": { + "provider": "claude", + "name": "claude-35-sonnet", + "full_name": "Claude Sonnet 3.5", + "bot_name": "ClaudeBot", + "version": "claude-3-5-sonnet-20241022", + "commands": ["sonnet", "claude", "s", "stool", "c", "ctool", "c0"], + "prefix": ["s. ", "c. ", "c0. "], + "api_spec": "https://www.anthropic.com/news/claude-3-5-sonnet", + "input_price": Decimal("0.003"), + "output_price": Decimal("0.015"), + "max_context_tokens": 8192, + "charge_type": "TOKEN", + "stream": True + }, + "claude-3-opus": { + "provider": "claude", + "name": "claude-3-opus", + "full_name": "Claude Opus", + "bot_name": "ClaudeBot", + "version": "claude-3-opus-20240229", + "commands": ["opus", "o", "otool"], + "prefix": ["o. "], + "api_spec": "https://www.anthropic.com/news/claude-3-family", + "input_price": Decimal("0.015"), + "output_price": Decimal("0.075"), + "max_context_tokens": 4096, + "charge_type": "TOKEN", + "stream": True + }, + "claude-3-5-haiku": { + "provider": "claude", + "name": "claude-3-5-haiku", + "full_name": "Claude Haiku", + "bot_name": "ClaudeBot", + "version": "claude-3-5-haiku-20241022", + "commands": ["haiku", "h"], + "prefix": ["h. "], + "api_spec": "https://www.anthropic.com/news/claude-3-family", + "input_price": Decimal("0.001"), + "output_price": Decimal("0.005"), + "max_context_tokens": 8192, + "charge_type": "TOKEN", + "stream": True + }, + "gpt-4o": { + "provider": "openai", + "name": "gpt-4o", + "full_name": "GPT-4o", + "bot_name": "OpenAIBot", + "version": "gpt-4o", + "commands": ["gpto", "ask", "chat", "gpt", "a"], + "prefix": ["a. ", ". "], + "api_spec": "https://platform.openai.com/docs/models/gpt-4o", + "input_price": Decimal("0.005"), + "output_price": Decimal("0.0015"), + "max_context_tokens": 128000, + "charge_type": "TOKEN", + "stream": True + }, + "gpt-4": { + "provider": "openai", + "name": "gpt-4", + "full_name": "GPT-4", + "bot_name": "OpenAIBot", + "version": "gpt-4", + "commands": ["gpt4"], + "prefix": None, + "api_spec": "https://openai.com/index/gpt-4/", + "input_price": Decimal("0.03"), + "output_price": Decimal("0.06"), + "max_context_tokens": 8192, + "charge_type": "TOKEN", + "stream": True + }, + "gpt-35-turbo": { + "provider": "openai", + "name": "gpt-35-turbo", + "full_name": "GPT-3.5 Turbo", + "bot_name": "OpenAIBot", + "version": "gpt-3.5-turbo", + "commands": ["ask35"], + "prefix": None, + "api_spec": "https://platform.openai.com/docs/models/gpt-3-5-turbo", + "input_price": Decimal("0.0015"), + "output_price": Decimal("0.002"), + "max_context_tokens": 4000, + "charge_type": "TOKEN", + "stream": True + }, + "gpt-4-vision": { + "provider": "openai", + "name": "gpt-4-vision", + "full_name": "GPT-4 Vision", + "bot_name": "OpenAIBot", + "version": "gpt-4-vision-preview", + "commands": ["vision", "v"], + "prefix": ["v. "], + "api_spec": "https://platform.openai.com/docs/guides/vision", + "input_price": Decimal("0.03"), + "output_price": Decimal("0.06"), + "max_context_tokens": 16000, + "charge_type": "TOKEN", + "stream": True + }, + "o1": { + "provider": "openai", + "name": "o1", + "full_name": "O1 Preview", + "bot_name": "OpenAIBot", + "version": "o1-preview", + "commands": ["o1", "ask1"], + "prefix": ["o1. "], + "api_spec": "https://platform.openai.com/docs/models/o1", + "input_price": Decimal("0.015"), + "output_price": Decimal("0.06"), + "max_context_tokens": 128000, + "charge_type": "TOKEN", + "stream": False + }, + "o1-mini": { + "provider": "openai", + "name": "o1-mini", + "full_name": "O1 Mini", + "bot_name": "OpenAIBot", + "version": "o1-mini-2024-09-12", + "commands": ["omini"], + "prefix": None, + "api_spec": "https://platform.openai.com/docs/models/o1", + "input_price": Decimal("0.003"), + "output_price": Decimal("0.012"), + "max_context_tokens": 128000, + "charge_type": "TOKEN", + "stream": False + }, + "grok": { + "provider": "xai", + "name": "grok", + "full_name": "Grok", + "bot_name": "xAIBot", + "version": "grok-beta", + "commands": ["gk", "grok", "x"], + "prefix": ["gk. ", "x. "], + "api_spec": "https://docs.x.ai/api#introduction", + "input_price": Decimal("0.005"), + "output_price": Decimal("0.015"), + "max_context_tokens": 131072, + "charge_type": "TOKEN", + "stream": False + } + }, + "image_models": { + "dalle-3": { + "provider": "openai", + "name": "dalle-3", + "full_name": "DALL-E 3", + "bot_name": "DalleBot", + "version": "dall-e-3", + "commands": ["dalle", "image", "img", "i"], + "prefix": ["i. ", ", ", "d. "], + "api_spec": "https://openai.com/index/dall-e-3/", + "price": { + "size_1024_1024": Decimal("0.08"), + "size_1024_1792": Decimal("0.12"), + "size_1792_1024": Decimal("0.12") + } + }, + "lumaai": { + "provider": "luma", + "name": "Luma AI", + "full_name": "Luma AI", + "bot_name": "LumaBot", + "version": "lumaai-1-0-2", + "commands": ["luma", "l"], + "prefix": ["l. "], + "api_spec": "https://docs.lumalabs.ai/docs/welcome", + "price": { + "size_1024_1024": Decimal("0.08"), + "size_1024_1792": Decimal("0.12"), + "size_1792_1024": Decimal("0.12") + } + } + }, + "provider_parameters": { + "openai": { + "default_parameters": { + "temperature": 0.7, + "max_tokens": 2000 + }, + "model_overrides": { + "o1": {"temperature": 1} + } + }, + "claude": { + "default_parameters": { + "max_tokens": 2000 + } + }, + "xai": { + "default_parameters": { + "max_tokens": 2000 + } + }, + "vertex": { + "default_parameters": { + "max_tokens": 2000 + } + }, + "luma": { + "default_parameters": { + "max_tokens": 2000 + } + } + } +} \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..eafd03a --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,11 @@ +from .test_config import TestConfig +from .token_manager import TokenManager +from .auth_tester import AuthTester +from .api_tester import APITester + +__all__= [ + 'TestConfig', + 'TokenManager', + 'AuthTester', + 'APITester' +] \ No newline at end of file diff --git a/test/api_tester.py b/test/api_tester.py new file mode 100644 index 0000000..4beeb73 --- /dev/null +++ b/test/api_tester.py @@ -0,0 +1,127 @@ +# api_tester.py +import json +import requests +from test_config import TestConfig +from token_manager import TokenManager +from auth_tester import AuthTester + + +class APITester: + def __init__(self, base_url=TestConfig.ENDPOINT): + self.base_url = base_url + self.session = requests.Session() + self.token_manager = TokenManager() + + def ensure_valid_tokens(self): + """Get valid tokens, either from cache or by authenticating""" + tokens = self.token_manager.get_tokens() + + if not tokens: + print("No valid tokens found in cache, authenticating...") + auth_tester = AuthTester(self.base_url) + auth_result = auth_tester.run_auth_test() + if auth_result: + self.token_manager.save_tokens( + auth_result['access_token'], + auth_result['refresh_token'] + ) + tokens = self.token_manager.get_tokens() + else: + raise Exception("Authentication failed") + + return tokens['access_token'], tokens['refresh_token'] + + + def test_gemini_api(self): + tokens = self.token_manager.get_tokens() + if not tokens: + print("No tokens available") + return None + + access_token = tokens['access_token'] + + print(f"Using token: {access_token[:30]}...") + + if not access_token.startswith('Bearer '): + access_token = f"Bearer {access_token}" + + headers = {"Authorization": access_token} + + try: + response = self.session.post( + f"{self.base_url}/vertex/completions/gemini", + headers=headers, + json={ + "model": "gemini-1.5-pro-latest", + "stream": True, + "max_tokens": 100, + "system": "Short answers", + "messages": [ + { + "parts": {"text": "Hello, how can I help you today?"}, + "role": "model" + }, + { + "parts": {"text": "Can you explain quantum computing in simple terms?"}, + "role": "user" + } + ], + }, + stream=True + ) + + # Add status code check + print(f"Gemini request status code: {response.status_code}") + if response.status_code != 200: + print(f"Error response content: {response.text}") + + response.raise_for_status() + + print("\nStreaming response from Gemini:") + for line in response.iter_lines(): + if line: + # Decode the line from bytes to string + decoded_line = line.decode('utf-8') + + # Skip SSE prefix if present + if decoded_line.startswith('data: '): + decoded_line = decoded_line[6:] + + try: + # Parse the JSON chunk + chunk = json.loads(decoded_line) + print(json.dumps(chunk, indent=2)) + except json.JSONDecodeError: + print(f"Could not parse line: {decoded_line}") + continue + + return {"status": "completed"} + + except requests.exceptions.RequestException as e: + print(f"Request failed: {str(e)}") + return {"error": str(e)} + except Exception as e: + print(f"Error processing stream: {str(e)}") + return {"error": str(e)} + + + def run_api_tests(self): + """Run API tests with automatic token handling""" + try: + access_token, refresh_token = self.ensure_valid_tokens() + + # Set up headers with token + headers = {"Authorization": f"Bearer {access_token}"} + + # Run your API tests here + print("\nTesting Gemini API...") + self.test_gemini_api() + + # print("\nTesting DALL-E API...") + # self.test_dalle_api(headers) + + except Exception as e: + print(f"Error during API tests: {e}") + # Optionally clear tokens if they might be invalid + self.token_manager.clear_tokens() + diff --git a/test/auth_tester.py b/test/auth_tester.py new file mode 100644 index 0000000..605f294 --- /dev/null +++ b/test/auth_tester.py @@ -0,0 +1,75 @@ +import os +import sys +import json +import requests +from eth_account import Account +from eth_account.messages import encode_defunct +from datetime import datetime, timedelta +from test_config import TestConfig + +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import config as app_config + + +class AuthTester: + def __init__(self, base_url=TestConfig.ENDPOINT): + self.base_url = base_url + self.session = requests.Session() + self.account = Account.create() + print(f"Test wallet address: {self.account.address}") + + def get_nonce(self): + response = self.session.post( + f"{self.base_url}/auth/nonce", + json={"address": self.account.address} + ) + return response.json()['nonce'] + + def sign_and_verify(self, nonce): + message = f"I'm signing my one-time nonce: {nonce}" + message_encoded = encode_defunct(text=message) + signed_message = Account.sign_message( + message_encoded, + private_key=self.account.key + ) + + response = self.session.post( + f"{self.base_url}/auth/verify-token", + json={ + "address": self.account.address, + "signature": signed_message.signature.hex() + } + ) + return response.json() + + def refresh_token(self, refresh_token): + response = self.session.post( + f"{self.base_url}/auth/refresh", + headers={"Authorization": f"Bearer {refresh_token}"} + ) + return response.json() + + def run_auth_test(self): + try: + print("\n=== Running Authentication Tests ===") + + print("\nTesting nonce generation...") + nonce = self.get_nonce() + print(f"Nonce received: {nonce}") + + print("\nTesting sign and verify...") + auth_result = self.sign_and_verify(nonce) + print(f"Received access token: {auth_result['access_token'][:30]}...") + if 'access_token' not in auth_result: + raise Exception("Authentication failed!") + print("Authentication successful!") + + print("\nTesting token refresh...") + refresh_result = self.refresh_token(auth_result['refresh_token']) + print("Token refresh successful!") + + return auth_result + + except Exception as e: + print(f"Authentication test failed: {str(e)}") + return None \ No newline at end of file diff --git a/test/run_test.py b/test/run_test.py new file mode 100644 index 0000000..55e0597 --- /dev/null +++ b/test/run_test.py @@ -0,0 +1,14 @@ +import os +import sys + +# Add the current directory to the Python path +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(current_dir) +from api_tester import APITester + +def main(): + tester = APITester() + tester.run_api_tests() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/test_api.py b/test/test_api.py deleted file mode 100644 index d2534e4..0000000 --- a/test/test_api.py +++ /dev/null @@ -1,172 +0,0 @@ -import sys -import os -import requests -import json -from eth_account import Account -from eth_account.messages import encode_defunct -from sqlalchemy import create_engine - -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -import config as app_config - - -ENDPOINT = 'http://127.0.0.1:5000' #'https://harmony-llm-api-dev.fly.dev' # 'http://127.0.0.1:5000'): -DATABASE_URL = app_config.config.DATABASE_URL - -class APITester: - def __init__(self, base_url=ENDPOINT): - self.base_url = base_url - self.session = requests.Session() - - # Create a test account if you don't have a real wallet - self.account = Account.create() - print(f"Test wallet address: {self.account.address}") - - if DATABASE_URL: - try: - self.engine = create_engine(DATABASE_URL) - print("Database connection established") - except Exception as e: - print(f"Failed to connect to database: {str(e)}") - self.engine = None - else: - print("DATABASE_URL not set") - self.engine = None - - - - def check_user_in_database(self, address): - """Query the database to check if user exists""" - if not self.engine: - print("Database connection not available") - return None - - try: - with self.engine.connect() as connection: - result = connection.execute( - f"SELECT * FROM users WHERE address = '{address.lower()}'" - ).fetchone() - - if result: - print("\nUser found in database:") - print(f"ID: {result[0]}") - print(f"Address: {result[1]}") - print(f"Username: {result[2]}") - return result - else: - print("\nUser not found in database") - return None - except Exception as e: - print(f"Database query failed: {str(e)}") - return None - - def get_nonce(self): - print('fco::: getNonce', self.account.address) - response = self.session.post( - f"{self.base_url}/auth/nonce", - json={"address": self.account.address} - ) - print("\nNonce Response:", json.dumps(response.json(), indent=2)) - return response.json()['nonce'] - - def sign_and_verify(self, nonce): - # Create the message - message = f"I'm signing my one-time nonce: {nonce}" - print(f"\nSigning message: {message}") - - # Create the signable message - message_encoded = encode_defunct(text=message) - - # Sign the message - signed_message = Account.sign_message( - message_encoded, - private_key=self.account.key - ) - - print(f"Generated signature: {signed_message.signature.hex()}") - - # Verify signature - response = self.session.post( - f"{self.base_url}/auth/verify", - json={ - "address": self.account.address, - "signature": signed_message.signature.hex() - } - ) - print("\nVerify Response:", response.text) # Print raw response text - try: - return response.json() - except Exception as e: - print(f"Error parsing response: {str(e)}") - return None - - def refresh_tokens(self, refresh_token): - response = self.session.post( - f"{self.base_url}/auth/refresh", - headers={"Authorization": f"Bearer {refresh_token}"} - ) - print("\nRefresh Response:", json.dumps(response.json(), indent=2)) - return response.json() - - - def generate_image(self, access_token, prompt, size="1024x1024", num_images=1, quality="standard", style="vivid"): - print(f"Using access token: {access_token[:10]}...") # Add this debug line - headers = {"Authorization": f"Bearer {access_token}"} - print(f"Headers: {headers}") # Add this debug line - - response = self.session.post( - f"{self.base_url}/openai/generate-image", - headers=headers, - json={ - "prompt": prompt, - "size": size, - "n": num_images, - "quality": quality, - "style": style - } - ) - print("\nDALL-E Response:", json.dumps(response.json(), indent=2)) - return response.json() - - def run_full_test(self): - try: - print("Starting API test flow...") - - # Get nonce - nonce = self.get_nonce() - - # Sign and verify - auth_result = self.sign_and_verify(nonce) - if 'access_token' not in auth_result: - print("Authentication failed!") - return - - # Check database for user - print("\nChecking database for user...") - user_result = self.check_user_in_database(self.account.address) - print("user result", user_result) - - # Store tokens - access_token = auth_result['access_token'] - refresh_token = auth_result['refresh_token'] - - # Wait for user input to test refresh - input("\nPress Enter to test token refresh...") - - # Test DALL-E image generation - print("\nTesting DALL-E image generation...") - dalle_result = self.generate_image(access_token, "kid playing with a ball") - print("DALL-E result:", dalle_result) - - # Refresh tokens - refresh_result = self.refresh_tokens(refresh_token) - - print('result', refresh_result) - print("\nTest completed successfully!") - - except Exception as e: - print(f"Test failed: {str(e)}") - -if __name__ == "__main__": - tester = APITester() - tester.run_full_test() \ No newline at end of file diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..98b1cdf --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,2 @@ +class TestConfig(object): + ENDPOINT = 'http://127.0.0.1:5000' #'https://harmony-llm-api-dev.fly.dev' # 'http://127.0.0.1:5000'): diff --git a/test/token_manager.py b/test/token_manager.py new file mode 100644 index 0000000..27f4b3d --- /dev/null +++ b/test/token_manager.py @@ -0,0 +1,79 @@ +# token_manager.py +import json +import os +from datetime import datetime, timedelta +from pathlib import Path + +class TokenManager: + def __init__(self, cache_file=None): + # Get the directory where token_manager.py is located + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # If no cache_file specified, create it in the current directory + if cache_file is None: + cache_file = os.path.join(current_dir, '.test_tokens.json') + + self.cache_file = cache_file + print(f"Token cache file location: {self.cache_file}") + self.tokens = self._load_tokens() + + def _load_tokens(self): + try: + if os.path.exists(self.cache_file): + print(f"Loading tokens from: {self.cache_file}") + with open(self.cache_file, 'r') as f: + data = json.load(f) + # Check if refresh token is still valid + if datetime.fromisoformat(data['refresh_expiry']) > datetime.now(): + print("Found valid cached tokens") + return data + else: + print("Cached tokens have expired") + else: + print("No token cache file found") + except Exception as e: + print(f"Error loading tokens: {e}") + return None + + def save_tokens(self, access_token, refresh_token): + try: + # Save tokens with expiry times + data = { + 'access_token': access_token, + 'refresh_token': refresh_token, + 'refresh_expiry': (datetime.now() + timedelta(days=1)).isoformat(), + 'created_at': datetime.now().isoformat() + } + + print(f"Saving tokens to: {self.cache_file}") + with open(self.cache_file, 'w') as f: + json.dump(data, f, indent=2) + self.tokens = data + print("Tokens saved successfully") + + # Verify the file was created + if os.path.exists(self.cache_file): + print(f"Token cache file created successfully at: {self.cache_file}") + else: + print("Warning: Token file was not created!") + + except Exception as e: + print(f"Error saving tokens: {e}") + + def get_tokens(self): + if self.tokens: + print(f"Returning cached token: {self.tokens['access_token'][:30]}...") + print("\nToken details from cache:") + print(f"Access token: {self.tokens['access_token'][:50]}...") + print(f"Created at: {self.tokens['created_at']}") + print(f"Refresh expiry: {self.tokens['refresh_expiry']}") + if not self.tokens: + print("No tokens available in memory") + return self.tokens if self.tokens else None + + def clear_tokens(self): + if os.path.exists(self.cache_file): + print(f"Removing token cache file: {self.cache_file}") + os.remove(self.cache_file) + print("Token cache file removed") + self.tokens = None \ No newline at end of file From a9a9293c553a0522d30666a379d45c67b27820c1 Mon Sep 17 00:00:00 2001 From: fegloff Date: Sat, 14 Dec 2024 17:53:47 -0500 Subject: [PATCH 05/11] update fly.toml file --- fly.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fly.toml b/fly.toml index a993c0e..605f6d0 100644 --- a/fly.toml +++ b/fly.toml @@ -1,6 +1,6 @@ # fly.toml app configuration file for harmony-llm-api -app = "harmony-llm-api" +app = "harmony-llm-api-dev" primary_region = "den" [build] @@ -14,12 +14,12 @@ primary_region = "den" processes = ["app"] [mounts] - source="llm_api_data" + source="llm_api_data_dev" destination="/data" [env] FLASK_APP = "main.py" - FLASK_ENV = "production" + FLASK_ENV = "test" [deploy] release_command = "flask db upgrade" From c2c1f618efc63d4d81a37d77143b20abd46769a0 Mon Sep 17 00:00:00 2001 From: fegloff Date: Sat, 14 Dec 2024 17:54:37 -0500 Subject: [PATCH 06/11] minor change --- fly.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fly.toml b/fly.toml index 605f6d0..5d21406 100644 --- a/fly.toml +++ b/fly.toml @@ -23,3 +23,21 @@ primary_region = "den" [deploy] release_command = "flask db upgrade" + + +# app = "harmony-llm-api-dev" +# primary_region = "den" + +# [build] + +# [http_service] +# internal_port = 8080 +# force_https = true +# auto_stop_machines = "stop" +# auto_start_machines = true +# min_machines_running = 1 +# processes = ["app"] + +# [mounts] +# source="llm_api_data_dev" +# destination="/data" From 5565b0cf9c2e07896f889a98a6d53c6ac37bfd8c Mon Sep 17 00:00:00 2001 From: fegloff Date: Sun, 15 Dec 2024 18:53:38 -0500 Subject: [PATCH 07/11] fix fly io database deployment error + fix debug level configuration --- Dockerfile | 62 ++++++++++++++++++++++++------------ apis/auth/auth_middleware.py | 3 +- config.py | 13 +++++--- fly.toml | 1 + requirements.txt | 6 ++-- res/__init__.py | 25 +++++++++++++-- 6 files changed, 78 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index 621be11..f16284f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,37 +3,57 @@ FROM python:3.10.13 # Set environment variables for configuration ENV FLASK_APP=main.py - -# ENV GOOGLE_APPLICATION_CREDENTIALS=/app/res/service_account.json - -# Set default values for environment variables ENV FLASK_ENV=production # Set the working directory inside the container WORKDIR /app - -# Mount the volume to /app/data VOLUME ["/app/data"] -# Copy the project files to the working directory -COPY . /app - RUN apt-get update && apt-get install -y \ - libgeos-dev -# Install the project dependencies -# RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir -r requirements.txt -# anthropic package is not installed with the previous command -RUN pip install --no-cache-dir anthropic -RUN pip install --no-cache-dir vertexai + libgeos-dev \ + libpq-dev \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt --no-deps && \ + pip install --no-cache-dir -r requirements.txt + +COPY . /app -# Ensure the start script is executable RUN chmod +x start.sh -# Expose the port on which the Flask app will run EXPOSE 8080 - -# Run the Flask app when the container starts -# CMD ["flask", "--app", "main.py", "run", "--host=0.0.0.0"] + CMD ["./start.sh"] + +# # Copy the project files to the working directory +# COPY . /app + +# RUN apt-get update && apt-get install -y \ +# libgeos-dev +# # Install the project dependencies +# # RUN pip install --no-cache-dir -r requirements.txt + +# COPY requirements.txt /app/ +# RUN pip install --no-cache-dir -r requirements.txt --no-deps && \ +# pip install --no-cache-dir -r requirements.txt + + + +# RUN pip install --no-cache-dir -r requirements.txt +# # anthropic package is not installed with the previous command +# RUN pip install --no-cache-dir anthropic +# RUN pip install --no-cache-dir vertexai + +# # Ensure the start script is executable +# RUN chmod +x start.sh + +# # Expose the port on which the Flask app will run +# EXPOSE 8080 + +# # Run the Flask app when the container starts +# # CMD ["flask", "--app", "main.py", "run", "--host=0.0.0.0"] +# CMD ["./start.sh"] + diff --git a/apis/auth/auth_middleware.py b/apis/auth/auth_middleware.py index 506f125..170a200 100644 --- a/apis/auth/auth_middleware.py +++ b/apis/auth/auth_middleware.py @@ -105,7 +105,8 @@ def __init__(self, id): if token in app_config.config.API_KEYS: g.auth_type = 'token' g.is_jwt_user = False - auth_successful = True + auth_successful = True + app.logger.debug(f"API token-based verification successful") return f(*args, **kwargs) except Exception as e: app.logger.error(f"API key verification error: {str(e)}") diff --git a/config.py b/config.py index ebb57a2..56458aa 100644 --- a/config.py +++ b/config.py @@ -1,17 +1,22 @@ import os -from dotenv import load_dotenv -load_dotenv() +if os.environ.get('ENV') != 'production': + from dotenv import load_dotenv, find_dotenv + load_dotenv(find_dotenv(), override=True) def generate_new_secret_key(): key = os.urandom(24).hex() return key +def str_to_bool(value): + """Convert string environment variable to boolean""" + return str(value).lower() in ('true', '1', 'yes', 'on') + class Config(object): ENV=os.environ.get('ENV') if os.environ.get('ENV') else 'production' SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else generate_new_secret_key() - DEBUG = os.environ.get('DEBUG') if os.environ.get('DEBUG') else True - TESTING = os.getenv('TESTING') if os.environ.get('DEBUG') else True + DEBUG = str_to_bool(os.environ.get('DEBUG', 'False')) + TESTING = str_to_bool(os.environ.get('TESTING', 'False')) MAX_WORKERS = 10 CHROMADB_SERVER_URL = os.getenv('CHROMADB_SERVER_URL') CHROMA_SERVER_PATH = os.getenv('CHROMA_SERVER_PATH') if os.getenv('CHROMA_SERVER_PATH') else "/app/data/chroma" diff --git a/fly.toml b/fly.toml index 5d21406..d6dda3e 100644 --- a/fly.toml +++ b/fly.toml @@ -20,6 +20,7 @@ primary_region = "den" [env] FLASK_APP = "main.py" FLASK_ENV = "test" + ENV = "test" [deploy] release_command = "flask db upgrade" diff --git a/requirements.txt b/requirements.txt index 6b238a7..9500e7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -146,6 +146,6 @@ zipp==3.18.1 flask-jwt-extended==4.7.1 eth-account==0.13.4 web3==7.6.0 -Flask-Migrate>=4.0.5 -alembic>=1.13.1 -psycopg2-binary>=2.9.9 +Flask-Migrate==4.0.5 +alembic==1.13.1 +psycopg2-binary==2.9.9 diff --git a/res/__init__.py b/res/__init__.py index 9cfbf05..eb60e28 100644 --- a/res/__init__.py +++ b/res/__init__.py @@ -1,10 +1,29 @@ -from dotenv import load_dotenv, find_dotenv import logging import sys +from config import config from .text_messages import EngMsg from .llm_exceptions import InvalidCollectionName, PdfFileInvalidFormat, DatabaseError, InvalidCollection from .custom_error import CustomError -logging.basicConfig(stream=sys.stdout, level=logging.INFO) -logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout)) \ No newline at end of file + +level = logging.DEBUG if config.DEBUG else logging.INFO + +logging.basicConfig( + stream=sys.stdout, + level=level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +root_logger = logging.getLogger() +for handler in root_logger.handlers: + handler.setLevel(level) + +__all__ = [ + 'EngMsg', + 'InvalidCollectionName', + 'PdfFileInvalidFormat', + 'DatabaseError', + 'InvalidCollection', + 'CustomError' +] \ No newline at end of file From 7b3b5294ad36be199c1cabc8ac216a62f639e51a Mon Sep 17 00:00:00 2001 From: fegloff Date: Mon, 16 Dec 2024 12:19:40 -0500 Subject: [PATCH 08/11] add token-base auth decorators to all endpoints --- apis/anthropic/anthropic_resource.py | 8 +++++++- apis/collections/collections_resource.py | 6 ++++++ apis/llms_resource.py | 6 +++++- apis/luma/luma_resource.py | 11 ++++++++--- apis/openai_resource.py | 9 ++++++--- apis/vertex_resource.py | 4 +++- apis/xai_resource.py | 2 ++ 7 files changed, 37 insertions(+), 9 deletions(-) diff --git a/apis/anthropic/anthropic_resource.py b/apis/anthropic/anthropic_resource.py index d1cad1d..dfd4fa6 100644 --- a/apis/anthropic/anthropic_resource.py +++ b/apis/anthropic/anthropic_resource.py @@ -6,6 +6,7 @@ import anthropic import json import os +from ..auth import require_any_auth, require_token from services import telegram_report_error from .anthropic_helper import anthropicHelper as helper from app_types import ToolsBetaMessage @@ -97,6 +98,7 @@ def create_message(text): @api.route('/completions') class AnthropicCompletionRes(Resource): + @require_token def post(self): """ Endpoint to handle Anthropic request. @@ -142,6 +144,7 @@ class AnthropicCompletionToolRes(Resource): runningTools = [] tools = helper.get_claude_tools_definition() + @require_token def post(self): """ Endpoint to handle Anthropic requests with Tools. @@ -280,7 +283,8 @@ def __tool_request_handler(self, data, tool_execution_id, context): @api.route('/completions/tools/') class CheckToolExecution(Resource): - @api.doc(params={"tool_execution_id": msg.API_DOC_PARAMS_COLLECTION_NAME}) + @api.doc(params={"tool_execution_id": msg.API_DOC_PARAMS_COLLECTION_NAME}) + @require_token def get(self, tool_execution_id): if (tool_execution_id): tool = helper.get_running_tool(tool_execution_id) @@ -318,6 +322,7 @@ class AnthropicPDFSummary(Resource): "model": msg.API_DOC_PARAMS_MODEL, "maxTokens": msg.API_DOC_PARAMS_MAX_TOKENS, "system": msg.API_DOC_PARAMS_SYSTEM}) + @require_token def post(self): """ Endpoint to handle PDF parser and summary. @@ -383,6 +388,7 @@ def extract_value(self, text, start_key, end_key): "jobDescription": msg.API_DOC_PARAMS_JOB_DESCRIPTION, "model": msg.API_DOC_PARAMS_MODEL, "maxTokens": msg.API_DOC_PARAMS_MAX_TOKENS}) + @require_token def post(self): """ Analyze a given CV with a given Job Description. diff --git a/apis/collections/collections_resource.py b/apis/collections/collections_resource.py index 5ce63dc..20bf4d4 100644 --- a/apis/collections/collections_resource.py +++ b/apis/collections/collections_resource.py @@ -5,6 +5,7 @@ import json import threading from llama_index.llms.base import ChatMessage +from ..auth import require_token from res import EngMsg as msg from storages import chromadb from res import PdfFileInvalidFormat, InvalidCollectionName, InvalidCollection, CustomError @@ -42,6 +43,8 @@ def post(self): @api.route('/document') class AddDocument(Resource): + + @require_token def post(self): """ Endpoint that creates collections @@ -90,6 +93,7 @@ def __collection_request_handler(self, url, collection_name, file_name, context) @api.route('/document/') class CheckDocument(Resource): + @require_token @api.doc(params={"collection_name": msg.API_DOC_PARAMS_COLLECTION_NAME}) def get(self, collection_name): """ @@ -131,6 +135,7 @@ def get(self, collection_name): current_app.logger.error(f"Unexpected Error: {error_message}") raise CustomError(500, "An unexpected error occurred.") + @require_token @api.doc(params={"collection_name": msg.API_DOC_PARAMS_COLLECTION_NAME}) def delete(self, collection_name): """ @@ -154,6 +159,7 @@ def delete(self, collection_name): class WebCrawlerTextRes(Resource): # # @copy_current_request_context + @require_token def post(self): """ Endpoint to handle LLMs request. diff --git a/apis/llms_resource.py b/apis/llms_resource.py index 3e47bc9..c79c9e8 100644 --- a/apis/llms_resource.py +++ b/apis/llms_resource.py @@ -1,5 +1,7 @@ from flask import request, jsonify, Response, make_response, current_app as app +from .auth import require_any_auth, require_token + from .vertex_resource import VertexGeminiCompletionRes from .anthropic import AnthropicCompletionRes from flask_restx import Namespace, Resource @@ -17,7 +19,8 @@ def data_generator(response): @api.route('/completions/j2') class LlmsCompletionJ2Res(Resource): - + + @require_token def post(self): """ Endpoint to handle LLMs request. @@ -49,6 +52,7 @@ def post(self): @api.route('/completions') class LlmsCompletionRes(Resource): + @require_token def post(self): """ Main Endpoint to handle Vertex and Anthropic request. diff --git a/apis/luma/luma_resource.py b/apis/luma/luma_resource.py index 4ee3fde..769989f 100644 --- a/apis/luma/luma_resource.py +++ b/apis/luma/luma_resource.py @@ -1,13 +1,15 @@ -from uuid import uuid4 +import concurrent.futures +import lumaai + from flask import request, jsonify, Response, make_response, abort, current_app as app from flask_restx import Namespace, Resource + +from ..auth import require_token from . import luna_client from .luma_helper import count_generation_in_progress, count_generation_states, get_queue_time, process_generation, LumaErrorHandler from res import EngMsg as msg, CustomError from config import config from services import telegram_report_error -import concurrent.futures -import lumaai api = Namespace('luma', description=msg.API_NAMESPACE_ANTHROPIC_DESCRIPTION) @@ -21,6 +23,7 @@ def __init__(self, id): @api.route('/generations') class LumaAiGenerationRes(Resource): + @require_token def post(self): """ Endpoint to handle Luma requests. @@ -56,6 +59,7 @@ def post(self): @api.route('/generations/list') class LumaAiGenerationListRes(Resource): + @require_token def get(self): """ Endpoint that returns the counts of different generation's states. @@ -70,6 +74,7 @@ def get(self): class GenerationRes(Resource): @api.doc(params={"generation_id": msg.API_DOC_PARAMS_COLLECTION_NAME}) + @require_token def delete(self, generation_id): """ Endpoint that deletes a generation diff --git a/apis/openai_resource.py b/apis/openai_resource.py index ab03116..45b2bb5 100644 --- a/apis/openai_resource.py +++ b/apis/openai_resource.py @@ -1,12 +1,13 @@ +import openai +import os + from flask import g, request, jsonify, make_response, current_app as app from flask_restx import Namespace, Resource from werkzeug.utils import secure_filename from openai.error import OpenAIError from models.enums import ModelType -from .auth import require_any_auth +from .auth import require_any_auth, require_token from res import EngMsg as msg, CustomError -import openai -import os from services import telegram_report_error from services.payment import check_balance, llm_models_manager @@ -31,7 +32,9 @@ def allowed_file(filename): @api.route('/upload-audio', methods=['POST']) class UploadAudioFile(Resource): + @api.doc(params={"data": msg.API_DOC_PARAMS_DATA}) + @require_token def post(self): """ Endpoint to handle Openai's Whisper request. diff --git a/apis/vertex_resource.py b/apis/vertex_resource.py index 86e19cb..202e4a5 100644 --- a/apis/vertex_resource.py +++ b/apis/vertex_resource.py @@ -11,7 +11,7 @@ import vertexai import json -from .auth import require_any_auth +from .auth import require_any_auth, require_token from services import telegram_report_error from services.payment import llm_models_manager from res import EngMsg as msg, CustomError @@ -53,6 +53,8 @@ def data_generator(response, input_token_count, model: GenerativeModel): @api.route('/completions') class VertexCompletionRes(Resource): + + @require_token def post(self): """ Endpoint to handle Google's Vertex/Palm2 LLMs. diff --git a/apis/xai_resource.py b/apis/xai_resource.py index ab1aba3..2ca3a81 100644 --- a/apis/xai_resource.py +++ b/apis/xai_resource.py @@ -3,6 +3,7 @@ from flask_restx import Namespace, Resource import anthropic import json +from .auth import require_token from res import EngMsg as msg, CustomError from config import config from services.telegram import telegram_report_error @@ -36,6 +37,7 @@ def extract_response_data(response): @api.route('/completions') class XaiCompletionRes(Resource): + @require_token def post(self): """ Endpoint to handle xAI request. From 476e9349a980713db75d320b3cad413380cb418f Mon Sep 17 00:00:00 2001 From: fegloff Date: Mon, 16 Dec 2024 18:14:18 -0500 Subject: [PATCH 09/11] fix db migration issues --- apis/auth/auth_helper.py | 36 ++-- apis/collections/collections_resource.py | 6 +- .../58a9a3aed9f6_initial_migration.py | 156 ++++++++++++++++++ models/__init__.py | 16 +- models/auth.py | 39 ++--- ...on_error_model.py => collection_errors.py} | 4 +- models/transactions.py | 39 ++--- services/payment/llm_manager.py | 6 +- 8 files changed, 222 insertions(+), 80 deletions(-) create mode 100644 migrations/versions/58a9a3aed9f6_initial_migration.py rename models/{collection_error_model.py => collection_errors.py} (91%) diff --git a/apis/auth/auth_helper.py b/apis/auth/auth_helper.py index 41e6e62..1ef777f 100644 --- a/apis/auth/auth_helper.py +++ b/apis/auth/auth_helper.py @@ -6,7 +6,7 @@ import logging import uuid from datetime import datetime, timedelta -from models.auth import SignInRequest, User, Token +from models.auth import SignInRequests, Users, Tokens from models import db from config import config @@ -15,23 +15,23 @@ def __init__(self): self.config = config self.w3 = Web3(Web3.HTTPProvider(config.WEB3_PROVIDER_URL)) - async def get_user(self, address: str) -> Optional[User]: + async def get_user(self, address: str) -> Optional[Users]: """Get user by address.""" - return User.query.filter_by(address=address.lower()).first() + return Users.query.filter_by(address=address.lower()).first() - async def get_user_by_username(self, username: str) -> Optional[User]: + async def get_user_by_username(self, username: str) -> Optional[Users]: """Get user by username.""" - return User.query.filter_by(username=username).first() + return Users.query.filter_by(username=username).first() - async def create_user(self, address: str) -> User: + async def create_user(self, address: str) -> Users: """Create a new user.""" address = address.lower() - username = User.generate_username(address) + username = Users.generate_username(address) if await self.get_user_by_username(username): username = address - user = User( + user = Users( address=address, username=username ) @@ -44,18 +44,18 @@ async def create_user(self, address: str) -> User: db.session.rollback() raise ValueError("User already exists") - def get_sign_in_request(self, address: str) -> Optional[SignInRequest]: + def get_sign_in_request(self, address: str) -> Optional[SignInRequests]: """Get existing sign in request for address""" - return SignInRequest.query.filter_by( + return SignInRequests.query.filter_by( address=address.lower() ).first() - def create_sign_in_request(self, address: str) -> SignInRequest: + def create_sign_in_request(self, address: str) -> SignInRequests: """Create new sign in request with nonce""" - SignInRequest.query.filter_by(address=address.lower()).delete() + SignInRequests.query.filter_by(address=address.lower()).delete() nonce = uuid.uuid4().int % 1000000 # Generate 6-digit nonce - request = SignInRequest( + request = SignInRequests( address=address.lower(), nonce=nonce ) @@ -79,10 +79,10 @@ def verify_signature(self, address: str, signature: str, nonce: int) -> bool: def delete_sign_in_request(self, address: str): """Delete sign in request after use""" - SignInRequest.query.filter_by(address=address.lower()).delete() + SignInRequests.query.filter_by(address=address.lower()).delete() db.session.commit() - async def get_or_create_user(self, address: str) -> User: + async def get_or_create_user(self, address: str) -> Users: """Get existing user or create new one""" user = await self.get_user(address) if user: @@ -97,7 +97,7 @@ async def get_or_create_user(self, address: str) -> User: def validate_token_jti(self, jti: str, user_id: int) -> bool: """Validate that the JTI exists and belongs to the user""" - token = Token.query.filter_by( + token = Tokens.query.filter_by( jti=jti, user_id=user_id ).first() @@ -108,7 +108,7 @@ def generate_tokens(self, user_id: int) -> Tuple[str, str]: jti = str(uuid.uuid4()) # Store token record - token = Token(user_id=user_id, jti=jti) + token = Tokens(user_id=user_id, jti=jti) db.session.add(token) db.session.commit() @@ -133,5 +133,5 @@ def generate_tokens(self, user_id: int) -> Tuple[str, str]: def revoke_token(self, jti: str): """Revoke a token by deleting it""" - Token.query.filter_by(jti=jti).delete() + Tokens.query.filter_by(jti=jti).delete() db.session.commit() \ No newline at end of file diff --git a/apis/collections/collections_resource.py b/apis/collections/collections_resource.py index 20bf4d4..fa4f2b5 100644 --- a/apis/collections/collections_resource.py +++ b/apis/collections/collections_resource.py @@ -12,7 +12,7 @@ from .collections_helper import CollectionHelper from models import db from services import WebCrawling, PdfHandler -from models import CollectionError +from models import CollectionErrors api = Namespace('collections', description=msg.API_NAMESPACE_LLMS_DESCRIPTION) @@ -87,7 +87,7 @@ def __collection_request_handler(self, url, collection_name, file_name, context) raise InvalidCollection('Invalid collection') except (Exception, InvalidCollection) as e: context.push() - error = CollectionError(dict( collection_name = collection_name)) + error = CollectionErrors(dict( collection_name = collection_name)) error.save() @api.route('/document/') @@ -103,7 +103,7 @@ def get(self, collection_name): try: current_app.logger.info('Checking collection status') if (collection_name): - collection_error = CollectionError.query.filter_by(collection_name=collection_name).first() + collection_error = CollectionErrors.query.filter_by(collection_name=collection_name).first() if (collection_error): response = { "price": -1, # TBD diff --git a/migrations/versions/58a9a3aed9f6_initial_migration.py b/migrations/versions/58a9a3aed9f6_initial_migration.py new file mode 100644 index 0000000..52a1c9b --- /dev/null +++ b/migrations/versions/58a9a3aed9f6_initial_migration.py @@ -0,0 +1,156 @@ +"""initial_migration + +Revision ID: 58a9a3aed9f6 +Revises: +Create Date: 2024-12-16 17:51:32.264106 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision = '58a9a3aed9f6' +down_revision = None +branch_labels = None +depends_on = None + +def table_exists(table_name): + # Get inspector to check table existence + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + return table_name in inspector.get_table_names() + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('collection_errors', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('collection_name', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('sign_in_requests', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('address', sa.String(length=42), nullable=False), + sa.Column('nonce', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('address') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('address', sa.String(length=42), nullable=False), + sa.Column('username', sa.String(length=100), nullable=False), + sa.Column('user_type', sa.Enum('WALLET', 'API_KEY', name='usertype'), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('address'), + sa.UniqueConstraint('username') + ) + op.create_table('tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('jti', sa.String(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('jti') + ) + op.create_table('transactions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('type', sa.Enum('DEPOSIT', 'WITHDRAWAL', 'API_USAGE', 'REFUND', name='transactiontype'), nullable=False), + sa.Column('amount', sa.Numeric(precision=18, scale=8), nullable=False), + sa.Column('tx_hash', sa.String(length=66), nullable=True), + sa.Column('model_type', sa.Enum('GPT_4', 'GPT_35', 'CLAUDE', 'GEMINI', name='modeltype'), nullable=True), + sa.Column('tokens_input', sa.Integer(), nullable=True), + sa.Column('tokens_output', sa.Integer(), nullable=True), + sa.Column('request_id', sa.String(length=36), nullable=True), + sa.Column('status', sa.String(length=20), nullable=True), + sa.Column('endpoint', sa.String(length=100), nullable=True), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('transaction_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('request_id'), + sa.UniqueConstraint('tx_hash') + ) + + # Safely drop old tables if they exist + if table_exists('token'): + op.drop_table('token') + if table_exists('transaction'): + op.drop_table('transaction') + if table_exists('app_collection_errors'): + op.drop_table('app_collection_errors') + if table_exists('user'): + op.drop_table('user') + if table_exists('sign_in_request'): + op.drop_table('sign_in_request') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('sign_in_request', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('address', sa.VARCHAR(length=42), autoincrement=False, nullable=False), + sa.Column('nonce', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='sign_in_request_pkey'), + sa.UniqueConstraint('address', name='sign_in_request_address_key') + ) + op.create_table('user', + sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('user_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('address', sa.VARCHAR(length=42), autoincrement=False, nullable=False), + sa.Column('username', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('user_type', postgresql.ENUM('WALLET', 'API_KEY', name='usertype'), autoincrement=False, nullable=False), + sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='user_pkey'), + sa.UniqueConstraint('address', name='user_address_key'), + sa.UniqueConstraint('username', name='user_username_key'), + postgresql_ignore_search_path=False + ) + op.create_table('app_collection_errors', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('collection_name', sa.VARCHAR(), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='app_collection_errors_pkey') + ) + op.create_table('transaction', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('type', postgresql.ENUM('DEPOSIT', 'WITHDRAWAL', 'API_USAGE', 'REFUND', name='transactiontype'), autoincrement=False, nullable=False), + sa.Column('amount', sa.NUMERIC(precision=18, scale=8), autoincrement=False, nullable=False), + sa.Column('tx_hash', sa.VARCHAR(length=66), autoincrement=False, nullable=True), + sa.Column('model_type', postgresql.ENUM('GPT_4', 'GPT_35', 'CLAUDE', 'GEMINI', name='modeltype'), autoincrement=False, nullable=True), + sa.Column('tokens_input', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('tokens_output', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('request_id', sa.VARCHAR(length=36), autoincrement=False, nullable=True), + sa.Column('status', sa.VARCHAR(length=20), autoincrement=False, nullable=True), + sa.Column('endpoint', sa.VARCHAR(length=100), autoincrement=False, nullable=True), + sa.Column('error', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('transaction_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='transaction_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='transaction_pkey'), + sa.UniqueConstraint('request_id', name='transaction_request_id_key'), + sa.UniqueConstraint('tx_hash', name='transaction_tx_hash_key') + ) + op.create_table('token', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('jti', sa.VARCHAR(length=36), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='token_user_id_fkey'), + sa.PrimaryKeyConstraint('id', name='token_pkey'), + sa.UniqueConstraint('jti', name='token_jti_key') + ) + op.drop_table('transactions') + op.drop_table('tokens') + op.drop_table('users') + op.drop_table('sign_in_requests') + op.drop_table('collection_errors') + # ### end Alembic commands ### \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index 886c0c9..58862ec 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -6,19 +6,19 @@ class Base(DeclarativeBase): db = SQLAlchemy(model_class=Base) -from .collection_error_model import CollectionError -from .auth import Token, User, SignInRequest -from .transactions import Transaction +from .collection_errors import CollectionErrors +from .auth import Tokens, Users, SignInRequests +from .transactions import Transactions from .enums import TransactionType, UserType, ModelType from .llm_data import * __all__ = [ 'db', - 'CollectionError', - 'Token', - 'User', - 'SignInRequest', - 'Transaction', + 'CollectionErrors', + 'Tokens', + 'Users', + 'SignInRequests', + 'Transactions', 'TransactionType', 'UserType', 'ModelType', diff --git a/models/auth.py b/models/auth.py index eb24724..5daaf9b 100644 --- a/models/auth.py +++ b/models/auth.py @@ -1,18 +1,22 @@ from datetime import datetime, timezone from sqlalchemy import func -from .enums import TransactionType, UserType -from .transactions import Transaction +from .enums import UserType +from .transactions import Transactions from . import db -class SignInRequest(db.Model): +class SignInRequests(db.Model): + __tablename__ = 'sign_in_requests' + id = db.Column(db.Integer, primary_key=True) address = db.Column(db.String(42), unique=True, nullable=False) nonce = db.Column(db.Integer, nullable=False) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) -class User(db.Model): +class Users(db.Model): + __tablename__ = 'users' + id = db.Column(db.Integer, primary_key=True) address = db.Column(db.String(42), unique=True, nullable=False) username = db.Column(db.String(100), unique=True, nullable=False) @@ -22,35 +26,22 @@ class User(db.Model): @staticmethod def generate_username(address: str) -> str: - # Similar to the TypeScript implementation return address.replace('0x', '')[:6] def get_balance(self): """Calculate current balance from transactions""" - result = db.session.query(func.sum(Transaction.amount)).filter( - Transaction.user_id == self.id + result = db.session.query(func.sum(Transactions.amount)).filter( + Transactions.user_id == self.id ).scalar() or 0 return result -class Token(db.Model): +class Tokens(db.Model): + __tablename__ = 'tokens' + id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) jti = db.Column(db.String(36), unique=True, nullable=False) # JWT ID created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) - user = db.relationship('User', backref=db.backref('tokens', lazy=True)) - - def get_usage_stats(self): - """Helper method to get API usage statistics""" - if self.type != TransactionType.API_USAGE: - return None - - return { - 'model': self.model_type, - 'tokens_input': self.tokens_input, - 'tokens_output': self.tokens_output, - 'cost': self.amount, - 'endpoint': self.endpoint, - 'status': self.status - } \ No newline at end of file + user = db.relationship('Users', backref=db.backref('tokens', lazy=True)) \ No newline at end of file diff --git a/models/collection_error_model.py b/models/collection_errors.py similarity index 91% rename from models/collection_error_model.py rename to models/collection_errors.py index 41470e0..58911e0 100644 --- a/models/collection_error_model.py +++ b/models/collection_errors.py @@ -6,8 +6,8 @@ from res import DatabaseError from . import db -class CollectionError (db.Model): - __tablename__ = 'app_collection_errors' +class CollectionErrors (db.Model): + __tablename__ = 'collection_errors' id = Column(Integer, primary_key=True) collection_name = Column(String) diff --git a/models/transactions.py b/models/transactions.py index ac3245a..e75e6cb 100644 --- a/models/transactions.py +++ b/models/transactions.py @@ -1,33 +1,28 @@ from datetime import datetime, timezone -from enum import Enum from . import db from .enums import ModelType, TransactionType -class Transaction(db.Model): +class Transactions(db.Model): + __tablename__ = 'transactions' + id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) type = db.Column(db.Enum(TransactionType), nullable=False) amount = db.Column(db.Numeric(precision=18, scale=8), nullable=False) - tx_hash = db.Column(db.String(66), unique=True, nullable=True) # For blockchain transactions - - # Fields for API usage transactions - model_type = db.Column(db.Enum(ModelType), nullable=True) # Only for API_USAGE - tokens_input = db.Column(db.Integer, nullable=True) # Only for API_USAGE - tokens_output = db.Column(db.Integer, nullable=True) # Only for API_USAGE - request_id = db.Column(db.String(36), unique=True, nullable=True) # For API request tracking - status = db.Column(db.String(20), nullable=True) # For API call status - endpoint = db.Column(db.String(100), nullable=True) # API endpoint used - error = db.Column(db.Text, nullable=True) # For failed API calls - - # Additional metadata as JSON for flexibility - transaction_metadata = db.Column(db.JSON) - + tx_hash = db.Column(db.String(66), unique=True, nullable=True) + model_type = db.Column(db.Enum(ModelType), nullable=True) + tokens_input = db.Column(db.Integer, nullable=True) + tokens_output = db.Column(db.Integer, nullable=True) + request_id = db.Column(db.String(36), unique=True, nullable=True) + status = db.Column(db.String(20), nullable=True) + endpoint = db.Column(db.String(100), nullable=True) + error = db.Column(db.Text, nullable=True) + transaction_metadata = db.Column(db.JSON) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) - + + user = db.relationship('Users', backref=db.backref('transactions', lazy=True)) + def __init__(self, **kwargs): - # Ensure amount is negative for usage and withdrawals if kwargs.get('type') in [TransactionType.API_USAGE, TransactionType.WITHDRAWAL]: kwargs['amount'] = -abs(kwargs['amount']) - super().__init__(**kwargs) - - user = db.relationship('User', backref=db.backref('transactions', lazy=True)) \ No newline at end of file + super().__init__(**kwargs) \ No newline at end of file diff --git a/services/payment/llm_manager.py b/services/payment/llm_manager.py index 56aa578..04d769a 100644 --- a/services/payment/llm_manager.py +++ b/services/payment/llm_manager.py @@ -2,7 +2,7 @@ from decimal import Decimal import uuid -from models import TransactionType, Transaction, ModelType +from models import TransactionType, Transactions, ModelType from models import db from typing import Optional, List, Dict, Union from models.llm_data import ChatModel, ImageModel, Provider, ModelParameters @@ -79,7 +79,7 @@ def estimate_request_cost(self, endpoint: str, request_data: dict) -> float: def record_transaction(self, user_id: int, model_version: str, input_tokens: int, output_tokens: int, endpoint: str, status: str = 'success', - error: str = None) -> Transaction: + error: str = None) -> Transactions: """Record a transaction for API usage""" model = self.get_model(model_version) if not model: @@ -97,7 +97,7 @@ def record_transaction(self, user_id: int, model_version: str, } model_type = model_type_mapping.get(model.provider, ModelType.GPT35) - transaction = Transaction( + transaction = Transactions( user_id=user_id, type=TransactionType.API_USAGE, amount=-amount, # Negative amount for usage From 2743644cedc148423b85e22e3b9560acc475755d Mon Sep 17 00:00:00 2001 From: fegloff Date: Mon, 16 Dec 2024 21:22:00 -0500 Subject: [PATCH 10/11] update transactions table + fix migration upgrate execution when deploying to fly io --- apis/openai_resource.py | 3 +- .../007e0d444e0c_initial_migration.py | 84 ++++++++++ .../58a9a3aed9f6_initial_migration.py | 156 ------------------ models/__init__.py | 3 +- models/enums.py | 6 - models/transactions.py | 6 +- services/payment/decorators.py | 2 +- services/payment/llm_manager.py | 14 +- 8 files changed, 93 insertions(+), 181 deletions(-) create mode 100644 migrations/versions/007e0d444e0c_initial_migration.py delete mode 100644 migrations/versions/58a9a3aed9f6_initial_migration.py diff --git a/apis/openai_resource.py b/apis/openai_resource.py index 45b2bb5..a8d21bc 100644 --- a/apis/openai_resource.py +++ b/apis/openai_resource.py @@ -5,7 +5,6 @@ from flask_restx import Namespace, Resource from werkzeug.utils import secure_filename from openai.error import OpenAIError -from models.enums import ModelType from .auth import require_any_auth, require_token from res import EngMsg as msg, CustomError from services import telegram_report_error @@ -93,7 +92,7 @@ def post(self): if g.is_jwt_user: transaction = llm_models_manager.record_transaction( user_id=g.user.id, - model_version=ModelType.DALL_E, + model_version='dalle', # ::::: NEEDS TO CHANGE tokens_input=n, tokens_output=0, endpoint='/generate-image', diff --git a/migrations/versions/007e0d444e0c_initial_migration.py b/migrations/versions/007e0d444e0c_initial_migration.py new file mode 100644 index 0000000..c318284 --- /dev/null +++ b/migrations/versions/007e0d444e0c_initial_migration.py @@ -0,0 +1,84 @@ +"""initial migration + +Revision ID: 007e0d444e0c +Revises: +Create Date: 2024-12-16 19:24:20.967005 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '007e0d444e0c' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('collection_errors', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('collection_name', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('sign_in_requests', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('address', sa.String(length=42), nullable=False), + sa.Column('nonce', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('address') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('address', sa.String(length=42), nullable=False), + sa.Column('username', sa.String(length=100), nullable=False), + sa.Column('user_type', sa.Enum('WALLET', 'API_KEY', name='usertype'), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('address'), + sa.UniqueConstraint('username') + ) + op.create_table('tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('jti', sa.String(length=36), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('jti') + ) + op.create_table('transactions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('type', sa.Enum('DEPOSIT', 'WITHDRAWAL', 'API_USAGE', 'REFUND', name='transactiontype'), nullable=False), + sa.Column('amount', sa.Numeric(precision=18, scale=8), nullable=False), + sa.Column('tx_hash', sa.String(length=66), nullable=True), + sa.Column('model_type', sa.String(length=50), nullable=True), + sa.Column('tokens_input', sa.Integer(), nullable=True), + sa.Column('tokens_output', sa.Integer(), nullable=True), + sa.Column('request_id', sa.String(length=36), nullable=True), + sa.Column('status', sa.String(length=20), nullable=True), + sa.Column('endpoint', sa.String(length=100), nullable=True), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('transaction_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('request_id'), + sa.UniqueConstraint('tx_hash') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('transactions') + op.drop_table('tokens') + op.drop_table('users') + op.drop_table('sign_in_requests') + op.drop_table('collection_errors') + # ### end Alembic commands ### diff --git a/migrations/versions/58a9a3aed9f6_initial_migration.py b/migrations/versions/58a9a3aed9f6_initial_migration.py deleted file mode 100644 index 52a1c9b..0000000 --- a/migrations/versions/58a9a3aed9f6_initial_migration.py +++ /dev/null @@ -1,156 +0,0 @@ -"""initial_migration - -Revision ID: 58a9a3aed9f6 -Revises: -Create Date: 2024-12-16 17:51:32.264106 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql -from sqlalchemy.engine.reflection import Inspector - -# revision identifiers, used by Alembic. -revision = '58a9a3aed9f6' -down_revision = None -branch_labels = None -depends_on = None - -def table_exists(table_name): - # Get inspector to check table existence - conn = op.get_bind() - inspector = Inspector.from_engine(conn) - return table_name in inspector.get_table_names() - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('collection_errors', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('collection_name', sa.String(), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('sign_in_requests', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('address', sa.String(length=42), nullable=False), - sa.Column('nonce', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('address') - ) - op.create_table('users', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('address', sa.String(length=42), nullable=False), - sa.Column('username', sa.String(length=100), nullable=False), - sa.Column('user_type', sa.Enum('WALLET', 'API_KEY', name='usertype'), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('address'), - sa.UniqueConstraint('username') - ) - op.create_table('tokens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('jti', sa.String(length=36), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('jti') - ) - op.create_table('transactions', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('type', sa.Enum('DEPOSIT', 'WITHDRAWAL', 'API_USAGE', 'REFUND', name='transactiontype'), nullable=False), - sa.Column('amount', sa.Numeric(precision=18, scale=8), nullable=False), - sa.Column('tx_hash', sa.String(length=66), nullable=True), - sa.Column('model_type', sa.Enum('GPT_4', 'GPT_35', 'CLAUDE', 'GEMINI', name='modeltype'), nullable=True), - sa.Column('tokens_input', sa.Integer(), nullable=True), - sa.Column('tokens_output', sa.Integer(), nullable=True), - sa.Column('request_id', sa.String(length=36), nullable=True), - sa.Column('status', sa.String(length=20), nullable=True), - sa.Column('endpoint', sa.String(length=100), nullable=True), - sa.Column('error', sa.Text(), nullable=True), - sa.Column('transaction_metadata', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('request_id'), - sa.UniqueConstraint('tx_hash') - ) - - # Safely drop old tables if they exist - if table_exists('token'): - op.drop_table('token') - if table_exists('transaction'): - op.drop_table('transaction') - if table_exists('app_collection_errors'): - op.drop_table('app_collection_errors') - if table_exists('user'): - op.drop_table('user') - if table_exists('sign_in_request'): - op.drop_table('sign_in_request') - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('sign_in_request', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('address', sa.VARCHAR(length=42), autoincrement=False, nullable=False), - sa.Column('nonce', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='sign_in_request_pkey'), - sa.UniqueConstraint('address', name='sign_in_request_address_key') - ) - op.create_table('user', - sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('user_id_seq'::regclass)"), autoincrement=True, nullable=False), - sa.Column('address', sa.VARCHAR(length=42), autoincrement=False, nullable=False), - sa.Column('username', sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column('user_type', postgresql.ENUM('WALLET', 'API_KEY', name='usertype'), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='user_pkey'), - sa.UniqueConstraint('address', name='user_address_key'), - sa.UniqueConstraint('username', name='user_username_key'), - postgresql_ignore_search_path=False - ) - op.create_table('app_collection_errors', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('collection_name', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint('id', name='app_collection_errors_pkey') - ) - op.create_table('transaction', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('type', postgresql.ENUM('DEPOSIT', 'WITHDRAWAL', 'API_USAGE', 'REFUND', name='transactiontype'), autoincrement=False, nullable=False), - sa.Column('amount', sa.NUMERIC(precision=18, scale=8), autoincrement=False, nullable=False), - sa.Column('tx_hash', sa.VARCHAR(length=66), autoincrement=False, nullable=True), - sa.Column('model_type', postgresql.ENUM('GPT_4', 'GPT_35', 'CLAUDE', 'GEMINI', name='modeltype'), autoincrement=False, nullable=True), - sa.Column('tokens_input', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('tokens_output', sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column('request_id', sa.VARCHAR(length=36), autoincrement=False, nullable=True), - sa.Column('status', sa.VARCHAR(length=20), autoincrement=False, nullable=True), - sa.Column('endpoint', sa.VARCHAR(length=100), autoincrement=False, nullable=True), - sa.Column('error', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('transaction_metadata', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='transaction_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='transaction_pkey'), - sa.UniqueConstraint('request_id', name='transaction_request_id_key'), - sa.UniqueConstraint('tx_hash', name='transaction_tx_hash_key') - ) - op.create_table('token', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('jti', sa.VARCHAR(length=36), autoincrement=False, nullable=False), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], name='token_user_id_fkey'), - sa.PrimaryKeyConstraint('id', name='token_pkey'), - sa.UniqueConstraint('jti', name='token_jti_key') - ) - op.drop_table('transactions') - op.drop_table('tokens') - op.drop_table('users') - op.drop_table('sign_in_requests') - op.drop_table('collection_errors') - # ### end Alembic commands ### \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index 58862ec..3a70716 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -9,7 +9,7 @@ class Base(DeclarativeBase): from .collection_errors import CollectionErrors from .auth import Tokens, Users, SignInRequests from .transactions import Transactions -from .enums import TransactionType, UserType, ModelType +from .enums import TransactionType, UserType from .llm_data import * __all__ = [ @@ -21,7 +21,6 @@ class Base(DeclarativeBase): 'Transactions', 'TransactionType', 'UserType', - 'ModelType', 'Provider', 'Provider', 'ChargeType', diff --git a/models/enums.py b/models/enums.py index fb55422..25e5270 100644 --- a/models/enums.py +++ b/models/enums.py @@ -9,9 +9,3 @@ class TransactionType(Enum): WITHDRAWAL = 'withdrawal' API_USAGE = 'api_usage' REFUND = 'refund' - -class ModelType(Enum): - GPT_4 = 'gpt-4' - GPT_35 = 'gpt-3.5-turbo' - CLAUDE = 'claude' - GEMINI = 'gemini' \ No newline at end of file diff --git a/models/transactions.py b/models/transactions.py index e75e6cb..e6b2612 100644 --- a/models/transactions.py +++ b/models/transactions.py @@ -1,16 +1,16 @@ from datetime import datetime, timezone from . import db -from .enums import ModelType, TransactionType +from .enums import TransactionType class Transactions(db.Model): __tablename__ = 'transactions' - id = db.Column(db.Integer, primary_key=True) + id = db.Column(db.Integer, primary_keyx=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) type = db.Column(db.Enum(TransactionType), nullable=False) amount = db.Column(db.Numeric(precision=18, scale=8), nullable=False) tx_hash = db.Column(db.String(66), unique=True, nullable=True) - model_type = db.Column(db.Enum(ModelType), nullable=True) + model = db.Column(db.String(50), nullable=True) tokens_input = db.Column(db.Integer, nullable=True) tokens_output = db.Column(db.Integer, nullable=True) request_id = db.Column(db.String(36), unique=True, nullable=True) diff --git a/services/payment/decorators.py b/services/payment/decorators.py index 8cc4fe5..0509d72 100644 --- a/services/payment/decorators.py +++ b/services/payment/decorators.py @@ -21,7 +21,7 @@ def decorated(*args, **kwargs): balance = user.get_balance() endpoint = request.endpoint request_data = request.get_json() if request.is_json else request.form.to_dict() - + print('FCO:::::::::::: endpoint', endpoint) try: estimated_cost = llm_models_manager.estimate_request_cost(endpoint, request_data) diff --git a/services/payment/llm_manager.py b/services/payment/llm_manager.py index 04d769a..52aa402 100644 --- a/services/payment/llm_manager.py +++ b/services/payment/llm_manager.py @@ -2,7 +2,7 @@ from decimal import Decimal import uuid -from models import TransactionType, Transactions, ModelType +from models import TransactionType, Transactions from models import db from typing import Optional, List, Dict, Union from models.llm_data import ChatModel, ImageModel, Provider, ModelParameters @@ -89,19 +89,11 @@ def record_transaction(self, user_id: int, model_version: str, price_info = self.get_prompt_price(model_version, input_tokens, output_tokens) amount = Decimal(str(price_info['price'])) - # Map provider to ModelType - model_type_mapping = { - 'openai': ModelType.GPT4 if 'gpt-4' in model_version else ModelType.GPT35, - 'claude': ModelType.CLAUDE, - 'vertex': ModelType.GEMINI - } - model_type = model_type_mapping.get(model.provider, ModelType.GPT35) - transaction = Transactions( user_id=user_id, type=TransactionType.API_USAGE, amount=-amount, # Negative amount for usage - model_type=model_type, + model=model_version, tokens_input=input_tokens, tokens_output=output_tokens, request_id=str(uuid.uuid4()), @@ -110,7 +102,7 @@ def record_transaction(self, user_id: int, model_version: str, error=error, transaction_metadata={ 'timestamp': datetime.now(timezone.utc).isoformat(), - 'model_type': model_type.value, + 'model': model_version, 'model_version': model_version, 'endpoint': endpoint, 'estimated_cost': str(amount) From 6105b0f42ddff9cfbbd42b10c51b5d5396ee89c4 Mon Sep 17 00:00:00 2001 From: fegloff Date: Thu, 19 Dec 2024 10:06:23 -0500 Subject: [PATCH 11/11] add model estimate cost + add dalle api test --- apis/openai_resource.py | 12 +- config.py | 1 + ...n.py => 77120d842962_initial_migration.py} | 8 +- models/transactions.py | 2 +- services/payment/decorators.py | 12 +- services/payment/llm_manager.py | 163 +++++++++++++++--- test/api_tester.py | 47 ++++- 7 files changed, 200 insertions(+), 45 deletions(-) rename migrations/versions/{007e0d444e0c_initial_migration.py => 77120d842962_initial_migration.py} (95%) diff --git a/apis/openai_resource.py b/apis/openai_resource.py index a8d21bc..40c34d8 100644 --- a/apis/openai_resource.py +++ b/apis/openai_resource.py @@ -86,13 +86,22 @@ def post(self): if not data or 'prompt' not in data: return {"error": "No prompt provided"}, 400 + # Get model version from request or use default + model_version = data.get('model') + if not model_version: + # Get default OpenAI image model (DALL-E 3) + default_model = llm_models_manager.get_default_image_model("openai") + if not default_model: + return {"error": "No default image model found"}, 500 + model_version = default_model.version + size = data.get('size', '1024x1024') n = min(max(1, data.get('n', 1)), 10) if g.is_jwt_user: transaction = llm_models_manager.record_transaction( user_id=g.user.id, - model_version='dalle', # ::::: NEEDS TO CHANGE + model_version=model_version, tokens_input=n, tokens_output=0, endpoint='/generate-image', @@ -104,6 +113,7 @@ def post(self): prompt=data['prompt'], size=size, n=n, + model=model_version ) return {"images": [img['url'] for img in response['data']]}, 200 diff --git a/config.py b/config.py index 56458aa..fc25b2f 100644 --- a/config.py +++ b/config.py @@ -32,5 +32,6 @@ class Config(object): WEB3_PROVIDER_URL = 'https://api.harmony.one' JWT_EXPIRATION_MINUTES = 30 REFRESH_EXPIRATION_DAYS = 7 + PRICE_ADJUSTMENT = 1 DATABASE_URL= os.environ.get('DATABASE_URL') if os.environ.get('DATABASE_URL') else 'sqlite:///:memory:' config = Config() diff --git a/migrations/versions/007e0d444e0c_initial_migration.py b/migrations/versions/77120d842962_initial_migration.py similarity index 95% rename from migrations/versions/007e0d444e0c_initial_migration.py rename to migrations/versions/77120d842962_initial_migration.py index c318284..97169fc 100644 --- a/migrations/versions/007e0d444e0c_initial_migration.py +++ b/migrations/versions/77120d842962_initial_migration.py @@ -1,8 +1,8 @@ """initial migration -Revision ID: 007e0d444e0c +Revision ID: 77120d842962 Revises: -Create Date: 2024-12-16 19:24:20.967005 +Create Date: 2024-12-17 11:21:14.263139 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '007e0d444e0c' +revision = '77120d842962' down_revision = None branch_labels = None depends_on = None @@ -57,7 +57,7 @@ def upgrade(): sa.Column('type', sa.Enum('DEPOSIT', 'WITHDRAWAL', 'API_USAGE', 'REFUND', name='transactiontype'), nullable=False), sa.Column('amount', sa.Numeric(precision=18, scale=8), nullable=False), sa.Column('tx_hash', sa.String(length=66), nullable=True), - sa.Column('model_type', sa.String(length=50), nullable=True), + sa.Column('model', sa.String(length=50), nullable=True), sa.Column('tokens_input', sa.Integer(), nullable=True), sa.Column('tokens_output', sa.Integer(), nullable=True), sa.Column('request_id', sa.String(length=36), nullable=True), diff --git a/models/transactions.py b/models/transactions.py index e6b2612..cda65cc 100644 --- a/models/transactions.py +++ b/models/transactions.py @@ -5,7 +5,7 @@ class Transactions(db.Model): __tablename__ = 'transactions' - id = db.Column(db.Integer, primary_keyx=True) + id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) type = db.Column(db.Enum(TransactionType), nullable=False) amount = db.Column(db.Numeric(precision=18, scale=8), nullable=False) diff --git a/services/payment/decorators.py b/services/payment/decorators.py index 0509d72..7d8dbfc 100644 --- a/services/payment/decorators.py +++ b/services/payment/decorators.py @@ -2,6 +2,7 @@ from flask import g, request, current_app as app from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request from .llm_manager import llm_models_manager +from models import Users def check_balance(f): @wraps(f) @@ -10,21 +11,18 @@ def decorated(*args, **kwargs): verify_jwt_in_request() user_id = get_jwt_identity() g.is_jwt_user = True - - from models import User - user = User.query.get(int(user_id)) - + + user = Users.query.get(int(user_id)) if not user: app.logger.error(f"User not found with ID: {user_id}") return {"msg": "User not found"}, 404 - + balance = user.get_balance() endpoint = request.endpoint request_data = request.get_json() if request.is_json else request.form.to_dict() - print('FCO:::::::::::: endpoint', endpoint) + try: estimated_cost = llm_models_manager.estimate_request_cost(endpoint, request_data) - if balance < estimated_cost: return { "msg": "Insufficient balance", diff --git a/services/payment/llm_manager.py b/services/payment/llm_manager.py index 52aa402..8f9d833 100644 --- a/services/payment/llm_manager.py +++ b/services/payment/llm_manager.py @@ -1,12 +1,13 @@ -from datetime import datetime, timezone -from decimal import Decimal import uuid +from datetime import datetime, timezone +from decimal import Decimal +from flask import current_app as app from models import TransactionType, Transactions from models import db from typing import Optional, List, Dict, Union from models.llm_data import ChatModel, ImageModel, Provider, ModelParameters -from config import Config +from config import config from .llm_models import llm_config class LLMModelsManager: @@ -41,40 +42,128 @@ def get_chat_model_price(self, model: ChatModel, input_tokens: int, price *= 100 return price / 1000 - def get_prompt_price(self, model_version: str, input_tokens: int, - output_tokens: Optional[int] = None) -> Dict[str, float]: - model = self.get_model(model_version) - if not isinstance(model, ChatModel): - raise ValueError(f"Model {model_version} is not a chat model") + def get_chat_model_price(self, model: ChatModel, input_tokens: int, + output_tokens: Optional[int] = None, + in_cents: bool = True, + apply_adjustment: bool = True) -> Decimal: + """ + Calculate price for chat model usage. + For TOKEN models: price per 1K tokens + For CHAR models: price per 1K characters + Returns: Decimal value in cents if in_cents=True, otherwise in dollars + """ + input_price = model.input_price * (Decimal(str(input_tokens)) / Decimal('1000')) + + if output_tokens is not None: + output_price = model.output_price * (Decimal(str(output_tokens)) / Decimal('1000')) + else: + estimated_output = min(input_tokens * 2, model.max_context_tokens) + output_price = model.output_price * (Decimal(str(estimated_output)) / Decimal('1000')) - price = self.get_chat_model_price(model, input_tokens, output_tokens) - price *= Config.PRICE_ADJUSTMENT + total_price = input_price + output_price - return { - "price": price, - "prompt_tokens": input_tokens, - "completion_tokens": output_tokens or 0 - } + if in_cents: + total_price *= Decimal('100') + + if apply_adjustment: + total_price *= Decimal(str(config.PRICE_ADJUSTMENT)) + + return total_price + + def get_models_by_provider(self, provider: str) -> List[Union[ChatModel, ImageModel]]: + """Get all models (chat and image) for a specific provider""" + return [model for model in self.models.values() if model.provider == provider] + + def get_model_by_name(self, name: str) -> Optional[Union[ChatModel, ImageModel]]: + """Get a model by its name (not version)""" + return next( + (model for model in self.models.values() if model.name == name), + None + ) - def estimate_request_cost(self, endpoint: str, request_data: dict) -> float: - print('fco:::::::: request_data', request_data, endpoint) - return 0.9 + def get_default_image_model(self, provider: str = "openai") -> Optional[ImageModel]: + """Get the default image model for a provider""" + image_models = [ + model for model in self.models.values() + if isinstance(model, ImageModel) and model.provider == provider + ] + return image_models[0] if image_models else None + + def estimate_request_cost(self, endpoint: str, request_data: dict) -> Decimal: + """ + Estimate request cost based on input size and model type. + Returns: Decimal value in cents + """ + app.logger.debug(f'Estimating cost for endpoint: {endpoint} with data: {request_data}') + model_version = request_data.get('model') if not model_version: - raise ValueError("Model version not provided in request") + raise ValueError("Model version not specified in request data") + + model = self.get_model(model_version) + if not model: + raise ValueError(f"Invalid model version: {model_version}") + + if isinstance(model, ImageModel): + return self._estimate_image_cost(model, request_data) + + messages = request_data.get('messages', []) + total_input_size = sum( + len(str(msg.get('content', '') or msg.get('parts', {}).get('text', ''))) + for msg in messages + ) + + if model.charge_type == "CHAR": + input_size = total_input_size + output_size = min(total_input_size * 2, model.max_context_tokens) - # Estimate tokens based on input text - input_text = request_data.get('prompt', '') - estimated_input_tokens = len(input_text.split()) # Simple estimation - estimated_output_tokens = None # Can be refined based on your needs + elif model.charge_type == "TOKEN": + # Estimate tokens from characters (roughly 4 chars per token) + input_size = total_input_size // 4 + output_size = min(input_size * 2, model.max_context_tokens) + + else: + raise ValueError(f"Unknown charge type: {model.charge_type}") + + price = self.get_chat_model_price( + model, + input_size, + output_size, + in_cents=True + ) - price_info = self.get_prompt_price( - model_version, - estimated_input_tokens, - estimated_output_tokens + app.logger.debug( + f'Cost estimate details:\n' + f'Model: {model_version}\n' + f'Charge type: {model.charge_type}\n' + f'Input size: {input_size} {model.charge_type.lower()}s\n' + f'Estimated output size: {output_size} {model.charge_type.lower()}s\n' + f'Total estimated cost (cents): {price}' ) - return price_info["price"] + return price + + # CHANGED: Added new method for image cost estimation + def _estimate_image_cost(self, model: ImageModel, request_data: dict) -> Decimal: + """Returns cost in cents as Decimal""" + size = request_data.get('size', '1024x1024') + n = request_data.get('n', 1) + + if size not in ['1024x1024', '1024x1792', '1792x1024']: + raise ValueError(f"Invalid size parameter: {size}") + + size_price_map = { + '1024x1024': 'size_1024_1024', + '1024x1792': 'size_1024_1792', + '1792x1024': 'size_1792_1024' + } + + price_key = size_price_map[size] + if price_key not in model.price: + raise ValueError(f"Price not configured for size: {size}") + + return model.price[price_key] * Decimal('100') * Decimal(str(n)) + def record_transaction(self, user_id: int, model_version: str, input_tokens: int, output_tokens: int, @@ -118,3 +207,21 @@ def record_transaction(self, user_id: int, model_version: str, raise e llm_models_manager = LLMModelsManager(llm_data=llm_config) + + + + # if not model_version: + # raise ValueError("Model version not provided in request") + + # # Estimate tokens based on input text + # input_text = request_data.get('prompt', '') + # estimated_input_tokens = len(input_text.split()) # Simple estimation + # estimated_output_tokens = None # Can be refined based on your needs + + # price_info = self.get_prompt_price( + # model_version, + # estimated_input_tokens, + # estimated_output_tokens + # ) + + # return price_info["price"] \ No newline at end of file diff --git a/test/api_tester.py b/test/api_tester.py index 4beeb73..c3ed4e3 100644 --- a/test/api_tester.py +++ b/test/api_tester.py @@ -104,6 +104,45 @@ def test_gemini_api(self): print(f"Error processing stream: {str(e)}") return {"error": str(e)} + def test_dalle_api(self, headers): + print("Testing DALL-E image generation...") + try: + response = self.session.post( + f"{self.base_url}/openai/generate-image", + headers=headers, + json={ + "prompt": "A cute robot painting a landscape", + "size": "1024x1024", + "n": 1, + "model": "dall-e-3" + } + ) + + print(f"DALL-E request status code: {response.status_code}") + + if response.status_code != 200: + print(f"Error response content: {response.json()}") + return {"error": response.text} + + response.raise_for_status() + + result = response.json() + print("\nDALL-E API Response:") + print(json.dumps(result, indent=2)) + + if 'images' in result: + print(f"\nGenerated {len(result['images'])} images:") + for i, image_url in enumerate(result['images'], 1): + print(f"Image {i}: {image_url}") + + return {"status": "completed", "data": result} + + except requests.exceptions.RequestException as e: + print(f"Request failed: {str(e)}") + return {"error": str(e)} + except Exception as e: + print(f"Error processing response: {str(e)}") + return {"error": str(e)} def run_api_tests(self): """Run API tests with automatic token handling""" @@ -114,11 +153,11 @@ def run_api_tests(self): headers = {"Authorization": f"Bearer {access_token}"} # Run your API tests here - print("\nTesting Gemini API...") - self.test_gemini_api() + # print("\nTesting Gemini API...") + # self.test_gemini_api() - # print("\nTesting DALL-E API...") - # self.test_dalle_api(headers) + print("\nTesting DALL-E API...") + self.test_dalle_api(headers) except Exception as e: print(f"Error during API tests: {e}")