commit b828d8b9bdde1495e275e317739871dbc49bb4b1 Author: adriano Date: Mon Jun 9 08:13:05 2025 -0300 feat: first commit diff --git a/backend/.env-example b/backend/.env-example new file mode 100644 index 0000000..07614ea --- /dev/null +++ b/backend/.env-example @@ -0,0 +1,8 @@ +PORT=5000 +FRONTEND_URL=http://localhost:3000 + +MONGO_URI=mongodb://admin:SODIOXX98*@172.31.187.24:27017 + +SECRET_KEY=TESTPSOJDFPSODIJFPDSJP +JWT_SECRET_KEY=sua_chave_supersecreta +JWT_ACCESS_TOKEN_EXPIRES_DAYS=15 \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..55c7dce --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,55 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +venv/ +.env/ +.env.bak +.env.* + +# Environment variable files +*.env + +# Flask instance folder +instance/ + +# Pytest cache +.pytest_cache/ + +# VSCode settings +.vscode/ + +# macOS +.DS_Store + +# Logs and database +*.log +*.sqlite3 +*.db + +# Coverage reports +htmlcov/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml + +# Mypy +.mypy_cache/ + +# Flask-Migrate files (optional) +migrations/ + +# Celery beat schedule file +celerybeat-schedule + +# Node.js (caso tenha frontend integrado) +node_modules/ +dist/ +build/ + +# Jupyter Notebook checkpoints (se usar notebooks) +.ipynb_checkpoints/ diff --git a/backend/Pipfile b/backend/Pipfile new file mode 100644 index 0000000..0c3d417 --- /dev/null +++ b/backend/Pipfile @@ -0,0 +1,26 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "*" +dotenv = "*" +flask-sqlalchemy = "*" +pymongo = "*" +pymysql = "*" +openpyxl = "*" +pandas = "*" +pydantic = "*" +flask-restx = "*" +pytest = "*" +pytest-mock = "*" +flask-jwt-extended = "*" +flask-bcrypt = "*" +flask-cors = "*" +mypy = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock new file mode 100644 index 0000000..0d0d396 --- /dev/null +++ b/backend/Pipfile.lock @@ -0,0 +1,1031 @@ +{ + "_meta": { + "hash": { + "sha256": "41c45852f7940e8cbf541673b9e42d7a8b139527a30b88edad5b6620405e117a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aniso8601": { + "hashes": [ + "sha256:25488f8663dd1528ae1f54f94ac1ea51ae25b4d531539b8bc707fed184d16845", + "sha256:eb19717fd4e0db6de1aab06f12450ab92144246b257423fe020af5748c0cb89e" + ], + "version": "==10.0.1" + }, + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, + "attrs": { + "hashes": [ + "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", + "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b" + ], + "markers": "python_version >= '3.8'", + "version": "==25.3.0" + }, + "bcrypt": { + "hashes": [ + "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", + "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", + "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", + "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", + "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", + "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", + "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", + "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", + "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", + "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", + "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", + "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", + "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", + "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", + "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", + "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", + "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", + "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", + "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", + "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", + "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", + "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", + "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", + "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", + "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", + "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", + "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", + "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", + "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", + "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", + "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", + "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", + "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", + "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", + "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", + "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", + "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", + "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", + "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", + "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", + "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", + "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", + "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", + "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", + "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", + "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", + "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", + "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", + "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", + "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", + "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d" + ], + "markers": "python_version >= '3.8'", + "version": "==4.3.0" + }, + "blinker": { + "hashes": [ + "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", + "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" + ], + "markers": "python_version >= '3.9'", + "version": "==1.9.0" + }, + "click": { + "hashes": [ + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" + ], + "markers": "python_version >= '3.10'", + "version": "==8.2.1" + }, + "dnspython": { + "hashes": [ + "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", + "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1" + ], + "markers": "python_version >= '3.9'", + "version": "==2.7.0" + }, + "dotenv": { + "hashes": [ + "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9" + ], + "index": "pypi", + "version": "==0.9.9" + }, + "et-xmlfile": { + "hashes": [ + "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", + "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.0.0" + }, + "flask": { + "hashes": [ + "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", + "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==3.1.1" + }, + "flask-bcrypt": { + "hashes": [ + "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a", + "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "flask-cors": { + "hashes": [ + "sha256:4592c1570246bf7beee96b74bc0adbbfcb1b0318f6ba05c412e8909eceec3393", + "sha256:6332073356452343a8ccddbfec7befdc3fdd040141fe776ec9b94c262f058657" + ], + "index": "pypi", + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==6.0.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:4f3d3fa7b6191fcc715b18c201a12cd875176f92ba4acc61626ccfd571ee1728", + "sha256:636c56c3fb3f2c1df979e748019f084a938c4da2035a3e535a4673e4fc177691" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "flask-sqlalchemy": { + "hashes": [ + "sha256:4ba4be7f419dc72f4efd8802d69974803c37259dd42f3913b0dcf75c9447e0a0", + "sha256:e4b68bb881802dda1a7d878b2fc84c06d1ee57fb40b874d3dc97dabfa36b8312" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.1.1" + }, + "greenlet": { + "hashes": [ + "sha256:003c930e0e074db83559edc8705f3a2d066d4aa8c2f198aff1e454946efd0f26", + "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", + "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", + "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", + "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", + "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", + "sha256:22eb5ba839c4b2156f18f76768233fe44b23a31decd9cc0d4cc8141c211fd1b4", + "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", + "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", + "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", + "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", + "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", + "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", + "sha256:42efc522c0bd75ffa11a71e09cd8a399d83fafe36db250a87cf1dacfaa15dc64", + "sha256:4532f0d25df67f896d137431b13f4cdce89f7e3d4a96387a41290910df4d3a57", + "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", + "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", + "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", + "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", + "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", + "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", + "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", + "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", + "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", + "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", + "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", + "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", + "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", + "sha256:7e70ea4384b81ef9e84192e8a77fb87573138aa5d4feee541d8014e452b434da", + "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", + "sha256:8324319cbd7b35b97990090808fdc99c27fe5338f87db50514959f8059999805", + "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", + "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", + "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", + "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", + "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", + "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", + "sha256:8c37ef5b3787567d322331d5250e44e42b58c8c713859b8a04c6065f27efbf72", + "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", + "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", + "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", + "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", + "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", + "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", + "sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322", + "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", + "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", + "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", + "sha256:ce539fb52fb774d0802175d37fcff5c723e2c7d249c65916257f0a940cee8904", + "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", + "sha256:d760f9bdfe79bff803bad32b4d8ffb2c1d2ce906313fc10a83976ffb73d64ca7", + "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", + "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", + "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728" + ], + "markers": "python_version < '3.14' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==3.2.3" + }, + "importlib-resources": { + "hashes": [ + "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", + "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec" + ], + "markers": "python_version >= '3.9'", + "version": "==6.5.2" + }, + "iniconfig": { + "hashes": [ + "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", + "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + }, + "itsdangerous": { + "hashes": [ + "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", + "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.0" + }, + "jinja2": { + "hashes": [ + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.6" + }, + "jsonschema": { + "hashes": [ + "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", + "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d" + ], + "markers": "python_version >= '3.9'", + "version": "==4.24.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", + "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608" + ], + "markers": "python_version >= '3.9'", + "version": "==2025.4.1" + }, + "markupsafe": { + "hashes": [ + "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.9'", + "version": "==3.0.2" + }, + "mypy": { + "hashes": [ + "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", + "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", + "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", + "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", + "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92", + "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", + "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21", + "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", + "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", + "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", + "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", + "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", + "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", + "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", + "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", + "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", + "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20", + "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", + "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", + "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", + "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", + "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", + "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", + "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", + "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", + "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", + "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", + "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2", + "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", + "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436", + "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3", + "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==1.16.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "numpy": { + "hashes": [ + "sha256:06d4fb37a8d383b769281714897420c5cc3545c79dc427df57fc9b852ee0bf58", + "sha256:0898c67a58cdaaf29994bc0e2c65230fd4de0ac40afaf1584ed0b02cd74c6fdd", + "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", + "sha256:2393a914db64b0ead0ab80c962e42d09d5f385802006a6c87835acb1f58adb96", + "sha256:2e6a1409eee0cb0316cb64640a49a49ca44deb1a537e6b1121dc7c458a1299a8", + "sha256:33a5a12a45bb82d9997e2c0b12adae97507ad7c347546190a18ff14c28bbca12", + "sha256:389b85335838155a9076e9ad7f8fdba0827496ec2d2dc32ce69ce7898bde03ba", + "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", + "sha256:43c55b6a860b0eb44d42341438b03513cf3879cb3617afb749ad49307e164edd", + "sha256:46d16f72c2192da7b83984aa5455baee640e33a9f1e61e656f29adf55e406c2b", + "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", + "sha256:4d8d294287fdf685281e671886c6dcdf0291a7c19db3e5cb4178d07ccf6ecc67", + "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", + "sha256:50080245365d75137a2bf46151e975de63146ae6d79f7e6bd5c0e85c9931d06a", + "sha256:54dfc8681c1906d239e95ab1508d0a533c4a9505e52ee2d71a5472b04437ef97", + "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", + "sha256:5814a0f43e70c061f47abd5857d120179609ddc32a613138cbb6c4e9e2dbdda5", + "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", + "sha256:622a65d40d8eb427d8e722fd410ac3ad4958002f109230bc714fa551044ebae2", + "sha256:6295f81f093b7f5769d1728a6bd8bf7466de2adfa771ede944ce6711382b89dc", + "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", + "sha256:7729c8008d55e80784bd113787ce876ca117185c579c0d626f59b87d433ea779", + "sha256:80b46117c7359de8167cc00a2c7d823bdd505e8c7727ae0871025a86d668283b", + "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", + "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", + "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", + "sha256:9498f60cd6bb8238d8eaf468a3d5bb031d34cd12556af53510f05fcf581c1b7e", + "sha256:99224862d1412d2562248d4710126355d3a8db7672170a39d6909ac47687a8a4", + "sha256:a0be278be9307c4ab06b788f2a077f05e180aea817b3e41cebbd5aaf7bd85ed3", + "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", + "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", + "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", + "sha256:b9446d9d8505aadadb686d51d838f2b6688c9e85636a0c3abaeb55ed54756459", + "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", + "sha256:bd8df082b6c4695753ad6193018c05aac465d634834dca47a3ae06d4bb22d9ea", + "sha256:c24bb4113c66936eeaa0dc1e47c74770453d34f46ee07ae4efd853a2ed1ad10a", + "sha256:c39ec392b5db5088259c68250e342612db82dc80ce044cf16496cf14cf6bc6f8", + "sha256:c3c9fdde0fa18afa1099d6257eb82890ea4f3102847e692193b54e00312a9ae9", + "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", + "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", + "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", + "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", + "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", + "sha256:e017a8a251ff4d18d71f139e28bdc7c31edba7a507f72b1414ed902cbe48c74d", + "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", + "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", + "sha256:e6648078bdd974ef5d15cecc31b0c410e2e24178a6e10bf511e0557eed0f2570", + "sha256:ee9d3ee70d62827bc91f3ea5eee33153212c41f639918550ac0475e3588da59f", + "sha256:ef6c1e88fd6b81ac6d215ed71dc8cd027e54d4bf1d2682d362449097156267a2", + "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", + "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b" + ], + "markers": "python_version >= '3.12'", + "version": "==2.3.0" + }, + "openpyxl": { + "hashes": [ + "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", + "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.1.5" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pandas": { + "hashes": [ + "sha256:034abd6f3db8b9880aaee98f4f5d4dbec7c4829938463ec046517220b2f8574e", + "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", + "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46", + "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", + "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", + "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", + "sha256:23c2b2dc5213810208ca0b80b8666670eb4660bbfd9d45f58592cc4ddcfd62e1", + "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", + "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", + "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", + "sha256:39ff73ec07be5e90330cc6ff5705c651ace83374189dcdcb46e6ff54b4a72cd6", + "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", + "sha256:40cecc4ea5abd2921682b57532baea5588cc5f80f0231c624056b146887274d2", + "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", + "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", + "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", + "sha256:625466edd01d43b75b1883a64d859168e4556261a5035b32f9d743b67ef44634", + "sha256:75651c14fde635e680496148a8526b328e09fe0572d9ae9b638648c46a544ba3", + "sha256:84141f722d45d0c2a89544dd29d35b3abfc13d2250ed7e68394eda7564bd6324", + "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca", + "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", + "sha256:9efc0acbbffb5236fbdf0409c04edce96bec4bdaa649d49985427bd1ec73e085", + "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", + "sha256:a6872d695c896f00df46b71648eea332279ef4077a409e2fe94220208b6bb675", + "sha256:b198687ca9c8529662213538a9bb1e60fa0bf0f6af89292eb68fea28743fcd5a", + "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", + "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", + "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", + "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", + "sha256:bf5be867a0541a9fb47a4be0c5790a4bccd5b77b92f0a59eeec9375fafc2aa14", + "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33", + "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", + "sha256:e0f51973ba93a9f97185049326d75b942b9aeb472bec616a129806facb129ebb", + "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", + "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef", + "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", + "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c", + "sha256:f4dd97c19bd06bc557ad787a15b6489d2614ddaab5d104a0310eb314c724b2d2", + "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", + "sha256:f95a2aef32614ed86216d3c450ab12a4e82084e8102e355707a1d96e33d51c34", + "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a", + "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.3.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pydantic": { + "hashes": [ + "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", + "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.11.5" + }, + "pydantic-core": { + "hashes": [ + "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", + "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", + "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", + "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", + "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", + "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", + "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", + "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", + "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", + "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", + "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", + "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", + "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", + "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", + "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", + "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", + "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", + "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", + "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", + "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", + "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", + "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", + "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", + "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", + "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", + "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", + "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", + "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", + "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", + "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", + "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", + "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", + "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", + "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", + "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", + "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", + "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", + "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", + "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", + "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", + "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", + "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", + "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", + "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", + "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", + "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", + "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", + "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", + "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", + "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", + "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", + "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", + "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", + "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", + "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", + "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", + "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", + "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", + "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", + "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", + "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", + "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", + "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", + "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", + "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", + "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", + "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", + "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", + "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", + "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", + "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", + "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", + "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", + "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", + "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", + "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", + "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", + "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", + "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", + "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", + "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", + "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", + "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", + "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", + "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", + "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", + "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", + "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", + "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", + "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", + "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", + "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", + "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", + "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", + "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", + "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", + "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", + "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", + "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d" + ], + "markers": "python_version >= '3.9'", + "version": "==2.33.2" + }, + "pygments": { + "hashes": [ + "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", + "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.1" + }, + "pyjwt": { + "hashes": [ + "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" + ], + "markers": "python_version >= '3.9'", + "version": "==2.10.1" + }, + "pymongo": { + "hashes": [ + "sha256:007450b8c8d17b4e5b779ab6e1938983309eac26b5b8f0863c48effa4b151b07", + "sha256:02f0e1af87280697a1a8304238b863d4eee98c8b97f554ee456c3041c0f3a021", + "sha256:1397eac713b84946210ab556666cfdd787eee824e910fbbe661d147e110ec516", + "sha256:1bac84ee40032bec4c089e92970893157fcd0ef40b81157404ceb4c1dac8ba72", + "sha256:209efd3b62cdbebd3cc7a76d5e37414ad08c9bfe8b28ae73695ade065d5b1277", + "sha256:267eff6a66da5cf5255b3bcd257984619e9c4d41a53578d4e1d827553a51cf40", + "sha256:2d377bb0811e0a9676bacb21a4f87ef307f2e9a40a625660c113a9c0ae897e8c", + "sha256:3d631d879e934b46222f5092d8951cbb9fe83542649697c8d342ea7b5479f118", + "sha256:3f33b8c1405d05517dce06756f2800b37dd098216cae5903cd80ad4f0a9dad08", + "sha256:46c8bce9af98556110a950939f3eaa3f7648308d60df65feb783c780f8b9bfa9", + "sha256:50c503b7e809e54740704ec4c87a0f2ccdb910c3b1d36c07dbd2029b6eaa6a50", + "sha256:51081910a91e3451db74b7265ee290c72220412aa8897d6dfe28f6e5d80b685b", + "sha256:5303e2074b85234e337ebe622d353ce38a35696cd47a7d970f84b545288aee01", + "sha256:5adc1349fd5c94d5dfbcbd1ad9858d1df61945a07f5905dcf17bb62eb4c81f93", + "sha256:5dea2f6b44697eda38a11ef754d2adfff5373c51b1ffda00b9fedc5facbd605f", + "sha256:61733c8f1ded90ab671a08033ee99b837073c73e505b3b3b633a55a0326e77f4", + "sha256:6208b83e7d566935218c0837f3b74c7d2dda83804d5d843ce21a55f22255ab74", + "sha256:66800de4f4487e7c437991b44bc1e717aadaf06e67451a760efe5cd81ce86575", + "sha256:6a8f060f8ad139d1d45f75ef7aa0084bd7f714fc666f98ef00009efc7db34acd", + "sha256:6b91f59137e46cd3ff17d5684a18e8006d65d0ee62eb1068b512262d1c2c5ae8", + "sha256:81b46d9bc62128c3d968336f8635bcfce33d8e9e1fc6be6ebdfb98effaccb9c7", + "sha256:82c36928c1c26580ce4f2497a6875968636e87c77108ff253d76b1355181a405", + "sha256:899a5ea9cd32b1b0880015fdceaa36a41140a8c2ce8621626c52f7023724aed6", + "sha256:8af08ba2886f08d334bc7e5d5c662c60ea2f16e813a2c35106f399463fa11087", + "sha256:8e11ea726ff8ddc8c8393895cd7e93a57e2558c27273d3712797895c53d25692", + "sha256:8e90195cb5aee24a67a29adde54c1dae4d9744e17e4585bea3a83bfff96db46c", + "sha256:92a06e3709e3c7e50820d352d3d4e60015406bcba69808937dac2a6d22226fde", + "sha256:92f5e75ae265e798be1a8a40a29e2ab934e156f3827ca0e1c47e69d43f4dcb31", + "sha256:936f7be9ed6919e3be7369b858d1c58ebaa4f3ef231cf4860779b8ba3b4fcd11", + "sha256:99a52cfbf31579cc63c926048cd0ada6f96c98c1c4c211356193e07418e6207c", + "sha256:a9fe172e93551ddfdb94b9ad34dccebc4b7b680dc1d131bc6bd661c4a5b2945c", + "sha256:b2afe49109b4d498d8e55ac9692915f2a3fce0bd31646bb7ed41f9ab3546ca19", + "sha256:b54e19e0f6c8a7ad0c5074a8cbefb29c12267c784ceb9a1577a62bbc43150161", + "sha256:b63d9d8be87f4be11972c5a63d815974c298ada59a2e1d56ef5b6984d81c544a", + "sha256:b9288188101506a9d1aa3f70f65b7f5f499f8f7d5c23ec86a47551d756e32059", + "sha256:bd0c9322fdf1b9e8a5c99ca337bd9a99d972ba57c976e77b5017366ba26725e1", + "sha256:bd23119f9d0358aa1f78174d2eda88ca5c882a722e25ca31197402278acddc6e", + "sha256:be048fb78e165243272a8cdbeb40d53eace82424b95417ab3ab6ec8e9b00c59b", + "sha256:c02160ab3a67eca393a2a2bb83dccddf4db2196d0d7c6a980a55157e4bdadc06", + "sha256:c03e02129ad202d8e146480b398c4a3ea18266ee0754b6a4805de6baf4a6a8c7", + "sha256:c7d740560710be0c514bc9d26f5dcbb3c85dbb6b450c4c3246d8136ca84055bd", + "sha256:cef461fae88ac51cd6b3f81adf58171113c58c0e77c82c751b3bdcef516cfeb1", + "sha256:d10d3967e87c21869f084af5716d02626a17f6f9ccc9379fcbece5821c2a9fb4", + "sha256:d4b4942e5566a134fe34c03d7182a0b346e4a478defe625dc430dd5a178ad96e", + "sha256:d684d9b385d97ab821d2ae74628c81a8bd12a4e5004a3ded0ec8c20381d62d0e", + "sha256:d81d159bd23d8ac53a6e819cccee991cb9350ab2541dfaa25aeb2f712d23b0a5", + "sha256:d842e11eb94f7074314ff1d97a05790539a1d74c3048ce50ea9f0da1f4f96b0a", + "sha256:d9a1d7d49d0d364520894116133d017b6e0e2d5131eb31c8553552fa77a65085", + "sha256:dc9e412911f210d9b0eca42d25c22d3725809dda03dedbaf6f9ffa192d461905", + "sha256:e4a7855933011026898ea0d4532fbd83cef63a76205c823a4ef5557d970df1f1", + "sha256:e7d349066f4c229d638a30f1f53ec3a4aaf4a4fc568491bdf77e7415a96003fb", + "sha256:ea47a64ed9918be0fa8a4a11146a80f546c09e0d65fd08e90a5c00366a59bdb0", + "sha256:f0b26cd4e090161927b7a81741a3627a41b74265dfb41c6957bfb474504b4b42", + "sha256:f39791a88cd5ec1760f65e878af419747c6f94ce74f9293735cbba6025ff4d0d", + "sha256:fb780d9d284ffdf7922edd4a6d7ba08e54a6680f85f64f91fa9cc2617dd488b7", + "sha256:fca24e4df05501420b2ce2207c03f21fcbdfac1e3f41e312e61b8f416c5b4963", + "sha256:fe497c885b08600a022646f00f4d3303697c5289990acec250e2be2e1699ca23" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==4.13.0" + }, + "pymysql": { + "hashes": [ + "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", + "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.1.1" + }, + "pytest": { + "hashes": [ + "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", + "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==8.4.0" + }, + "pytest-mock": { + "hashes": [ + "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", + "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.14.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "python-dotenv": { + "hashes": [ + "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", + "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d" + ], + "markers": "python_version >= '3.9'", + "version": "==1.1.0" + }, + "pytz": { + "hashes": [ + "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", + "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" + ], + "version": "==2025.2" + }, + "referencing": { + "hashes": [ + "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", + "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0" + ], + "markers": "python_version >= '3.9'", + "version": "==0.36.2" + }, + "rpds-py": { + "hashes": [ + "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d", + "sha256:036ded36bedb727beeabc16dc1dad7cb154b3fa444e936a03b67a86dc6a5066e", + "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f", + "sha256:0701942049095741a8aeb298a31b203e735d1c61f4423511d2b1a41dcd8a16da", + "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c", + "sha256:09eab132f41bf792c7a0ea1578e55df3f3e7f61888e340779b06050a9a3f16e9", + "sha256:0e6a327af8ebf6baba1c10fadd04964c1965d375d318f4435d5f3f9651550f4a", + "sha256:0eb90e94f43e5085623932b68840b6f379f26db7b5c2e6bcef3179bd83c9330f", + "sha256:114a07e85f32b125404f28f2ed0ba431685151c037a26032b213c882f26eb908", + "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b", + "sha256:140f61d9bed7839446bdd44852e30195c8e520f81329b4201ceead4d64eb3a9f", + "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd", + "sha256:1c0c434a53714358532d13539272db75a5ed9df75a4a090a753ac7173ec14e11", + "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf", + "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65", + "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0", + "sha256:245550f5a1ac98504147cba96ffec8fabc22b610742e9150138e5d60774686d7", + "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9", + "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea", + "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523", + "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692", + "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda", + "sha256:35a8d1a24b5936b35c5003313bc177403d8bdef0f8b24f28b1c4a255f94ea992", + "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b", + "sha256:3bcce0edc1488906c2d4c75c94c70a0417e83920dd4c88fec1078c94843a6ce9", + "sha256:3c5b317ecbd8226887994852e85de562f7177add602514d4ac40f87de3ae45a8", + "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40", + "sha256:3ebd879ab996537fc510a2be58c59915b5dd63bccb06d1ef514fee787e05984a", + "sha256:3f0b1798cae2bbbc9b9db44ee068c556d4737911ad53a4e5093d09d04b3bbc24", + "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763", + "sha256:454601988aab2c6e8fd49e7634c65476b2b919647626208e376afcd22019eeb8", + "sha256:4593c4eae9b27d22df41cde518b4b9e4464d139e4322e2127daa9b5b981b76be", + "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd", + "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65", + "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255", + "sha256:4fd52d3455a0aa997734f3835cbc4c9f32571345143960e7d7ebfe7b5fbfa3b2", + "sha256:50f2c501a89c9a5f4e454b126193c5495b9fb441a75b298c60591d8a2eb92e1b", + "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66", + "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4", + "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79", + "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31", + "sha256:5d9e40f32745db28c1ef7aad23f6fc458dc1e29945bd6781060f0d15628b8ddf", + "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d", + "sha256:605ffe7769e24b1800b4d024d24034405d9404f0bc2f55b6db3362cd34145a6f", + "sha256:6099263f526efff9cf3883dfef505518730f7a7a93049b1d90d42e50a22b4793", + "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559", + "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9", + "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1", + "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34", + "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728", + "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b", + "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038", + "sha256:796ad874c89127c91970652a4ee8b00d56368b7e00d3477f4415fe78164c8000", + "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98", + "sha256:7c9409b47ba0650544b0bb3c188243b83654dfe55dcc173a86832314e1a6a35d", + "sha256:7d779b325cc8238227c47fbc53964c8cc9a941d5dbae87aa007a1f08f2f77b23", + "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb", + "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e", + "sha256:85608eb70a659bf4c1142b2781083d4b7c0c4e2c90eff11856a9754e965b2540", + "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1", + "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd", + "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", + "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f", + "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba", + "sha256:8c742af695f7525e559c16f1562cf2323db0e3f0fbdcabdf6865b095256b2d40", + "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72", + "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78", + "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5", + "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe", + "sha256:9c006f3aadeda131b438c3092124bd196b66312f0caa5823ef09585a669cf449", + "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b", + "sha256:9e5fc7484fa7dce57e25063b0ec9638ff02a908304f861d81ea49273e43838c1", + "sha256:9f2f48ab00181600ee266a095fe815134eb456163f7d6699f525dee471f312cf", + "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c", + "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325", + "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129", + "sha256:a61d0b2c7c9a0ae45732a77844917b427ff16ad5464b4d4f5e4adb955f582890", + "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa", + "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500", + "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb", + "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762", + "sha256:b24bf3cd93d5b6ecfbedec73b15f143596c88ee249fa98cefa9a9dc9d92c6f28", + "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c", + "sha256:bc120d1132cff853ff617754196d0ac0ae63befe7c8498bd67731ba368abe451", + "sha256:bd035756830c712b64725a76327ce80e82ed12ebab361d3a1cdc0f51ea21acb0", + "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042", + "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7", + "sha256:c4feb9211d15d9160bc85fa72fed46432cdc143eb9cf6d5ca377335a921ac37b", + "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6", + "sha256:c98f126c4fc697b84c423e387337d5b07e4a61e9feac494362a59fd7a2d9ed80", + "sha256:ccc6f3ddef93243538be76f8e47045b4aad7a66a212cd3a0f23e34469473d36b", + "sha256:ccfa689b9246c48947d31dd9d8b16d89a0ecc8e0e26ea5253068efb6c542b76e", + "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc", + "sha256:ce4c8e485a3c59593f1a6f683cf0ea5ab1c1dc94d11eea5619e4fb5228b40fbd", + "sha256:d3c10228d6cf6fe2b63d2e7985e94f6916fa46940df46b70449e9ff9297bd3d1", + "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2", + "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309", + "sha256:d50e4864498a9ab639d6d8854b25e80642bd362ff104312d9770b05d66e5fb13", + "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295", + "sha256:d8222acdb51a22929c3b2ddb236b69c59c72af4019d2cba961e2f9add9b6e634", + "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192", + "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4", + "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5", + "sha256:dec21e02e6cc932538b5203d3a8bd6aa1480c98c4914cb88eea064ecdbc6396a", + "sha256:e1dafef8df605fdb46edcc0bf1573dea0d6d7b01ba87f85cd04dc855b2b4479e", + "sha256:e2f6a2347d3440ae789505693a02836383426249d5293541cd712e07e7aecf54", + "sha256:e37caa8cdb3b7cf24786451a0bdb853f6347b8b92005eeb64225ae1db54d1c2b", + "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72", + "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe", + "sha256:e87798852ae0b37c88babb7f7bbbb3e3fecc562a1c340195b44c7e24d403e380", + "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954", + "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d", + "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194", + "sha256:f4ad628b5174d5315761b67f212774a32f5bad5e61396d38108bd801c0a8f5d9", + "sha256:f70316f760174ca04492b5ab01be631a8ae30cadab1d1081035136ba12738cfa", + "sha256:f73ce1512e04fbe2bc97836e89830d6b4314c171587a99688082d090f934d20a", + "sha256:ff7c23ba0a88cb7b104281a99476cccadf29de2a0ef5ce864959a52675b1ca83" + ], + "markers": "python_version >= '3.9'", + "version": "==0.25.1" + }, + "six": { + "hashes": [ + "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", + "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.17.0" + }, + "sqlalchemy": { + "hashes": [ + "sha256:023b3ee6169969beea3bb72312e44d8b7c27c75b347942d943cf49397b7edeb5", + "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", + "sha256:05132c906066142103b83d9c250b60508af556982a385d96c4eaa9fb9720ac2b", + "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b", + "sha256:0b3dbf1e7e9bc95f4bac5e2fb6d3fb2f083254c3fdd20a1789af965caf2d2348", + "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", + "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", + "sha256:1e3f196a0c59b0cae9a0cd332eb1a4bda4696e863f4f1cf84ab0347992c548c2", + "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", + "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", + "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", + "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826", + "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", + "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", + "sha256:4d44522480e0bf34c3d63167b8cfa7289c1c54264c2950cc5fc26e7850967e45", + "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", + "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", + "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", + "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", + "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", + "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71", + "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", + "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", + "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", + "sha256:6854175807af57bdb6425e47adbce7d20a4d79bbfd6f6d6519cd10bb7109a7f8", + "sha256:6ab60a5089a8f02009f127806f777fca82581c49e127f08413a66056bd9166dd", + "sha256:725875a63abf7c399d4548e686debb65cdc2549e1825437096a0af1f7e374814", + "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", + "sha256:81965cc20848ab06583506ef54e37cf15c83c7e619df2ad16807c03100745dea", + "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", + "sha256:81eedafa609917040d39aa9332e25881a8e7a0862495fcdf2023a9667209deda", + "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", + "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923", + "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", + "sha256:8b4af17bda11e907c51d10686eda89049f9ce5669b08fbe71a29747f1e876036", + "sha256:90144d3b0c8b139408da50196c5cad2a6909b51b23df1f0538411cd23ffa45d3", + "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f", + "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", + "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", + "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2", + "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", + "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", + "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769", + "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", + "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", + "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", + "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", + "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", + "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440", + "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", + "sha256:c0b0e5e1b5d9f3586601048dd68f392dc0cc99a59bb5faf18aab057ce00d00b2", + "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", + "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", + "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", + "sha256:dd5ec3aa6ae6e4d5b5de9357d2133c07be1aff6405b136dad753a16afb6717dd", + "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", + "sha256:ff8e80c4c4932c10493ff97028decfdb622de69cae87e0f127a7ebe32b4069c6" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.41" + }, + "typing-extensions": { + "hashes": [ + "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", + "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af" + ], + "markers": "python_version >= '3.9'", + "version": "==4.14.0" + }, + "typing-inspection": { + "hashes": [ + "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", + "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28" + ], + "markers": "python_version >= '3.9'", + "version": "==0.4.1" + }, + "tzdata": { + "hashes": [ + "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", + "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9" + ], + "markers": "python_version >= '2'", + "version": "==2025.2" + }, + "werkzeug": { + "hashes": [ + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + ], + "markers": "python_version >= '3.9'", + "version": "==3.1.3" + } + }, + "develop": {} +} diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..161b3c0 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,41 @@ +from flask import Flask +from flask_restx import Api +from .config import Config +from .extensions import init_mongo, init_jwt +from .errors.handlers import register_error_handlers +from .routes.usage_routes import usage_ns +from .routes.auth_routes import auth_ns +from flask_cors import CORS + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + CORS(app, origins=app.config["FRONTEND_URL"], supports_credentials=True) + + init_mongo(app) + init_jwt(app) + + api = Api( + app, + title='Trascription usage API', + version='1.0', + description='API documentation', + doc='/docs', + prefix='/api/v1', + authorizations={ + 'Bearer Auth': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'Authorization', + 'description': "Enter your JWT token as: **Bearer <token>**" + } + } + ) + + api.add_namespace(usage_ns, path='/usage') + api.add_namespace(auth_ns, path='/auth') + + register_error_handlers(app) + + return app diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..9425d94 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,19 @@ +import os +from dotenv import load_dotenv +from datetime import timedelta + +load_dotenv() + +class Config: + PORT = os.getenv("PORT", 8001) + DEBUG = True + TESTING = False + SECRET_KEY = os.getenv("SECRET_KEY", "default-secret-key") + MONGO_URI = os.getenv("MONGO_URI") + JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "chave_secreta") + JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=int(os.getenv("JWT_ACCESS_TOKEN_EXPIRES_DAYS", 1))) + FRONTEND_URL = os.getenv("FRONTEND_URL", 3000) + + + + diff --git a/backend/app/db/models.py b/backend/app/db/models.py new file mode 100644 index 0000000..ffaac16 --- /dev/null +++ b/backend/app/db/models.py @@ -0,0 +1,19 @@ +from flask_bcrypt import generate_password_hash, check_password_hash +from flask import current_app +from pymongo import MongoClient + +class UserModel: + def __init__(self): + client = current_app.mongo_client + self.collection = client["billing-api"]["users"] + + def create_user(self, email, password): + hashed = generate_password_hash(password).decode('utf-8') + user = {"email": email, "password": hashed} + return self.collection.insert_one(user) + + def find_by_email(self, email): + return self.collection.find_one({"email": email}) + + def verify_password(self, hashed_password, plain_password): + return check_password_hash(hashed_password, plain_password) diff --git a/backend/app/db/mysql_router.py b/backend/app/db/mysql_router.py new file mode 100644 index 0000000..4b959c4 --- /dev/null +++ b/backend/app/db/mysql_router.py @@ -0,0 +1,19 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +from urllib.parse import quote_plus + +def get_engine_for_company(company_id: str): + schema = f"hitpbx_{company_id}" + user = "appuser" + password = quote_plus("nmvP$x23Vzb@T%Su") + + # Dev + # db_url = f"mysql+pymysql://root:mypass@127.0.0.1:3307/{schema}?charset=utf8mb4" + + db_url = f"mysql+pymysql://{user}:{password}@172.31.187.150:6033/{schema}?charset=utf8mb4" + return create_engine(db_url, pool_pre_ping=True) + +def get_session_for_company(company_id): + engine = get_engine_for_company(company_id) + Session = scoped_session(sessionmaker(bind=engine)) + return Session \ No newline at end of file diff --git a/backend/app/docs/auth_models.py b/backend/app/docs/auth_models.py new file mode 100644 index 0000000..547f888 --- /dev/null +++ b/backend/app/docs/auth_models.py @@ -0,0 +1,8 @@ +from flask_restx import fields, Namespace + +auth_ns = Namespace('auth', description='Authentication') + +signup_model = auth_ns.model('Signup', { + 'email': fields.String(required=True), + 'password': fields.String(required=True) +}) \ No newline at end of file diff --git a/backend/app/docs/usage_models.py b/backend/app/docs/usage_models.py new file mode 100644 index 0000000..82f6ffb --- /dev/null +++ b/backend/app/docs/usage_models.py @@ -0,0 +1,54 @@ +from flask_restx import fields, Namespace + +usage_ns = Namespace('usage', description='Usage transcription data, export, price and cost update operations') + +usage_cost_model = usage_ns.model('UpdateUsageCost', { + 'company_ids': fields.List(fields.String, required=False, description='Company id list'), + 'start_date': fields.String(required=True, description='Start date (YYYY-MM-DD)'), + 'end_date': fields.String(required=True, description='End date (YYYY-MM-DD)'), + 'product': fields.String(required=True, description='Product name'), + 'price': fields.String(required=True, description='Price'), + 'billing_unit': fields.Integer(required=True, description='Billing unit') +}) + +model_price_update = usage_ns.model('UpdateModelPrice', { + 'product': fields.String(required=False), + 'provider': fields.String(required=False), + 'type': fields.String(required=False), + 'billingBy': fields.String(required=False), + 'billingUnit': fields.Integer(required=False), + 'currency': fields.String(required=False), + 'price': fields.String(required=False), + 'clientPrice': fields.String(required=False), +}) + +model_prices_query_params = { + 'type': { + 'description': 'Type of the model. Ex: stt, tts (Optional)', + 'type': 'string' + }, + 'provider': { + 'description': 'The API provider. E.g., google, openai, meta (Optional)', + 'type': 'string' + }, +} + + +transcription_data_query_params = { + 'companyId': { + 'description': 'Company ID (required)', + 'type': 'string' + }, + 'startDate': { + 'description': 'Start date (YYYY-MM-DD) (required)', + 'type': 'string' + }, + 'endDate': { + 'description': 'End date (YYYY-MM-DD) (required)', + 'type': 'string' + }, + 'who': { + 'description': 'Who made: "hit" or "client" (required)', + 'type': 'string' + } +} diff --git a/backend/app/errors/handlers.py b/backend/app/errors/handlers.py new file mode 100644 index 0000000..3f4089f --- /dev/null +++ b/backend/app/errors/handlers.py @@ -0,0 +1,21 @@ +import traceback +from flask import jsonify +from werkzeug.exceptions import HTTPException +from pydantic import ValidationError +import traceback + +def register_error_handlers(app): + @app.errorhandler(ValidationError) + def handle_validation_error(e): + return jsonify({"error": e.errors()}), 400 + + @app.errorhandler(HTTPException) + def handle_http_exception(e): + return jsonify({"errror": e.description}), e.code + + @app.errorhandler(Exception) + def handle_unexpected_exception(e): + app.logger.error(traceback.format_exc()) + return jsonify({"error": str(e)}), 500 + + \ No newline at end of file diff --git a/backend/app/extensions.py b/backend/app/extensions.py new file mode 100644 index 0000000..3e92de6 --- /dev/null +++ b/backend/app/extensions.py @@ -0,0 +1,10 @@ +from pymongo import MongoClient +from flask_jwt_extended import JWTManager + +jwt = JWTManager() + +def init_mongo(app): + app.mongo_client = MongoClient(app.config["MONGO_URI"]) + +def init_jwt(app): + jwt.init_app(app) \ No newline at end of file diff --git a/backend/app/routes/__init__.py b/backend/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/routes/auth_routes.py b/backend/app/routes/auth_routes.py new file mode 100644 index 0000000..50f8ff5 --- /dev/null +++ b/backend/app/routes/auth_routes.py @@ -0,0 +1,31 @@ +from flask_restx import Namespace, Resource, fields +from flask import request +from flask_jwt_extended import create_access_token +from app.db.models import UserModel +from app.docs.auth_models import auth_ns, signup_model + +@auth_ns.route('/signup') +class SignUp(Resource): + @auth_ns.expect(signup_model) + def post(self): + data = request.get_json() + user_model = UserModel() + if user_model.find_by_email(data['email']): + return {'message': 'User already exists'}, 400 + user_model.create_user(data['email'], data['password']) + return {'message': 'success'}, 201 + + +@auth_ns.route('/login') +class Login(Resource): + @auth_ns.expect(signup_model) + def post(self): + data = request.get_json() + user_model = UserModel() + user = user_model.find_by_email(data['email']) + + if not user or not user_model.verify_password(user['password'], data['password']): + return {'message': 'Invalid credentials'}, 401 + + access_token = create_access_token(identity=user['email']) + return {'access_token': access_token}, 200 diff --git a/backend/app/routes/usage_routes.py b/backend/app/routes/usage_routes.py new file mode 100644 index 0000000..cbbf262 --- /dev/null +++ b/backend/app/routes/usage_routes.py @@ -0,0 +1,199 @@ +import os +from bson import json_util +from flask_restx import Resource +from flask_jwt_extended import jwt_required +from app.schemas.prices_schema import GetTranscriptionRequest +from app.schemas.transcription_schema import TranscriptionRequest +from app.schemas.model_price_update_schema import UpdateModelPriceRequest +from app.schemas.usage_cost_update_schema import UpdateUsageCostRequest +from flask import current_app, request, send_file, after_this_request +from app.services.report_service import TranscriptionReportService +from app.services.mongo_billing_service import MongoBillingService +from app.docs.usage_models import ( + usage_cost_model, + model_price_update, + model_prices_query_params, + transcription_data_query_params, usage_ns) + + +@usage_ns.route('/export/trascription') +class TranscriptionExport(Resource): + @usage_ns.doc(security='Bearer Auth') + @usage_ns.doc(params=transcription_data_query_params) + @usage_ns.response(200, 'success') + @usage_ns.response(400, 'Validation error') + @usage_ns.response(404, 'File not found') + @jwt_required() + def get(self): + """ + Export transcription report in XLSX. + """ + data = { + "company_id": request.args.get("companyId", type=str), + "start_date": request.args.get("startDate"), + "end_date": request.args.get("endDate"), + "who": request.args.get("who", type=str) + } + + validated = TranscriptionRequest(**data) + + service = TranscriptionReportService( + validated.company_id, + validated.start_date, + validated.end_date + ) + + if validated.who == "hit": + report_path = service.reportDataXLSX(hit_report=True) + else: + report_path = service.reportDataXLSX() + + if not os.path.exists(report_path): + return {"error": "File not found"}, 404 + + @after_this_request + def remove_file(response): + try: + os.remove(report_path) + except Exception as delete_error: + current_app.logger.warning(f"Error trying to delete file: {delete_error}") + return response + + return send_file( + path_or_file=report_path, + as_attachment=True, + download_name=os.path.basename(report_path), + mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + ) + + +@usage_ns.route('/data/trascription') +class TranscriptionUsageData(Resource): + @usage_ns.doc(security='Bearer Auth') + @usage_ns.doc(params=transcription_data_query_params) + @usage_ns.response(200, 'success') + @usage_ns.response(400, 'Validation error') + @jwt_required() + def get(self): + """ + Get transcription report data. + """ + data = { + "company_id": request.args.get("companyId", type=str), + "start_date": request.args.get("startDate"), + "end_date": request.args.get("endDate"), + "who": request.args.get("who", type=str) + } + + validated = TranscriptionRequest(**data) + + page = request.args.get("page", default=1, type=int) + page_size = request.args.get("page_size", default=20, type=int) + + service = TranscriptionReportService( + validated.company_id, + validated.start_date, + validated.end_date + ) + + if validated.who == "hit": + result = service.reportData(page=page, page_size=page_size,hit_report=True) + else: + result = service.reportData(page=page, page_size=page_size) + + return {"success": True, "data": {"data": result["data"], "pagination": result["pagination"]}}, 200 + + + +@usage_ns.route('/model/prices') +class TranscriptionModelPrices(Resource): + @usage_ns.doc(security='Bearer Auth') + @usage_ns.doc(params=model_prices_query_params) + @usage_ns.response(200, 'success') + @usage_ns.response(400, 'Validation error') + @jwt_required() + def get(self): + """ + Get model pricing and supplier information + """ + data = { + "type": request.args.get("type", "").split(",") if request.args.get("type") else [], + "provider": request.args.get("provider", "").split(",") if request.args.get("provider") else [] + } + + validated = GetTranscriptionRequest(**data) + + mongo = current_app.mongo_client + collection = mongo["billing-api"]["api_pricings"] + + query = {"product": {"$nin": ["whatsapp",]}} + + if validated.type: + query["type"] = {"$in": validated.type} + if validated.provider: + query["provider"] = {"$in": validated.provider} + + prices_cursor = collection.find(query) + + prices_list = list(prices_cursor) + + return current_app.response_class( + response=json_util.dumps({"success": True, "data": prices_list}), + mimetype='application/json' + ) + + +@usage_ns.route('/cost') +class UpdateUsageCost(Resource): + @usage_ns.doc(security='Bearer Auth') + @usage_ns.expect(usage_cost_model) + @usage_ns.response(200, 'success') + @usage_ns.response(400, 'Validation error') + @jwt_required() + def patch(self): + """ + Updates the total cost of using products by company + """ + data = request.get_json() + validated = UpdateUsageCostRequest(**data) + + service = MongoBillingService() + + count_udpated = service.update_usage_total_cost( + product=validated.product, + start_date=validated.start_date, + end_date=validated.end_date, + price=validated.price, + billing_unit=validated.billing_unit, + company_ids=validated.company_ids, + ) + + return {"success": True, "docs_updated": count_udpated}, 200 + + +@usage_ns.route('/model/prices/') +@usage_ns.param('id', 'ID of the model to be updated') +class UpdateModelPrice(Resource): + @usage_ns.doc(security='Bearer Auth') + @usage_ns.expect(model_price_update) + @usage_ns.response(200, 'Sucess') + @usage_ns.response(400, 'Validation error') + @jwt_required() + def patch(self, id): + """ + Updates the price of a specific model by ID. + Only the fields provided in the body will be changed. + """ + data = request.get_json() + + data["id"] = id + + validated = UpdateModelPriceRequest(**data) + + del data["id"] + + service = MongoBillingService() + service.update_model_policy_price(validated.id, data) + return {"success": True}, 200 + + \ No newline at end of file diff --git a/backend/app/schemas/model_price_update_schema.py b/backend/app/schemas/model_price_update_schema.py new file mode 100644 index 0000000..e9bd865 --- /dev/null +++ b/backend/app/schemas/model_price_update_schema.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Literal + +class UpdateModelPriceRequest(BaseModel): + id: str + product: Optional[str] = None + provider: Optional[str] = None + type: Optional[str] = None + billingBy: Optional[str] = None + billingUnit: Optional[int] = None + currency: Optional[str] = None + price: Optional[str] = None + clientPrice: Optional[str] = None + + class Config: + extra = "forbid" \ No newline at end of file diff --git a/backend/app/schemas/prices_schema.py b/backend/app/schemas/prices_schema.py new file mode 100644 index 0000000..d3a06cf --- /dev/null +++ b/backend/app/schemas/prices_schema.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Literal + +class GetTranscriptionRequest(BaseModel): + type: Optional[List[str]] = None + provider: Optional[List[str]] = None \ No newline at end of file diff --git a/backend/app/schemas/transcription_schema.py b/backend/app/schemas/transcription_schema.py new file mode 100644 index 0000000..bbbc1da --- /dev/null +++ b/backend/app/schemas/transcription_schema.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Literal + +class TranscriptionRequest(BaseModel): + company_id: str + start_date: str + end_date: str + who: Literal['hit', 'client'] diff --git a/backend/app/schemas/usage_cost_update_schema.py b/backend/app/schemas/usage_cost_update_schema.py new file mode 100644 index 0000000..e17423f --- /dev/null +++ b/backend/app/schemas/usage_cost_update_schema.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field +from typing import List, Optional, Literal + +class UpdateUsageCostRequest(BaseModel): + company_ids: Optional[List[str]] = None + start_date: str + end_date: str + product: str + price: float + billing_unit: int \ No newline at end of file diff --git a/backend/app/services/mongo_billing_service.py b/backend/app/services/mongo_billing_service.py new file mode 100644 index 0000000..78a22d7 --- /dev/null +++ b/backend/app/services/mongo_billing_service.py @@ -0,0 +1,66 @@ +from datetime import datetime +from app.utils.calc_api_usage import calculate_api_usage +from flask import current_app +from typing import List, Dict, Any, Optional +from bson import ObjectId + +class MongoBillingService: + def __init__(self): + self.mongo_client = current_app.mongo_client + + def update_usage_total_cost(self, + product: str, + start_date: str, + end_date: str, + price: float, + billing_unit: int, + company_ids: Optional[List[str]] = None, ) -> int: + query = { + "product": product, + "createdAt": { + "$gte": datetime.strptime(f"{start_date} 00:00:00", "%Y-%m-%d %H:%M:%S"), + "$lte": datetime.strptime(f"{end_date} 23:59:59", "%Y-%m-%d %H:%M:%S") + } + } + + if company_ids: + query["companyId"] = {"$in": company_ids} + + collection = self.mongo_client["billing-api"]["api_usages"] + + cursor = collection.find(query) + + count = 0 + + for doc in cursor: + usage = float(doc.get("usage", 0)) + new_total_cost = calculate_api_usage(float(price), int(billing_unit), float(usage)) + + collection.update_one( + {"_id": doc["_id"]}, + { + "$set": { + "total_cost": new_total_cost, + "price": f"{price}", + "billingUnit": billing_unit, + "updatedAt": datetime.utcnow() + } + } + ) + count+=1 + return count + + def update_model_policy_price(self, id:str, update_data: Dict[str, Any]) -> int: + + collection = self.mongo_client["billing-api"]["api_pricings"] + + update_data["updatedAt"] = datetime.utcnow() + + result = collection.update_one( + {"_id": ObjectId(id)}, + {"$set": update_data} + ) + + return result.modified_count + + \ No newline at end of file diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py new file mode 100644 index 0000000..3044749 --- /dev/null +++ b/backend/app/services/report_service.py @@ -0,0 +1,237 @@ + +from flask import current_app +from datetime import datetime +from decimal import Decimal +from openpyxl import Workbook +from openpyxl.utils.dataframe import dataframe_to_rows +from openpyxl.styles import Font, PatternFill +import pandas as pd +import os +from typing import List, Dict, Any, Optional +from app.utils.mysql_query import execute_query +from app.utils.calc_api_usage import calculate_api_usage + + +class TranscriptionReportService: + def __init__(self, company_id: str, start_date: str, end_date: str): + self.company_id = str(company_id) + self.start_date = start_date + self.end_date = end_date + self.end_date = end_date + self.mongo_client = current_app.mongo_client + self.mongo_results = [] + self.unique_ids= [] + + + def _fetch_mongo_data(self, page: int = 1, page_size: int = 20, all_data: Optional[bool] = False) -> Dict[str, int]: + collection = self.mongo_client["billing-api"]["api_pricings"] + result_stt = collection.find({"type": "stt"}) + products = [t["product"] for t in result_stt] + + match_stage = { + "$match": { + "companyId": self.company_id, + "product": {"$in": products}, + "createdAt": { + "$gte": datetime.strptime(f"{self.start_date} 00:00:00", "%Y-%m-%d %H:%M:%S"), + "$lte": datetime.strptime(f"{self.end_date} 23:59:59", "%Y-%m-%d %H:%M:%S") + } + } + } + + group_stage = { + "$group": { + "_id": "$sessionId", + "count": {"$sum": 1}, + "firstCreatedAt": {"$first": "$createdAt"}, + "callerIds": {"$addToSet": "$callerId"}, + "totalCost": {"$sum": {"$toDouble": "$total_cost"}}, + "totalUsage": {"$sum": {"$toDouble": "$usage"}}, + "price": {"$first": {"$toDouble": "$price"}}, + "product": {"$first": "$product"} + } + } + + sort_stage = {"$sort": {"firstCreatedAt": 1}} + + pipeline = [match_stage, group_stage, sort_stage] + + if not all_data: + # Aplica paginação se all_data for False + pipeline.extend([ + {"$skip": (page - 1) * page_size}, + {"$limit": page_size} + ]) + + # Executa pipeline com ou sem paginação + collection = self.mongo_client["billing-api"]["api_usages"] + self.mongo_results = list(collection.aggregate(pipeline)) + self.unique_ids = [doc["_id"] for doc in self.mongo_results] + + # print("=====> mongoResults: ", self.mongo_results) + + # Sempre calcula o total (para controle) + count_pipeline = [ + match_stage, + group_stage, + {"$count": "total"} + ] + count_result = list(collection.aggregate(count_pipeline)) + total = count_result[0]["total"] if count_result else 0 + + return { + "total": total, + "page": page, + "page_size": page_size, + "total_pages": (total + page_size - 1) // page_size + } + + + def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]: + sql = f"""SELECT + uniqueid, + src, + dst, + MIN(calldate) AS start_call, + MAX(calldate) AS end_call, + SUM(CASE + WHEN dstchannel LIKE 'PJSIP/%' AND lastapp = 'Queue' + THEN billsec + ELSE 0 + END) AS total_billsec + FROM + tab_cdr + WHERE + uniqueid IN {tuple(self.unique_ids)} + GROUP BY + uniqueid, src, dst;""" + rows = execute_query(self.company_id, sql) + + if hit_report: + collection = self.mongo_client["billing-api"]["api_pricings"] + result_stt = collection.find({"type": "stt"}) + + result_stt = [{"product": t["product"], "clientPrice": t["clientPrice"]} for t in result_stt if "clientPrice" in t] + + for row in rows: + row["companyId"] = self.company_id + + if rowMongo := next((m for m in self.mongo_results if m["_id"] == row["uniqueid"] ), None): + row["custo_HIT"] = rowMongo["totalCost"] + row["price"] = rowMongo["price"] + + p = [p for p in result_stt if p['product'] == rowMongo["product"]] + + if len(p) > 0: + row["client_price"] = p[0]["clientPrice"] + # row["client_total_cost"] = calculate_api_usage(float(p[0]["clientPrice"]), 60, float(rowMongo["totalUsage"])) + + + for key in row: + if isinstance(row[key], datetime): + row[key] = row[key].isoformat(sep=' ') + elif isinstance(row[key], Decimal): + row[key] = float(row[key]) + elif key == "uniqueid": + row[key] = str(row[key]) + else: + for row in rows: + row["total_min"] = f"{(int(row['total_billsec']) / 60):.2f}" + del row["end_call"] + for key in row: + if isinstance(row[key], datetime): + row[key] = row[key].isoformat(sep=' ') + elif isinstance(row[key], Decimal): + row[key] = float(row[key]) + elif key == "uniqueid": + row[key] = str(row[key]) + + return rows + + + def _create_excel(self, data: list, hit_report: Optional[bool] = False) -> str: + + if hit_report: + header_mapping = { + "companyId": "companyId", + "uniqueid": "sessionId", + "total_billsec": "tempo (billsec)", + "custo_HIT": "custo_HIT", + "price": "tarifa ($/min)", + "client_price": "valor_cobrado" + } + else: + header_mapping = { + "uniqueid": "Identificador da chamada", + "src": "Origem", + "dst": "Destino", + "start_call": "Inicio da Chamada", + "total_billsec": "Duração (Em segundos)", + "total_min": "Duração (Em minutos)" + } + + # Filtrar e ordenar os dados conforme header_mapping + selected_keys = list(header_mapping.keys()) + + filtered_data = [{k: row.get(k, "") for k in selected_keys} for row in data] + df = pd.DataFrame(filtered_data, columns=selected_keys) + + # Criação do Excel + wb = Workbook() + ws = wb.active + ws.title = "tab_cdr" + + header_font = Font(bold=True) + yellow_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") + + # Adiciona cabeçalhos personalizados + custom_headers = [header_mapping[col] for col in selected_keys] + ws.append(custom_headers) + for cell in ws[ws.max_row]: + cell.font = header_font + cell.fill = yellow_fill + + # Adiciona os dados + for row in df.itertuples(index=False, name=None): + ws.append(row) + + # Define caminho e salva o arquivo + filename = f"HISTORICO-CHAMADAS-GRAVADAS-{self.start_date}_{self.end_date}.xlsx" + BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + reports_dir = os.path.join(BASE_DIR, "reports") + os.makedirs(reports_dir, exist_ok=True) + path = os.path.join(reports_dir, filename) + wb.save(path) + return path + + + def reportDataXLSX(self, hit_report: Optional[bool] = False) -> str: + self._fetch_mongo_data(all_data=True) + + if hit_report: + mysql_data = self._fetch_mysql_data(hit_report=True) + return self._create_excel(mysql_data, hit_report=True) + + mysql_data = self._fetch_mysql_data() + return self._create_excel(mysql_data) + + + def reportData(self, page: int = 1, page_size: int = 20, hit_report: Optional[bool] = False) -> Dict[str, Any]: + mongo_data = self._fetch_mongo_data(page=page, page_size=page_size) + + if hit_report: + mysql_data = self._fetch_mysql_data(hit_report=True) + else: + mysql_data = self._fetch_mysql_data() + + return { + "pagination": mongo_data, + "data": mysql_data + } + + + + + + + diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/calc_api_usage.py b/backend/app/utils/calc_api_usage.py new file mode 100644 index 0000000..864816f --- /dev/null +++ b/backend/app/utils/calc_api_usage.py @@ -0,0 +1,5 @@ + +def calculate_api_usage(price: float, billing_unit: int, usage: float) -> str: + num_billing_units = (usage / billing_unit) + total_cost = num_billing_units * price + return f"{total_cost:.10f}" \ No newline at end of file diff --git a/backend/app/utils/mysql_query.py b/backend/app/utils/mysql_query.py new file mode 100644 index 0000000..2455f39 --- /dev/null +++ b/backend/app/utils/mysql_query.py @@ -0,0 +1,15 @@ +from typing import Any +from sqlalchemy import text +from app.db.mysql_router import get_engine_for_company + +def execute_query(company_id: str, sql_query: str) -> list[dict[Any, Any]]: + engine = get_engine_for_company(company_id) + + try: + with engine.connect() as connection: + result = connection.execute(text(sql_query)) + columns = result.keys() + rows = [dict(zip(columns, row)) for row in result] + return rows + except Exception as e: + raise e diff --git a/backend/mypy.ini b/backend/mypy.ini new file mode 100644 index 0000000..976ba02 --- /dev/null +++ b/backend/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/backend/reports/.gitkeep b/backend/reports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/run.py b/backend/run.py new file mode 100644 index 0000000..46e8aa6 --- /dev/null +++ b/backend/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port= app.config["PORT"]) \ No newline at end of file diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_usage_routes.py b/backend/tests/test_usage_routes.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/backend/tests/test_usage_routes.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..e006431 --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -0,0 +1,106 @@ +"use client" + +import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { LogOut } from "lucide-react" +import TranscriptionTable from "@/components/transcription-table" +import ModelPricesTable from "@/components/model-prices-table" +import CostUpdateForm from "@/components/cost-update-form" + +export default function Dashboard() { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const router = useRouter() + + useEffect(() => { + const token = localStorage.getItem("access_token") + if (!token) { + router.push("/") + } else { + setIsAuthenticated(true) + setIsLoading(false) + } + }, [router]) + + const handleLogout = () => { + localStorage.removeItem("access_token") + router.push("/") + } + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (!isAuthenticated) { + return null + } + + return ( +
+
+
+
+

Dashboard - Sistema de Transcrição

+ +
+
+
+ +
+ + + Dados de Transcrição + Preços dos Modelos + Atualizar Custos + + + + + + Dados de Transcrição + Visualize e exporte dados de transcrição por cliente ou empresa + + + + + + + + + + + Preços dos Modelos + Gerencie os preços dos modelos de IA disponíveis + + + + + + + + + + + Atualizar Custos Consumidos + Atualize os custos de uso dos produtos consumidos + + + + + + + +
+
+ ) +} diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/app/favicon.ico differ diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..dc98be7 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..87437cd --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Sistema de Trascrição", + description: "Trascriçã de audios de telefonia usando modelos de llm", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..a14856c --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,43 @@ +"use client" + +import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import LoginForm from "@/components/login-form" + +export default function HomePage() { + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const router = useRouter() + + useEffect(() => { + const token = localStorage.getItem("access_token") + if (token) { + setIsAuthenticated(true) + router.push("/dashboard") + } else { + setIsLoading(false) + } + }, [router]) + + if (isLoading) { + return ( +
+
+
+ ) + } + + return ( +
+
+

Sistema de Transcrição

+

Faça login para acessar o dashboard

+
+
+
+ +
+
+
+ ) +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..335484f --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/components/cost-update-form.tsx b/frontend/components/cost-update-form.tsx new file mode 100644 index 0000000..0216865 --- /dev/null +++ b/frontend/components/cost-update-form.tsx @@ -0,0 +1,332 @@ +"use client" + +import type React from "react" + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Textarea } from "@/components/ui/textarea" +import { Loader2, Save, RefreshCw } from "lucide-react" + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:5000/api/v1" + +interface ModelPrice { + _id: { $oid: string } + provider: string + product: string + currency: string + price: string + billingBy: string + billingUnit: number + type: string + createdAt: { $date: string } + updatedAt: { $date: string } + __v: number +} + +interface ModelPricesResponse { + success: boolean + data: ModelPrice[] +} + +interface CostUpdateResponse { + success: boolean + docs_updated: number +} + +export default function CostUpdateForm() { + const [isLoading, setIsLoading] = useState(false) + const [isLoadingData, setIsLoadingData] = useState(false) + const [error, setError] = useState("") + const [success, setSuccess] = useState("") + + // Estados para os dados dos combos + const [products, setProducts] = useState([]) + const [billingOptions, setBillingOptions] = useState<{ billingBy: string; billingUnit: number }[]>([]) + + const [formData, setFormData] = useState({ + product: "", + start_date: "", + end_date: "", + price: "", + billing_unit: "", + company_ids: "", + }) + + const getAuthHeaders = () => { + const token = localStorage.getItem("access_token") + return { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + } + } + + // Função para buscar dados dos modelos + const fetchModelPrices = async () => { + setIsLoadingData(true) + setError("") + + try { + const response = await fetch(`${API_BASE_URL}/usage/model/prices?type=stt`, { + headers: getAuthHeaders(), + }) + + if (response.ok) { + const result: ModelPricesResponse = await response.json() + if (result.success && Array.isArray(result.data)) { + // Extrair produtos únicos + const uniqueProducts = [...new Set(result.data.map((item) => item.product))] + setProducts(uniqueProducts) + + // Extrair opções de billing únicas + const uniqueBillingOptions = result.data.reduce( + (acc, item) => { + const existing = acc.find( + (option) => option.billingBy === item.billingBy && option.billingUnit === item.billingUnit, + ) + if (!existing) { + acc.push({ + billingBy: item.billingBy, + billingUnit: item.billingUnit, + }) + } + return acc + }, + [] as { billingBy: string; billingUnit: number }[], + ) + + setBillingOptions(uniqueBillingOptions) + } + } else { + const errorData = await response.json() + setError(errorData.message || "Erro ao buscar dados dos modelos") + } + } catch (err) { + setError("Erro de conexão com o servidor") + } finally { + setIsLoadingData(false) + } + } + + // Carregar dados ao montar o componente + useEffect(() => { + fetchModelPrices() + }, []) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError("") + setSuccess("") + + try { + // Converte company_ids de string para array (se fornecido) + let company_ids: string[] | undefined + if (formData.company_ids.trim()) { + company_ids = formData.company_ids + .split(",") + .map((id) => id.trim()) + .filter((id) => id.length > 0) + } + + const payload: any = { + product: formData.product, + start_date: formData.start_date, + end_date: formData.end_date, + price: formData.price, + billing_unit: Number(formData.billing_unit), + } + + // Só adiciona company_ids se foi fornecido + if (company_ids && company_ids.length > 0) { + payload.company_ids = company_ids + } + + const response = await fetch(`${API_BASE_URL}/usage/cost`, { + method: "PATCH", + headers: getAuthHeaders(), + body: JSON.stringify(payload), + }) + + if (response.ok) { + const result: CostUpdateResponse = await response.json() + if (result.success) { + setSuccess(`Custos atualizados com sucesso! ${result.docs_updated} registros foram atualizados.`) + setFormData({ + product: "", + start_date: "", + end_date: "", + price: "", + billing_unit: "", + company_ids: "", + }) + } else { + setError("Erro na resposta do servidor") + } + } else { + const errorData = await response.json() + setError(errorData.message || "Erro ao atualizar custos") + } + } catch (err) { + setError("Erro de conexão com o servidor") + } finally { + setIsLoading(false) + } + } + + const handleInputChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })) + } + + const handleBillingChange = (billingBy: string) => { + // Encontrar o billingUnit correspondente + const selectedOption = billingOptions.find((option) => option.billingBy === billingBy) + if (selectedOption) { + setFormData((prev) => ({ + ...prev, + billing_unit: selectedOption.billingUnit.toString(), + })) + } + } + + return ( + + + + Atualizar Custos de Uso + + + + +
+
+
+ + +
+ +
+ + +
+ +
+ + handleInputChange("start_date", e.target.value)} + required + /> +
+ +
+ + handleInputChange("end_date", e.target.value)} + required + /> +
+ +
+ + handleInputChange("price", e.target.value)} + placeholder="0.024" + required + /> +
+ +
+ +