From ea3f00a32a829947a71f8b80a8addaf6a0cc3e0e Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 4 Oct 2024 15:05:35 -0500 Subject: [PATCH 01/23] bypass consumption --- .flags | 1 + Pipfile.lock | 638 +++++++++--------- .../commands/tests_set_permissions.py | 4 +- .../media/tests/urls/v1/tests_upload.py | 6 +- .../mentorship/permissions/consumers.py | 25 +- .../tests_meet_slug_service_slug.py | 223 +++++- breathecode/payments/apps.py | 1 + breathecode/payments/flags.py | 29 + .../tests/urls/v1/tests_academy_asset.py | 3 + .../registry/tests/urls/v1/tests_asset.py | 2 +- .../activecampaign/actions/deal_update.py | 4 +- breathecode/utils/decorators/consume.py | 39 +- .../utils/tests/decorators/tests_consume.py | 92 ++- 13 files changed, 732 insertions(+), 335 deletions(-) create mode 100644 .flags create mode 100644 breathecode/payments/flags.py diff --git a/.flags b/.flags new file mode 100644 index 000000000..ecfccc94a --- /dev/null +++ b/.flags @@ -0,0 +1 @@ +BYPASS_CONSUMPTION=0 diff --git a/Pipfile.lock b/Pipfile.lock index f8340d777..ff40050cc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -42,111 +42,111 @@ }, "aiohappyeyeballs": { "hashes": [ - "sha256:4ca893e6c5c1f5bf3888b04cb5a3bee24995398efef6e0b9f747b5e89d84fd74", - "sha256:8522691d9a154ba1145b157d6d5c15e5c692527ce6a53c5e5f9876977f6dab2f" + "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", + "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572" ], "markers": "python_version >= '3.8'", - "version": "==2.4.2" + "version": "==2.4.3" }, "aiohttp": { "extras": [ "speedups" ], "hashes": [ - "sha256:0245e1a71f3503b01d2c304529779a70277ccc0fe9847b48d437363de6e4336e", - "sha256:0246659d9a54a23a83f11842bdd58f335a1370aa66b376eeae16b7cf29009dde", - "sha256:02b4aa816cd3ab876f96ce8c6986648392137cbd6feddbf4189322515f34e1f6", - "sha256:0b210484fccff00cafa9bd8abedea8749b6d975df8c8e21c82d92bb25403db85", - "sha256:0d09e40e2ae6723af487ffde019055d0b6ce4eae0749fcfe9de624b61f1af6ec", - "sha256:0f25a79ac4ac0bd94cf283d3e86e6f3ec78fc39e2de6949b902c342148b7b5f6", - "sha256:10d19997f2f8d49d53b76163b71e263bb7b23f48041d0d4050a43445a0052c35", - "sha256:13085c0129a906b001d87dd43e247155f6c76820d98147c079b746e8a0665b17", - "sha256:1378164474a3866f7684a95efede1bee4016cd104bc10bf885e492c4459b715a", - "sha256:14dbfb208ffe3388e0770fd23bf9114cc933c10bb1dba35b538f3c9d685334d8", - "sha256:150deb28d5302cfec89fc31ea4bce774df06f5c03d95519f7588ca6517a472d7", - "sha256:164068b338c52dfe44f3490c70ef7b33c0e73d441c89f599ae2d93f7dcf3e395", - "sha256:171f1f5364a0ef5873480e6fddc3870ee37f1dfe216fa67507bbd4c91306f110", - "sha256:189979c7e9d8f40236534760daf5b41d2026d5ebabdf913e771d9b6bfbc992af", - "sha256:18c72a69ba20713f26fa40932cac17437b0c1d25edff2e27437a204c12275bd9", - "sha256:1d26881d98274ef0dbd4f069f383e5e90eb6e42e957289db14c47186386832ce", - "sha256:278cd430ba93a157ad1faf490fdd6051801085ffa31a27762133472555e56888", - "sha256:2b7794b3d23451e355b4a87959943125afff8dd31d8059651c2734de12f9e7f2", - "sha256:2c6140d6cbf8eebbcf1528364ce0b26d0a95788111659cfc008fba3a12fc874f", - "sha256:2d8d12d6a192f7b9f8a335cad8634a4f081d8319b75dd42257a1a3e557848d00", - "sha256:318824b98a2bdf84e9a21d413737a3c4f27bbad0a9ce16141488f631dbffb9b2", - "sha256:342600665e74eea20b3286045ebeb0aa2f9cececf2eb0acc6f6817205b112b29", - "sha256:365eff442a47b13e0e12c37240a6f75940ebee0b7943af43c84d5b43643fc80c", - "sha256:3915944c87c9bf488db4ca1ae6edca40b5bc77c4c2cf2f49b69886bc47b97db1", - "sha256:4296dd120e7e9728625eef1091039aff1a454c7147913d47839876c94b202226", - "sha256:431852e77cd72f60a0278f8cf557c8e568cd856f755a4b6c5232c7d8c6343d2e", - "sha256:4d23df9f01c8945d03cffcdd9ba9bfd88aa21ac567a39d0ac4d0c80499ed0d23", - "sha256:4f6b014f2176d2774b759b8e2951af4a613385ebcc08841cb5c0ca6d5dee74ee", - "sha256:582536d3d7f95a6d4d072d2326dd03eeb1549c1cc86d02c9bcec71899f4c66f2", - "sha256:5fc3538efae4e4df222a563559f8766234f49e845e8dbb2dd477eb8f3fd97242", - "sha256:636e3efb0bb024817cefa1ef86d678d1a73eb210ae162aff4234214060011ff5", - "sha256:63c9de949e05a5f729aecba6bf4b3d5403846caf546ea5020f8b9bf315bd8f12", - "sha256:68120c12c98bfc0e024ef1279be5f41327a54a5094710adc970ecc9724b91871", - "sha256:6bae913cbb183cd34863905088ef26a17c75332bd6bdd451ee8bf158c987cf19", - "sha256:73f151a1e21369a84d56b91a209590c23270c847463029fdcbda710516217644", - "sha256:77bc82d7b10f377957ba8e99bb1b13d946e9e9038fe89ba0888ad0b12e60c9c0", - "sha256:7a372f9ea521741667cec2ef4a64419448030411af2e844dfa8dbbb8074baea6", - "sha256:7b75cfa1e5fc7c87fc5f9de7124bb039b898791bb87207d2107bed5e3509670f", - "sha256:7ce1b54feaaf264e28a4474e13635d302a59aafb720b18c3c2885b8f35ce5040", - "sha256:7ed4435dcf507ef2de5b4be64276933eb19c78e5c7d00ca376fcd9a67d0139a0", - "sha256:80531f6f4fff5a1f7e495afbc4aff5c4230b605f26d56c40ecad27a269665608", - "sha256:81d3fc1b187656b6b465ed4ed4c9858f16ff2d9864da6225d80b8018abd7739b", - "sha256:82fa5fb983922b03f2b08d1140550c68b50313305115639e19b13489c284c30c", - "sha256:85d8a1d716516ef92c769eadb020600d27223899018ef8d07c09c117001cc7d5", - "sha256:871c2bf68ecc55056e5e3b0ae5929a1149f41c4255bbf99b1f858005f63360d1", - "sha256:87652147515031dafc1b37c9c3c42fbe9e2697af6264ec26080a6fe603cc5196", - "sha256:87d0e52b2905dbc1aeffcbf0611fa82e27874764332c11b984293a4b91cc8e9f", - "sha256:87e243b1df27ff685ab08228b7a938c0530beb60ad3dea7554da1554d46c9ad4", - "sha256:8ddf2c8c9ec6bb3f5c057e5c95605adb8e3f1e2d999e8801736f448aff29280e", - "sha256:8fbf91559400fe1a98d84af36f5a66aa59c359ac3cb113b17d304ced6a4601b4", - "sha256:99c11c5d632fa2222cc5805105841f6f3c40df116368fde40fbd71f8b14ea692", - "sha256:9cd67e5c84cb75a471b2e35f3fb0da52e6d359d1794d3465a87052fb240e64b5", - "sha256:a1fe407bec2f14a3d79ec92aa767b930857a6782589ea87ac76fd8081dea3dab", - "sha256:a84fe27904dbb43a236532d6d841d6132200b7bb53ba73d0300b0b586ceab6cc", - "sha256:aa42c4e78925a438a6f7df0d9b165d29cdc0a44fc5ce838d6c293a0161a2bd9a", - "sha256:aeea07c89a5a53463c70957feb85d4b846982c0f054b521fc44f52862e7871cf", - "sha256:af10344fb1ee195b2cd5840b61d8c8121b16d3b3baa2da5a86cf4001a7e5bd98", - "sha256:b5d8c94fd23f41007799ec657e18661f9f8c5b566a1e4fe944e3514e505a6b49", - "sha256:b5f8270946777d6971c27479cb6e7f54578be960928a8922cb59130e856d8484", - "sha256:b6fb89edeadfd69df75f8cea97c3533805a9960cc56034ad296abe9b18771842", - "sha256:b92100555f86b314ed840ed61d937fc30ca39ad453c9aa9020414a3cce955d9b", - "sha256:bab2544f09cd1db154c105e03b1c941032fd7237da5da184595771999ca90daa", - "sha256:bc1f4e0f4b1ae9289b4d0cc3bf5d6d55176c38ef1d41484550f3f9a0a78bedae", - "sha256:beda1abd7b23d489a5b66a46eba5a9e0db58e4ad91d68697409eeabda343fb9d", - "sha256:befc2f0794bc4bbbb1f8d0e245d32ee13331205b58f54910789e9e78d2a6fbf5", - "sha256:bf1cd9bfd598899396bdb8a4dc5234144a77e482e7489972b7956cf66e272872", - "sha256:bfa8c8af8c92e3d6c1eff02cf5127f62c1e7564e7b0f1a9767035f81a2e6bb20", - "sha256:bff7ef30cb6fc186ea6dda9e19d6105b1c213e3a3f759b5a23c271c778027260", - "sha256:c161f9e353f291d23069a8f67180fd52c76d72d4671f4f53602ea9ac29f47d50", - "sha256:c6052d92b47b8cf3736b1f01ac8f83cf02f188ef7542848055a5e227db0e16cb", - "sha256:caf083bf26b1e286ab1929dbd8d8cab6230160576a0ed5e3bfb3487bb19474c2", - "sha256:cd658aeaa65fb99fcc3b93882bb33cbd600501d40473488aec163a981d7b05ee", - "sha256:ce7c12bfbb1579e81cdf2e7db4338f8c768da2493aa0db60a858a542d551563c", - "sha256:ced77f4dd0c4f0107ee96f8df162b984470ac9f94ef93dd44dba62838fd85cde", - "sha256:da5a03cbe746f182f7b61e119dde24d388cf77965fea320bc8aba61b75039d06", - "sha256:dd8a0a0ef895e4c3f1afd31c2a6f89d68a94baacdbe2eb9bf90ac54b997cf99b", - "sha256:df23cb35bec54b73fba371c7c904994433651458acf8bfb7c84464fef5834c0a", - "sha256:e083e29b6db8e34a507cd678f89eab3ae5f307486ea6010c6473436d3769628d", - "sha256:e152296b2c50417445eacdb2353d3c10e702f6593aa774277510fb7761304302", - "sha256:e19337d6552af197ebb8c886daea0b938ae34eff776c1fa914ad433f6db3970f", - "sha256:e1a9b4026b6fe41adde784e308b0ad0d6a8b5cc9062f9c349125fd57149bc8a9", - "sha256:e2783754bfcee0b13b8e55932b418cf8984c17099fd1b37341d4696447d0c328", - "sha256:e2e0083e6f9f9cb0a0bedd694782e7fb8a54eb4de40e1743d9bb526f1c1eea88", - "sha256:e8ccaa99871303323bd2cda120043039729497642da5c6f53e066b19f73d9df8", - "sha256:eea89c47ae8d592f7563f4355132fe844b5e2f8660292deacc292253bef291cd", - "sha256:f33a6d023b207ad8227e607814c0020b42c53e01a66004fc0f2555e1a4941282", - "sha256:f44f09b67a458400215d9efedb9cfb5e3256dbeb2cc2da68e4592b7b36bac0c9", - "sha256:f8aaa0bc8e39352684982b378ba3f7e32e78a746da433aaeceb7e93d7fdf9ce3", - "sha256:fcfabf9338fed009fd9e11bf496a927ea67b1ce15d34847cb0a98aa6f042b989", - "sha256:fee4d2246b091b7e252cd5bcdbd4362fa21c3cc6a445fef54de793731546ab24", - "sha256:feff2170b23921a526f31d78c8f76bbb9cde825e78035286d8571ce0c81901ab" - ], - "markers": "python_version >= '3.8'", - "version": "==3.10.7" + "sha256:10c7932337285a6bfa3a5fe1fd4da90b66ebfd9d0cbd1544402e1202eb9a8c3e", + "sha256:177126e971782769b34933e94fddd1089cef0fe6b82fee8a885e539f5b0f0c6a", + "sha256:1ce46dfb49cfbf9e92818be4b761d4042230b1f0e05ffec0aad15b3eb162b905", + "sha256:1e7a6af57091056a79a35104d6ec29d98ec7f1fb7270ad9c6fff871b678d1ff8", + "sha256:21a72f4a9c69a8567a0aca12042f12bba25d3139fd5dd8eeb9931f4d9e8599cd", + "sha256:21c1925541ca84f7b5e0df361c0a813a7d6a56d3b0030ebd4b220b8d232015f9", + "sha256:21f8225f7dc187018e8433c9326be01477fb2810721e048b33ac49091b19fb4a", + "sha256:22cdeb684d8552490dd2697a5138c4ecb46f844892df437aaf94f7eea99af879", + "sha256:270e653b5a4b557476a1ed40e6b6ce82f331aab669620d7c95c658ef976c9c5e", + "sha256:2df786c96c57cd6b87156ba4c5f166af7b88f3fc05f9d592252fdc83d8615a3c", + "sha256:32710d6b3b6c09c60c794d84ca887a3a2890131c0b02b3cefdcc6709a2260a7c", + "sha256:33a68011a38020ed4ff41ae0dbf4a96a202562ecf2024bdd8f65385f1d07f6ef", + "sha256:365783e1b7c40b59ed4ce2b5a7491bae48f41cd2c30d52647a5b1ee8604c68ad", + "sha256:3a95d2686bc4794d66bd8de654e41b5339fab542b2bca9238aa63ed5f4f2ce82", + "sha256:3b2036479b6b94afaaca7d07b8a68dc0e67b0caf5f6293bb6a5a1825f5923000", + "sha256:3c7f270f4ca92760f98a42c45a58674fff488e23b144ec80b1cc6fa2effed377", + "sha256:3f6d47e392c27206701565c8df4cac6ebed28fdf6dcaea5b1eea7a4631d8e6db", + "sha256:40d2d719c3c36a7a65ed26400e2b45b2d9ed7edf498f4df38b2ae130f25a0d01", + "sha256:4618f0d2bf523043866a9ff8458900d8eb0a6d4018f251dae98e5f1fb699f3a8", + "sha256:471a8c47344b9cc309558b3fcc469bd2c12b49322b4b31eb386c4a2b2d44e44a", + "sha256:4954e6b06dd0be97e1a5751fc606be1f9edbdc553c5d9b57d72406a8fbd17f9d", + "sha256:497a7d20caea8855c5429db3cdb829385467217d7feb86952a6107e033e031b9", + "sha256:4b91f4f62ad39a8a42d511d66269b46cb2fb7dea9564c21ab6c56a642d28bff5", + "sha256:4dbf252ac19860e0ab56cd480d2805498f47c5a2d04f5995d8d8a6effd04b48c", + "sha256:4e10b04542d27e21538e670156e88766543692a0a883f243ba8fad9ddea82e53", + "sha256:5284997e3d88d0dfb874c43e51ae8f4a6f4ca5b90dcf22995035187253d430db", + "sha256:57359785f27394a8bcab0da6dcd46706d087dfebf59a8d0ad2e64a4bc2f6f94f", + "sha256:597128cb7bc5f068181b49a732961f46cb89f85686206289d6ccb5e27cb5fbe2", + "sha256:5aa1a073514cf59c81ad49a4ed9b5d72b2433638cd53160fd2f3a9cfa94718db", + "sha256:680dbcff5adc7f696ccf8bf671d38366a1f620b5616a1d333d0cb33956065395", + "sha256:6984dda9d79064361ab58d03f6c1e793ea845c6cfa89ffe1a7b9bb400dfd56bd", + "sha256:69de056022e7abf69cb9fec795515973cc3eeaff51e3ea8d72a77aa933a91c52", + "sha256:6c7efa6616a95e3bd73b8a69691012d2ef1f95f9ea0189e42f338fae080c2fc6", + "sha256:6d1ad868624f6cea77341ef2877ad4e71f7116834a6cd7ec36ec5c32f94ee6ae", + "sha256:713dff3f87ceec3bde4f3f484861464e722cf7533f9fa6b824ec82bb5a9010a7", + "sha256:71462f8eeca477cbc0c9700a9464e3f75f59068aed5e9d4a521a103692da72dc", + "sha256:7c38cfd355fd86c39b2d54651bd6ed7d63d4fe3b5553f364bae3306e2445f847", + "sha256:8296edd99d0dd9d0eb8b9e25b3b3506eef55c1854e9cc230f0b3f885f680410b", + "sha256:85431c9131a9a0f65260dc7a65c800ca5eae78c4c9931618f18c8e0933a0e0c1", + "sha256:85e4d7bd05d18e4b348441e7584c681eff646e3bf38f68b2626807f3add21aa2", + "sha256:8885ca09d3a9317219c0831276bfe26984b17b2c37b7bf70dd478d17092a4772", + "sha256:8960fabc20bfe4fafb941067cda8e23c8c17c98c121aa31c7bf0cdab11b07842", + "sha256:9443d9ebc5167ce1fbb552faf2d666fb22ef5716a8750be67efd140a7733738c", + "sha256:9721554bfa9e15f6e462da304374c2f1baede3cb06008c36c47fa37ea32f1dc4", + "sha256:98a4eb60e27033dee9593814ca320ee8c199489fbc6b2699d0f710584db7feb7", + "sha256:98fae99d5c2146f254b7806001498e6f9ffb0e330de55a35e72feb7cb2fa399b", + "sha256:9a281cba03bdaa341c70b7551b2256a88d45eead149f48b75a96d41128c240b3", + "sha256:a087c84b4992160ffef7afd98ef24177c8bd4ad61c53607145a8377457385100", + "sha256:a1ba7bc139592339ddeb62c06486d0fa0f4ca61216e14137a40d626c81faf10c", + "sha256:a3081246bab4d419697ee45e555cef5cd1def7ac193dff6f50be761d2e44f194", + "sha256:a72f89aea712c619b2ca32c6f4335c77125ede27530ad9705f4f349357833695", + "sha256:a78ba86d5a08207d1d1ad10b97aed6ea48b374b3f6831d02d0b06545ac0f181e", + "sha256:a961ee6f2cdd1a2be4735333ab284691180d40bad48f97bb598841bfcbfb94ec", + "sha256:ab1546fc8e00676febc81c548a876c7bde32f881b8334b77f84719ab2c7d28dc", + "sha256:ab2d6523575fc98896c80f49ac99e849c0b0e69cc80bf864eed6af2ae728a52b", + "sha256:aff048793d05e1ce05b62e49dccf81fe52719a13f4861530706619506224992b", + "sha256:b1a012677b8e0a39e181e218de47d6741c5922202e3b0b65e412e2ce47c39337", + "sha256:b667e2a03407d79a76c618dc30cedebd48f082d85880d0c9c4ec2faa3e10f43e", + "sha256:b91557ee0893da52794b25660d4f57bb519bcad8b7df301acd3898f7197c5d81", + "sha256:badb51d851358cd7535b647bb67af4854b64f3c85f0d089c737f75504d5910ec", + "sha256:c36074b26f3263879ba8e4dbd33db2b79874a3392f403a70b772701363148b9f", + "sha256:c4916070e12ae140110aa598031876c1bf8676a36a750716ea0aa5bd694aa2e7", + "sha256:c6769d71bfb1ed60321363a9bc05e94dcf05e38295ef41d46ac08919e5b00d19", + "sha256:c887019dbcb4af58a091a45ccf376fffe800b5531b45c1efccda4bedf87747ea", + "sha256:cd9716ef0224fe0d0336997eb242f40619f9f8c5c57e66b525a1ebf9f1d8cebe", + "sha256:ceacea31f8a55cdba02bc72c93eb2e1b77160e91f8abd605969c168502fd71eb", + "sha256:d088ca05381fd409793571d8e34eca06daf41c8c50a05aeed358d2d340c7af81", + "sha256:d3a79200a9d5e621c4623081ddb25380b713c8cf5233cd11c1aabad990bb9381", + "sha256:d82404a0e7b10e0d7f022cf44031b78af8a4f99bd01561ac68f7c24772fed021", + "sha256:d95ae4420669c871667aad92ba8cce6251d61d79c1a38504621094143f94a8b4", + "sha256:da57af0c54a302b7c655fa1ccd5b1817a53739afa39924ef1816e7b7c8a07ccb", + "sha256:ddb9b9764cfb4459acf01c02d2a59d3e5066b06a846a364fd1749aa168efa2be", + "sha256:de23085cf90911600ace512e909114385026b16324fa203cc74c81f21fd3276a", + "sha256:e1f0f7b27171b2956a27bd8f899751d0866ddabdd05cbddf3520f945130a908c", + "sha256:e32148b4a745e70a255a1d44b5664de1f2e24fcefb98a75b60c83b9e260ddb5b", + "sha256:e45fdfcb2d5bcad83373e4808825b7512953146d147488114575780640665027", + "sha256:e56bb7e31c4bc79956b866163170bc89fd619e0581ce813330d4ea46921a4881", + "sha256:e860985f30f3a015979e63e7ba1a391526cdac1b22b7b332579df7867848e255", + "sha256:ee3587506898d4a404b33bd19689286ccf226c3d44d7a73670c8498cd688e42c", + "sha256:ee97c4e54f457c366e1f76fbbf3e8effee9de57dae671084a161c00f481106ce", + "sha256:ef9b484604af05ca745b6108ca1aaa22ae1919037ae4f93aaf9a37ba42e0b835", + "sha256:f21e8f2abed9a44afc3d15bba22e0dfc71e5fa859bea916e42354c16102b036f", + "sha256:f23a6c1d09de5de89a33c9e9b229106cb70dcfdd55e81a3a3580eaadaa32bc92", + "sha256:f5d5d5401744dda50b943d8764508d0e60cc2d3305ac1e6420935861a9d544bc", + "sha256:f78e2a78432c537ae876a93013b7bc0027ba5b93ad7b3463624c4b6906489332", + "sha256:f8179855a4e4f3b931cb1764ec87673d3fbdcca2af496c8d30567d7b034a13db", + "sha256:fc0e7f91705445d79beafba9bb3057dd50830e40fe5417017a76a214af54e122", + "sha256:fe285a697c851734285369614443451462ce78aac2b77db23567507484b1dc6f", + "sha256:fe3d79d6af839ffa46fdc5d2cf34295390894471e9875050eafa584cb781508d", + "sha256:fecd55e7418fabd297fd836e65cbd6371aa4035a264998a091bbf13f94d9c44d", + "sha256:ffef3d763e4c8fc97e740da5b4d0f080b78630a3914f4e772a122bbfa608c1db" + ], + "markers": "python_version >= '3.8'", + "version": "==3.10.8" }, "aiohttp-retry": { "hashes": [ @@ -379,11 +379,11 @@ "django" ], "hashes": [ - "sha256:6fd5b17cda53adf9ec3829ada084e91091ea293cad93d2295c2a6ddde8bacaa1", - "sha256:ea70dd183a3a999f329ab7ff52e3b65f442f3a418b8acec81367f57f36a58012" + "sha256:702794d0db45746118ec63b02e81f8d8edd1b87b69337a9c3898eb6ea2830f0e", + "sha256:cefa43f74ba301d4f60d5ed6bfdc5b50152228922958415d2fd9a99b3eca103f" ], "markers": "python_version >= '3.11'", - "version": "==1.0.2" + "version": "==1.0.3" }, "celery": { "hashes": [ @@ -1358,66 +1358,75 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "python_version >= '3' 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')))))", + "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", "version": "==3.1.1" }, "grpcio": { "hashes": [ - "sha256:0e6c9b42ded5d02b6b1fea3a25f036a2236eeb75d0579bfd43c0018c88bf0a3e", - "sha256:161d5c535c2bdf61b95080e7f0f017a1dfcb812bf54093e71e5562b16225b4ce", - "sha256:17663598aadbedc3cacd7bbde432f541c8e07d2496564e22b214b22c7523dac8", - "sha256:1c17ebcec157cfb8dd445890a03e20caf6209a5bd4ac5b040ae9dbc59eef091d", - "sha256:292a846b92cdcd40ecca46e694997dd6b9be6c4c01a94a0dfb3fcb75d20da858", - "sha256:2ca2559692d8e7e245d456877a85ee41525f3ed425aa97eb7a70fc9a79df91a0", - "sha256:307b1d538140f19ccbd3aed7a93d8f71103c5d525f3c96f8616111614b14bf2a", - "sha256:30a1c2cf9390c894c90bbc70147f2372130ad189cffef161f0432d0157973f45", - "sha256:31a049daa428f928f21090403e5d18ea02670e3d5d172581670be006100db9ef", - "sha256:35334f9c9745add3e357e3372756fd32d925bd52c41da97f4dfdafbde0bf0ee2", - "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac", - "sha256:3885f037eb11f1cacc41f207b705f38a44b69478086f40608959bf5ad85826dd", - "sha256:4573608e23f7e091acfbe3e84ac2045680b69751d8d67685ffa193a4429fedb1", - "sha256:4825a3aa5648010842e1c9d35a082187746aa0cdbf1b7a2a930595a94fb10fce", - "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492", - "sha256:48b0d92d45ce3be2084b92fb5bae2f64c208fea8ceed7fccf6a7b524d3c4942e", - "sha256:4d813316d1a752be6f5c4360c49f55b06d4fe212d7df03253dfdae90c8a402bb", - "sha256:5dd67ed9da78e5121efc5c510f0122a972216808d6de70953a740560c572eb44", - "sha256:6f914386e52cbdeb5d2a7ce3bf1fdfacbe9d818dd81b6099a05b741aaf3848bb", - "sha256:7101db1bd4cd9b880294dec41a93fcdce465bdbb602cd8dc5bd2d6362b618759", - "sha256:7e06aa1f764ec8265b19d8f00140b8c4b6ca179a6dc67aa9413867c47e1fb04e", - "sha256:84ca1be089fb4446490dd1135828bd42a7c7f8421e74fa581611f7afdf7ab761", - "sha256:8a1e224ce6f740dbb6b24c58f885422deebd7eb724aff0671a847f8951857c26", - "sha256:97ae7edd3f3f91480e48ede5d3e7d431ad6005bfdbd65c1b56913799ec79e791", - "sha256:9c9bebc6627873ec27a70fc800f6083a13c70b23a5564788754b9ee52c5aef6c", - "sha256:a013c5fbb12bfb5f927444b477a26f1080755a931d5d362e6a9a720ca7dbae60", - "sha256:a66fe4dc35d2330c185cfbb42959f57ad36f257e0cc4557d11d9f0a3f14311df", - "sha256:a92c4f58c01c77205df6ff999faa008540475c39b835277fb8883b11cada127a", - "sha256:aa8ba945c96e73de29d25331b26f3e416e0c0f621e984a3ebdb2d0d0b596a3b3", - "sha256:b0aa03d240b5539648d996cc60438f128c7f46050989e35b25f5c18286c86734", - "sha256:b1b24c23d51a1e8790b25514157d43f0a4dce1ac12b3f0b8e9f66a5e2c4c132f", - "sha256:b7ffb8ea674d68de4cac6f57d2498fef477cef582f1fa849e9f844863af50083", - "sha256:b9feb4e5ec8dc2d15709f4d5fc367794d69277f5d680baf1910fc9915c633524", - "sha256:bff2096bdba686019fb32d2dde45b95981f0d1490e054400f70fc9a8af34b49d", - "sha256:c30aeceeaff11cd5ddbc348f37c58bcb96da8d5aa93fed78ab329de5f37a0d7a", - "sha256:c9f80f9fad93a8cf71c7f161778ba47fd730d13a343a46258065c4deb4b550c0", - "sha256:cfd349de4158d797db2bd82d2020554a121674e98fbe6b15328456b3bf2495bb", - "sha256:d0cd7050397b3609ea51727b1811e663ffda8bda39c6a5bb69525ef12414b503", - "sha256:d639c939ad7c440c7b2819a28d559179a4508783f7e5b991166f8d7a34b52815", - "sha256:e3ba04659e4fce609de2658fe4dbf7d6ed21987a94460f5f92df7579fd5d0e22", - "sha256:ecfe735e7a59e5a98208447293ff8580e9db1e890e232b8b292dc8bd15afc0d2", - "sha256:ef82d361ed5849d34cf09105d00b94b6728d289d6b9235513cb2fcc79f7c432c", - "sha256:f03a5884c56256e08fd9e262e11b5cfacf1af96e2ce78dc095d2c41ccae2c80d", - "sha256:f1fe60d0772831d96d263b53d83fb9a3d050a94b0e94b6d004a5ad111faa5b5b", - "sha256:f517fd7259fe823ef3bd21e508b653d5492e706e9f0ef82c16ce3347a8a5620c", - "sha256:fdb14bad0835914f325349ed34a51940bc2ad965142eb3090081593c6e347be9" - ], - "version": "==1.66.1" + "sha256:02697eb4a5cbe5a9639f57323b4c37bcb3ab2d48cec5da3dc2f13334d72790dd", + "sha256:03b0b307ba26fae695e067b94cbb014e27390f8bc5ac7a3a39b7723fed085604", + "sha256:05bc2ceadc2529ab0b227b1310d249d95d9001cd106aa4d31e8871ad3c428d73", + "sha256:06de8ec0bd71be123eec15b0e0d457474931c2c407869b6c349bd9bed4adbac3", + "sha256:0be4e0490c28da5377283861bed2941d1d20ec017ca397a5df4394d1c31a9b50", + "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", + "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", + "sha256:2018b053aa15782db2541ca01a7edb56a0bf18c77efed975392583725974b249", + "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75", + "sha256:2335c58560a9e92ac58ff2bc5649952f9b37d0735608242973c7a8b94a6437d8", + "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", + "sha256:38b68498ff579a3b1ee8f93a05eb48dc2595795f2f62716e797dc24774c1aaa8", + "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", + "sha256:3ed71e81782966ffead60268bbda31ea3f725ebf8aa73634d5dda44f2cf3fb9c", + "sha256:45a3d462826f4868b442a6b8fdbe8b87b45eb4f5b5308168c156b21eca43f61c", + "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", + "sha256:4e504572433f4e72b12394977679161d495c4c9581ba34a88d843eaf0f2fbd39", + "sha256:4ea1d062c9230278793820146c95d038dc0f468cbdd172eec3363e42ff1c7d01", + "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", + "sha256:6001e575b8bbd89eee11960bb640b6da6ae110cf08113a075f1e2051cc596cae", + "sha256:66a0cd8ba6512b401d7ed46bb03f4ee455839957f28b8d61e7708056a806ba6a", + "sha256:6851de821249340bdb100df5eacfecfc4e6075fa85c6df7ee0eb213170ec8e5d", + "sha256:728bdf36a186e7f51da73be7f8d09457a03061be848718d0edf000e709418987", + "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a", + "sha256:73fc8f8b9b5c4a03e802b3cd0c18b2b06b410d3c1dcbef989fdeb943bd44aff7", + "sha256:78fa51ebc2d9242c0fc5db0feecc57a9943303b46664ad89921f5079e2e4ada7", + "sha256:7b2c86457145ce14c38e5bf6bdc19ef88e66c5fee2c3d83285c5aef026ba93b3", + "sha256:7d69ce1f324dc2d71e40c9261d3fdbe7d4c9d60f332069ff9b2a4d8a257c7b2b", + "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", + "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", + "sha256:8ac475e8da31484efa25abb774674d837b343afb78bb3bcdef10f81a93e3d6bf", + "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", + "sha256:99a641995a6bc4287a6315989ee591ff58507aa1cbe4c2e70d88411c4dcc0839", + "sha256:9c3a99c519f4638e700e9e3f83952e27e2ea10873eecd7935823dab0c1c9250e", + "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b", + "sha256:a18e20d8321c6400185b4263e27982488cb5cdd62da69147087a76a24ef4e7e3", + "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", + "sha256:a9539f01cb04950fd4b5ab458e64a15f84c2acc273670072abe49a3f29bbad54", + "sha256:ad2efdbe90c73b0434cbe64ed372e12414ad03c06262279b104a029d1889d13e", + "sha256:b672abf90a964bfde2d0ecbce30f2329a47498ba75ce6f4da35a2f4532b7acbc", + "sha256:bbd27c24a4cc5e195a7f56cfd9312e366d5d61b86e36d46bbe538457ea6eb8dd", + "sha256:c400ba5675b67025c8a9f48aa846f12a39cf0c44df5cd060e23fda5b30e9359d", + "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", + "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7", + "sha256:ce89f5876662f146d4c1f695dda29d4433a5d01c8681fbd2539afff535da14d4", + "sha256:d25a14af966438cddf498b2e338f88d1c9706f3493b1d73b93f695c99c5f0e2a", + "sha256:d8d4732cc5052e92cea2f78b233c2e2a52998ac40cd651f40e398893ad0d06ec", + "sha256:d9a9724a156c8ec6a379869b23ba3323b7ea3600851c91489b871e375f710bc8", + "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", + "sha256:e88264caad6d8d00e7913996030bac8ad5f26b7411495848cc218bd3a9040b6c", + "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46", + "sha256:fb57870449dfcfac428afbb5a877829fcb0d6db9d9baa1148705739e9083880e", + "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf", + "sha256:fe96281713168a3270878255983d2cb1a97e034325c8c2c25169a69289d3ecfa", + "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679" + ], + "version": "==1.66.2" }, "grpcio-status": { "hashes": [ - "sha256:b3f7d34ccc46d83fea5261eea3786174459f763c31f6e34f1d24eba6d515d024", - "sha256:cf9ed0b4a83adbe9297211c95cb5488b0cd065707e812145b842c85c4782ff02" + "sha256:e5fe189f6897d12aa9cd74408a17ca41e44fad30871cf84f5cbd17bd713d2455", + "sha256:fb55cbb5c2e67062f7a4d5c99e489d074fb57e98678d5c3c6692a2d74d89e9ae" ], - "version": "==1.66.1" + "version": "==1.66.2" }, "gunicorn": { "hashes": [ @@ -1554,11 +1563,11 @@ }, "httpcore": { "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" ], "markers": "python_version >= '3.8'", - "version": "==1.0.5" + "version": "==1.0.6" }, "httplib2": { "hashes": [ @@ -1634,12 +1643,12 @@ }, "icalendar": { "hashes": [ - "sha256:5ded5415e2e1edef5ab230024a75878a7a81d518a3b1ae4f34bf20b173c84dc2", - "sha256:92799fde8cce0b61daa8383593836d1e19136e504fa1671f471f98be9b029706" + "sha256:567e718551d800362db04ca09777295336e1803f6fc6bc0a7a5e258917fa8ed0", + "sha256:7ddf60d343f3c1f716de9b62f6e80ffd95d03cab62464894a0539feab7b5c76e" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==5.0.13" + "markers": "python_version >= '3.8'", + "version": "==6.0.0" }, "idna": { "hashes": [ @@ -1807,12 +1816,12 @@ }, "launchdarkly-server-sdk": { "hashes": [ - "sha256:42f4cb25ebf547d5ebf228f0852bc3e435f395a6eb437de68c47e393729502b2", - "sha256:e547fe5d49aaf5c9537718467545dc1e38f837bb396259465035e30c1a54798a" + "sha256:192396e1f2b02416722d4ce279abeea38978b606d8553307e4b7bd0f756fc096", + "sha256:249e9cf4095ac4f2fa3bf9b1df151e1d96421d68f1580b3ca2a51c47a8bd2940" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.7.1" + "version": "==9.7.2" }, "linked-services": { "extras": [ @@ -2448,12 +2457,12 @@ }, "openai": { "hashes": [ - "sha256:7967fc8372d5e005ad61514586fb286d593facafccedbee00416bc38ee07c2e6", - "sha256:80cbdf275488894c70bfbad711dbba6f31ea71d579b97e364bfd99cdf030158e" + "sha256:8dc4f9d75ccdd5466fc8c99a952186eddceb9fd6ba694044773f3736a847149d", + "sha256:d9affafb7e51e5a27dce78589d4964ce4d6f6d560307265933a94b2e3f3c5d2c" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.50.1" + "version": "==1.51.0" }, "packaging": { "hashes": [ @@ -2699,80 +2708,80 @@ "pool" ], "hashes": [ - "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0", - "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2" + "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", + "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2" ], "markers": "python_version >= '3.8'", - "version": "==3.2.2" + "version": "==3.2.3" }, "psycopg-binary": { "hashes": [ - "sha256:00273dd011892e8216fcef76b42f775ddaa6348664a7fffae2a27c9557f45bfa", - "sha256:020c5154be144a1440cf87eae012b9004fb414ae4b9e7b1b9fb808fe39e96e83", - "sha256:035753f80cbbf6aceca6386f53e139df70c7aca057b0592711047b5a8cfef8bb", - "sha256:05406b96139912574571b1c56bb023839a9146cf4b57c4548f36251dd5909fa1", - "sha256:059aa5e8fa119de328b4cb02ee80775443763b25682a02dd7d026b8d4f565834", - "sha256:05a50f94e1e4fa37a0074b09263b83b0aa038c3c72068a61f1ad61ea449ef9d5", - "sha256:06963f88916a177df95aaed27101af0989ba206654743b1a0e050b9d8e734686", - "sha256:0718be095cefdad712542169d16fa58b3bd9200a3de1b0217ae761cdec1cf569", - "sha256:0ad9c09de4c262f516ae6891d042a4325649b18efa39dd82bbe0f7bc95c37bfb", - "sha256:0b32b0e838841d5b109d32fc706b8bc64e50c161fee3f1371ccf696e5598bc49", - "sha256:0dd314229885a81f9497875295d8788e651b78945627540f1e78ed71595e614a", - "sha256:1a4eb737682c02a602a12aa85a492608066f77793dab681b1c4e885fedc160b1", - "sha256:1b3c5a04eaf8866e399315cff2e810260cce10b797437a9f49fd71b5f4b94d0a", - "sha256:1e1f013bfb744023df23750fde51edcb606def8328473361db3c192c392c6060", - "sha256:1ee891287c2da57e7fee31fbe2fbcdf57125768133d811b02e9523d5a052eb28", - "sha256:2eb6f8f410dbbb71b8c633f283b8588b63bee0a7321f00ab76e9c800c593f732", - "sha256:2ec4986c4ac2503e865acd3943d179531c3bbfa5a1c8ee81fcfccb551dad645f", - "sha256:366cc4e194f7feb4e3038d6775fd4b69835e7d923972aee5baec986de972abd6", - "sha256:3c482c3236ded54add31136a91d5223b233ec301f297fa2db79747404222dca6", - "sha256:3c701507a49340de422d77a6ce95918a0019990bbf27daec35aa40050c6eadb6", - "sha256:43b209be0424e8abece428a884cb711f504e3526dfbcb0bf51529907a55eda15", - "sha256:4afbb97d64cd8078edec859b07859a18ef3de7261a3a873ba52f32548373ae92", - "sha256:4bcb489615d7e56d1de42937e6a0fc13f766505729afdb54c2947a52db295220", - "sha256:4cf64e41e238620f05aad862f06bc8424f8f320d8075f1499bd85a225d18bd57", - "sha256:4f12640ba92c538b3b64a199a918d3bb0cc0d7f7123c6ba93cb065e1a2d049f0", - "sha256:51f56ae2898acaa33623adad96ddc5acbb5e2f72f2fc020065c8be05c0e01dce", - "sha256:554d208757129d34fa47b7c890f9ef922f754e99c6b089cb3a209aa0fe282682", - "sha256:566b1c530898590f0ac9d949cf94351c08d73c89f8800c74c0a63ffd89a383c8", - "sha256:5e95e4a8076ac7611e571623e1113fa84fd48c0459601969ffbf534d7aa236e7", - "sha256:6269d79a3d7d76b6fcf0fafae8444da00e83777a6c68c43851351a571ad37155", - "sha256:66de2dd7d37bf66eb234ca9d907f5cd8caca43ff8d8a50dd5c15844d1cf0390c", - "sha256:68d03efab7e2830a0df3aa4c29a708930e3f6b9fd98774ff9c4fd1f33deafecc", - "sha256:6c7b6a8d4e1b77cdb50192b61235b33fc2f1d28c67627fc93a1d43e9130dd479", - "sha256:705da5bc4364bd7529473225fca02b795653bc5bd824dbe43e1df0b1a40fe691", - "sha256:71dc3cc10d1fd7d26a3079d0a5b4a8e8ad0d7b89a702ceb7605a52e4395be122", - "sha256:7c357cf87e8d7612cfe781225be7669f35038a765d1b53ec9605f6c5aef9ee85", - "sha256:849d518e7d4c6186e1e48ea2ac2671912edf7e732fffe6f01dfed61cf0245de4", - "sha256:87cceaf07760a04023596f9ca1d4e929d38ae8d778161cb3e8d27a0f990dd264", - "sha256:8937dc548621b336b0d8383a3470fb7192b42a108c760a152282909867bf5b26", - "sha256:8eacbf58d4f8d7bc82e0a60476afa2622b5a58f639a3cc2710e3e37b72aff3cb", - "sha256:8ee2b19152bcec8f356f989c31768702be5f139b4d51094273c4a9ddc8c55380", - "sha256:951507b3d77a64c907afe893e01e09b41051fd7e27e9462f450fb8bb64bc22b0", - "sha256:989acbe2f552769cdb780346cea32d86e7c117044238d5172ac10b025fe47194", - "sha256:9e120a576e74e4e612c48f4b021e322e320ca102534d78a0ca4db2ffd058ae8d", - "sha256:9efe0ca78be4a573b4b81226904c711cfadc4783d64bfdf58a3394da7c1a1354", - "sha256:9fee41c99312002e5d1f7462b1954aefed44c6efe5f021c3eac311640c16f6b7", - "sha256:a06136aab55a2de7dd4e2555badae276846827cfb023e6ba1b22f7a7b88e3f1b", - "sha256:a60674dff4a4194e88312b463fb84ac80924c2b9e25d0e0460f3176bf1af4a6b", - "sha256:a86f578d63f2e1fdf87c9adaed4ff23d7919bda8791cf1380fa4cf3a857ccb8b", - "sha256:b286ed65a891928bd457ffa0cd5fec09b9b5208bfd096d087e45369f07c5cb85", - "sha256:b45553c6b614d02e1486585980afdfd18f0000aac668e2e87c6e32da1adb051a", - "sha256:bf1d3582185cb43ecc27403bee2f5405b7a45ccaab46c8508d9a9327341574fc", - "sha256:c22e615ee0ecfc6687bb8a39a4ed9d6bac030b5e72ac15e7324fd6e48979af71", - "sha256:c432710bdf8ccfdd75b0bc9cdf1fd21ff394363e4daec099c667f3c5f1721e2b", - "sha256:c9ee99336151ff7c30682f2ef9cb1174d235bc1471322faabba97f9db1398167", - "sha256:d07e62476ee8c54853b2b8cfdf3858a574218103b4cd213211f64326c7812437", - "sha256:d3c147eea9f3950a34133dc187e8d3534e54ff4a178a4ebd8993b2c97e123200", - "sha256:d6dd5d21a298c3c53af20ced8da4ae4cd038c6fe88c80842a8888fa3660b2094", - "sha256:e234edc4bb746d8ac3daae8753ee38eaa7af2ee333a1d35ce6b02a02874aed18", - "sha256:ec29c7ec136263628e3f09a53e51d0a4b1ad765a6e45135707bfa848b39113f9", - "sha256:ed1ad836a0c21890c7f84e73c7ef1ed0950e0e4b0d8e49b609b6fd9c13f2ca21", - "sha256:ef341c556aeaa43a2729b07b04e20bfffdcf3d96c4a96e728ca94fe4ce632d8c", - "sha256:fb303b03c243a9041e1873b596e246f7caaf01710b312fafa65b1db5cd77dd6f", - "sha256:fdc74a83348477b28bea9e7b391c9fc189b480fe3cd0e46bb989514410b64d60" + "sha256:0463a11b1cace5a6aeffaf167920707b912b8986a9c7920341c75e3686277920", + "sha256:05a1bdce30356e70a05428928717765f4a9229999421013f41338d9680d03a63", + "sha256:06b5cc915e57621eebf2393f4173793ed7e3387295f07fed93ed3fb6a6ccf585", + "sha256:07d019a786eb020c0f984691aa1b994cb79430061065a694cf6f94056c603d26", + "sha256:09baa041856b35598d335b1a74e19a49da8500acedf78164600694c0ba8ce21b", + "sha256:1303bf8347d6be7ad26d1362af2c38b3a90b8293e8d56244296488ee8591058e", + "sha256:192a5f8496e6e1243fdd9ac20e117e667c0712f148c5f9343483b84435854c78", + "sha256:1985ab05e9abebfbdf3163a16ebb37fbc5d49aff2bf5b3d7375ff0920bbb54cd", + "sha256:1f8b0d0e99d8e19923e6e07379fa00570be5182c201a8c0b5aaa9a4d4a4ea20b", + "sha256:257c4aea6f70a9aef39b2a77d0658a41bf05c243e2bf41895eb02220ac6306f3", + "sha256:261f0031ee6074765096a19b27ed0f75498a8338c3dcd7f4f0d831e38adf12d1", + "sha256:2773f850a778575dd7158a6dd072f7925b67f3ba305e2003538e8831fec77a1d", + "sha256:2a29f5294b0b6360bfda69653697eff70aaf2908f58d1073b0acd6f6ab5b5a4f", + "sha256:2bb342a01c76f38a12432848e6013c57eb630103e7556cf79b705b53814c3949", + "sha256:2c0419cdad8c70eaeb3116bb28e7b42d546f91baf5179d7556f230d40942dc78", + "sha256:3bffb61e198a91f712cc3d7f2d176a697cb05b284b2ad150fb8edb308eba9002", + "sha256:41fdec0182efac66b27478ac15ef54c9ebcecf0e26ed467eb7d6f262a913318b", + "sha256:48f8ca6ee8939bab760225b2ab82934d54330eec10afe4394a92d3f2a0c37dd6", + "sha256:4926ea5c46da30bec4a85907aa3f7e4ea6313145b2aa9469fdb861798daf1502", + "sha256:4c57615791a337378fe5381143259a6c432cdcbb1d3e6428bfb7ce59fff3fb5c", + "sha256:4e76ce2475ed4885fe13b8254058be710ec0de74ebd8ef8224cf44a9a3358e5f", + "sha256:5361ea13c241d4f0ec3f95e0bf976c15e2e451e9cc7ef2e5ccfc9d170b197a40", + "sha256:5905729668ef1418bd36fbe876322dcb0f90b46811bba96d505af89e6fbdce2f", + "sha256:5938b257b04c851c2d1e6cb2f8c18318f06017f35be9a5fe761ee1e2e344dfb7", + "sha256:5e37d5027e297a627da3551a1e962316d0f88ee4ada74c768f6c9234e26346d9", + "sha256:64a607e630d9f4b2797f641884e52b9f8e239d35943f51bef817a384ec1678fe", + "sha256:64dc6e9ec64f592f19dc01a784e87267a64a743d34f68488924251253da3c818", + "sha256:69320f05de8cdf4077ecd7fefdec223890eea232af0d58f2530cbda2871244a0", + "sha256:6d8f2144e0d5808c2e2aed40fbebe13869cd00c2ae745aca4b3b16a435edb056", + "sha256:700679c02f9348a0d0a2adcd33a0275717cd0d0aee9d4482b47d935023629505", + "sha256:709447bd7203b0b2debab1acec23123eb80b386f6c29e7604a5d4326a11e5bd6", + "sha256:71adcc8bc80a65b776510bc39992edf942ace35b153ed7a9c6c573a6849ce308", + "sha256:71db8896b942770ed7ab4efa59b22eee5203be2dfdee3c5258d60e57605d688c", + "sha256:74fbf5dd3ef09beafd3557631e282f00f8af4e7a78fbfce8ab06d9cd5a789aae", + "sha256:79498df398970abcee3d326edd1d4655de7d77aa9aecd578154f8af35ce7bbd2", + "sha256:7ad357e426b0ea5c3043b8ec905546fa44b734bf11d33b3da3959f6e4447d350", + "sha256:7d784f614e4d53050cbe8abf2ae9d1aaacf8ed31ce57b42ce3bf2a48a66c3a5c", + "sha256:80a2337e2dfb26950894c8301358961430a0304f7bfe729d34cc036474e9c9b1", + "sha256:824c867a38521d61d62b60aca7db7ca013a2b479e428a0db47d25d8ca5067410", + "sha256:842da42a63ecb32612bb7f5b9e9f8617eab9bc23bd58679a441f4150fcc51c96", + "sha256:8b7be9a6c06518967b641fb15032b1ed682fd3b0443f64078899c61034a0bca6", + "sha256:9099e443d4cc24ac6872e6a05f93205ba1a231b1a8917317b07c9ef2b955f1f4", + "sha256:94253be2b57ef2fea7ffe08996067aabf56a1eb9648342c9e3bad9e10c46e045", + "sha256:949551752930d5e478817e0b49956350d866b26578ced0042a61967e3fcccdea", + "sha256:96334bb64d054e36fed346c50c4190bad9d7c586376204f50bede21a913bf942", + "sha256:965455eac8547f32b3181d5ec9ad8b9be500c10fe06193543efaaebe3e4ce70c", + "sha256:967b47a0fd237aa17c2748fdb7425015c394a6fb57cdad1562e46a6eb070f96d", + "sha256:9994f7db390c17fc2bd4c09dca722fd792ff8a49bb3bdace0c50a83f22f1767d", + "sha256:9b60b465773a52c7d4705b0a751f7f1cdccf81dd12aee3b921b31a6e76b07b0e", + "sha256:aeddf7b3b3f6e24ccf7d0edfe2d94094ea76b40e831c16eff5230e040ce3b76b", + "sha256:c64c4cd0d50d5b2288ab1bcb26c7126c772bbdebdfadcd77225a77df01c4a57e", + "sha256:cb987f14af7da7c24f803111dbc7392f5070fd350146af3345103f76ea82e339", + "sha256:dc4fa2240c9fceddaa815a58f29212826fafe43ce80ff666d38c4a03fb036955", + "sha256:e56b1fd529e5dde2d1452a7d72907b37ed1b4f07fdced5d8fb1e963acfff6749", + "sha256:e8630943143c6d6ca9aefc88bbe5e76c90553f4e1a3b2dc339e67dc34aa86f7e", + "sha256:e8eb9a4e394926b93ad919cad1b0a918e9b4c846609e8c1cfb6b743683f64da0", + "sha256:e90352d7b610b4693fad0feea48549d4315d10f1eba5605421c92bb834e90170", + "sha256:f0b018e37608c3bfc6039a1dc4eb461e89334465a19916be0153c757a78ea426", + "sha256:f73adc05452fb85e7a12ed3f69c81540a8875960739082e6ea5e28c373a30774", + "sha256:fa33ead69ed133210d96af0c63448b1385df48b9c0247eda735c5896b9e6dbbf", + "sha256:fc6d87a1c44df8d493ef44988a3ded751e284e02cdf785f746c2d357e99782a6", + "sha256:fd40af959173ea0d087b6b232b855cfeaa6738f47cb2a0fd10a7f4fa8b74293f", + "sha256:fd65774ed7d65101b314808b6893e1a75b7664f680c3ef18d2e5c84d570fa393", + "sha256:fda0162b0dbfa5eaed6cdc708179fa27e148cb8490c7d62e5cf30713909658ea" ], - "version": "==3.2.2" + "version": "==3.2.3" }, "psycopg-pool": { "hashes": [ @@ -3109,12 +3118,12 @@ }, "pyright": { "hashes": [ - "sha256:21a4749dd1740e209f88d3a601e9f40748670d39481ea32b9d77edf7f3f1fb2e", - "sha256:66a5d4e83be9452853d73e9dd9e95ba0ac3061845270e4e331d0070a597d3445" + "sha256:1df7f12407f3710c9c6df938d98ec53f70053e6c6bbf71ce7bcb038d42f10070", + "sha256:d864d1182a313f45aaf99e9bfc7d2668eeabc99b29a556b5344894fd73cb1959" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.1.382.post1" + "version": "==1.1.383" }, "pytest": { "hashes": [ @@ -3656,12 +3665,12 @@ }, "stripe": { "hashes": [ - "sha256:0c79c1f3a844533c8d30cc283b43afb622aaa402539fca19167a9004fea3471c", - "sha256:5abec44548d3814bc1e070aa1852bcb3fc5cc029e947c0f733156eb1f8c87030" + "sha256:37a1f6ab0cee4ad0fa7d01e6f1e5ed9be33ee4aeae85e1a0b4ed81d42b9721bd", + "sha256:d144b49fc51a50be67d5025ab59fc0884e120929798a699e775369b0c214b9ae" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==10.12.0" + "version": "==11.0.0" }, "text-unidecode": { "hashes": [ @@ -4450,11 +4459,11 @@ "django" ], "hashes": [ - "sha256:6fd5b17cda53adf9ec3829ada084e91091ea293cad93d2295c2a6ddde8bacaa1", - "sha256:ea70dd183a3a999f329ab7ff52e3b65f442f3a418b8acec81367f57f36a58012" + "sha256:702794d0db45746118ec63b02e81f8d8edd1b87b69337a9c3898eb6ea2830f0e", + "sha256:cefa43f74ba301d4f60d5ed6bfdc5b50152228922958415d2fd9a99b3eca103f" ], "markers": "python_version >= '3.11'", - "version": "==1.0.2" + "version": "==1.0.3" }, "certifi": { "hashes": [ @@ -4844,12 +4853,12 @@ }, "google-api-python-client-stubs": { "hashes": [ - "sha256:148e16613e070969727f39691e23a73cdb87c65a4fc8133abd4c41d17b80b313", - "sha256:3c1f9f2a7cac8d1e9a7e84ed24e6c29cf4c643b0f94e39ed09ac1b7e91ab239a" + "sha256:7327c058fb5ba975309922f962f17931b9c82af51d95a5dc04061ed0c20b9f06", + "sha256:75b3dfe67b9d74ac3b58d78725326836769d0b2df1cbef354a5455a5cc57d68d" ], "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==1.27.0" + "markers": "python_version >= '3.7'", + "version": "==1.28.0" }, "google-apps-meet": { "hashes": [ @@ -4969,74 +4978,83 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "python_version >= '3' 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')))))", + "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", "version": "==3.1.1" }, "griffe": { "hashes": [ - "sha256:3f86a716b631a4c0f96a43cb75d05d3c85975003c20540426c0eba3b0581c56a", - "sha256:940aeb630bc3054b4369567f150b6365be6f11eef46b0ed8623aea96e6d17b19" + "sha256:1ec50335aa507ed2445f2dd45a15c9fa3a45f52c9527e880571dfc61912fd60c", + "sha256:2e34b5e46507d615915c8e6288bb1a2234bd35dee44d01e40a2bc2f25bd4d10c" ], "markers": "python_version >= '3.8'", - "version": "==1.3.1" + "version": "==1.3.2" }, "grpcio": { "hashes": [ - "sha256:0e6c9b42ded5d02b6b1fea3a25f036a2236eeb75d0579bfd43c0018c88bf0a3e", - "sha256:161d5c535c2bdf61b95080e7f0f017a1dfcb812bf54093e71e5562b16225b4ce", - "sha256:17663598aadbedc3cacd7bbde432f541c8e07d2496564e22b214b22c7523dac8", - "sha256:1c17ebcec157cfb8dd445890a03e20caf6209a5bd4ac5b040ae9dbc59eef091d", - "sha256:292a846b92cdcd40ecca46e694997dd6b9be6c4c01a94a0dfb3fcb75d20da858", - "sha256:2ca2559692d8e7e245d456877a85ee41525f3ed425aa97eb7a70fc9a79df91a0", - "sha256:307b1d538140f19ccbd3aed7a93d8f71103c5d525f3c96f8616111614b14bf2a", - "sha256:30a1c2cf9390c894c90bbc70147f2372130ad189cffef161f0432d0157973f45", - "sha256:31a049daa428f928f21090403e5d18ea02670e3d5d172581670be006100db9ef", - "sha256:35334f9c9745add3e357e3372756fd32d925bd52c41da97f4dfdafbde0bf0ee2", - "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac", - "sha256:3885f037eb11f1cacc41f207b705f38a44b69478086f40608959bf5ad85826dd", - "sha256:4573608e23f7e091acfbe3e84ac2045680b69751d8d67685ffa193a4429fedb1", - "sha256:4825a3aa5648010842e1c9d35a082187746aa0cdbf1b7a2a930595a94fb10fce", - "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492", - "sha256:48b0d92d45ce3be2084b92fb5bae2f64c208fea8ceed7fccf6a7b524d3c4942e", - "sha256:4d813316d1a752be6f5c4360c49f55b06d4fe212d7df03253dfdae90c8a402bb", - "sha256:5dd67ed9da78e5121efc5c510f0122a972216808d6de70953a740560c572eb44", - "sha256:6f914386e52cbdeb5d2a7ce3bf1fdfacbe9d818dd81b6099a05b741aaf3848bb", - "sha256:7101db1bd4cd9b880294dec41a93fcdce465bdbb602cd8dc5bd2d6362b618759", - "sha256:7e06aa1f764ec8265b19d8f00140b8c4b6ca179a6dc67aa9413867c47e1fb04e", - "sha256:84ca1be089fb4446490dd1135828bd42a7c7f8421e74fa581611f7afdf7ab761", - "sha256:8a1e224ce6f740dbb6b24c58f885422deebd7eb724aff0671a847f8951857c26", - "sha256:97ae7edd3f3f91480e48ede5d3e7d431ad6005bfdbd65c1b56913799ec79e791", - "sha256:9c9bebc6627873ec27a70fc800f6083a13c70b23a5564788754b9ee52c5aef6c", - "sha256:a013c5fbb12bfb5f927444b477a26f1080755a931d5d362e6a9a720ca7dbae60", - "sha256:a66fe4dc35d2330c185cfbb42959f57ad36f257e0cc4557d11d9f0a3f14311df", - "sha256:a92c4f58c01c77205df6ff999faa008540475c39b835277fb8883b11cada127a", - "sha256:aa8ba945c96e73de29d25331b26f3e416e0c0f621e984a3ebdb2d0d0b596a3b3", - "sha256:b0aa03d240b5539648d996cc60438f128c7f46050989e35b25f5c18286c86734", - "sha256:b1b24c23d51a1e8790b25514157d43f0a4dce1ac12b3f0b8e9f66a5e2c4c132f", - "sha256:b7ffb8ea674d68de4cac6f57d2498fef477cef582f1fa849e9f844863af50083", - "sha256:b9feb4e5ec8dc2d15709f4d5fc367794d69277f5d680baf1910fc9915c633524", - "sha256:bff2096bdba686019fb32d2dde45b95981f0d1490e054400f70fc9a8af34b49d", - "sha256:c30aeceeaff11cd5ddbc348f37c58bcb96da8d5aa93fed78ab329de5f37a0d7a", - "sha256:c9f80f9fad93a8cf71c7f161778ba47fd730d13a343a46258065c4deb4b550c0", - "sha256:cfd349de4158d797db2bd82d2020554a121674e98fbe6b15328456b3bf2495bb", - "sha256:d0cd7050397b3609ea51727b1811e663ffda8bda39c6a5bb69525ef12414b503", - "sha256:d639c939ad7c440c7b2819a28d559179a4508783f7e5b991166f8d7a34b52815", - "sha256:e3ba04659e4fce609de2658fe4dbf7d6ed21987a94460f5f92df7579fd5d0e22", - "sha256:ecfe735e7a59e5a98208447293ff8580e9db1e890e232b8b292dc8bd15afc0d2", - "sha256:ef82d361ed5849d34cf09105d00b94b6728d289d6b9235513cb2fcc79f7c432c", - "sha256:f03a5884c56256e08fd9e262e11b5cfacf1af96e2ce78dc095d2c41ccae2c80d", - "sha256:f1fe60d0772831d96d263b53d83fb9a3d050a94b0e94b6d004a5ad111faa5b5b", - "sha256:f517fd7259fe823ef3bd21e508b653d5492e706e9f0ef82c16ce3347a8a5620c", - "sha256:fdb14bad0835914f325349ed34a51940bc2ad965142eb3090081593c6e347be9" - ], - "version": "==1.66.1" + "sha256:02697eb4a5cbe5a9639f57323b4c37bcb3ab2d48cec5da3dc2f13334d72790dd", + "sha256:03b0b307ba26fae695e067b94cbb014e27390f8bc5ac7a3a39b7723fed085604", + "sha256:05bc2ceadc2529ab0b227b1310d249d95d9001cd106aa4d31e8871ad3c428d73", + "sha256:06de8ec0bd71be123eec15b0e0d457474931c2c407869b6c349bd9bed4adbac3", + "sha256:0be4e0490c28da5377283861bed2941d1d20ec017ca397a5df4394d1c31a9b50", + "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", + "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", + "sha256:2018b053aa15782db2541ca01a7edb56a0bf18c77efed975392583725974b249", + "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75", + "sha256:2335c58560a9e92ac58ff2bc5649952f9b37d0735608242973c7a8b94a6437d8", + "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", + "sha256:38b68498ff579a3b1ee8f93a05eb48dc2595795f2f62716e797dc24774c1aaa8", + "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", + "sha256:3ed71e81782966ffead60268bbda31ea3f725ebf8aa73634d5dda44f2cf3fb9c", + "sha256:45a3d462826f4868b442a6b8fdbe8b87b45eb4f5b5308168c156b21eca43f61c", + "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", + "sha256:4e504572433f4e72b12394977679161d495c4c9581ba34a88d843eaf0f2fbd39", + "sha256:4ea1d062c9230278793820146c95d038dc0f468cbdd172eec3363e42ff1c7d01", + "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", + "sha256:6001e575b8bbd89eee11960bb640b6da6ae110cf08113a075f1e2051cc596cae", + "sha256:66a0cd8ba6512b401d7ed46bb03f4ee455839957f28b8d61e7708056a806ba6a", + "sha256:6851de821249340bdb100df5eacfecfc4e6075fa85c6df7ee0eb213170ec8e5d", + "sha256:728bdf36a186e7f51da73be7f8d09457a03061be848718d0edf000e709418987", + "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a", + "sha256:73fc8f8b9b5c4a03e802b3cd0c18b2b06b410d3c1dcbef989fdeb943bd44aff7", + "sha256:78fa51ebc2d9242c0fc5db0feecc57a9943303b46664ad89921f5079e2e4ada7", + "sha256:7b2c86457145ce14c38e5bf6bdc19ef88e66c5fee2c3d83285c5aef026ba93b3", + "sha256:7d69ce1f324dc2d71e40c9261d3fdbe7d4c9d60f332069ff9b2a4d8a257c7b2b", + "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", + "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", + "sha256:8ac475e8da31484efa25abb774674d837b343afb78bb3bcdef10f81a93e3d6bf", + "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", + "sha256:99a641995a6bc4287a6315989ee591ff58507aa1cbe4c2e70d88411c4dcc0839", + "sha256:9c3a99c519f4638e700e9e3f83952e27e2ea10873eecd7935823dab0c1c9250e", + "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b", + "sha256:a18e20d8321c6400185b4263e27982488cb5cdd62da69147087a76a24ef4e7e3", + "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", + "sha256:a9539f01cb04950fd4b5ab458e64a15f84c2acc273670072abe49a3f29bbad54", + "sha256:ad2efdbe90c73b0434cbe64ed372e12414ad03c06262279b104a029d1889d13e", + "sha256:b672abf90a964bfde2d0ecbce30f2329a47498ba75ce6f4da35a2f4532b7acbc", + "sha256:bbd27c24a4cc5e195a7f56cfd9312e366d5d61b86e36d46bbe538457ea6eb8dd", + "sha256:c400ba5675b67025c8a9f48aa846f12a39cf0c44df5cd060e23fda5b30e9359d", + "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", + "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7", + "sha256:ce89f5876662f146d4c1f695dda29d4433a5d01c8681fbd2539afff535da14d4", + "sha256:d25a14af966438cddf498b2e338f88d1c9706f3493b1d73b93f695c99c5f0e2a", + "sha256:d8d4732cc5052e92cea2f78b233c2e2a52998ac40cd651f40e398893ad0d06ec", + "sha256:d9a9724a156c8ec6a379869b23ba3323b7ea3600851c91489b871e375f710bc8", + "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", + "sha256:e88264caad6d8d00e7913996030bac8ad5f26b7411495848cc218bd3a9040b6c", + "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46", + "sha256:fb57870449dfcfac428afbb5a877829fcb0d6db9d9baa1148705739e9083880e", + "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf", + "sha256:fe96281713168a3270878255983d2cb1a97e034325c8c2c25169a69289d3ecfa", + "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679" + ], + "version": "==1.66.2" }, "grpcio-status": { "hashes": [ - "sha256:b3f7d34ccc46d83fea5261eea3786174459f763c31f6e34f1d24eba6d515d024", - "sha256:cf9ed0b4a83adbe9297211c95cb5488b0cd065707e812145b842c85c4782ff02" + "sha256:e5fe189f6897d12aa9cd74408a17ca41e44fad30871cf84f5cbd17bd713d2455", + "sha256:fb55cbb5c2e67062f7a4d5c99e489d074fb57e98678d5c3c6692a2d74d89e9ae" ], - "version": "==1.66.1" + "version": "==1.66.2" }, "httplib2": { "hashes": [ @@ -5197,12 +5215,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:1843c5171ad6b489550aeaf7358e5b7128cc03ddcf0fb4d91d19aa1e691a63b8", - "sha256:d4779051d52ba9f1e7e344b34de95449c7c366c212b388e4a2db9a3db043c228" + "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b", + "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.38" + "version": "==9.5.39" }, "mkdocs-material-extensions": { "hashes": [ @@ -5567,11 +5585,11 @@ }, "pymdown-extensions": { "hashes": [ - "sha256:2653fb658bca5f278029f8c67a67f0f08b7bd3c657e2630d261ad542e97c4192", - "sha256:e68080eac44634406b31f4aec58fbad17b0ec5fca6b086e29008616d54c3906b" + "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf", + "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049" ], "markers": "python_version >= '3.8'", - "version": "==10.11" + "version": "==10.11.2" }, "pyparsing": { "hashes": [ @@ -5583,12 +5601,12 @@ }, "pyright": { "hashes": [ - "sha256:21a4749dd1740e209f88d3a601e9f40748670d39481ea32b9d77edf7f3f1fb2e", - "sha256:66a5d4e83be9452853d73e9dd9e95ba0ac3061845270e4e331d0070a597d3445" + "sha256:1df7f12407f3710c9c6df938d98ec53f70053e6c6bbf71ce7bcb038d42f10070", + "sha256:d864d1182a313f45aaf99e9bfc7d2668eeabc99b29a556b5344894fd73cb1959" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.1.382.post1" + "version": "==1.1.383" }, "pytest": { "hashes": [ diff --git a/breathecode/authenticate/tests/management/commands/tests_set_permissions.py b/breathecode/authenticate/tests/management/commands/tests_set_permissions.py index 12e968672..59a296535 100644 --- a/breathecode/authenticate/tests/management/commands/tests_set_permissions.py +++ b/breathecode/authenticate/tests/management/commands/tests_set_permissions.py @@ -63,8 +63,8 @@ def setUp(self): # the behavior of permissions is not exact, this changes every time you add a model self.latest_content_type_id = content_type.id self.latest_permission_id = permission.id - self.job_content_type_id = self.latest_content_type_id - 63 - self.can_delete_job_permission_id = self.latest_permission_id - 253 + self.job_content_type_id = self.latest_content_type_id - 64 + self.can_delete_job_permission_id = self.latest_permission_id - 257 """ 🔽🔽🔽 format of PERMISSIONS diff --git a/breathecode/media/tests/urls/v1/tests_upload.py b/breathecode/media/tests/urls/v1/tests_upload.py index ff20f2102..53e0c820e 100644 --- a/breathecode/media/tests/urls/v1/tests_upload.py +++ b/breathecode/media/tests/urls/v1/tests_upload.py @@ -791,9 +791,7 @@ def test_upload_invalid_format(self): model = self.generate_models(authenticate=True, profile_academy=True, capability="crud_media", role="potato") url = reverse_lazy("media:upload") - file = tempfile.NamedTemporaryFile(suffix=".txt", delete=False) - text = self.bc.fake.text() - file.write(text.encode("utf-8")) + file = open("breathecode/settings.py", "r") file.close() with open(file.name, "rb") as data: @@ -807,7 +805,7 @@ def test_upload_invalid_format(self): self.assertHash(hash) expected = { - "detail": f'You can upload only files on the following formats: {",".join(MIME_ALLOWED)}, got text/plain', + "detail": f'You can upload only files on the following formats: {",".join(MIME_ALLOWED)}, got text/x-python', "status_code": 400, } diff --git a/breathecode/mentorship/permissions/consumers.py b/breathecode/mentorship/permissions/consumers.py index ffd033a94..4d3419b4e 100644 --- a/breathecode/mentorship/permissions/consumers.py +++ b/breathecode/mentorship/permissions/consumers.py @@ -1,5 +1,8 @@ import logging +from capyc.core.managers import feature +from capyc.rest_framework.exceptions import PaymentException, ValidationException + from breathecode.admissions.actions import is_no_saas_student_up_to_date_in_any_cohort from breathecode.authenticate.actions import get_user_language from breathecode.authenticate.models import User @@ -7,7 +10,6 @@ from breathecode.payments.models import Consumable, ConsumptionSession from breathecode.utils.decorators import ServiceContext from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import PaymentException, ValidationException logger = logging.getLogger(__name__) @@ -111,16 +113,17 @@ def mentorship_service_by_url_param(context: ServiceContext, args: tuple, kwargs ) ) ): - - raise ValidationException( - translation( - lang, - en=f'Mentee do not have enough credits to access this service: {context["service"]}', - es="El mentee no tiene suficientes créditos para acceder a este servicio: " f'{context["service"]}', - ), - slug="mentee-not-enough-consumables", - code=402, - ) + c = feature.context(context=context, user=mentee) + if feature.is_enabled("payments.bypass_consumption", c, False) is False: + raise ValidationException( + translation( + lang, + en=f'Mentee do not have enough credits to access this service: {context["service"]}', + es="El mentee no tiene suficientes créditos para acceder a este servicio: " f'{context["service"]}', + ), + slug="mentee-not-enough-consumables", + code=402, + ) if consumable: session = ConsumptionSession.build_session(request, consumable, mentorship_service.max_duration, mentee) diff --git a/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py b/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py index 6da3b58b5..d1ed9253c 100644 --- a/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py +++ b/breathecode/mentorship/tests/urls_shortner/tests_meet_slug_service_slug.py @@ -10,6 +10,7 @@ import capyc.pytest as capy import pytest import timeago +from capyc.core.managers import feature from django.core.handlers.wsgi import WSGIRequest from django.template import loader from django.test.client import FakePayload @@ -3241,7 +3242,8 @@ def test_with_mentor_profile__redirect_to_session__no_saas(self): ), ) @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) - def test_with_mentor_profile__redirect_to_session__saas(self): + @patch("capyc.core.managers.feature.is_enabled", MagicMock(return_value=False)) + def test_with_mentor_profile__redirect_to_session__saas__bypass_consumption_false(self): mentor_profile_cases = [ { "status": x, @@ -3322,12 +3324,231 @@ def test_with_mentor_profile__redirect_to_session__saas(self): ) self.assertEqual(self.bc.database.list_of("payments.Consumable"), []) self.assertEqual(self.bc.database.list_of("payments.ConsumptionSession"), []) + calls = [ + call( + args[0], + { + **args[1], + "context": { + **args[1]["context"], + "request": type(args[1]["context"]["request"]), + "consumer": callable(args[1]["context"]["consumer"]), + "consumables": [x for x in args[1]["context"]["consumables"]], + }, + }, + *args[2:], + **kwargs, + ) + for args, kwargs in feature.is_enabled.call_args_list + ] + context1 = feature.context( + context={ + "utc_now": UTC_NOW, + "consumer": True, + "service": "join_mentorship", + "request": WSGIRequest, + "consumables": [], + "lifetime": None, + "price": 0, + "is_consumption_session": False, + "flags": {"bypass_consumption": False}, + }, + ) + context2 = feature.context( + context={ + "utc_now": UTC_NOW, + "consumer": True, + "service": "join_mentorship", + "request": WSGIRequest, + "consumables": [], + "lifetime": None, + "price": 0, + "is_consumption_session": False, + "flags": {"bypass_consumption": False}, + }, + user=base.user, + ) + assert calls == [ + call("payments.bypass_consumption", context1, False), + call("payments.bypass_consumption", context2, False), + ] + + # teardown + self.bc.database.delete("mentorship.MentorProfile") + + self.bc.database.delete("auth.Permission") + self.bc.database.delete("payments.Service") + feature.is_enabled.call_args_list = [] + + @patch("breathecode.mentorship.actions.mentor_is_ready", MagicMock()) + @patch( + "os.getenv", + MagicMock( + side_effect=apply_get_env( + { + "DAILY_API_URL": URL, + "DAILY_API_KEY": API_KEY, + } + ) + ), + ) + @patch( + "requests.request", + apply_requests_request_mock( + [ + ( + 201, + f"{URL}/v1/rooms", + { + "name": ROOM_NAME, + "url": ROOM_URL, + }, + ) + ] + ), + ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + @patch("capyc.core.managers.feature.is_enabled", MagicMock(side_effect=[False, True, False, True])) + def test_with_mentor_profile__redirect_to_session__saas__bypass_consumption_true(self): + mentor_profile_cases = [ + { + "status": x, + "online_meeting_url": self.bc.fake.url(), + "booking_url": self.bc.fake.url(), + } + for x in ["ACTIVE", "UNLISTED"] + ] + + id = 0 + for mentor_profile in mentor_profile_cases: + id += 1 + + user = {"first_name": "", "last_name": ""} + service = {"consumer": "JOIN_MENTORSHIP"} + base = self.bc.database.create(user=user, token=1, service=service) + + ends_at = UTC_NOW - timedelta(seconds=3600 / 2 + 1) + + academy = {"available_as_saas": True} + mentorship_session = { + "mentee_id": base.user.id, + "ends_at": ends_at, + "allow_mentee_to_extend": True, + } + token = 1 + + model = self.bc.database.create( + mentor_profile=mentor_profile, + mentorship_session=mentorship_session, + user=user, + token=token, + mentorship_service={"language": "en", "video_provider": "DAILY"}, + service=base.service, + academy=academy, + ) + + model.mentorship_session.mentee = None + model.mentorship_session.save() + + token = model.token if "token" in model else base.token + + querystring = self.bc.format.to_querystring( + { + "token": token.key, + "extend": "true", + "mentee": base.user.id, + "session": model.mentorship_session.id, + } + ) + url = ( + reverse_lazy( + "mentorship_shortner:meet_slug_service_slug", + kwargs={"mentor_slug": model.mentor_profile.slug, "service_slug": model.mentorship_service.slug}, + ) + + f"?{querystring}" + ) + response = self.client.get(url) + + content = self.bc.format.from_bytes(response.content) + expected = "" + + # dump error in external files + if content != expected: + with open("content.html", "w") as f: + f.write(content) + + with open("expected.html", "w") as f: + f.write(expected) + + self.assertEqual(content, expected) + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + assert ( + response.url + == f"/mentor/session/{model.mentorship_session.id}?token={token.key}&message=You%20have%20a%20session%20that%20expired%2030%20minutes%20ago.%20Only%20sessions%20with%20less%20than%2030min%20from%20expiration%20can%20be%20extended%20(if%20allowed%20by%20the%20academy)" + ) + self.assertEqual( + self.bc.database.list_of("mentorship.MentorProfile"), + [ + self.bc.format.to_dict(model.mentor_profile), + ], + ) + self.assertEqual(self.bc.database.list_of("payments.Consumable"), []) + self.assertEqual(self.bc.database.list_of("payments.ConsumptionSession"), []) + calls = [ + call( + args[0], + { + **args[1], + "context": { + **args[1]["context"], + "request": type(args[1]["context"]["request"]), + "consumer": callable(args[1]["context"]["consumer"]), + "consumables": [x for x in args[1]["context"]["consumables"]], + }, + }, + *args[2:], + **kwargs, + ) + for args, kwargs in feature.is_enabled.call_args_list + ] + context1 = feature.context( + context={ + "utc_now": UTC_NOW, + "consumer": True, + "service": "join_mentorship", + "request": WSGIRequest, + "consumables": [], + "lifetime": None, + "price": 0, + "is_consumption_session": False, + "flags": {"bypass_consumption": False}, + }, + ) + context2 = feature.context( + context={ + "utc_now": UTC_NOW, + "consumer": True, + "service": "join_mentorship", + "request": WSGIRequest, + "consumables": [], + "lifetime": None, + "price": 0, + "is_consumption_session": False, + "flags": {"bypass_consumption": False}, + }, + user=base.user, + ) + assert calls == [ + call("payments.bypass_consumption", context1, False), + call("payments.bypass_consumption", context2, False), + ] # teardown self.bc.database.delete("mentorship.MentorProfile") self.bc.database.delete("auth.Permission") self.bc.database.delete("payments.Service") + feature.is_enabled.call_args_list = [] @patch("breathecode.mentorship.actions.mentor_is_ready", MagicMock()) @patch( diff --git a/breathecode/payments/apps.py b/breathecode/payments/apps.py index 83529ece9..0d7c3b9d9 100644 --- a/breathecode/payments/apps.py +++ b/breathecode/payments/apps.py @@ -6,5 +6,6 @@ class PaymentsConfig(AppConfig): name = "breathecode.payments" def ready(self): + from . import flags # noqa: F401 from . import receivers # noqa: F401 from . import supervisors # noqa: F401 diff --git a/breathecode/payments/flags.py b/breathecode/payments/flags.py new file mode 100644 index 000000000..56a6ef509 --- /dev/null +++ b/breathecode/payments/flags.py @@ -0,0 +1,29 @@ +from typing import Optional + +from capyc.core.managers import feature + +from breathecode.authenticate.models import User +from breathecode.utils.decorators.consume import ServiceContext + +flags = feature.flags + + +@feature.availability("payments.bypass_consumption") +def bypass_consumption(context: ServiceContext, user: Optional[User] = None) -> bool: + """ + This flag is used to bypass the consumption of a service. + + Arguments: + context: ServiceContext - The context of the service. + user: Optional[User] - The user to bypass the consumption for, if none it will use request.user. + """ + + if flags.get("BYPASS_CONSUMPTION") not in feature.TRUE: + return False + + # write logic here + + return False + + +feature.add(bypass_consumption) diff --git a/breathecode/registry/tests/urls/v1/tests_academy_asset.py b/breathecode/registry/tests/urls/v1/tests_academy_asset.py index d54b39375..ce33174d5 100644 --- a/breathecode/registry/tests/urls/v1/tests_academy_asset.py +++ b/breathecode/registry/tests/urls/v1/tests_academy_asset.py @@ -19,6 +19,8 @@ def database_item(academy, category, data={}): return { "academy_id": academy.id, + "learnpack_deploy_url": None, + "agent": None, "assessment_id": None, "asset_type": "PROJECT", "author_id": None, @@ -88,6 +90,7 @@ def post_serializer(academy, category, data={}): "slug": category.slug, "title": category.title, }, + "agent": None, "delivery_formats": "url", "delivery_instructions": None, "delivery_regex_url": None, diff --git a/breathecode/registry/tests/urls/v1/tests_asset.py b/breathecode/registry/tests/urls/v1/tests_asset.py index 4805cc93c..9703736d1 100644 --- a/breathecode/registry/tests/urls/v1/tests_asset.py +++ b/breathecode/registry/tests/urls/v1/tests_asset.py @@ -95,6 +95,7 @@ def get_serializer_technology(technology, data={}): def get_mid_serializer(asset, data={}): return { **get_serializer(asset), + "agent": None, "with_solutions": asset.with_solutions, "with_video": asset.with_solutions, "updated_at": asset.updated_at, @@ -260,7 +261,6 @@ def test_assets_expand_readme_ipynb(bc: Breathecode, client): json = response.json() asset_readme = model.asset.get_readme() - print(asset_readme) expected = [ get_mid_serializer( diff --git a/breathecode/services/activecampaign/actions/deal_update.py b/breathecode/services/activecampaign/actions/deal_update.py index 1f3bff869..165c5b3e5 100644 --- a/breathecode/services/activecampaign/actions/deal_update.py +++ b/breathecode/services/activecampaign/actions/deal_update.py @@ -31,7 +31,9 @@ def deal_update(ac_cls, webhook, payload: dict, acp_ids): ) if entry is None and "deal[contact_email]" in payload: entry = ( - FormEntry.objects.filter(email=payload["deal[contact_email]"], storage_status__in=["PERSISTED", "MANUALLY_PERSISTED"]) + FormEntry.objects.filter( + email=payload["deal[contact_email]"], storage_status__in=["PERSISTED", "MANUALLY_PERSISTED"] + ) .order_by("-created_at") .first() ) diff --git a/breathecode/utils/decorators/consume.py b/breathecode/utils/decorators/consume.py index 66688bdc7..8df59a3b7 100644 --- a/breathecode/utils/decorators/consume.py +++ b/breathecode/utils/decorators/consume.py @@ -7,6 +7,8 @@ from adrf.requests import AsyncRequest from asgiref.sync import sync_to_async +from capyc.core.managers import feature +from capyc.rest_framework.exceptions import PaymentException, ValidationException from django.contrib.auth.models import AnonymousUser from django.core.handlers.wsgi import WSGIRequest from django.db.models import QuerySet, Sum @@ -18,7 +20,6 @@ from breathecode.authenticate.models import User from breathecode.payments.signals import consume_service -from capyc.rest_framework.exceptions import PaymentException, ValidationException from ..exceptions import ProgrammingError @@ -27,6 +28,14 @@ logger = logging.getLogger(__name__) +class Flags(TypedDict): + bypass_consumption: bool + + +class FlagsParams(Flags, total=False): + pass + + class ServiceContext(TypedDict): utc_now: datetime consumer: bool @@ -36,6 +45,7 @@ class ServiceContext(TypedDict): lifetime: Optional[timedelta] price: float is_consumption_session: bool + flags: Flags type Consumer = Callable[[ServiceContext, tuple, dict], tuple[ServiceContext, tuple, dict, Optional[timedelta]]] @@ -200,8 +210,15 @@ def validate_and_get_request(permission: str, args: Any) -> HttpRequest | AsyncR return request def build_context( - request: HttpRequest | AsyncRequest, utc_now: datetime, **opts: Unpack[ServiceContext] + request: HttpRequest | AsyncRequest, + utc_now: datetime, + flags: Optional[FlagsParams] = None, + **opts: Unpack[ServiceContext], ) -> ServiceContext: + + if flags is None: + flags = {} + return { "utc_now": utc_now, "consumer": consumer, @@ -211,6 +228,10 @@ def build_context( "lifetime": None, "price": 1, "is_consumption_session": False, + "flags": { + "bypass_consumption": False, + **flags, + }, **opts, } @@ -238,9 +259,16 @@ def wrapper(*args, **kwargs): items = Consumable.list(user=request.user, service=service) context["consumables"] = items + flag_context = feature.context(context=context) + bypass_consumption = feature.is_enabled("payments.bypass_consumption", flag_context, False) + context["flags"]["bypass_consumption"] = bypass_consumption + if callable(consumer): context, args, kwargs = consumer(context, args, kwargs) + if bypass_consumption: + return function(*args, **kwargs) + # exclude consumables that is being used in a session. if consumer and context["lifetime"]: consumables = context["consumables"] @@ -343,12 +371,19 @@ async def async_wrapper(*args, **kwargs): items = await Consumable.alist(user=user, service=service) context["consumables"] = items + flag_context = feature.context(context=context) + bypass_consumption = feature.is_enabled("payments.bypass_consumption", flag_context, False) + context["flags"]["bypass_consumption"] = bypass_consumption + if callable(consumer): if asyncio.iscoroutinefunction(consumer) is False: consumer = sync_to_async(consumer) context, args, kwargs = await consumer(context, args, kwargs) + if bypass_consumption: + return await function(*args, **kwargs) + # exclude consumables that is being used in a session. if consumer and context["lifetime"]: consumables: QuerySet[Consumable] = context["consumables"] diff --git a/breathecode/utils/tests/decorators/tests_consume.py b/breathecode/utils/tests/decorators/tests_consume.py index cc90fe533..44af072ec 100644 --- a/breathecode/utils/tests/decorators/tests_consume.py +++ b/breathecode/utils/tests/decorators/tests_consume.py @@ -6,7 +6,9 @@ import capyc.pytest as capy import pytest from adrf.decorators import APIView, api_view +from adrf.requests import AsyncRequest from asgiref.sync import sync_to_async +from capyc.core.managers import feature from django.http.response import JsonResponse from django.utils import timezone from rest_framework import status @@ -60,6 +62,27 @@ def consumer(context: ServiceContext, args: tuple, kwargs: dict) -> tuple[dict, time_of_life = timedelta(days=random.randint(1, 100)) +@sync_to_async +def is_enabled_call_list(): + return [ + call( + args[0], + { + **args[1], + "context": { + **args[1]["context"], + "request": type(args[1]["context"]["request"]), + "consumer": callable(args[1]["context"]["consumer"]), + "consumables": [x for x in args[1]["context"]["consumables"]], + }, + }, + *args[2:], + **kwargs, + ) + for args, kwargs in feature.is_enabled.call_args_list + ] + + def consumer_with_time_of_life(context: ServiceContext, args: tuple, kwargs: dict) -> tuple[dict, tuple, dict]: # remember the objects are passed by reference, so you need to clone them to avoid modify the object # receive by the mock causing side effects @@ -398,11 +421,13 @@ def check_consume_service(): @pytest.mark.asyncio @pytest.mark.django_db(reset_sequences=True) - async def test_with_user__with_group_related_to_permission__consumable__how_many_0( - self, bc: Breathecode, make_view_all_cases + async def test_with_user__with_group_related_to_permission__consumable__how_many_0__bypass_consumption_false( + self, bc: Breathecode, make_view_all_cases, monkeypatch, utc_now ): + monkeypatch.setattr(feature, "is_enabled", MagicMock(return_value=False)) user = {"user_permissions": []} - services = [{}, {"consumer": SERVICE.upper()}] + consumer = SERVICE.upper() + services = [{}, {"consumer": consumer}] consumable = {"how_many": 0} model = await bc.database.acreate( @@ -426,6 +451,67 @@ def check_consume_service(): await check_consume_service() + context = feature.context( + context={ + "utc_now": utc_now, + "consumer": False, + "service": consumer, + "request": AsyncRequest, + "consumables": [], + "lifetime": None, + "price": 1, + "is_consumption_session": False, + "flags": {"bypass_consumption": False}, + } + ) + assert await is_enabled_call_list() == [call("payments.bypass_consumption", context, False)] + + @pytest.mark.asyncio + @pytest.mark.django_db(reset_sequences=True) + async def test_with_user__with_group_related_to_permission__consumable__how_many_0__bypass_consumption_true( + self, bc: Breathecode, make_view_all_cases, monkeypatch, utc_now + ): + monkeypatch.setattr(feature, "is_enabled", MagicMock(return_value=True)) + user = {"user_permissions": []} + consumer = SERVICE.upper() + services = [{}, {"consumer": consumer}] + + consumable = {"how_many": 0} + model = await bc.database.acreate( + user=user, service=services, service_item={"service_id": 2}, consumable=consumable + ) + + view, expected, _, _ = await make_view_all_cases(user=model.user, decorator_params={}, url_params={}) + + response, _ = await view() + + assert json.loads(response.content.decode("utf-8")) == expected + assert response.status_code == status.HTTP_200_OK + # self.assertEqual(CONSUMER_MOCK.call_args_list, []) + assert await bc.database.alist_of("payments.ConsumptionSession") == [] + assert models.ConsumptionSession.build_session.call_args_list == [] + + @sync_to_async + def check_consume_service(): + assert payments_signals.consume_service.send_robust.call_args_list == [] + + await check_consume_service() + + context = feature.context( + context={ + "utc_now": utc_now, + "consumer": False, + "service": consumer, + "request": AsyncRequest, + "consumables": [], + "lifetime": None, + "price": 1, + "is_consumption_session": False, + "flags": {"bypass_consumption": True}, + } + ) + assert await is_enabled_call_list() == [call("payments.bypass_consumption", context, False)] + @pytest.mark.asyncio @pytest.mark.django_db(reset_sequences=True) async def test_with_user__with_group_related_to_permission__consumable__how_many_gte_1( From 2ddc105a05045396f2d2f965e5c64c8257290915 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 15 Oct 2024 17:29:43 -0500 Subject: [PATCH 02/23] virtual consumables in the balance --- breathecode/admissions/serializers.py | 2 +- breathecode/payments/actions.py | 162 ++- breathecode/payments/data.py | 20 + .../tests/urls/tests_me_service_consumable.py | 1070 +++++++++++++++++ breathecode/payments/utils.py | 203 ++++ breathecode/payments/views.py | 7 +- 6 files changed, 1457 insertions(+), 7 deletions(-) create mode 100644 breathecode/payments/data.py create mode 100644 breathecode/payments/utils.py diff --git a/breathecode/admissions/serializers.py b/breathecode/admissions/serializers.py index 9d82b16cf..6ba8e3e6e 100644 --- a/breathecode/admissions/serializers.py +++ b/breathecode/admissions/serializers.py @@ -1,6 +1,7 @@ import logging from collections import OrderedDict +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import User from django.db.models import Q @@ -9,7 +10,6 @@ from breathecode.assignments.serializers import TaskGETSmallSerializer from breathecode.authenticate.models import CredentialsGithub, ProfileAcademy from breathecode.utils import localize_query, serializers, serpy -from capyc.rest_framework.exceptions import ValidationException from .actions import haversine, test_syllabus from .models import ( diff --git a/breathecode/payments/actions.py b/breathecode/payments/actions.py index bd9bcd67f..d4d27bee6 100644 --- a/breathecode/payments/actions.py +++ b/breathecode/payments/actions.py @@ -1,9 +1,11 @@ import os import re +from datetime import datetime from functools import lru_cache -from typing import Optional, Type +from typing import Literal, Optional, Type, TypedDict from adrf.requests import AsyncRequest +from capyc.rest_framework.exceptions import ValidationException from dateutil.relativedelta import relativedelta from django.contrib.auth.models import User from django.core.handlers.wsgi import WSGIRequest @@ -22,15 +24,17 @@ from breathecode.utils import getLogger from breathecode.utils.i18n import translation from breathecode.utils.validate_conversion_info import validate_conversion_info -from capyc.rest_framework.exceptions import ValidationException from .models import ( SERVICE_UNITS, Bag, + CohortSet, Consumable, Coupon, Currency, + EventTypeSet, Invoice, + MentorshipServiceSet, PaymentMethod, Plan, PlanFinancing, @@ -735,15 +739,20 @@ def filter_void_consumable_balance(request: WSGIRequest, items: QuerySet[Consuma } ) - return result.values() + return list(result.values()) -def get_balance_by_resource(queryset: QuerySet, key: str): +def get_balance_by_resource( + queryset: QuerySet[Consumable], + key: str, +): result = [] ids = {getattr(x, key).id for x in queryset} for id in ids: current = queryset.filter(**{f"{key}__id": id}) + # current_virtual = [x for x in x if x[key] == id] + instance = current.first() balance = {} items = [] @@ -754,6 +763,9 @@ def get_balance_by_resource(queryset: QuerySet, key: str): -1 if per_unit.filter(how_many=-1).exists() else per_unit.aggregate(Sum("how_many"))["how_many__sum"] ) + # for unit in current_virtual: + # ... + for x in queryset: valid_until = x.valid_until if valid_until: @@ -1108,3 +1120,145 @@ def validate_and_create_subscriptions( tasks.build_plan_financing.delay(bag.id, invoice.id, conversion_info=conversion_info) return invoice, coupons + + +class UnitBalance(TypedDict): + unit: int + + +class ConsumableItem(TypedDict): + id: int + how_many: int + unit_type: str + valid_until: Optional[datetime] + + +class ResourceBalance(TypedDict): + id: int + slug: str + balance: UnitBalance + items: list[ConsumableItem] + + +class ConsumableBalance(TypedDict): + mentorship_service_sets: ResourceBalance + cohort_sets: list[ResourceBalance] + event_type_sets: list[ResourceBalance] + voids: list[ResourceBalance] + + +def set_virtual_balance(balance: ConsumableBalance, user: User) -> None: + from breathecode.payments.data import get_virtual_consumables + + virtuals = get_virtual_consumables() + + event_type_set_ids = [virtual["event_type_set"]["id"] for virtual in virtuals if virtual["event_type_set"]] + cohort_set_ids = [virtual["cohort_set"]["id"] for virtual in virtuals if virtual["cohort_set"]] + mentorship_service_set_ids = [ + virtual["mentorship_service_set"]["id"] for virtual in virtuals if virtual["mentorship_service_set"] + ] + + available_services = [ + virtual["service_item"]["service"]["id"] + for virtual in virtuals + if virtual["service_item"]["service"]["type"] == Service.Type.VOID + ] + + available_event_type_sets = EventTypeSet.objects.filter( + academy__profileacademy__user=user, id__in=event_type_set_ids + ).values_list("id", flat=True) + + available_cohort_sets = CohortSet.objects.filter(cohorts__cohortuser__user=user, id__in=cohort_set_ids).values_list( + "id", flat=True + ) + + available_mentorship_service_sets = MentorshipServiceSet.objects.filter( + academy__profileacademy__user=user, id__in=mentorship_service_set_ids + ).values_list("id", flat=True) + + balance_mapping: dict[str, dict[int, int]] = { + "cohort_sets": dict( + [(v["id"], i) for (i, v) in enumerate(balance["cohort_sets"]) if v["id"] in available_cohort_sets] + ), + "event_type_sets": dict( + [(v["id"], i) for (i, v) in enumerate(balance["event_type_sets"]) if v["id"] in available_event_type_sets] + ), + "mentorship_service_sets": dict( + [ + (v["id"], i) + for (i, v) in enumerate(balance["mentorship_service_sets"]) + if v["id"] in available_mentorship_service_sets + ] + ), + "voids": dict([(v["id"], i) for (i, v) in enumerate(balance["voids"]) if v["id"] in available_services]), + } + + def append( + key: Literal["cohort_sets", "event_type_sets", "mentorship_service_sets", "voids"], + id: int, + slug: str, + how_many: int, + unit_type: str, + valid_until: Optional[datetime] = None, + ): + + index = balance_mapping[key].get(id) + + # index = balance[key].append(id) + unit_type = unit_type.lower() + if index is None: + balance[key].append({"id": id, "slug": slug, "balance": {unit_type: 0}, "items": []}) + index = len(balance[key]) - 1 + balance_mapping[key][id] = index + + obj = balance[key][index] + + if how_many == -1: + obj["balance"][unit_type] = how_many + + elif obj["balance"][unit_type] != -1: + obj["balance"][unit_type] += how_many + + obj["items"].append( + { + "id": None, + "how_many": how_many, + "unit_type": unit_type.upper(), + "valid_until": valid_until, + } + ) + + for virtual in virtuals: + if ( + virtual["service_item"]["service"]["type"] == Service.Type.VOID + and virtual["service_item"]["service"]["id"] in available_services + ): + id = virtual["service_item"]["service"]["id"] + slug = virtual["service_item"]["service"]["slug"] + how_many = virtual["service_item"]["how_many"] + unit_type = virtual["service_item"]["unit_type"] + append("voids", id, slug, how_many, unit_type) + + if virtual["event_type_set"] and virtual["event_type_set"]["id"] in available_event_type_sets: + id = virtual["event_type_set"]["id"] + slug = virtual["event_type_set"]["slug"] + how_many = virtual["service_item"]["how_many"] + unit_type = virtual["service_item"]["unit_type"] + append("event_type_sets", id, slug, how_many, unit_type) + + if ( + virtual["mentorship_service_set"] + and virtual["mentorship_service_set"]["id"] in available_mentorship_service_sets + ): + id = virtual["mentorship_service_set"]["id"] + slug = virtual["mentorship_service_set"]["slug"] + how_many = virtual["service_item"]["how_many"] + unit_type = virtual["service_item"]["unit_type"] + append("mentorship_service_sets", id, slug, how_many, unit_type) + + if virtual["cohort_set"] and virtual["cohort_set"]["id"] in available_cohort_sets: + id = virtual["cohort_set"]["id"] + slug = virtual["cohort_set"]["slug"] + how_many = virtual["service_item"]["how_many"] + unit_type = virtual["service_item"]["unit_type"] + append("cohort_sets", id, slug, how_many, unit_type) diff --git a/breathecode/payments/data.py b/breathecode/payments/data.py new file mode 100644 index 000000000..e5471e09c --- /dev/null +++ b/breathecode/payments/data.py @@ -0,0 +1,20 @@ +from breathecode.payments.utils import ConsumableType, consumable, service_item + +__all__ = ["get_virtual_consumables"] + + +def get_virtual_consumables() -> list[ConsumableType]: + return [ + consumable( + service_item=1, + cohort_set=1, + event_type_set=1, + mentorship_service_set=1, + ), + consumable( + service_item=service_item(service=1, unit_type="unit", how_many=1), + cohort_set=1, + event_type_set=1, + mentorship_service_set=1, + ), + ] diff --git a/breathecode/payments/tests/urls/tests_me_service_consumable.py b/breathecode/payments/tests/urls/tests_me_service_consumable.py index 3522a093b..1453a5198 100644 --- a/breathecode/payments/tests/urls/tests_me_service_consumable.py +++ b/breathecode/payments/tests/urls/tests_me_service_consumable.py @@ -1,7 +1,10 @@ import math import random +from typing import Callable, Literal, TypedDict from unittest.mock import MagicMock, patch +import pytest +from aiohttp_retry import Any from django.urls import reverse_lazy from django.utils import timezone from rest_framework import status @@ -108,6 +111,41 @@ def serialize_consumable(consumable, data={}): } +class GenericConsumableMockArg(TypedDict, total=False): + resource: Literal["cohort_set", "event_type_set", "mentorship_service_set"] + id: int + + +class ConsumableMockArg(GenericConsumableMockArg): + service: int + how_many: int + + +def get_virtual_consumables_mock( + *consumables: ConsumableMockArg, +) -> Callable[[], list[dict[str, Any]]]: + + # the wrapper avoid raising an error during the setup + def wrapper(): + from breathecode.payments.utils import consumable, service_item + + nonlocal consumables + + result = [] + for x in consumables: + kwargs = { + "service_item": service_item(service=x["service"], unit_type="unit", how_many=x["how_many"]), + } + if "resource" in x and "id" in x: + kwargs[x["resource"]] = x["id"] + + result.append(consumable(**kwargs)) + + return result + + return wrapper + + class TestSignal(LegacyAPITestCase): """ 🔽🔽🔽 GET without auth @@ -1319,3 +1357,1035 @@ def test__nine_consumables__related_to_three_services__with_cohort_slugs_in_quer assert json == expected assert response.status_code == status.HTTP_200_OK assert self.bc.database.list_of("payments.Consumable") == self.bc.format.to_dict(model.consumable) + + """ + 🔽🔽🔽 Virtual Consumables, append to the real balance + """ + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___cohort_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"resource": "cohort_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + consumables = [{"how_many": random.randint(1, 30), "cohort_set_id": math.floor(n / 3) + 1} for n in range(9)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + sum([rand1 * (1 + n) for n in range(3)]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + cohort_set=3, + cohort_set_cohort=[{"cohort_set_id": 1 + n} for n in range(3)], + academy=academy, + service=(10, {"type": "COHORT_SET"}), + cohort={"available_as_saas": True}, + cohort_user=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.cohort_set[0].id, + "slug": model.cohort_set[0].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "how_many": rand1 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.cohort_set[1].id, + "slug": model.cohort_set[1].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "how_many": rand2 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.cohort_set[2].id, + "slug": model.cohort_set[2].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "how_many": rand3 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + ], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___mentorship_service_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[ + {"resource": "mentorship_service_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "mentorship_service_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "mentorship_service_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], + ), + ) + consumables = [ + {"how_many": random.randint(1, 30), "mentorship_service_set_id": math.floor(n / 3) + 1} for n in range(9) + ] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + sum([rand1 * (1 + n) for n in range(3)]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + mentorship_service_set=3, + profile_academy=1, + academy=academy, + service=(10, {"type": "MENTORSHIP_SERVICE_SET"}), + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.mentorship_service_set[0].id, + "slug": model.mentorship_service_set[0].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "id": None, + "how_many": rand1 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.mentorship_service_set[1].id, + "slug": model.mentorship_service_set[1].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "id": None, + "how_many": rand2 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.mentorship_service_set[2].id, + "slug": model.mentorship_service_set[2].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "id": None, + "how_many": rand3 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + ], + "cohort_sets": [], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___event_type_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[ + {"resource": "event_type_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], + ), + ) + consumables = [ + {"how_many": random.randint(1, 30), "event_type_set_id": math.floor(n / 3) + 1} for n in range(9) + ] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + sum([rand1 * (1 + n) for n in range(3)]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + event_type_set=3, + profile_academy=1, + academy=academy, + service=(10, {"type": "EVENT_TYPE_SET"}), + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.event_type_set[0].id, + "slug": model.event_type_set[0].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "id": None, + "how_many": rand1 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.event_type_set[1].id, + "slug": model.event_type_set[1].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "id": None, + "how_many": rand2 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.event_type_set[2].id, + "slug": model.event_type_set[2].slug, + "items": [ + *[serialize_consumable(model.consumable[n]) for n in range(9)], + *[ + { + "id": None, + "how_many": rand3 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + ], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___service__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"service": 1, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"service": 2, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"service": 3, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + + consumables = [{"how_many": random.randint(1, 30), "service_item_id": math.floor(n / 3) + 1} for n in range(9)] + service_items = [{"service_id": n + 1} for n in range(3)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + sum([rand1 * (1 + n) for n in range(3)]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + + model = self.bc.database.create( + user=1, + consumable=consumables, + service_item=service_items, + service=[{"type": "VOID"} for _ in range(3)], + ) + + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [], + "voids": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.service[0].id, + "slug": model.service[0].slug, + "items": [ + *[serialize_consumable(consumable) for consumable in model.consumable[:3]], + *[ + { + "id": None, + "how_many": rand1 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.service[1].id, + "slug": model.service[1].slug, + "items": [ + *[serialize_consumable(consumable) for consumable in model.consumable[3:6]], + *[ + { + "id": None, + "how_many": rand2 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.service[2].id, + "slug": model.service[2].slug, + "items": [ + *[serialize_consumable(consumable) for consumable in model.consumable[6:]], + *[ + { + "id": None, + "how_many": rand3 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + ], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + """ + 🔽🔽🔽 Virtual Consumables, append to a new balance + """ + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"resource": "cohort_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + consumables = [{"how_many": random.randint(1, 30), "cohort_set_id": math.floor(n / 3) + 1} for n in range(9)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + cohort_set=6, + cohort_set_cohort=[{"cohort_set_id": 4 + n} for n in range(3)], + academy=academy, + service=(10, {"type": "COHORT_SET"}), + cohort={"available_as_saas": True}, + cohort_user=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.cohort_set[0].id, + "slug": model.cohort_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.cohort_set[1].id, + "slug": model.cohort_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.cohort_set[2].id, + "slug": model.cohort_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + # + { + "balance": { + "unit": sum([rand1 * (1 + n) for n in range(3)]), + }, + "id": model.cohort_set[3].id, + "slug": model.cohort_set[3].slug, + "items": [ + { + "how_many": rand1 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand2 * (1 + n) for n in range(3)]), + }, + "id": model.cohort_set[4].id, + "slug": model.cohort_set[4].slug, + "items": [ + { + "how_many": rand2 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand3 * (1 + n) for n in range(3)]), + }, + "id": model.cohort_set[5].id, + "slug": model.cohort_set[5].slug, + "items": [ + { + "how_many": rand3 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + ], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___event_type_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[ + {"resource": "event_type_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], + ), + ) + consumables = [ + {"how_many": random.randint(1, 30), "event_type_set_id": math.floor(n / 3) + 1} for n in range(9) + ] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + event_type_set=6, + academy=academy, + service=(10, {"type": "EVENT_TYPE_SET"}), + profile_academy=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.event_type_set[0].id, + "slug": model.event_type_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.event_type_set[1].id, + "slug": model.event_type_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.event_type_set[2].id, + "slug": model.event_type_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + # + { + "balance": { + "unit": sum([rand1 * (1 + n) for n in range(3)]), + }, + "id": model.event_type_set[3].id, + "slug": model.event_type_set[3].slug, + "items": [ + { + "how_many": rand1 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand2 * (1 + n) for n in range(3)]), + }, + "id": model.event_type_set[4].id, + "slug": model.event_type_set[4].slug, + "items": [ + { + "how_many": rand2 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand3 * (1 + n) for n in range(3)]), + }, + "id": model.event_type_set[5].id, + "slug": model.event_type_set[5].slug, + "items": [ + { + "how_many": rand3 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + ], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___mentorship_service_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[ + {"resource": "mentorship_service_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "mentorship_service_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "mentorship_service_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], + ), + ) + consumables = [ + {"how_many": random.randint(1, 30), "mentorship_service_set_id": math.floor(n / 3) + 1} for n in range(9) + ] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + mentorship_service_set=6, + academy=academy, + service=(10, {"type": "MENTORSHIP_SERVICE_SET"}), + profile_academy=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.mentorship_service_set[0].id, + "slug": model.mentorship_service_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.mentorship_service_set[1].id, + "slug": model.mentorship_service_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.mentorship_service_set[2].id, + "slug": model.mentorship_service_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + # + { + "balance": { + "unit": sum([rand1 * (1 + n) for n in range(3)]), + }, + "id": model.mentorship_service_set[3].id, + "slug": model.mentorship_service_set[3].slug, + "items": [ + { + "how_many": rand1 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand2 * (1 + n) for n in range(3)]), + }, + "id": model.mentorship_service_set[4].id, + "slug": model.mentorship_service_set[4].slug, + "items": [ + { + "how_many": rand2 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand3 * (1 + n) for n in range(3)]), + }, + "id": model.mentorship_service_set[5].id, + "slug": model.mentorship_service_set[5].slug, + "items": [ + { + "how_many": rand3 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + ], + "cohort_sets": [], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___service__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"service": 4, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"service": 5, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"service": 6, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + + consumables = [{"how_many": random.randint(1, 30), "service_item_id": math.floor(n / 3) + 1} for n in range(9)] + service_items = [{"service_id": n + 1} for n in range(3)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + service_item=service_items, + academy=academy, + service=(6, {"type": "VOID"}), + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + consumables = [serialize_consumable(model.consumable[n]) for n in range(9)] + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [], + "voids": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.service[0].id, + "slug": model.service[0].slug, + "items": consumables[:3], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.service[1].id, + "slug": model.service[1].slug, + "items": consumables[3:6], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.service[2].id, + "slug": model.service[2].slug, + "items": consumables[6:9], + }, + # + { + "balance": { + "unit": sum([rand1 * (1 + n) for n in range(3)]), + }, + "id": model.service[3].id, + "slug": model.service[3].slug, + "items": [ + { + "how_many": rand1 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand2 * (1 + n) for n in range(3)]), + }, + "id": model.service[4].id, + "slug": model.service[4].slug, + "items": [ + { + "how_many": rand2 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + { + "balance": { + "unit": sum([rand3 * (1 + n) for n in range(3)]), + }, + "id": model.service[5].id, + "slug": model.service[5].slug, + "items": [ + { + "how_many": rand3 * (1 + n), + "id": None, + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + }, + ], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) diff --git a/breathecode/payments/utils.py b/breathecode/payments/utils.py new file mode 100644 index 000000000..936687862 --- /dev/null +++ b/breathecode/payments/utils.py @@ -0,0 +1,203 @@ +import re +from typing import Any, Optional, Type, TypedDict, Unpack, overload + +from capyc.rest_framework.exceptions import ValidationException + +from breathecode.payments.models import CohortSet, EventTypeSet, MentorshipServiceSet, Service, ServiceItem + +__all__ = ["consumable", "service_item", "ConsumableType", "reset_cache"] + + +class GenericType(TypedDict): + id: int + slug: str + + +class ServiceType(GenericType): + type: Service.Type + + +class ServiceItemType(TypedDict): + service: ServiceType + unit_type: str + how_many: int + + +class ConsumableType(TypedDict): + service_item: ServiceItemType + cohort_set: Optional[GenericType] + event_type_set: Optional[GenericType] + mentorship_service_set: Optional[GenericType] + + +type ID = dict[str, Any] +SERVICES: dict[ID, ServiceType] = {} +SERVICE_ITEMS: dict[ID, ServiceItemType] = {} +# VIRTUAL_SERVICE_ITEMS: list[ServiceItemType] = [] +FIELDS: dict[str, tuple[str, ...]] = { + "Service": ("id", "slug", "type"), +} + +FIELDS["ServiceItem"] = ("id", "unit_type", "how_many", "service_id", *(f"service__{x}" for x in FIELDS["Service"])) + +type Model = Type[Service | ServiceItem] + + +def get_hash(d: dict[str, Any]) -> str: + return tuple(sorted(d.items())) + + +@overload +def serialize(model: Type[Service], **kwargs: Any) -> ServiceType: ... + + +@overload +def serialize(model: Type[ServiceItem], **kwargs: Any) -> ServiceItemType: ... + + +@overload +def serialize(instance: ServiceItem, **kwargs: Any) -> ServiceItemType: ... + + +@overload +def serialize(instance: Service, **kwargs: Any) -> ServiceType: ... + + +def serialize( + model: Optional[Type[Model]] = None, instance: Optional[Model] = None, **kwargs: Any +) -> ServiceType | ServiceItemType: + if model is None and instance is None: + raise ValueError("Either model or instance must be provided") + + if model and instance: + raise ValueError("Both model and instance cannot be provided") + + if model: + key = model.__name__ + else: + key = instance.__class__.__name__ + + fields = FIELDS.get(key, tuple()) + + result = {} + if not instance: + instance = model.objects.filter(**kwargs).only(*fields).first() + + if not instance: + raise ValidationException(f"{key} with params {kwargs} not found") + + for field in fields: + override = field + if "__" in field: + continue + + if field.endswith("_id"): + override = field.replace("_id", "") + + result[override] = getattr(instance, override) + + if key == "Service": + SERVICES[get_hash(kwargs)] = result + elif key == "ServiceItem": + SERVICE_ITEMS[get_hash(kwargs)] = result + + return result + + +def get_service(id: int) -> ServiceType: + key = {"id": id} + + if get_hash(key) in SERVICES: + return SERVICES[get_hash(key)] + + return serialize(model=Service, **key) + + +def get_service_item(id: int) -> ServiceItemType: + key = {"id": id} + if get_hash(key) in SERVICE_ITEMS: + return SERVICE_ITEMS[get_hash(key)] + + v = serialize(model=ServiceItem, **key) + v["service"] = serialize(instance=v["service"]) + return v + + +def service_item(service: ServiceType | int, **kwargs: Unpack[ServiceItemType]) -> ServiceItemType: + if isinstance(service, int): + service = get_service(service) + + kwargs["unit_type"] = kwargs["unit_type"].upper() + + return {"service": service, **kwargs} + + +class GenericType(TypedDict): + id: int + slug: str + + +# EXITS: set[str] = set() +EXISTS: dict[str, GenericType] = {} + + +# def serialize_generic(instance: EventTypeSet | CohortSet | MentorshipServiceSet) -> GenericType: +# return { +# "id": instance.id, +# "slug": instance.slug, +# } + + +def camel_to_snake(name): + # Add an underscore before each capital letter and convert the string to lowercase + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + # Handle cases where there are multiple capital letters in a row + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + + +def resource(model: Type[EventTypeSet | CohortSet | MentorshipServiceSet], **kwargs: dict[str, Any]) -> GenericType: + name = model.__name__ + key = f"{camel_to_snake(name)}__{kwargs}" + if key not in EXISTS: + x = model.objects.filter(**kwargs).only("id", "slug").first() + if not x: + raise ValidationException(f"{name} with {kwargs} not found") + + EXISTS[key] = {"id": x.id, "slug": x.slug} + + return EXISTS[key] + + +def reset_cache(): + global EXISTS, SERVICES, SERVICE_ITEMS + EXISTS = {} + SERVICES = {} + SERVICE_ITEMS = {} + + +def consumable( + *, + service_item: ServiceItemType | int, + cohort_set: Optional[int] = None, + event_type_set: Optional[int] = None, + mentorship_service_set: Optional[int] = None, +) -> ConsumableType: + + if cohort_set: + cohort_set = resource(CohortSet, id=cohort_set) + + if event_type_set: + event_type_set = resource(EventTypeSet, id=event_type_set) + + if mentorship_service_set: + mentorship_service_set = resource(MentorshipServiceSet, id=mentorship_service_set) + + if isinstance(service_item, int): + service_item = get_service_item(service_item) + + return { + "service_item": service_item, + "cohort_set": cohort_set, + "event_type_set": event_type_set, + "mentorship_service_set": mentorship_service_set, + } diff --git a/breathecode/payments/views.py b/breathecode/payments/views.py index c7dfe75fe..62b2d82a6 100644 --- a/breathecode/payments/views.py +++ b/breathecode/payments/views.py @@ -1,5 +1,7 @@ from datetime import timedelta +from capyc.core.shorteners import C +from capyc.rest_framework.exceptions import PaymentException, ValidationException from django.core.cache import cache from django.db import transaction from django.db.models import CharField, Q, Value @@ -78,8 +80,6 @@ from breathecode.utils.decorators.capable_of import capable_of from breathecode.utils.i18n import translation from breathecode.utils.redis import Lock -from capyc.core.shorteners import C -from capyc.rest_framework.exceptions import PaymentException, ValidationException logger = getLogger(__name__) @@ -601,6 +601,9 @@ def get(self, request): "voids": filter_void_consumable_balance(request, items), } + if request.GET.get("virtual") in ["true", "1", "y"]: + actions.set_virtual_balance(balance, request.user) + return Response(balance) From 261af9b711cf339791313d25588487cdfef909e1 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 25 Oct 2024 15:55:36 -0500 Subject: [PATCH 03/23] modify bypass logic and instrumentalize pagination issues --- .../mentorship/permissions/consumers.py | 2 +- breathecode/middlewares.py | 50 +- breathecode/monitoring/admin.py | 13 + .../migrations/0024_nopagination.py | 21 + breathecode/monitoring/models.py | 5 + breathecode/payments/data.py | 3 + breathecode/payments/flags.py | 107 +++- breathecode/payments/models.py | 15 +- .../tests/flags/tests_bypass_consumption.py | 561 ++++++++++++++++++ breathecode/settings.py | 1 + breathecode/utils/decorators/consume.py | 10 +- .../utils/tests/decorators/tests_consume.py | 10 +- 12 files changed, 769 insertions(+), 29 deletions(-) create mode 100644 breathecode/monitoring/migrations/0024_nopagination.py create mode 100644 breathecode/payments/tests/flags/tests_bypass_consumption.py diff --git a/breathecode/mentorship/permissions/consumers.py b/breathecode/mentorship/permissions/consumers.py index 4d3419b4e..c8d940ecd 100644 --- a/breathecode/mentorship/permissions/consumers.py +++ b/breathecode/mentorship/permissions/consumers.py @@ -113,7 +113,7 @@ def mentorship_service_by_url_param(context: ServiceContext, args: tuple, kwargs ) ) ): - c = feature.context(context=context, user=mentee) + c = feature.context(context=context, args=args, kwargs=kwargs, user=mentee) if feature.is_enabled("payments.bypass_consumption", c, False) is False: raise ValidationException( translation( diff --git a/breathecode/middlewares.py b/breathecode/middlewares.py index c1e8309b5..486f1082e 100644 --- a/breathecode/middlewares.py +++ b/breathecode/middlewares.py @@ -6,8 +6,8 @@ import brotli import zstandard -from asgiref.sync import iscoroutinefunction -from django.http import HttpResponseRedirect +from asgiref.sync import iscoroutinefunction, sync_to_async +from django.http import HttpRequest, HttpResponseRedirect from django.utils.decorators import sync_and_async_middleware from django.utils.deprecation import MiddlewareMixin @@ -148,3 +148,49 @@ def middleware(request): return response return middleware + + +NO_PAGINATED = set() + + +@sync_and_async_middleware +def detect_pagination_issues_middleware(get_response): + from breathecode.monitoring.models import NoPagination + + def instrument_no_pagination(request: HttpRequest) -> None: + if request.method not in ["GET"]: + return + + path = request.path + method = request.method + + if (path, method) in NO_PAGINATED: + return + + is_paginated = request.GET.get("limit") and request.GET.get("offset") + + if is_paginated is False and NoPagination.objects.filter(path=path, method=method).exists() is False: + NO_PAGINATED.add((path, method)) + NoPagination.objects.create(path=path, method=method) + + @sync_to_async + def ainstrument_no_pagination(request: HttpRequest) -> None: + instrument_no_pagination(request) + + if iscoroutinefunction(get_response): + + async def middleware(request: HttpRequest): + await ainstrument_no_pagination(request) + + response = await get_response(request) + return response + + else: + + def middleware(request: HttpRequest): + instrument_no_pagination(request) + + response = get_response(request) + return response + + return middleware diff --git a/breathecode/monitoring/admin.py b/breathecode/monitoring/admin.py index fd8ed7883..1a2532969 100644 --- a/breathecode/monitoring/admin.py +++ b/breathecode/monitoring/admin.py @@ -16,6 +16,7 @@ CSVUpload, Endpoint, MonitorScript, + NoPagination, RepositorySubscription, RepositoryWebhook, Supervisor, @@ -315,3 +316,15 @@ class SupervisorIssueAdmin(admin.ModelAdmin): list_filter = ["supervisor"] search_fields = ["supervisor__task_module", "supervisor__task_name"] actions = [] + + +def delete_all(modeladmin, request, queryset): + NoPagination.objects.all().delete() + + +@admin.register(NoPagination) +class NoPaginationAdmin(admin.ModelAdmin): + list_display = ("path", "method") + list_filter = ["method"] + search_fields = ["path", "method"] + actions = [delete_all] diff --git a/breathecode/monitoring/migrations/0024_nopagination.py b/breathecode/monitoring/migrations/0024_nopagination.py new file mode 100644 index 000000000..d4c4bb73b --- /dev/null +++ b/breathecode/monitoring/migrations/0024_nopagination.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2024-10-25 16:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("monitoring", "0023_repositorysubscription_last_call"), + ] + + operations = [ + migrations.CreateModel( + name="NoPagination", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("path", models.CharField(max_length=255)), + ("method", models.CharField(max_length=9)), + ], + ), + ] diff --git a/breathecode/monitoring/models.py b/breathecode/monitoring/models.py index e564e5e93..ce2cfc44d 100644 --- a/breathecode/monitoring/models.py +++ b/breathecode/monitoring/models.py @@ -346,3 +346,8 @@ def save(self, *args, **kwargs): self.full_clean() return super().save(*args, **kwargs) + + +class NoPagination(models.Model): + path = models.CharField(max_length=255) + method = models.CharField(max_length=9) diff --git a/breathecode/payments/data.py b/breathecode/payments/data.py index e5471e09c..2697fc29b 100644 --- a/breathecode/payments/data.py +++ b/breathecode/payments/data.py @@ -1,8 +1,11 @@ +from functools import lru_cache + from breathecode.payments.utils import ConsumableType, consumable, service_item __all__ = ["get_virtual_consumables"] +@lru_cache(maxsize=1) def get_virtual_consumables() -> list[ConsumableType]: return [ consumable( diff --git a/breathecode/payments/flags.py b/breathecode/payments/flags.py index 56a6ef509..d54ce2483 100644 --- a/breathecode/payments/flags.py +++ b/breathecode/payments/flags.py @@ -1,27 +1,128 @@ from typing import Optional from capyc.core.managers import feature +from django.db.models.query_utils import Q +from breathecode.admissions.models import Cohort +from breathecode.assignments.models import Task from breathecode.authenticate.models import User +from breathecode.events.models import Event, LiveClass +from breathecode.payments.models import CohortSet, MentorshipServiceSet +from breathecode.registry.models import Asset from breathecode.utils.decorators.consume import ServiceContext flags = feature.flags @feature.availability("payments.bypass_consumption") -def bypass_consumption(context: ServiceContext, user: Optional[User] = None) -> bool: +def bypass_consumption(context: ServiceContext, kwargs: Optional[dict] = None, user: Optional[User] = None) -> bool: """ This flag is used to bypass the consumption of a service. Arguments: context: ServiceContext - The context of the service. + args: Optional[tuple] - The arguments of the service. + kwargs: Optional[dict] - The keyword arguments of the service. user: Optional[User] - The user to bypass the consumption for, if none it will use request.user. """ + from breathecode.payments.data import get_virtual_consumables + + if kwargs is None: + kwargs = {} + + if flags.get("BYPASS_CONSUMPTION") in feature.TRUE: + return True + + virtual_consumables = get_virtual_consumables() + user = user or context["request"].user + + event_id = kwargs.get("event_id") + event_slug = kwargs.get("event_slug") + if context["service"] == "event_join" and (event_id or event_slug): + + pk = Q(id=event_id) | Q(slug=event_slug, slug__isnull=False) + event_type_set_ids = [ + consumable["event_type_set"]["id"] for consumable in virtual_consumables if consumable["event_type_set"] + ] + + event = Event.objects.filter(pk, event_type__eventtypeset__in=event_type_set_ids).first() + if not event: + return False + + if event.academy and event.academy.available_as_saas: + return False + + return True + + hash = kwargs.get("hash") + if context["service"] == "live_class_join" and (hash := kwargs.get("hash")): + live_class = LiveClass.objects.filter(hash=hash).first() + if live_class is None: + return False + + cohort = live_class.cohort_time_slot.cohort + if cohort.available_as_saas is True or ( + cohort.available_as_saas is None and cohort.academy.available_as_saas is True + ): + return False + + cohort_set_ids = [consumable["cohort_set"]["id"] for consumable in virtual_consumables] + if CohortSet.objects.filter(cohorts=cohort, id__in=cohort_set_ids).exists(): + return True - if flags.get("BYPASS_CONSUMPTION") not in feature.TRUE: return False - # write logic here + if context["service"] == "join_mentorship" and (service_slug := kwargs.get("service_slug")): + + mentorship_service_set_ids = [ + consumable["mentorship_service_set"]["id"] + for consumable in virtual_consumables + if consumable["mentorship_service_set"] + ] + + if MentorshipServiceSet.objects.filter( + mentorship_services__slug=service_slug, + mentorship_services__academy__available_as_saas=False, + id__in=mentorship_service_set_ids, + ).exists(): + return True + + return False + + if context["service"] == "add_code_review" and (task_id := kwargs.get("task_id")): + cohort_set_ids = [ + consumable["cohort_set"]["id"] for consumable in virtual_consumables if consumable["cohort_set"] + ] + + task = Task.objects.filter(id=task_id, cohort__cohortset__id__in=cohort_set_ids).first() + if task is None: + return False + + if task.cohort.available_as_saas is True or ( + task.cohort.available_as_saas is None and task.cohort.academy.available_as_saas is True + ): + return False + + return True + + if context["service"] == "read_lesson" and (asset_slug := kwargs.get("asset_slug")): + cohort_set_ids = [ + consumable["cohort_set"]["id"] for consumable in virtual_consumables if consumable["cohort_set"] + ] + request = context["request"] + asset = Asset.get_by_slug(asset_slug, request) + if asset is None: + return False + + if Cohort.objects.filter( + Q(available_as_saas=False) | Q(available_as_saas=None, academy__available_as_saas=False), + cohortuser__user=user, + syllabus_version__json__icontains=f'"{asset_slug}"', + cohortset__id__in=cohort_set_ids, + ).exists(): + return True + + return False return False diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 9479bacc5..67bd33422 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -7,6 +7,7 @@ from typing import Any, Optional from asgiref.sync import sync_to_async +from capyc.rest_framework.exceptions import ValidationException from currencies import Currency as CurrencyFormatter from django import forms from django.contrib.auth.models import Group, Permission, User @@ -24,7 +25,6 @@ from breathecode.payments import signals from breathecode.utils.i18n import translation from breathecode.utils.validators.language import validate_language_code -from capyc.rest_framework.exceptions import ValidationException # https://devdocs.prestashop-project.org/1.7/webservice/resources/warehouses/ @@ -328,19 +328,6 @@ class CohortSet(models.Model): Cohort, blank=True, through="CohortSetCohort", through_fields=("cohort_set", "cohort") ) - def clean(self) -> None: - if self.academy.available_as_saas == False: - raise forms.ValidationError( - translation( - self._lang, - en="Academy is not available as SaaS", - es="La academia no está disponible como SaaS", - slug="academy-not-available-as-saas", - ) - ) - - return super().clean() - def save(self, *args, **kwargs) -> None: self.full_clean() return super().save(*args, **kwargs) diff --git a/breathecode/payments/tests/flags/tests_bypass_consumption.py b/breathecode/payments/tests/flags/tests_bypass_consumption.py new file mode 100644 index 000000000..aaf9058af --- /dev/null +++ b/breathecode/payments/tests/flags/tests_bypass_consumption.py @@ -0,0 +1,561 @@ +from datetime import datetime +from typing import Any, Callable, Optional, Tuple, Unpack +from unittest.mock import MagicMock + +import capyc.pytest as capy +import pytest +from adrf.requests import AsyncRequest +from adrf.test import AsyncAPIRequestFactory +from capyc.core.managers import feature +from django.contrib.auth.models import User +from django.core.handlers.wsgi import WSGIRequest +from django.db.models import QuerySet, Sum +from django.urls.base import reverse_lazy +from rest_framework.test import APIRequestFactory + +from breathecode.payments.models import Consumable +from breathecode.payments.utils import ConsumableType, consumable, service_item +from breathecode.utils.decorators.consume import ServiceContext + + +@pytest.fixture(autouse=True) +def setup(db: None, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr("breathecode.events.models.LiveClass._get_hash", lambda self: "abc") + + yield + + +def build_context( + request: WSGIRequest | AsyncRequest, + service: str, + utc_now: datetime, + consumables: QuerySet[Consumable] = Consumable.objects.none(), + flags: Optional[dict[str, Any]] = None, + **opts: Unpack[ServiceContext], +) -> ServiceContext: + + if flags is None: + flags = {} + + return { + "utc_now": utc_now, + "consumer": None, + "service": service, + "request": request, + "consumables": consumables, + "lifetime": None, + "price": 1, + "is_consumption_session": False, + "flags": flags, + **opts, + } + + +PatchBypassConsumption = Callable[[Tuple[ConsumableType, ...]], list[ConsumableType]] + + +@pytest.fixture +def patch_bypass_consumption(monkeypatch: pytest.MonkeyPatch): + def wrapper(*args: ConsumableType) -> list[ConsumableType]: + monkeypatch.setattr("breathecode.payments.data.get_virtual_consumables", MagicMock(return_value=[*args])) + + yield wrapper + + +PatchFlag = Callable[[str], None] + + +@pytest.fixture(autouse=True) +def patch_flag(monkeypatch: pytest.MonkeyPatch): + def wrapper(v: str) -> None: + monkeypatch.setattr("breathecode.payments.flags.flags", {"BYPASS_CONSUMPTION": v}) + + wrapper("0") + + yield wrapper + + +def test_flag_true(database: capy.Database, patch_flag: PatchFlag): + patch_flag("1") + model = database.create(user=1) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {} + service_context = ServiceContext( + # consumer=False, + service="event_join", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is True + + +class TestEventJoin: + @pytest.mark.parametrize( + "models", + [ + { + "event_type": {"description": "abc"}, + "event_type_set": {"event_types": []}, + "service_item": 1, + }, + { + "event": 1, + "event_type": {"description": "abc"}, + "event_type_set": {"event_types": [1]}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + ], + ) + def test_false( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, event_type_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"event_id": 1} + service_context = ServiceContext( + # consumer=False, + service="event_join", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is False + + @pytest.mark.parametrize( + "models", + [ + { + "event": 1, + "event_type": {"description": "abc"}, + "event_type_set": {"event_types": [1]}, + "service_item": 1, + "academy": {"available_as_saas": False}, + }, + ], + ) + def test_true( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, event_type_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"event_id": 1} + service_context = ServiceContext( + # consumer=False, + service="event_join", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is True + + +class TestLiveClassJoin: + @pytest.mark.parametrize( + "models", + [ + { + "cohort": {"available_as_saas": True}, + "cohort_set": {"cohorts": [1]}, + "live_class": {"hash": "abc"}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "cohort": {"available_as_saas": None}, + "cohort_set": {"cohorts": [1]}, + "live_class": {"hash": "abc"}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": []}, + "live_class": {"hash": "abc"}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": [1]}, + "live_class": {"hash": "abcd"}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + ], + ) + def test_false( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, cohort_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"event_id": 1} + service_context = ServiceContext( + # consumer=False, + service="live_class_join", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is False + + @pytest.mark.parametrize( + "models", + [ + { + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": [1]}, + "live_class": {"hash": "abc"}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "cohort": {"available_as_saas": None}, + "cohort_set": {"cohorts": [1]}, + "live_class": {"hash": "abc"}, + "service_item": 1, + "academy": {"available_as_saas": False}, + }, + ], + ) + def test_true( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, cohort_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"hash": "abc"} + service_context = ServiceContext( + # consumer=False, + service="live_class_join", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is True + + +class TestJoinMentorship: + @pytest.mark.parametrize( + "models", + [ + { + "mentorship_service": {"slug": "abc"}, + "mentorship_service_set": {"mentorship_services": [1]}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "mentorship_service": {"slug": "abcd"}, + "mentorship_service_set": {"mentorship_services": [1]}, + "service_item": 1, + "academy": {"available_as_saas": False}, + }, + ], + ) + def test_false( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, mentorship_service_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"service_slug": "abc"} + service_context = ServiceContext( + # consumer=False, + service="join_mentorship", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is False + + @pytest.mark.parametrize( + "models", + [ + { + "mentorship_service": {"slug": "abc"}, + "mentorship_service_set": {"mentorship_services": [1]}, + "service_item": 1, + "academy": {"available_as_saas": False}, + }, + ], + ) + def test_true( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, mentorship_service_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"service_slug": "abc"} + service_context = ServiceContext( + # consumer=False, + service="join_mentorship", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is True + + +class TestAddCodeReview: + @pytest.mark.parametrize( + "models", + [ + { + "task": 1, + "cohort": {"available_as_saas": True}, + "cohort_set": {"cohorts": [1]}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "task": 1, + "cohort": {"available_as_saas": None}, + "cohort_set": {"cohorts": [1]}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "task": 1, + "cohort": {"available_as_saas": True}, + "cohort_set": {"cohorts": []}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + ], + ) + def test_false( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, cohort_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"task_id": 1} + service_context = ServiceContext( + # consumer=False, + service="add_code_review", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is False + + @pytest.mark.parametrize( + "models", + [ + { + "task": 1, + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": [1]}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "task": 1, + "cohort": {"available_as_saas": None}, + "cohort_set": {"cohorts": [1]}, + "service_item": 1, + "academy": {"available_as_saas": False}, + }, + ], + ) + def test_true( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, cohort_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"task_id": 1} + service_context = ServiceContext( + # consumer=False, + service="add_code_review", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is True + + +class TestReadLesson: + @pytest.mark.parametrize( + "models", + [ + { + "asset": {"slug": "abc"}, + "asset_category": 1, + "cohort": {"available_as_saas": True}, + "cohort_set": {"cohorts": [1]}, + "cohort_user": 1, + "syllabus_version": {"json": {"bla": "abc"}}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "asset": {"slug": "abc"}, + "asset_category": 1, + "cohort": {"available_as_saas": None}, + "cohort_set": {"cohorts": [1]}, + "cohort_user": 1, + "syllabus_version": {"json": {"bla": "abc"}}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "asset": {"slug": "abcd"}, + "asset_category": 1, + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": [1]}, + "cohort_user": 1, + "syllabus_version": {"json": {"bla": "abc"}}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "asset": {"slug": "abc"}, + "asset_category": 1, + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": [1]}, + "cohort_user": 1, + "syllabus_version": {"json": {"bla": "abcd"}}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + ], + ) + def test_false( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, cohort_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"asset_slug": "abc"} + service_context = ServiceContext( + # consumer=False, + service="read_lesson", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is False + + @pytest.mark.parametrize( + "models", + [ + { + "asset": {"slug": "abc"}, + "asset_category": 1, + "cohort": {"available_as_saas": False}, + "cohort_set": {"cohorts": [1]}, + "cohort_user": 1, + "syllabus_version": {"json": {"bla": "abc"}}, + "service_item": 1, + "academy": {"available_as_saas": True}, + }, + { + "asset": {"slug": "abc"}, + "asset_category": 1, + "cohort": {"available_as_saas": None}, + "cohort_set": {"cohorts": [1]}, + "cohort_user": 1, + "syllabus_version": {"json": {"bla": "abc"}}, + "service_item": 1, + "academy": {"available_as_saas": False}, + }, + ], + ) + def test_true( + self, database: capy.Database, patch_bypass_consumption: PatchBypassConsumption, models: dict[str, int] + ): + model = database.create(city=1, country=1, user=1, **models) + patch_bypass_consumption(consumable(service_item=1, cohort_set=1)) + + factory = APIRequestFactory() + url = "/my/url" + + request = factory.get(url) + request.user = model.user + + kwargs = {"asset_slug": "abc"} + service_context = ServiceContext( + # consumer=False, + service="read_lesson", + request=request, + ) + + context = feature.context(context=service_context, kwargs=kwargs) + res = feature.is_enabled("payments.bypass_consumption", context=context) + assert res is True diff --git a/breathecode/settings.py b/breathecode/settings.py index f35e3ed04..fcd004de6 100644 --- a/breathecode/settings.py +++ b/breathecode/settings.py @@ -119,6 +119,7 @@ "django.middleware.security.SecurityMiddleware", "breathecode.middlewares.static_redirect_middleware", "breathecode.middlewares.set_service_header_middleware", + "breathecode.middlewares.detect_pagination_issues_middleware", "django.contrib.sessions.middleware.SessionMiddleware", "corsheaders.middleware.CorsMiddleware", # Cache diff --git a/breathecode/utils/decorators/consume.py b/breathecode/utils/decorators/consume.py index 8df59a3b7..0daa382b6 100644 --- a/breathecode/utils/decorators/consume.py +++ b/breathecode/utils/decorators/consume.py @@ -12,7 +12,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.handlers.wsgi import WSGIRequest from django.db.models import QuerySet, Sum -from django.http import HttpRequest, JsonResponse +from django.http import JsonResponse from django.shortcuts import render from django.utils import timezone from rest_framework.response import Response @@ -190,7 +190,7 @@ def consume(service: str, consumer: Optional[Consumer] = None, format: str = "js def decorator(function: callable) -> callable: - def validate_and_get_request(permission: str, args: Any) -> HttpRequest | AsyncRequest: + def validate_and_get_request(permission: str, args: Any) -> WSGIRequest | AsyncRequest: if isinstance(permission, str) == False: raise ProgrammingError("Service must be a string") @@ -210,7 +210,7 @@ def validate_and_get_request(permission: str, args: Any) -> HttpRequest | AsyncR return request def build_context( - request: HttpRequest | AsyncRequest, + request: WSGIRequest | AsyncRequest, utc_now: datetime, flags: Optional[FlagsParams] = None, **opts: Unpack[ServiceContext], @@ -259,7 +259,7 @@ def wrapper(*args, **kwargs): items = Consumable.list(user=request.user, service=service) context["consumables"] = items - flag_context = feature.context(context=context) + flag_context = feature.context(context=context, kwargs=kwargs) bypass_consumption = feature.is_enabled("payments.bypass_consumption", flag_context, False) context["flags"]["bypass_consumption"] = bypass_consumption @@ -371,7 +371,7 @@ async def async_wrapper(*args, **kwargs): items = await Consumable.alist(user=user, service=service) context["consumables"] = items - flag_context = feature.context(context=context) + flag_context = feature.context(context=context, kwargs=kwargs) bypass_consumption = feature.is_enabled("payments.bypass_consumption", flag_context, False) context["flags"]["bypass_consumption"] = bypass_consumption diff --git a/breathecode/utils/tests/decorators/tests_consume.py b/breathecode/utils/tests/decorators/tests_consume.py index 44af072ec..7f740c95f 100644 --- a/breathecode/utils/tests/decorators/tests_consume.py +++ b/breathecode/utils/tests/decorators/tests_consume.py @@ -434,7 +434,7 @@ async def test_with_user__with_group_related_to_permission__consumable__how_many user=user, service=services, service_item={"service_id": 2}, consumable=consumable ) - view, _, _, _ = await make_view_all_cases(user=model.user, decorator_params={}, url_params={}) + view, _, _, kwargs = await make_view_all_cases(user=model.user, decorator_params={}, url_params={}) response, _ = await view() expected = {"detail": "with-consumer-not-enough-consumables", "status_code": 402} @@ -462,7 +462,8 @@ def check_consume_service(): "price": 1, "is_consumption_session": False, "flags": {"bypass_consumption": False}, - } + }, + kwargs=kwargs, ) assert await is_enabled_call_list() == [call("payments.bypass_consumption", context, False)] @@ -481,7 +482,7 @@ async def test_with_user__with_group_related_to_permission__consumable__how_many user=user, service=services, service_item={"service_id": 2}, consumable=consumable ) - view, expected, _, _ = await make_view_all_cases(user=model.user, decorator_params={}, url_params={}) + view, expected, _, kwargs = await make_view_all_cases(user=model.user, decorator_params={}, url_params={}) response, _ = await view() @@ -508,7 +509,8 @@ def check_consume_service(): "price": 1, "is_consumption_session": False, "flags": {"bypass_consumption": True}, - } + }, + kwargs=kwargs, ) assert await is_enabled_call_list() == [call("payments.bypass_consumption", context, False)] From ffc4a58463e3931a44c93fd3802cbe090e8242c3 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 25 Oct 2024 15:59:28 -0500 Subject: [PATCH 04/23] fix a line --- breathecode/mentorship/permissions/consumers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/mentorship/permissions/consumers.py b/breathecode/mentorship/permissions/consumers.py index c8d940ecd..24ee0abeb 100644 --- a/breathecode/mentorship/permissions/consumers.py +++ b/breathecode/mentorship/permissions/consumers.py @@ -113,7 +113,7 @@ def mentorship_service_by_url_param(context: ServiceContext, args: tuple, kwargs ) ) ): - c = feature.context(context=context, args=args, kwargs=kwargs, user=mentee) + c = feature.context(context=context, kwargs=kwargs, user=mentee) if feature.is_enabled("payments.bypass_consumption", c, False) is False: raise ValidationException( translation( From 2edff47843700f587055c0f2134813228df64a51 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 29 Oct 2024 15:02:26 +0100 Subject: [PATCH 05/23] Update data.py --- breathecode/payments/data.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/breathecode/payments/data.py b/breathecode/payments/data.py index 2697fc29b..8f3cc8b12 100644 --- a/breathecode/payments/data.py +++ b/breathecode/payments/data.py @@ -15,9 +15,6 @@ def get_virtual_consumables() -> list[ConsumableType]: mentorship_service_set=1, ), consumable( - service_item=service_item(service=1, unit_type="unit", how_many=1), - cohort_set=1, - event_type_set=1, - mentorship_service_set=1, + service_item=service_item(service=76, unit_type="unit", how_many=10) ), ] From b0af3e681d6982beb15a87adc08991a969c92a08 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 29 Oct 2024 18:25:35 +0100 Subject: [PATCH 06/23] Update data.py --- breathecode/payments/data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/breathecode/payments/data.py b/breathecode/payments/data.py index 8f3cc8b12..dd4e496dc 100644 --- a/breathecode/payments/data.py +++ b/breathecode/payments/data.py @@ -15,6 +15,7 @@ def get_virtual_consumables() -> list[ConsumableType]: mentorship_service_set=1, ), consumable( - service_item=service_item(service=76, unit_type="unit", how_many=10) + service_item=service_item(service=76, unit_type="unit", how_many=10), + cohort_set=1, ), ] From ac4a6f02883239b7e3ca50ed28df43c3a411a86c Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 29 Oct 2024 15:52:24 -0400 Subject: [PATCH 07/23] Update admin.py --- breathecode/assessment/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/admin.py b/breathecode/assessment/admin.py index 34e93a682..6d3d9262c 100644 --- a/breathecode/assessment/admin.py +++ b/breathecode/assessment/admin.py @@ -74,7 +74,7 @@ def change_status_answered(modeladmin, request, queryset): @admin.register(UserAssessment) class UserAssessmentAdmin(admin.ModelAdmin): - search_fields = ["title", "question__assessment__title"] + search_fields = ["title", "assessment__title"] readonly_fields = ("token",) list_display = ["id", "title", "current_status", "lang", "owner", "total_score", "assessment", "academy"] list_filter = ["lang", "status", "academy"] From 1c7cb9ebfc33d25d155d6fb16a76378b7fee1745 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 29 Oct 2024 23:27:40 -0500 Subject: [PATCH 08/23] fix virtual consumables --- breathecode/payments/actions.py | 17 +- .../tests/urls/tests_me_service_consumable.py | 740 +++++++++++++++--- 2 files changed, 651 insertions(+), 106 deletions(-) diff --git a/breathecode/payments/actions.py b/breathecode/payments/actions.py index d4d27bee6..34b1169fa 100644 --- a/breathecode/payments/actions.py +++ b/breathecode/payments/actions.py @@ -1165,15 +1165,22 @@ def set_virtual_balance(balance: ConsumableBalance, user: User) -> None: ] available_event_type_sets = EventTypeSet.objects.filter( - academy__profileacademy__user=user, id__in=event_type_set_ids + academy__available_as_saas=False, + academy__profileacademy__user=user, + id__in=event_type_set_ids, ).values_list("id", flat=True) - available_cohort_sets = CohortSet.objects.filter(cohorts__cohortuser__user=user, id__in=cohort_set_ids).values_list( - "id", flat=True - ) + available_cohort_sets = CohortSet.objects.filter( + academy__available_as_saas=False, + cohorts__cohortuser__user=user, + # cohorts__cohortuser__cohort__academy__available_as_saas=False, + id__in=cohort_set_ids, + ).values_list("id", flat=True) available_mentorship_service_sets = MentorshipServiceSet.objects.filter( - academy__profileacademy__user=user, id__in=mentorship_service_set_ids + academy__available_as_saas=False, + academy__profileacademy__user=user, + id__in=mentorship_service_set_ids, ).values_list("id", flat=True) balance_mapping: dict[str, dict[int, int]] = { diff --git a/breathecode/payments/tests/urls/tests_me_service_consumable.py b/breathecode/payments/tests/urls/tests_me_service_consumable.py index 1453a5198..97c136f75 100644 --- a/breathecode/payments/tests/urls/tests_me_service_consumable.py +++ b/breathecode/payments/tests/urls/tests_me_service_consumable.py @@ -1389,7 +1389,7 @@ def test__append_to_same_balance___cohort_set__with_three_virtual_consumables(se how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) - academy = {"available_as_saas": True} + academy = {"available_as_saas": False} model = self.bc.database.create( user=1, @@ -1478,6 +1478,89 @@ def test__append_to_same_balance___cohort_set__with_three_virtual_consumables(se self.bc.format.to_dict(model.consumable), ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___cohort_set__with_three_virtual_consumables__as_saas(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"resource": "cohort_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + consumables = [{"how_many": random.randint(1, 30), "cohort_set_id": math.floor(n / 3) + 1} for n in range(9)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + cohort_set=3, + cohort_set_cohort=[{"cohort_set_id": 1 + n} for n in range(3)], + academy=academy, + service=(10, {"type": "COHORT_SET"}), + cohort={"available_as_saas": True}, + cohort_user=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.cohort_set[0].id, + "slug": model.cohort_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.cohort_set[1].id, + "slug": model.cohort_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.cohort_set[2].id, + "slug": model.cohort_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + ], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) def test__append_to_same_balance___mentorship_service_set__with_three_virtual_consumables(self, monkeypatch): from breathecode.payments.utils import reset_cache @@ -1516,7 +1599,7 @@ def test__append_to_same_balance___mentorship_service_set__with_three_virtual_co how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) - academy = {"available_as_saas": True} + academy = {"available_as_saas": False} model = self.bc.database.create( user=1, @@ -1604,7 +1687,9 @@ def test__append_to_same_balance___mentorship_service_set__with_three_virtual_co ) @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) - def test__append_to_same_balance___event_type_set__with_three_virtual_consumables(self, monkeypatch): + def test__append_to_same_balance___mentorship_service_set__with_three_virtual_consumables__as_saas( + self, monkeypatch + ): from breathecode.payments.utils import reset_cache reset_cache() @@ -1617,39 +1702,39 @@ def test__append_to_same_balance___event_type_set__with_three_virtual_consumable "breathecode.payments.data.get_virtual_consumables", get_virtual_consumables_mock( *[ - {"resource": "event_type_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} + {"resource": "mentorship_service_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3) ], *[ - {"resource": "event_type_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} + {"resource": "mentorship_service_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3) ], *[ - {"resource": "event_type_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} + {"resource": "mentorship_service_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3) ], ), ) consumables = [ - {"how_many": random.randint(1, 30), "event_type_set_id": math.floor(n / 3) + 1} for n in range(9) + {"how_many": random.randint(1, 30), "mentorship_service_set_id": math.floor(n / 3) + 1} for n in range(9) ] belong_to1 = consumables[:3] belong_to2 = consumables[3:6] belong_to3 = consumables[6:] - how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + sum([rand1 * (1 + n) for n in range(3)]) - how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) - how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) academy = {"available_as_saas": True} model = self.bc.database.create( user=1, consumable=consumables, - event_type_set=3, + mentorship_service_set=3, profile_academy=1, academy=academy, - service=(10, {"type": "EVENT_TYPE_SET"}), + service=(10, {"type": "MENTORSHIP_SERVICE_SET"}), ) self.client.force_authenticate(model.user) @@ -1659,65 +1744,32 @@ def test__append_to_same_balance___event_type_set__with_three_virtual_consumable json = response.json() expected = { - "mentorship_service_sets": [], - "cohort_sets": [], - "event_type_sets": [ + "mentorship_service_sets": [ { "balance": {"unit": how_many_belong_to1}, - "id": model.event_type_set[0].id, - "slug": model.event_type_set[0].slug, - "items": [ - *[serialize_consumable(model.consumable[n]) for n in range(9)], - *[ - { - "id": None, - "how_many": rand1 * (1 + n), - "unit_type": "UNIT", - "valid_until": None, - } - for n in range(3) - ], - ], + "id": model.mentorship_service_set[0].id, + "slug": model.mentorship_service_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], }, { "balance": { "unit": how_many_belong_to2, }, - "id": model.event_type_set[1].id, - "slug": model.event_type_set[1].slug, - "items": [ - *[serialize_consumable(model.consumable[n]) for n in range(9)], - *[ - { - "id": None, - "how_many": rand2 * (1 + n), - "unit_type": "UNIT", - "valid_until": None, - } - for n in range(3) - ], - ], + "id": model.mentorship_service_set[1].id, + "slug": model.mentorship_service_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], }, { "balance": { "unit": how_many_belong_to3, }, - "id": model.event_type_set[2].id, - "slug": model.event_type_set[2].slug, - "items": [ - *[serialize_consumable(model.consumable[n]) for n in range(9)], - *[ - { - "id": None, - "how_many": rand3 * (1 + n), - "unit_type": "UNIT", - "valid_until": None, - } - for n in range(3) - ], - ], + "id": model.mentorship_service_set[2].id, + "slug": model.mentorship_service_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], }, ], + "cohort_sets": [], + "event_type_sets": [], "voids": [], } @@ -1729,7 +1781,7 @@ def test__append_to_same_balance___event_type_set__with_three_virtual_consumable ) @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) - def test__append_to_same_balance___service__with_three_virtual_consumables(self, monkeypatch): + def test__append_to_same_balance___event_type_set__with_three_virtual_consumables(self, monkeypatch): from breathecode.payments.utils import reset_cache reset_cache() @@ -1741,14 +1793,23 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, monkeypatch.setattr( "breathecode.payments.data.get_virtual_consumables", get_virtual_consumables_mock( - *[{"service": 1, "how_many": rand1 * (1 + n)} for n in range(3)], - *[{"service": 2, "how_many": rand2 * (1 + n)} for n in range(3)], - *[{"service": 3, "how_many": rand3 * (1 + n)} for n in range(3)], + *[ + {"resource": "event_type_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], ), ) - - consumables = [{"how_many": random.randint(1, 30), "service_item_id": math.floor(n / 3) + 1} for n in range(9)] - service_items = [{"service_id": n + 1} for n in range(3)] + consumables = [ + {"how_many": random.randint(1, 30), "event_type_set_id": math.floor(n / 3) + 1} for n in range(9) + ] belong_to1 = consumables[:3] belong_to2 = consumables[3:6] belong_to3 = consumables[6:] @@ -1757,30 +1818,33 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + academy = {"available_as_saas": False} + model = self.bc.database.create( user=1, consumable=consumables, - service_item=service_items, - service=[{"type": "VOID"} for _ in range(3)], + event_type_set=3, + profile_academy=1, + academy=academy, + service=(10, {"type": "EVENT_TYPE_SET"}), ) - self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" response = self.client.get(url) + self.client.force_authenticate(model.user) json = response.json() expected = { "mentorship_service_sets": [], "cohort_sets": [], - "event_type_sets": [], - "voids": [ + "event_type_sets": [ { "balance": {"unit": how_many_belong_to1}, - "id": model.service[0].id, - "slug": model.service[0].slug, + "id": model.event_type_set[0].id, + "slug": model.event_type_set[0].slug, "items": [ - *[serialize_consumable(consumable) for consumable in model.consumable[:3]], + *[serialize_consumable(model.consumable[n]) for n in range(9)], *[ { "id": None, @@ -1796,10 +1860,10 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, "balance": { "unit": how_many_belong_to2, }, - "id": model.service[1].id, - "slug": model.service[1].slug, + "id": model.event_type_set[1].id, + "slug": model.event_type_set[1].slug, "items": [ - *[serialize_consumable(consumable) for consumable in model.consumable[3:6]], + *[serialize_consumable(model.consumable[n]) for n in range(9)], *[ { "id": None, @@ -1815,10 +1879,10 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, "balance": { "unit": how_many_belong_to3, }, - "id": model.service[2].id, - "slug": model.service[2].slug, + "id": model.event_type_set[2].id, + "slug": model.event_type_set[2].slug, "items": [ - *[serialize_consumable(consumable) for consumable in model.consumable[6:]], + *[serialize_consumable(model.consumable[n]) for n in range(9)], *[ { "id": None, @@ -1831,6 +1895,7 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, ], }, ], + "voids": [], } assert json == expected @@ -1840,12 +1905,8 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, self.bc.format.to_dict(model.consumable), ) - """ - 🔽🔽🔽 Virtual Consumables, append to a new balance - """ - @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) - def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(self, monkeypatch): + def test__append_to_same_balance___event_type_set__with_three_virtual_consumables__as_saas(self, monkeypatch): from breathecode.payments.utils import reset_cache reset_cache() @@ -1857,12 +1918,23 @@ def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(sel monkeypatch.setattr( "breathecode.payments.data.get_virtual_consumables", get_virtual_consumables_mock( - *[{"resource": "cohort_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3)], - *[{"resource": "cohort_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3)], - *[{"resource": "cohort_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3)], + *[ + {"resource": "event_type_set", "id": 1, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 2, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 3, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], ), ) - consumables = [{"how_many": random.randint(1, 30), "cohort_set_id": math.floor(n / 3) + 1} for n in range(9)] + consumables = [ + {"how_many": random.randint(1, 30), "event_type_set_id": math.floor(n / 3) + 1} for n in range(9) + ] belong_to1 = consumables[:3] belong_to2 = consumables[3:6] belong_to3 = consumables[6:] @@ -1876,12 +1948,10 @@ def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(sel model = self.bc.database.create( user=1, consumable=consumables, - cohort_set=6, - cohort_set_cohort=[{"cohort_set_id": 4 + n} for n in range(3)], + event_type_set=3, + profile_academy=1, academy=academy, - service=(10, {"type": "COHORT_SET"}), - cohort={"available_as_saas": True}, - cohort_user=1, + service=(10, {"type": "EVENT_TYPE_SET"}), ) self.client.force_authenticate(model.user) @@ -1892,20 +1962,219 @@ def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(sel json = response.json() expected = { "mentorship_service_sets": [], - "cohort_sets": [ + "cohort_sets": [], + "event_type_sets": [ { "balance": {"unit": how_many_belong_to1}, - "id": model.cohort_set[0].id, - "slug": model.cohort_set[0].slug, + "id": model.event_type_set[0].id, + "slug": model.event_type_set[0].slug, "items": [serialize_consumable(model.consumable[n]) for n in range(9)], }, { "balance": { "unit": how_many_belong_to2, }, - "id": model.cohort_set[1].id, - "slug": model.cohort_set[1].slug, - "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + "id": model.event_type_set[1].id, + "slug": model.event_type_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.event_type_set[2].id, + "slug": model.event_type_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + ], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___service__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"service": 1, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"service": 2, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"service": 3, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + + consumables = [{"how_many": random.randint(1, 30), "service_item_id": math.floor(n / 3) + 1} for n in range(9)] + service_items = [{"service_id": n + 1} for n in range(3)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + sum([rand1 * (1 + n) for n in range(3)]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + sum([rand2 * (1 + n) for n in range(3)]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + sum([rand3 * (1 + n) for n in range(3)]) + + model = self.bc.database.create( + user=1, + consumable=consumables, + service_item=service_items, + service=[{"type": "VOID"} for _ in range(3)], + ) + + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [], + "voids": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.service[0].id, + "slug": model.service[0].slug, + "items": [ + *[serialize_consumable(consumable) for consumable in model.consumable[:3]], + *[ + { + "id": None, + "how_many": rand1 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.service[1].id, + "slug": model.service[1].slug, + "items": [ + *[serialize_consumable(consumable) for consumable in model.consumable[3:6]], + *[ + { + "id": None, + "how_many": rand2 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.service[2].id, + "slug": model.service[2].slug, + "items": [ + *[serialize_consumable(consumable) for consumable in model.consumable[6:]], + *[ + { + "id": None, + "how_many": rand3 * (1 + n), + "unit_type": "UNIT", + "valid_until": None, + } + for n in range(3) + ], + ], + }, + ], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + + """ + 🔽🔽🔽 Virtual Consumables, append to a new balance + """ + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"resource": "cohort_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + consumables = [{"how_many": random.randint(1, 30), "cohort_set_id": math.floor(n / 3) + 1} for n in range(9)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": False} + + model = self.bc.database.create( + user=1, + consumable=consumables, + cohort_set=6, + cohort_set_cohort=[{"cohort_set_id": 4 + n} for n in range(3)], + academy=academy, + service=(10, {"type": "COHORT_SET"}), + cohort={"available_as_saas": True}, + cohort_user=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.cohort_set[0].id, + "slug": model.cohort_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.cohort_set[1].id, + "slug": model.cohort_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], }, { "balance": { @@ -1976,6 +2245,89 @@ def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(sel self.bc.format.to_dict(model.consumable), ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___cohort_set__with_three_virtual_consumables__as_saas(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"resource": "cohort_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"resource": "cohort_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + consumables = [{"how_many": random.randint(1, 30), "cohort_set_id": math.floor(n / 3) + 1} for n in range(9)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + cohort_set=6, + cohort_set_cohort=[{"cohort_set_id": 4 + n} for n in range(3)], + academy=academy, + service=(10, {"type": "COHORT_SET"}), + cohort={"available_as_saas": True}, + cohort_user=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.cohort_set[0].id, + "slug": model.cohort_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.cohort_set[1].id, + "slug": model.cohort_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.cohort_set[2].id, + "slug": model.cohort_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + ], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) def test__append_to_new_balance___event_type_set__with_three_virtual_consumables(self, monkeypatch): from breathecode.payments.utils import reset_cache @@ -2014,7 +2366,7 @@ def test__append_to_new_balance___event_type_set__with_three_virtual_consumables how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) - academy = {"available_as_saas": True} + academy = {"available_as_saas": False} model = self.bc.database.create( user=1, @@ -2117,6 +2469,98 @@ def test__append_to_new_balance___event_type_set__with_three_virtual_consumables self.bc.format.to_dict(model.consumable), ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___event_type_set__with_three_virtual_consumables__as_saas(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[ + {"resource": "event_type_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "event_type_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], + ), + ) + consumables = [ + {"how_many": random.randint(1, 30), "event_type_set_id": math.floor(n / 3) + 1} for n in range(9) + ] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + event_type_set=6, + academy=academy, + service=(10, {"type": "EVENT_TYPE_SET"}), + profile_academy=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.event_type_set[0].id, + "slug": model.event_type_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.event_type_set[1].id, + "slug": model.event_type_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.event_type_set[2].id, + "slug": model.event_type_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + ], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) def test__append_to_new_balance___mentorship_service_set__with_three_virtual_consumables(self, monkeypatch): from breathecode.payments.utils import reset_cache @@ -2155,7 +2599,7 @@ def test__append_to_new_balance___mentorship_service_set__with_three_virtual_con how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) - academy = {"available_as_saas": True} + academy = {"available_as_saas": False} model = self.bc.database.create( user=1, @@ -2258,6 +2702,100 @@ def test__append_to_new_balance___mentorship_service_set__with_three_virtual_con self.bc.format.to_dict(model.consumable), ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___mentorship_service_set__with_three_virtual_consumables__as_saas( + self, monkeypatch + ): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[ + {"resource": "mentorship_service_set", "id": 4, "service": 2 + n, "how_many": rand1 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "mentorship_service_set", "id": 5, "service": 5 + n, "how_many": rand2 * (1 + n)} + for n in range(3) + ], + *[ + {"resource": "mentorship_service_set", "id": 6, "service": 8 + n, "how_many": rand3 * (1 + n)} + for n in range(3) + ], + ), + ) + consumables = [ + {"how_many": random.randint(1, 30), "mentorship_service_set_id": math.floor(n / 3) + 1} for n in range(9) + ] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": True} + + model = self.bc.database.create( + user=1, + consumable=consumables, + mentorship_service_set=6, + academy=academy, + service=(10, {"type": "MENTORSHIP_SERVICE_SET"}), + profile_academy=1, + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + response = self.client.get(url) + self.client.force_authenticate(model.user) + + json = response.json() + expected = { + "mentorship_service_sets": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.mentorship_service_set[0].id, + "slug": model.mentorship_service_set[0].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.mentorship_service_set[1].id, + "slug": model.mentorship_service_set[1].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.mentorship_service_set[2].id, + "slug": model.mentorship_service_set[2].slug, + "items": [serialize_consumable(model.consumable[n]) for n in range(9)], + }, + ], + "cohort_sets": [], + "event_type_sets": [], + "voids": [], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) def test__append_to_new_balance___service__with_three_virtual_consumables(self, monkeypatch): from breathecode.payments.utils import reset_cache @@ -2287,7 +2825,7 @@ def test__append_to_new_balance___service__with_three_virtual_consumables(self, how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) - academy = {"available_as_saas": True} + academy = {"available_as_saas": False} model = self.bc.database.create( user=1, From fc664ebdaf74882ce8206d7cd1d8a75d089b664f Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Wed, 30 Oct 2024 12:04:39 +0000 Subject: [PATCH 09/23] add endpoint to fetch asset context --- breathecode/registry/serializers.py | 6 +++ .../tests/urls/v1/tests_asset_context.py | 37 +++++++++++++++++++ breathecode/registry/urls/v1.py | 2 + breathecode/registry/views.py | 28 ++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 breathecode/registry/tests/urls/v1/tests_asset_context.py diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index 8a8c75cfa..472531f5f 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -514,6 +514,12 @@ class AssetCategorySerializer(serpy.Serializer): academy = AcademySmallSerializer() +class AssetContextSerializer(serpy.Serializer): + id = serpy.Field() + asset = AssetTinySerializer() + ai_context = serpy.Field() + + class _Keyword(serpy.Serializer): id = serpy.Field() slug = serpy.Field() diff --git a/breathecode/registry/tests/urls/v1/tests_asset_context.py b/breathecode/registry/tests/urls/v1/tests_asset_context.py new file mode 100644 index 000000000..082cd8442 --- /dev/null +++ b/breathecode/registry/tests/urls/v1/tests_asset_context.py @@ -0,0 +1,37 @@ +from unittest.mock import MagicMock, patch + +import pytest +from django.urls.base import reverse_lazy +from django.utils import timezone + +from breathecode.tests.mixins.breathecode_mixin import Breathecode +from breathecode.utils.api_view_extensions.extensions import lookup_extension + +UTC_NOW = timezone.now() + +# enable this file to use the database +pytestmark = pytest.mark.usefixtures("db") + + +def get_serializer(asset_context, data={}): + + return { + "id": asset_context.id, + "ai_context": asset_context.ai_context, + "category": { + "id": asset_context.asset.id, + "slug": asset_context.asset.slug, + "title": asset_context.asset.title, + }, + **data, + } + + +def test_with_no_context(bc: Breathecode, client): + + url = reverse_lazy("registry:asset_context", kwargs={"asset_id": 1}) + response = client.get(url) + json = response.json() + + assert json == {"detail": "context-not-found", "status_code": 404} + assert response.status_code == 404 diff --git a/breathecode/registry/urls/v1.py b/breathecode/registry/urls/v1.py index a0470b2c5..6d4f449e6 100644 --- a/breathecode/registry/urls/v1.py +++ b/breathecode/registry/urls/v1.py @@ -26,6 +26,7 @@ render_preview_html, render_readme, AssetSupersedesView, + AssetContextView, ) app_name = "registry" @@ -39,6 +40,7 @@ path("asset//github/config", get_config), path("asset/.", render_readme), path("asset/", AssetView.as_view()), + path("asset//context", AssetContextView.as_view(), name="asset_context"), path("academy/contentvariable", AcademyContentVariableView.as_view()), path("academy/contentvariable/", AcademyContentVariableView.as_view()), path("academy/asset", AcademyAssetView.as_view(), name="academy_asset"), diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index d8e62f69f..5a6cd5d34 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -47,6 +47,7 @@ AssetImage, AssetKeyword, AssetTechnology, + AssetContext, ContentVariable, KeywordCluster, OriginalityScan, @@ -84,6 +85,7 @@ SEOReportSerializer, TechnologyPUTSerializer, VariableSmallSerializer, + AssetContextSerializer, ) from .tasks import async_pull_from_github from .utils import is_url @@ -719,6 +721,32 @@ def get(self, request, asset_slug=None): return handler.response(serializer.data) +class AssetContextView(APIView, GenerateLookupsMixin): + """ + List all snippets, or create a new snippet. + """ + + permission_classes = [AllowAny] + extensions = APIViewExtensions(cache=AssetCache, paginate=True) + + def get(self, request, asset_id=None): + handler = self.extensions(request) + + cache = handler.cache.get() + if cache is not None: + return cache + + asset_context = AssetContext.objects.filter(asset__id=asset_id).first() + if asset_context is None: + raise ValidationException( + f"No context found for asset {asset_id}", status.HTTP_404_NOT_FOUND, slug="context-not-found" + ) + + serializer = AssetContextSerializer(asset_context, many=False) + + return handler.response(serializer.data) + + # Create your views here. class AcademyAssetActionView(APIView): """ From 02a589df7fe98809c0b11147fa850fcd6b20e376 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 30 Oct 2024 14:29:03 -0500 Subject: [PATCH 10/23] skip virtual calculation on no saas student --- breathecode/admissions/actions.py | 4 +- ...o_saas_student_up_to_date_in_any_cohort.py | 113 ++++++++ breathecode/payments/actions.py | 21 +- .../tests/urls/tests_me_service_consumable.py | 268 ++++++++++++++++-- 4 files changed, 364 insertions(+), 42 deletions(-) create mode 100644 breathecode/admissions/tests/actions/tests_is_no_saas_student_up_to_date_in_any_cohort.py diff --git a/breathecode/admissions/actions.py b/breathecode/admissions/actions.py index 76acc08c6..c472a93fd 100644 --- a/breathecode/admissions/actions.py +++ b/breathecode/admissions/actions.py @@ -308,7 +308,7 @@ def validate(_type, _log, day, index): def is_no_saas_student_up_to_date_in_any_cohort( - user: User, cohort: Optional[Cohort] = None, academy: Optional[Cohort] = None + user: User, cohort: Optional[Cohort] = None, academy: Optional[Cohort] = None, default: bool = True ) -> str: no_available_as_saas = Q(cohort__available_as_saas=False) | Q( cohort__available_as_saas=None, cohort__academy__available_as_saas=False @@ -337,4 +337,4 @@ def is_no_saas_student_up_to_date_in_any_cohort( return False # if no cohorts were found, we assume that the user is up to date - return True + return default diff --git a/breathecode/admissions/tests/actions/tests_is_no_saas_student_up_to_date_in_any_cohort.py b/breathecode/admissions/tests/actions/tests_is_no_saas_student_up_to_date_in_any_cohort.py new file mode 100644 index 000000000..52eba1603 --- /dev/null +++ b/breathecode/admissions/tests/actions/tests_is_no_saas_student_up_to_date_in_any_cohort.py @@ -0,0 +1,113 @@ +""" +Test mentorhips +""" + +import capyc.pytest as capy +import pytest + +from ...actions import is_no_saas_student_up_to_date_in_any_cohort + + +@pytest.fixture(autouse=True) +def setup(db: None): + yield + + +@pytest.mark.parametrize("default", [True, False]) +def test_default(database: capy.Database, default: bool): + model = database.create(user=1) + res = is_no_saas_student_up_to_date_in_any_cohort(model.user, default=default) + assert res == default + + +class TestNoSaasStudent: + + @pytest.mark.parametrize("educational_status", ["ACTIVE", "GRADUATED"]) + @pytest.mark.parametrize("finantial_status", ["FULLY_PAID", "UP_TO_DATE"]) + @pytest.mark.parametrize( + "extra", + [ + {"cohort": {"available_as_saas": False}, "academy": {"available_as_saas": True}}, + {"cohort": {"available_as_saas": None}, "academy": {"available_as_saas": False}}, + ], + ) + def test_true(self, database: capy.Database, educational_status: str, finantial_status: str, extra: dict): + model = database.create( + user=1, + city=1, + country=1, + cohort_user={"educational_status": educational_status, "finantial_status": finantial_status}, + **extra, + ) + res = is_no_saas_student_up_to_date_in_any_cohort(model.user, default=False) + assert res == True + + @pytest.mark.parametrize("educational_status", ["ACTIVE"]) + @pytest.mark.parametrize("finantial_status", ["LATE"]) + @pytest.mark.parametrize( + "extra", + [ + {"cohort": {"available_as_saas": False}, "academy": {"available_as_saas": True}}, + {"cohort": {"available_as_saas": None}, "academy": {"available_as_saas": False}}, + ], + ) + def test_false(self, database: capy.Database, educational_status: str, finantial_status: str, extra: dict): + model = database.create( + user=1, + city=1, + country=1, + cohort_user={"educational_status": educational_status, "finantial_status": finantial_status}, + **extra, + ) + res = is_no_saas_student_up_to_date_in_any_cohort(model.user, default=True) + assert res == False + + +@pytest.mark.parametrize("default", [True, False]) +def test_default(database: capy.Database, default: bool): + model = database.create(user=1) + res = is_no_saas_student_up_to_date_in_any_cohort(model.user, default=default) + assert res == default + + +class TestSaasStudent: + + @pytest.mark.parametrize("educational_status", ["ACTIVE", "GRADUATED"]) + @pytest.mark.parametrize("finantial_status", ["FULLY_PAID", "UP_TO_DATE"]) + @pytest.mark.parametrize( + "extra", + [ + {"cohort": {"available_as_saas": True}, "academy": {"available_as_saas": False}}, + {"cohort": {"available_as_saas": None}, "academy": {"available_as_saas": True}}, + ], + ) + def test_default_false(self, database: capy.Database, educational_status: str, finantial_status: str, extra: dict): + model = database.create( + user=1, + city=1, + country=1, + cohort_user={"educational_status": educational_status, "finantial_status": finantial_status}, + **extra, + ) + res = is_no_saas_student_up_to_date_in_any_cohort(model.user, default=False) + assert res == False + + @pytest.mark.parametrize("educational_status", ["ACTIVE"]) + @pytest.mark.parametrize("finantial_status", ["LATE"]) + @pytest.mark.parametrize( + "extra", + [ + {"cohort": {"available_as_saas": True}, "academy": {"available_as_saas": False}}, + {"cohort": {"available_as_saas": None}, "academy": {"available_as_saas": True}}, + ], + ) + def test_default_true(self, database: capy.Database, educational_status: str, finantial_status: str, extra: dict): + model = database.create( + user=1, + city=1, + country=1, + cohort_user={"educational_status": educational_status, "finantial_status": finantial_status}, + **extra, + ) + res = is_no_saas_student_up_to_date_in_any_cohort(model.user, default=True) + assert res == True diff --git a/breathecode/payments/actions.py b/breathecode/payments/actions.py index 34b1169fa..59d896245 100644 --- a/breathecode/payments/actions.py +++ b/breathecode/payments/actions.py @@ -1148,8 +1148,12 @@ class ConsumableBalance(TypedDict): def set_virtual_balance(balance: ConsumableBalance, user: User) -> None: + from breathecode.admissions.actions import is_no_saas_student_up_to_date_in_any_cohort from breathecode.payments.data import get_virtual_consumables + if is_no_saas_student_up_to_date_in_any_cohort(user, default=False) is False: + return + virtuals = get_virtual_consumables() event_type_set_ids = [virtual["event_type_set"]["id"] for virtual in virtuals if virtual["event_type_set"]] @@ -1165,22 +1169,15 @@ def set_virtual_balance(balance: ConsumableBalance, user: User) -> None: ] available_event_type_sets = EventTypeSet.objects.filter( - academy__available_as_saas=False, - academy__profileacademy__user=user, - id__in=event_type_set_ids, + academy__profileacademy__user=user, id__in=event_type_set_ids ).values_list("id", flat=True) - available_cohort_sets = CohortSet.objects.filter( - academy__available_as_saas=False, - cohorts__cohortuser__user=user, - # cohorts__cohortuser__cohort__academy__available_as_saas=False, - id__in=cohort_set_ids, - ).values_list("id", flat=True) + available_cohort_sets = CohortSet.objects.filter(cohorts__cohortuser__user=user, id__in=cohort_set_ids).values_list( + "id", flat=True + ) available_mentorship_service_sets = MentorshipServiceSet.objects.filter( - academy__available_as_saas=False, - academy__profileacademy__user=user, - id__in=mentorship_service_set_ids, + academy__profileacademy__user=user, id__in=mentorship_service_set_ids ).values_list("id", flat=True) balance_mapping: dict[str, dict[int, int]] = { diff --git a/breathecode/payments/tests/urls/tests_me_service_consumable.py b/breathecode/payments/tests/urls/tests_me_service_consumable.py index 97c136f75..29c5c0966 100644 --- a/breathecode/payments/tests/urls/tests_me_service_consumable.py +++ b/breathecode/payments/tests/urls/tests_me_service_consumable.py @@ -1,7 +1,7 @@ import math import random from typing import Callable, Literal, TypedDict -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch import pytest from aiohttp_retry import Any @@ -1404,8 +1404,11 @@ def test__append_to_same_balance___cohort_set__with_three_virtual_consumables(se self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -1520,8 +1523,11 @@ def test__append_to_same_balance___cohort_set__with_three_virtual_consumables__a self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -1612,8 +1618,11 @@ def test__append_to_same_balance___mentorship_service_set__with_three_virtual_co self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -1739,8 +1748,11 @@ def test__append_to_same_balance___mentorship_service_set__with_three_virtual_co self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -1831,8 +1843,11 @@ def test__append_to_same_balance___event_type_set__with_three_virtual_consumable self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -1956,8 +1971,11 @@ def test__append_to_same_balance___event_type_set__with_three_virtual_consumable self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2036,7 +2054,11 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2109,6 +2131,89 @@ def test__append_to_same_balance___service__with_three_virtual_consumables(self, self.bc.format.to_dict(model.consumable), ) + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_same_balance___service__with_three_virtual_consumables__as_saas(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"service": 1, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"service": 2, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"service": 3, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + + consumables = [{"how_many": random.randint(1, 30), "service_item_id": math.floor(n / 3) + 1} for n in range(9)] + service_items = [{"service_id": n + 1} for n in range(3)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + model = self.bc.database.create( + user=1, + consumable=consumables, + service_item=service_items, + service=[{"type": "VOID"} for _ in range(3)], + ) + + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] + + json = response.json() + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [], + "voids": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.service[0].id, + "slug": model.service[0].slug, + "items": [serialize_consumable(consumable) for consumable in model.consumable[:3]], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.service[1].id, + "slug": model.service[1].slug, + "items": [serialize_consumable(consumable) for consumable in model.consumable[3:6]], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.service[2].id, + "slug": model.service[2].slug, + "items": [serialize_consumable(consumable) for consumable in model.consumable[6:]], + }, + ], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) + """ 🔽🔽🔽 Virtual Consumables, append to a new balance """ @@ -2155,8 +2260,11 @@ def test__append_to_new_balance___cohort_set__with_three_virtual_consumables(sel self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2287,8 +2395,11 @@ def test__append_to_new_balance___cohort_set__with_three_virtual_consumables__as self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2379,8 +2490,11 @@ def test__append_to_new_balance___event_type_set__with_three_virtual_consumables self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2520,8 +2634,11 @@ def test__append_to_new_balance___event_type_set__with_three_virtual_consumables self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2612,8 +2729,11 @@ def test__append_to_new_balance___mentorship_service_set__with_three_virtual_con self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2755,8 +2875,11 @@ def test__append_to_new_balance___mentorship_service_set__with_three_virtual_con self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() expected = { @@ -2837,8 +2960,11 @@ def test__append_to_new_balance___service__with_three_virtual_consumables(self, self.client.force_authenticate(model.user) url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" - response = self.client.get(url) - self.client.force_authenticate(model.user) + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=True + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] json = response.json() consumables = [serialize_consumable(model.consumable[n]) for n in range(9)] @@ -2927,3 +3053,89 @@ def test__append_to_new_balance___service__with_three_virtual_consumables(self, self.bc.database.list_of("payments.Consumable"), self.bc.format.to_dict(model.consumable), ) + + @patch("django.utils.timezone.now", MagicMock(return_value=UTC_NOW)) + def test__append_to_new_balance___service__with_three_virtual_consumables__as_saas(self, monkeypatch): + from breathecode.payments.utils import reset_cache + + reset_cache() + + rand1 = random.randint(1, 9) + rand2 = random.randint(1, 9) + rand3 = random.randint(1, 9) + + monkeypatch.setattr( + "breathecode.payments.data.get_virtual_consumables", + get_virtual_consumables_mock( + *[{"service": 4, "how_many": rand1 * (1 + n)} for n in range(3)], + *[{"service": 5, "how_many": rand2 * (1 + n)} for n in range(3)], + *[{"service": 6, "how_many": rand3 * (1 + n)} for n in range(3)], + ), + ) + + consumables = [{"how_many": random.randint(1, 30), "service_item_id": math.floor(n / 3) + 1} for n in range(9)] + service_items = [{"service_id": n + 1} for n in range(3)] + belong_to1 = consumables[:3] + belong_to2 = consumables[3:6] + belong_to3 = consumables[6:] + + how_many_belong_to1 = sum([x["how_many"] for x in belong_to1]) + how_many_belong_to2 = sum([x["how_many"] for x in belong_to2]) + how_many_belong_to3 = sum([x["how_many"] for x in belong_to3]) + + academy = {"available_as_saas": False} + + model = self.bc.database.create( + user=1, + consumable=consumables, + service_item=service_items, + academy=academy, + service=(6, {"type": "VOID"}), + ) + self.client.force_authenticate(model.user) + + url = reverse_lazy("payments:me_service_consumable") + "?virtual=true" + with patch( + "breathecode.admissions.actions.is_no_saas_student_up_to_date_in_any_cohort", return_value=False + ) as mock: + response = self.client.get(url) + mock.call_args_list == [call(model.user, default=False)] + + json = response.json() + consumables = [serialize_consumable(model.consumable[n]) for n in range(9)] + expected = { + "mentorship_service_sets": [], + "cohort_sets": [], + "event_type_sets": [], + "voids": [ + { + "balance": {"unit": how_many_belong_to1}, + "id": model.service[0].id, + "slug": model.service[0].slug, + "items": consumables[:3], + }, + { + "balance": { + "unit": how_many_belong_to2, + }, + "id": model.service[1].id, + "slug": model.service[1].slug, + "items": consumables[3:6], + }, + { + "balance": { + "unit": how_many_belong_to3, + }, + "id": model.service[2].id, + "slug": model.service[2].slug, + "items": consumables[6:9], + }, + ], + } + + assert json == expected + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + self.bc.database.list_of("payments.Consumable"), + self.bc.format.to_dict(model.consumable), + ) From f87a23f5b243c5362bcb37cbeaf2ddf6eba6ca05 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 30 Oct 2024 20:53:45 -0500 Subject: [PATCH 11/23] migrate to capy core i18n --- Pipfile.lock | 787 ++++++++++-------- breathecode/activity/views.py | 4 +- breathecode/admissions/views.py | 6 +- breathecode/assessment/serializers.py | 4 +- breathecode/assessment/views.py | 5 +- .../assignments/permissions/consumers.py | 5 +- breathecode/assignments/receivers.py | 2 +- breathecode/assignments/views.py | 2 +- breathecode/authenticate/actions.py | 4 +- breathecode/authenticate/serializers.py | 4 +- breathecode/authenticate/tasks.py | 4 +- breathecode/authenticate/views.py | 2 +- breathecode/events/permissions/consumers.py | 4 +- breathecode/events/serializers.py | 4 +- breathecode/events/views.py | 4 +- breathecode/marketing/actions.py | 2 +- breathecode/marketing/views.py | 2 +- breathecode/media/settings.py | 2 +- breathecode/media/tasks.py | 2 +- breathecode/media/utils.py | 2 +- breathecode/media/views.py | 2 +- .../mentorship/permissions/consumers.py | 2 +- breathecode/mentorship/serializers.py | 4 +- breathecode/mentorship/views.py | 2 +- breathecode/monitoring/serializers.py | 4 +- breathecode/monitoring/views.py | 4 +- breathecode/payments/actions.py | 2 +- breathecode/payments/models.py | 2 +- breathecode/payments/services/stripe.py | 4 +- breathecode/payments/tasks.py | 2 +- breathecode/payments/views.py | 2 +- breathecode/provisioning/actions.py | 4 +- breathecode/provisioning/serializers.py | 4 +- breathecode/provisioning/views.py | 6 +- breathecode/registry/permissions/consumers.py | 4 +- .../tests/signals/tests_asset_saved.py | 2 +- breathecode/registry/views.py | 8 +- breathecode/utils/__init__.py | 1 - .../extensions/lookup_extension.py | 4 +- breathecode/utils/i18n.py | 167 ---- breathecode/utils/tests/i18n/__init__.py | 0 .../utils/tests/i18n/tests_translation.py | 113 --- breathecode/utils/validate_conversion_info.py | 2 +- staging/pytest/core/fixtures/http.py | 4 +- 44 files changed, 512 insertions(+), 688 deletions(-) delete mode 100644 breathecode/utils/i18n.py delete mode 100644 breathecode/utils/tests/i18n/__init__.py delete mode 100644 breathecode/utils/tests/i18n/tests_translation.py diff --git a/Pipfile.lock b/Pipfile.lock index 3c1e90fa1..ba3cf041e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -150,11 +150,11 @@ }, "aiohttp-retry": { "hashes": [ - "sha256:3aeeead8f6afe48272db93ced9440cf4eda8b6fd7ee2abb25357b7eb28525b45", - "sha256:9a8e637e31682ad36e1ff9f8bcba912fcfc7d7041722bc901a4b948da4d71ea9" + "sha256:7661af92471e9a96c69d9b8f32021360272073397e6a15bc44c1726b12f46056", + "sha256:92c47f1580040208bac95d9a8389a87227ef22758530f2e3f4683395e42c41b5" ], "markers": "python_version >= '3.7'", - "version": "==2.8.3" + "version": "==2.9.0" }, "aiosignal": { "hashes": [ @@ -264,11 +264,11 @@ }, "bleach": { "hashes": [ - "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", - "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6" + "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", + "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f" ], - "markers": "python_version >= '3.8'", - "version": "==6.1.0" + "markers": "python_version >= '3.9'", + "version": "==6.2.0" }, "brotli": { "hashes": [ @@ -414,11 +414,11 @@ "django" ], "hashes": [ - "sha256:702794d0db45746118ec63b02e81f8d8edd1b87b69337a9c3898eb6ea2830f0e", - "sha256:cefa43f74ba301d4f60d5ed6bfdc5b50152228922958415d2fd9a99b3eca103f" + "sha256:7cb8678ae393d78a299cf2f95cffb6118ae1f55bad9f691d3044b82c246fc694", + "sha256:9d7f86da5a36e2c19ba91bb126789608977b2e29ae5255622285a5c040e36244" ], "markers": "python_version >= '3.11'", - "version": "==1.0.3" + "version": "==1.1.0" }, "celery": { "hashes": [ @@ -835,12 +835,12 @@ }, "django-cors-headers": { "hashes": [ - "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6", - "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f" + "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8", + "sha256:8edbc0497e611c24d5150e0055d3b178c6534b8ed826fb6f53b21c63f5d48ba3" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.5.0" + "version": "==4.6.0" }, "django-cryptography-django5": { "hashes": [ @@ -860,12 +860,12 @@ }, "django-minify-html": { "hashes": [ - "sha256:1bae5742b54388b69346a6c29a7b193ab71f3cada038d633c36186c515fca228", - "sha256:ed6a2e06c290107bc0ff38f26f5faa703c952cca9c142c191088493087482b5a" + "sha256:50722ffa2c4cf83f7ac1848a00ecfa234b242bf76b4f33b5416cf37e244d6c5f", + "sha256:de04c86d062f0c4922314985480974d6ecfad5256c1480bb328aa0d3093f820f" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.10.0" + "version": "==1.11.0" }, "django-phonenumber-field": { "extras": [ @@ -1138,11 +1138,11 @@ "grpc" ], "hashes": [ - "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", - "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" + "sha256:26f8d76b96477db42b55fd02a33aae4a42ec8b86b98b94969b7333a2c828bf35", + "sha256:a6652b6bd51303902494998626653671703c420f6f4c88cfd3f50ed723e9d021" ], "markers": "python_version >= '3.7'", - "version": "==2.21.0" + "version": "==2.22.0" }, "google-api-python-client": { "hashes": [ @@ -1155,12 +1155,12 @@ }, "google-apps-meet": { "hashes": [ - "sha256:36e600f61a8bf0cccee6a3c3c0e77736f46da39ccd7bcbf76162971961739d8d", - "sha256:64e0e4a2e5fa1ce00203997ca633c76e2d97809ddf6de42e8ce61bf58d8701e1" + "sha256:282e6c3c8aa49f4a34a6b2ee31c94720801adf200777c7a08f491736de45e4ba", + "sha256:3125f4f8dcdb78f4973a02653540ce6c8c19c4369e2cbed731b7a9f1cbe41d1d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.1.8" + "version": "==0.1.9" }, "google-auth": { "hashes": [ @@ -1240,12 +1240,12 @@ }, "google-cloud-recaptcha-enterprise": { "hashes": [ - "sha256:69b0347b3f834812633e1902dddf62b10a3b3aca1e31b52a8861d3302304ace2", - "sha256:af9f7dbd56ee9d79e850307515118da42f465837a95b7af860ee315dab164042" + "sha256:169aeae9982d6c85b27caff228ae3ec66dd955ce2e80bb3d3fcf9f842a2b7696", + "sha256:651a1179f9ffa5ab6b519e693b1f808d5058681183273395b534a17575cb3592" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.22.1" + "version": "==1.23.0" }, "google-cloud-storage": { "hashes": [ @@ -1307,10 +1307,10 @@ }, "graphene": { "hashes": [ - "sha256:28bf359b802cdb808130a5521135d4c88a262564598cfdc91628d2c172b99dce", - "sha256:65e5ec84c5b7fb4fc41518acfbafb62ebb393d3982fbba00cd5393e431a80b97" + "sha256:828a8d7b1bce450566a72cc8733716c20f3acfc659960de73dd38f46dc302040", + "sha256:ca98f853201293871cdc7f55faf2523a9bc077181fe0f4947db5a243e5c67083" ], - "version": "==3.4" + "version": "==3.4.1" }, "graphene-django": { "hashes": [ @@ -1420,75 +1420,75 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "python_version >= '3' 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'))))))", + "markers": "python_version >= '3.7'", "version": "==3.1.1" }, "grpcio": { "hashes": [ - "sha256:014dfc020e28a0d9be7e93a91f85ff9f4a87158b7df9952fe23cc42d29d31e1e", - "sha256:0892dd200ece4822d72dd0952f7112c542a487fc48fe77568deaaa399c1e717d", - "sha256:0bb94e66cd8f0baf29bd3184b6aa09aeb1a660f9ec3d85da615c5003154bc2bf", - "sha256:0c69bf11894cad9da00047f46584d5758d6ebc9b5950c0dc96fec7e0bce5cde9", - "sha256:15c05a26a0f7047f720da41dc49406b395c1470eef44ff7e2c506a47ac2c0591", - "sha256:16724ffc956ea42967f5758c2f043faef43cb7e48a51948ab593570570d1e68b", - "sha256:227316b5631260e0bef8a3ce04fa7db4cc81756fea1258b007950b6efc90c05d", - "sha256:2b7183c80b602b0ad816315d66f2fb7887614ead950416d60913a9a71c12560d", - "sha256:2f55c1e0e2ae9bdd23b3c63459ee4c06d223b68aeb1961d83c48fb63dc29bc03", - "sha256:30d47dbacfd20cbd0c8be9bfa52fdb833b395d4ec32fe5cff7220afc05d08571", - "sha256:323741b6699cd2b04a71cb38f502db98f90532e8a40cb675393d248126a268af", - "sha256:3840994689cc8cbb73d60485c594424ad8adb56c71a30d8948d6453083624b52", - "sha256:391df8b0faac84d42f5b8dfc65f5152c48ed914e13c522fd05f2aca211f8bfad", - "sha256:42199e704095b62688998c2d84c89e59a26a7d5d32eed86d43dc90e7a3bd04aa", - "sha256:54d16383044e681f8beb50f905249e4e7261dd169d4aaf6e52eab67b01cbbbe2", - "sha256:5a1e03c3102b6451028d5dc9f8591131d6ab3c8a0e023d94c28cb930ed4b5f81", - "sha256:62492bd534979e6d7127b8a6b29093161a742dee3875873e01964049d5250a74", - "sha256:662c8e105c5e5cee0317d500eb186ed7a93229586e431c1bf0c9236c2407352c", - "sha256:682968427a63d898759474e3b3178d42546e878fdce034fd7474ef75143b64e3", - "sha256:74b900566bdf68241118f2918d312d3bf554b2ce0b12b90178091ea7d0a17b3d", - "sha256:77196216d5dd6f99af1c51e235af2dd339159f657280e65ce7e12c1a8feffd1d", - "sha256:7f200aca719c1c5dc72ab68be3479b9dafccdf03df530d137632c534bb6f1ee3", - "sha256:7fc1d2b9fd549264ae585026b266ac2db53735510a207381be509c315b4af4e8", - "sha256:82e5bd4b67b17c8c597273663794a6a46a45e44165b960517fe6d8a2f7f16d23", - "sha256:8c9a35b8bc50db35ab8e3e02a4f2a35cfba46c8705c3911c34ce343bd777813a", - "sha256:985b2686f786f3e20326c4367eebdaed3e7aa65848260ff0c6644f817042cb15", - "sha256:9d75641a2fca9ae1ae86454fd25d4c298ea8cc195dbc962852234d54a07060ad", - "sha256:a4e95e43447a02aa603abcc6b5e727d093d161a869c83b073f50b9390ecf0fa8", - "sha256:a6b9a5c18863fd4b6624a42e2712103fb0f57799a3b29651c0e5b8119a519d65", - "sha256:aa8d025fae1595a207b4e47c2e087cb88d47008494db258ac561c00877d4c8f8", - "sha256:ac11ecb34a86b831239cc38245403a8de25037b448464f95c3315819e7519772", - "sha256:ae6de510f670137e755eb2a74b04d1041e7210af2444103c8c95f193340d17ee", - "sha256:b2a44e572fb762c668e4812156b81835f7aba8a721b027e2d4bb29fb50ff4d33", - "sha256:b6eb68493a05d38b426604e1dc93bfc0137c4157f7ab4fac5771fd9a104bbaa6", - "sha256:b9bca3ca0c5e74dea44bf57d27e15a3a3996ce7e5780d61b7c72386356d231db", - "sha256:bd79929b3bb96b54df1296cd3bf4d2b770bd1df6c2bdf549b49bab286b925cdc", - "sha256:c4c425f440fb81f8d0237c07b9322fc0fb6ee2b29fbef5f62a322ff8fcce240d", - "sha256:cb204a742997277da678611a809a8409657b1398aaeebf73b3d9563b7d154c13", - "sha256:cf51d28063338608cd8d3cd64677e922134837902b70ce00dad7f116e3998210", - "sha256:cfd9306511fdfc623a1ba1dc3bc07fbd24e6cfbe3c28b4d1e05177baa2f99617", - "sha256:cff8e54d6a463883cda2fab94d2062aad2f5edd7f06ae3ed030f2a74756db365", - "sha256:d01793653248f49cf47e5695e0a79805b1d9d4eacef85b310118ba1dfcd1b955", - "sha256:d4ea4509d42c6797539e9ec7496c15473177ce9abc89bc5c71e7abe50fc25737", - "sha256:d90cfdafcf4b45a7a076e3e2a58e7bc3d59c698c4f6470b0bb13a4d869cf2273", - "sha256:e090b2553e0da1c875449c8e75073dd4415dd71c9bde6a406240fdf4c0ee467c", - "sha256:e91d154689639932305b6ea6f45c6e46bb51ecc8ea77c10ef25aa77f75443ad4", - "sha256:eef1dce9d1a46119fd09f9a992cf6ab9d9178b696382439446ca5f399d7b96fe", - "sha256:efe32b45dd6d118f5ea2e5deaed417d8a14976325c93812dd831908522b402c9", - "sha256:f4d613fbf868b2e2444f490d18af472ccb47660ea3df52f068c9c8801e1f3e85", - "sha256:f55f077685f61f0fbd06ea355142b71e47e4a26d2d678b3ba27248abfe67163a", - "sha256:f623c57a5321461c84498a99dddf9d13dac0e40ee056d884d6ec4ebcab647a78", - "sha256:f6bd2ab135c64a4d1e9e44679a616c9bc944547357c830fafea5c3caa3de5153", - "sha256:f95e15db43e75a534420e04822df91f645664bf4ad21dfaad7d51773c80e6bb4", - "sha256:fd6bc27861e460fe28e94226e3673d46e294ca4673d46b224428d197c5935e69", - "sha256:fe89295219b9c9e47780a0f1c75ca44211e706d1c598242249fe717af3385ec8" - ], - "version": "==1.67.0" + "sha256:01f616a964e540638af5130469451cf580ba8c7329f45ca998ab66e0c7dcdb04", + "sha256:0489063974d1452436139501bf6b180f63d4977223ee87488fe36858c5725292", + "sha256:0e6f255980afef598a9e64a24efce87b625e3e3c80a45162d111a461a9f92955", + "sha256:0f3e49c738396e93b7ba9016e153eb09e0778e776df6090c1b8c91877cc1c426", + "sha256:178f5db771c4f9a9facb2ab37a434c46cb9be1a75e820f187ee3d1e7805c4f65", + "sha256:1a65b503d008f066e994f34f456e0647e5ceb34cfcec5ad180b1b44020ad4970", + "sha256:1d7616d2ded471231c701489190379e0c311ee0a6c756f3c03e6a62b95a7146e", + "sha256:24e8a26dbfc5274d7474c27759b54486b8de23c709d76695237515bc8b5baeab", + "sha256:267d1745894200e4c604958da5f856da6293f063327cb049a51fe67348e4f953", + "sha256:299b3d8c4f790c6bcca485f9963b4846dd92cf6f1b65d3697145d005c80f9fe8", + "sha256:3b6c16489326d79ead41689c4b84bc40d522c9a7617219f4ad94bc7f448c5085", + "sha256:3dc2ed4cabea4dc14d5e708c2b426205956077cc5de419b4d4079315017e9732", + "sha256:43112046864317498a33bdc4797ae6a268c36345a910de9b9c17159d8346602f", + "sha256:4422581cdc628f77302270ff839a44f4c24fdc57887dc2a45b7e53d8fc2376af", + "sha256:4e7b904484a634a0fff132958dabdb10d63e0927398273917da3ee103e8d1f78", + "sha256:5721e66a594a6c4204458004852719b38f3d5522082be9061d6510b455c90afc", + "sha256:5db70d32d6703b89912af16d6d45d78406374a8b8ef0d28140351dd0ec610e98", + "sha256:5ed601c4c6008429e3d247ddb367fe8c7259c355757448d7c1ef7bd4a6739e8e", + "sha256:60336bff760fbb47d7e86165408126f1dded184448e9a4c892189eb7c9d3f90f", + "sha256:608d87d1bdabf9e2868b12338cd38a79969eaf920c89d698ead08f48de9c0f9e", + "sha256:60e6a4dcf5af7bbc36fd9f81c9f372e8ae580870a9e4b6eafe948cd334b81cf3", + "sha256:638354e698fd0c6c76b04540a850bf1db27b4d2515a19fcd5cf645c48d3eb1ed", + "sha256:699e964923b70f3101393710793289e42845791ea07565654ada0969522d0a38", + "sha256:7818c0454027ae3384235a65210bbf5464bd715450e30a3d40385453a85a70cb", + "sha256:786a5b18544622bfb1e25cc08402bd44ea83edfb04b93798d85dca4d1a0b5be5", + "sha256:804c6457c3cd3ec04fe6006c739579b8d35c86ae3298ffca8de57b493524b771", + "sha256:80b866f73224b0634f4312a4674c1be21b2b4afa73cb20953cbbb73a6b36c3cc", + "sha256:85f69fdc1d28ce7cff8de3f9c67db2b0ca9ba4449644488c1e0303c146135ddb", + "sha256:85f862069b86a305497e74d0dc43c02de3d1d184fc2c180993aa8aa86fbd19b8", + "sha256:8a00efecde9d6fcc3ab00c13f816313c040a28450e5e25739c24f432fc6d3c75", + "sha256:8a23cbcc5bb11ea7dc6163078be36c065db68d915c24f5faa4f872c573bb400f", + "sha256:8b0341d66a57f8a3119b77ab32207072be60c9bf79760fa609c5609f2deb1f3f", + "sha256:917e8d8994eed1d86b907ba2a61b9f0aef27a2155bca6cbb322430fc7135b7bb", + "sha256:95b5f2b857856ed78d72da93cd7d09b6db8ef30102e5e7fe0961fe4d9f7d48e8", + "sha256:9e838cad2176ebd5d4a8bb03955138d6589ce9e2ce5d51c3ada34396dbd2dba8", + "sha256:9fd042de4a82e3e7aca44008ee2fb5da01b3e5adb316348c21980f7f58adc311", + "sha256:a25bdea92b13ff4d7790962190bf6bf5c4639876e01c0f3dda70fc2769616335", + "sha256:a6703916c43b1d468d0756c8077b12017a9fcb6a1ef13faf49e67d20d7ebda62", + "sha256:a93deda571a1bf94ec1f6fcda2872dad3ae538700d94dc283c672a3b508ba3af", + "sha256:aa0162e56fd10a5547fac8774c4899fc3e18c1aa4a4759d0ce2cd00d3696ea6b", + "sha256:b49359977c6ec9f5d0573ea4e0071ad278ef905aa74e420acc73fd28ce39e9ce", + "sha256:beee96c8c0b1a75d556fe57b92b58b4347c77a65781ee2ac749d550f2a365dc1", + "sha256:c7a01337407dd89005527623a4a72c5c8e2894d22bead0895306b23c6695698f", + "sha256:c9b929f13677b10f63124c1a410994a401cdd85214ad83ab67cc077fc7e480f0", + "sha256:cdc491ae35a13535fd9196acb5afe1af37c8237df2e54427be3eecda3653127e", + "sha256:e279330bef1744040db8fc432becc8a727b84f456ab62b744d3fdb83f327e121", + "sha256:e29ca27bec8e163dca0c98084040edec3bc49afd10f18b412f483cc68c712744", + "sha256:e7d1797a8a3845437d327145959a2c0c47c05947c9eef5ff1a4c80e499dcc6fa", + "sha256:ea33986b70f83844cd00814cee4451055cd8cab36f00ac64a31f5bb09b31919e", + "sha256:ec74ef02010186185de82cc594058a3ccd8d86821842bbac9873fd4a2cf8be8d", + "sha256:f26b0b547eb8d00e195274cdfc63ce64c8fc2d3e2d00b12bf468ece41a0423a0", + "sha256:f5a27dddefe0e2357d3e617b9079b4bfdc91341a91565111a21ed6ebbc51b22d", + "sha256:f5b76ff64aaac53fede0cc93abf57894ab2a7362986ba22243d06218b93efe46", + "sha256:f9fff78ba10d4250bfc07a01bd6254a6d87dc67f9627adece85c0b2ed754fa96", + "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba" + ], + "version": "==1.67.1" }, "grpcio-status": { "hashes": [ - "sha256:0e79e2e01ba41a6ca6ed9d7a825323c511fe1653a646f8014c7e3c8132527acc", - "sha256:c3e5a86fa007e9e263cd5f988a8a907484da4caab582874ea2a4a6092734046b" + "sha256:16e6c085950bdacac97c779e6a502ea671232385e6e37f258884d6883392c2bd", + "sha256:2bf38395e028ceeecfd8866b081f61628114b384da7d51ae064ddc8d766a5d11" ], - "version": "==1.67.0" + "version": "==1.67.1" }, "gunicorn": { "hashes": [ @@ -2547,12 +2547,12 @@ }, "openai": { "hashes": [ - "sha256:57e9e37bc407f39bb6ec3a27d7e8fb9728b2779936daa1fcf95df17d3edfaccc", - "sha256:87b7d0f69d85f5641678d414b7ee3082363647a5c66a462ed7f3ccb59582da0d" + "sha256:20f408c32fc5cb66e60c6882c994cdca580a5648e10045cd840734194f033418", + "sha256:be2c4e77721b166cce8130e544178b7d579f751b4b074ffbaade3854b6f85ec5" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.52.2" + "version": "==1.53.0" }, "packaging": { "hashes": [ @@ -2710,12 +2710,12 @@ }, "pip": { "hashes": [ - "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2", - "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8" + "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed", + "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==24.3.1" }, "platformdirs": { "hashes": [ @@ -2996,44 +2996,50 @@ }, "pyarrow": { "hashes": [ - "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a", - "sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca", - "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597", - "sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c", - "sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb", - "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977", - "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3", - "sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687", - "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7", - "sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204", - "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28", - "sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087", - "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15", - "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc", - "sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2", - "sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155", - "sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df", - "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22", - "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a", - "sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b", - "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03", - "sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda", - "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07", - "sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204", - "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b", - "sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c", - "sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545", - "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655", - "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420", - "sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5", - "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4", - "sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8", - "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053", - "sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145", - "sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047", - "sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8" - ], - "version": "==17.0.0" + "sha256:00178509f379415a3fcf855af020e3340254f990a8534294ec3cf674d6e255fd", + "sha256:03f40b65a43be159d2f97fd64dc998f769d0995a50c00f07aab58b0b3da87e1f", + "sha256:082ba62bdcb939824ba1ce10b8acef5ab621da1f4c4805e07bfd153617ac19d4", + "sha256:09f30690b99ce34e0da64d20dab372ee54431745e4efb78ac938234a282d15f9", + "sha256:2333f93260674e185cfbf208d2da3007132572e56871f451ba1a556b45dae6e2", + "sha256:28f9c39a56d2c78bf6b87dcc699d520ab850919d4a8c7418cd20eda49874a2ea", + "sha256:2c664ab88b9766413197733c1720d3dcd4190e8fa3bbdc3710384630a0a7207b", + "sha256:2c992716cffb1088414f2b478f7af0175fd0a76fea80841b1706baa8fb0ebaad", + "sha256:2e549a748fa8b8715e734919923f69318c953e077e9c02140ada13e59d043310", + "sha256:320ae9bd45ad7ecc12ec858b3e8e462578de060832b98fc4d671dee9f10d9954", + "sha256:336addb8b6f5208be1b2398442c703a710b6b937b1a046065ee4db65e782ff5a", + "sha256:3ac24b2be732e78a5a3ac0b3aa870d73766dd00beba6e015ea2ea7394f8b4e55", + "sha256:45476490dd4adec5472c92b4d253e245258745d0ccaabe706f8d03288ed60a79", + "sha256:4c381857754da44326f3a49b8b199f7f87a51c2faacd5114352fc78de30d3aba", + "sha256:4d5ca5d707e158540312e09fd907f9f49bacbe779ab5236d9699ced14d2293b8", + "sha256:58a62549a3e0bc9e03df32f350e10e1efb94ec6cf63e3920c3385b26663948ce", + "sha256:5f0510608ccd6e7f02ca8596962afb8c6cc84c453e7be0da4d85f5f4f7b0328a", + "sha256:603cd8ad4976568954598ef0a6d4ed3dfb78aff3d57fa8d6271f470f0ce7d34f", + "sha256:606e9a3dcb0f52307c5040698ea962685fb1c852d72379ee9412be7de9c5f9e2", + "sha256:616ea2826c03c16e87f517c46296621a7c51e30400f6d0a61be645f203aa2b93", + "sha256:66dcc216ebae2eb4c37b223feaf82f15b69d502821dde2da138ec5a3716e7463", + "sha256:6dd1b52d0d58dd8f685ced9971eb49f697d753aa7912f0a8f50833c7a7426319", + "sha256:871b292d4b696b09120ed5bde894f79ee2a5f109cb84470546471df264cae136", + "sha256:8c70c1965cde991b711a98448ccda3486f2a336457cf4ec4dca257a926e149c9", + "sha256:8f40ec677e942374e3d7f2fad6a67a4c2811a8b975e8703c6fd26d3b168a90e2", + "sha256:907ee0aa8ca576f5e0cdc20b5aeb2ad4d3953a3b4769fc4b499e00ef0266f02f", + "sha256:a1824f5b029ddd289919f354bc285992cb4e32da518758c136271cf66046ef22", + "sha256:a6aa027b1a9d2970cf328ccd6dbe4a996bc13c39fd427f502782f5bdb9ca20f5", + "sha256:a71ab0589a63a3e987beb2bc172e05f000a5c5be2636b4b263c44034e215b5d7", + "sha256:b30a927c6dff89ee702686596f27c25160dd6c99be5bcc1513a763ae5b1bfc03", + "sha256:b46591222c864e7da7faa3b19455196416cd8355ff6c2cc2e65726a760a3c420", + "sha256:b5bd7fd32e3ace012d43925ea4fc8bd1b02cc6cc1e9813b518302950e89b5a22", + "sha256:bc1daf7c425f58527900876354390ee41b0ae962a73ad0959b9d829def583bb1", + "sha256:bc97316840a349485fbb137eb8d0f4d7057e1b2c1272b1a20eebbbe1848f5122", + "sha256:be08af84808dff63a76860847c48ec0416928a7b3a17c2f49a072cac7c45efbd", + "sha256:d5795e37c0a33baa618c5e054cd61f586cf76850a251e2b21355e4085def6280", + "sha256:d6331f280c6e4521c69b201a42dd978f60f7e129511a55da9e0bfe426b4ebb8d", + "sha256:dc892be34dbd058e8d189b47db1e33a227d965ea8805a235c8a7286f7fd17d3a", + "sha256:e7ab04f272f98ebffd2a0661e4e126036f6936391ba2889ed2d44c5006237802", + "sha256:eb7e3abcda7e1e6b83c2dc2909c8d045881017270a119cc6ee7fdcfe71d02df8", + "sha256:f1a198a50c409ab2d009fbf20956ace84567d67f2c5701511d4dd561fae6f32e", + "sha256:fe92efcdbfa0bcf2fa602e466d7f2905500f33f09eb90bf0bcf2e6ca41b574c8" + ], + "version": "==18.0.0" }, "pyasn1": { "hashes": [ @@ -3303,12 +3309,12 @@ }, "pyright": { "hashes": [ - "sha256:7071ac495593b2258ccdbbf495f1a5c0e5f27951f6b429bed4e8b296eb5cd21d", - "sha256:8e9975e34948ba5f8e07792a9c9d2bdceb2c6c0b61742b068d2229ca2bc4a9d9" + "sha256:577de60224f7fe36505d5b181231e3a395d427b7873be0bbcaa962a29ea93a60", + "sha256:6a1f495a261a72e12ad17e20d1ae3df4511223c773b19407cfa006229b1b08a5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.1.386" + "version": "==1.1.387" }, "pytest": { "hashes": [ @@ -3741,18 +3747,18 @@ }, "service-identity": { "hashes": [ - "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221", - "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a" + "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", + "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09" ], - "version": "==24.1.0" + "version": "==24.2.0" }, "setuptools": { "hashes": [ - "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec", - "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8" + "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", + "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686" ], "markers": "python_version >= '3.8'", - "version": "==75.2.0" + "version": "==75.3.0" }, "six": { "hashes": [ @@ -3850,12 +3856,12 @@ }, "stripe": { "hashes": [ - "sha256:0bbdfe54a09728fc54db6bb099b2f440ffc111d07d9674b0f04bfd0d3c1cbdcf", - "sha256:e79e02238d0ec7c89a64986af941dcae41e4857489b7cc83497acce9def356e5" + "sha256:4c53d61d7b596070324bfa5d7215843145fe5466e48973d828aab41ad209b5ce", + "sha256:dec812eabc95488862be40e6c799acdaf2e1225d686490a793f949fab745fdd0" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==11.1.1" + "version": "==11.2.0" }, "text-unidecode": { "hashes": [ @@ -3898,11 +3904,11 @@ }, "tqdm": { "hashes": [ - "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", - "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad" + "sha256:223e8b5359c2efc4b30555531f09e9f2f3589bcd7fdd389271191031b49b7a63", + "sha256:4bdd694238bef1485ce839d67967ab50af8f9272aab687c0d7702a01da0be090" ], "markers": "python_version >= '3.7'", - "version": "==4.66.5" + "version": "==4.66.6" }, "traitlets": { "hashes": [ @@ -3914,12 +3920,12 @@ }, "twilio": { "hashes": [ - "sha256:608d78a903d403465aac1840c58a6546a090b7e222d2bf539a93c3831072880c", - "sha256:d6a97a77b98cc176a61c960f11894af385bc1c11b93e2e8b79fdfb9601788fb0" + "sha256:c5d7f4cfeb50a7928397b8f819c8f7fb2bb956a1a2cabbda1df1d7a40f9ce1d7", + "sha256:d42691f7fe1faaa5ba82942f169bfea4d7f01a0a542a456d82018fb49bd1f5b2" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==9.3.5" + "version": "==9.3.6" }, "twisted": { "extras": [ @@ -3927,11 +3933,11 @@ "tls" ], "hashes": [ - "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394", - "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81" + "sha256:02951299672595fea0f70fa2d5f7b5e3d56836157eda68859a6ad6492d36756e", + "sha256:67aa7c8aa94387385302acf44ade12967c747858c8bcce0f11d38077a11c5326" ], "markers": "python_full_version >= '3.8.0'", - "version": "==24.7.0" + "version": "==24.10.0" }, "txaio": { "hashes": [ @@ -4242,11 +4248,11 @@ "brotli" ], "hashes": [ - "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636", - "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6" + "sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4", + "sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280" ], - "markers": "python_version >= '3.8'", - "version": "==6.7.0" + "markers": "python_version >= '3.9'", + "version": "==6.8.2" }, "wrapt": { "hashes": [ @@ -4333,93 +4339,101 @@ }, "yarl": { "hashes": [ - "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9", - "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36", - "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240", - "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2", - "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581", - "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929", - "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3", - "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6", - "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552", - "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472", - "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2", - "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb", - "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7", - "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b", - "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b", - "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058", - "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a", - "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656", - "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71", - "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3", - "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837", - "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6", - "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0", - "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104", - "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca", - "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb", - "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7", - "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07", - "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b", - "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202", - "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d", - "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532", - "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f", - "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5", - "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3", - "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724", - "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2", - "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09", - "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732", - "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2", - "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120", - "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4", - "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027", - "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e", - "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d", - "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b", - "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16", - "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120", - "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5", - "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97", - "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84", - "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00", - "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596", - "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d", - "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56", - "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7", - "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283", - "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67", - "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c", - "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968", - "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916", - "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae", - "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8", - "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604", - "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4", - "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af", - "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f", - "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a", - "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428", - "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9", - "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b", - "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059", - "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3", - "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49", - "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3", - "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade", - "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3", - "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c", - "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7", - "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349", - "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243", - "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7" + "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", + "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", + "sha256:0b1794853124e2f663f0ea54efb0340b457f08d40a1cef78edfa086576179c91", + "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", + "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", + "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3", + "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", + "sha256:16bca6678a83657dd48df84b51bd56a6c6bd401853aef6d09dc2506a78484c7b", + "sha256:1a3b91c44efa29e6c8ef8a9a2b583347998e2ba52c5d8280dbd5919c02dfc3b5", + "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74", + "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", + "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3", + "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4", + "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", + "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", + "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", + "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", + "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", + "sha256:380e6c38ef692b8fd5a0f6d1fa8774d81ebc08cfbd624b1bca62a4d4af2f9931", + "sha256:3b74ff4767d3ef47ffe0cd1d89379dc4d828d4873e5528976ced3b44fe5b0a21", + "sha256:3e844be8d536afa129366d9af76ed7cb8dfefec99f5f1c9e4f8ae542279a6dc3", + "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", + "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", + "sha256:482c122b72e3c5ec98f11457aeb436ae4aecca75de19b3d1de7cf88bc40db82f", + "sha256:561c87fea99545ef7d692403c110b2f99dced6dff93056d6e04384ad3bc46243", + "sha256:578d00c9b7fccfa1745a44f4eddfdc99d723d157dad26764538fbdda37209857", + "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f", + "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca", + "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", + "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", + "sha256:62a91aefff3d11bf60e5956d340eb507a983a7ec802b19072bb989ce120cd948", + "sha256:64cc6e97f14cf8a275d79c5002281f3040c12e2e4220623b5759ea7f9868d6a5", + "sha256:6f4c9156c4d1eb490fe374fb294deeb7bc7eaccda50e23775b2354b6a6739934", + "sha256:7294e38f9aa2e9f05f765b28ffdc5d81378508ce6dadbe93f6d464a8c9594473", + "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7", + "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", + "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", + "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", + "sha256:7fac95714b09da9278a0b52e492466f773cfe37651cf467a83a1b659be24bf71", + "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", + "sha256:846dd2e1243407133d3195d2d7e4ceefcaa5f5bf7278f0a9bda00967e6326b04", + "sha256:84c063af19ef5130084db70ada40ce63a84f6c1ef4d3dbc34e5e8c4febb20822", + "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11", + "sha256:8994b29c462de9a8fce2d591028b986dbbe1b32f3ad600b2d3e1c482c93abad6", + "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0", + "sha256:8ee427208c675f1b6e344a1f89376a9613fc30b52646a04ac0c1f6587c7e46ec", + "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", + "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", + "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4", + "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", + "sha256:a7ac5b4984c468ce4f4a553df281450df0a34aefae02e58d77a0847be8d1e11f", + "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", + "sha256:ae3476e934b9d714aa8000d2e4c01eb2590eee10b9d8cd03e7983ad65dfbfcba", + "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", + "sha256:b40d1bf6e6f74f7c0a567a9e5e778bbd4699d1d3d2c0fe46f4b717eef9e96b95", + "sha256:b5c4804e4039f487e942c13381e6c27b4b4e66066d94ef1fae3f6ba8b953f383", + "sha256:b5d6a6c9602fd4598fa07e0389e19fe199ae96449008d8304bf5d47cb745462e", + "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", + "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", + "sha256:c180ac742a083e109c1a18151f4dd8675f32679985a1c750d2ff806796165b55", + "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", + "sha256:c7e177c619342e407415d4f35dec63d2d134d951e24b5166afcdfd1362828e17", + "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217", + "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", + "sha256:cc7c92c1baa629cb03ecb0c3d12564f172218fb1739f54bf5f3881844daadc6d", + "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe", + "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", + "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d", + "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", + "sha256:d6324274b4e0e2fa1b3eccb25997b1c9ed134ff61d296448ab8269f5ac068c4c", + "sha256:d8a8b74d843c2638f3864a17d97a4acda58e40d3e44b6303b8cc3d3c44ae2d29", + "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", + "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860", + "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", + "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", + "sha256:eb6dce402734575e1a8cc0bb1509afca508a400a57ce13d306ea2c663bad1138", + "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", + "sha256:f5efe0661b9fcd6246f27957f6ae1c0eb29bc60552820f01e970b4996e016004", + "sha256:f9cbfbc5faca235fbdf531b93aa0f9f005ec7d267d9d738761a4d42b744ea159", + "sha256:fbea1751729afe607d84acfd01efd95e3b31db148a181a441984ce9b3d3469da", + "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988", + "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75" ], "markers": "python_version >= '3.9'", - "version": "==1.16.0" + "version": "==1.17.1" }, - "zope-interface": { + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, + "zope.interface": { "hashes": [ "sha256:0de23bcb93401994ea00bc5c677ef06d420340ac0a4e9c10d80e047b9ce5af3f", "sha256:179ad46ece518c9084cb272e4a69d266b659f7f8f48e51706746c2d8a426433e", @@ -4462,14 +4476,6 @@ "markers": "python_version >= '3.8'", "version": "==7.1.1" }, - "zope.event": { - "hashes": [ - "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", - "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0" - }, "zstandard": { "hashes": [ "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", @@ -4643,11 +4649,11 @@ "django" ], "hashes": [ - "sha256:702794d0db45746118ec63b02e81f8d8edd1b87b69337a9c3898eb6ea2830f0e", - "sha256:cefa43f74ba301d4f60d5ed6bfdc5b50152228922958415d2fd9a99b3eca103f" + "sha256:7cb8678ae393d78a299cf2f95cffb6118ae1f55bad9f691d3044b82c246fc694", + "sha256:9d7f86da5a36e2c19ba91bb126789608977b2e29ae5255622285a5c040e36244" ], "markers": "python_version >= '3.11'", - "version": "==1.0.3" + "version": "==1.1.0" }, "certifi": { "hashes": [ @@ -4892,20 +4898,20 @@ }, "django-stubs": { "hashes": [ - "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5", - "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40" + "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b", + "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "django-stubs-ext": { "hashes": [ - "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d", - "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926" + "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c", + "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "djangorestframework-stubs": { "hashes": [ @@ -5022,11 +5028,11 @@ "grpc" ], "hashes": [ - "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", - "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" + "sha256:26f8d76b96477db42b55fd02a33aae4a42ec8b86b98b94969b7333a2c828bf35", + "sha256:a6652b6bd51303902494998626653671703c420f6f4c88cfd3f50ed723e9d021" ], "markers": "python_version >= '3.7'", - "version": "==2.21.0" + "version": "==2.22.0" }, "google-api-python-client": { "hashes": [ @@ -5048,12 +5054,12 @@ }, "google-apps-meet": { "hashes": [ - "sha256:36e600f61a8bf0cccee6a3c3c0e77736f46da39ccd7bcbf76162971961739d8d", - "sha256:64e0e4a2e5fa1ce00203997ca633c76e2d97809ddf6de42e8ce61bf58d8701e1" + "sha256:282e6c3c8aa49f4a34a6b2ee31c94720801adf200777c7a08f491736de45e4ba", + "sha256:3125f4f8dcdb78f4973a02653540ce6c8c19c4369e2cbed731b7a9f1cbe41d1d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.1.8" + "version": "==0.1.9" }, "google-auth": { "hashes": [ @@ -5164,7 +5170,7 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "python_version >= '3' 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'))))))", + "markers": "python_version >= '3.7'", "version": "==3.1.1" }, "griffe": { @@ -5177,70 +5183,70 @@ }, "grpcio": { "hashes": [ - "sha256:014dfc020e28a0d9be7e93a91f85ff9f4a87158b7df9952fe23cc42d29d31e1e", - "sha256:0892dd200ece4822d72dd0952f7112c542a487fc48fe77568deaaa399c1e717d", - "sha256:0bb94e66cd8f0baf29bd3184b6aa09aeb1a660f9ec3d85da615c5003154bc2bf", - "sha256:0c69bf11894cad9da00047f46584d5758d6ebc9b5950c0dc96fec7e0bce5cde9", - "sha256:15c05a26a0f7047f720da41dc49406b395c1470eef44ff7e2c506a47ac2c0591", - "sha256:16724ffc956ea42967f5758c2f043faef43cb7e48a51948ab593570570d1e68b", - "sha256:227316b5631260e0bef8a3ce04fa7db4cc81756fea1258b007950b6efc90c05d", - "sha256:2b7183c80b602b0ad816315d66f2fb7887614ead950416d60913a9a71c12560d", - "sha256:2f55c1e0e2ae9bdd23b3c63459ee4c06d223b68aeb1961d83c48fb63dc29bc03", - "sha256:30d47dbacfd20cbd0c8be9bfa52fdb833b395d4ec32fe5cff7220afc05d08571", - "sha256:323741b6699cd2b04a71cb38f502db98f90532e8a40cb675393d248126a268af", - "sha256:3840994689cc8cbb73d60485c594424ad8adb56c71a30d8948d6453083624b52", - "sha256:391df8b0faac84d42f5b8dfc65f5152c48ed914e13c522fd05f2aca211f8bfad", - "sha256:42199e704095b62688998c2d84c89e59a26a7d5d32eed86d43dc90e7a3bd04aa", - "sha256:54d16383044e681f8beb50f905249e4e7261dd169d4aaf6e52eab67b01cbbbe2", - "sha256:5a1e03c3102b6451028d5dc9f8591131d6ab3c8a0e023d94c28cb930ed4b5f81", - "sha256:62492bd534979e6d7127b8a6b29093161a742dee3875873e01964049d5250a74", - "sha256:662c8e105c5e5cee0317d500eb186ed7a93229586e431c1bf0c9236c2407352c", - "sha256:682968427a63d898759474e3b3178d42546e878fdce034fd7474ef75143b64e3", - "sha256:74b900566bdf68241118f2918d312d3bf554b2ce0b12b90178091ea7d0a17b3d", - "sha256:77196216d5dd6f99af1c51e235af2dd339159f657280e65ce7e12c1a8feffd1d", - "sha256:7f200aca719c1c5dc72ab68be3479b9dafccdf03df530d137632c534bb6f1ee3", - "sha256:7fc1d2b9fd549264ae585026b266ac2db53735510a207381be509c315b4af4e8", - "sha256:82e5bd4b67b17c8c597273663794a6a46a45e44165b960517fe6d8a2f7f16d23", - "sha256:8c9a35b8bc50db35ab8e3e02a4f2a35cfba46c8705c3911c34ce343bd777813a", - "sha256:985b2686f786f3e20326c4367eebdaed3e7aa65848260ff0c6644f817042cb15", - "sha256:9d75641a2fca9ae1ae86454fd25d4c298ea8cc195dbc962852234d54a07060ad", - "sha256:a4e95e43447a02aa603abcc6b5e727d093d161a869c83b073f50b9390ecf0fa8", - "sha256:a6b9a5c18863fd4b6624a42e2712103fb0f57799a3b29651c0e5b8119a519d65", - "sha256:aa8d025fae1595a207b4e47c2e087cb88d47008494db258ac561c00877d4c8f8", - "sha256:ac11ecb34a86b831239cc38245403a8de25037b448464f95c3315819e7519772", - "sha256:ae6de510f670137e755eb2a74b04d1041e7210af2444103c8c95f193340d17ee", - "sha256:b2a44e572fb762c668e4812156b81835f7aba8a721b027e2d4bb29fb50ff4d33", - "sha256:b6eb68493a05d38b426604e1dc93bfc0137c4157f7ab4fac5771fd9a104bbaa6", - "sha256:b9bca3ca0c5e74dea44bf57d27e15a3a3996ce7e5780d61b7c72386356d231db", - "sha256:bd79929b3bb96b54df1296cd3bf4d2b770bd1df6c2bdf549b49bab286b925cdc", - "sha256:c4c425f440fb81f8d0237c07b9322fc0fb6ee2b29fbef5f62a322ff8fcce240d", - "sha256:cb204a742997277da678611a809a8409657b1398aaeebf73b3d9563b7d154c13", - "sha256:cf51d28063338608cd8d3cd64677e922134837902b70ce00dad7f116e3998210", - "sha256:cfd9306511fdfc623a1ba1dc3bc07fbd24e6cfbe3c28b4d1e05177baa2f99617", - "sha256:cff8e54d6a463883cda2fab94d2062aad2f5edd7f06ae3ed030f2a74756db365", - "sha256:d01793653248f49cf47e5695e0a79805b1d9d4eacef85b310118ba1dfcd1b955", - "sha256:d4ea4509d42c6797539e9ec7496c15473177ce9abc89bc5c71e7abe50fc25737", - "sha256:d90cfdafcf4b45a7a076e3e2a58e7bc3d59c698c4f6470b0bb13a4d869cf2273", - "sha256:e090b2553e0da1c875449c8e75073dd4415dd71c9bde6a406240fdf4c0ee467c", - "sha256:e91d154689639932305b6ea6f45c6e46bb51ecc8ea77c10ef25aa77f75443ad4", - "sha256:eef1dce9d1a46119fd09f9a992cf6ab9d9178b696382439446ca5f399d7b96fe", - "sha256:efe32b45dd6d118f5ea2e5deaed417d8a14976325c93812dd831908522b402c9", - "sha256:f4d613fbf868b2e2444f490d18af472ccb47660ea3df52f068c9c8801e1f3e85", - "sha256:f55f077685f61f0fbd06ea355142b71e47e4a26d2d678b3ba27248abfe67163a", - "sha256:f623c57a5321461c84498a99dddf9d13dac0e40ee056d884d6ec4ebcab647a78", - "sha256:f6bd2ab135c64a4d1e9e44679a616c9bc944547357c830fafea5c3caa3de5153", - "sha256:f95e15db43e75a534420e04822df91f645664bf4ad21dfaad7d51773c80e6bb4", - "sha256:fd6bc27861e460fe28e94226e3673d46e294ca4673d46b224428d197c5935e69", - "sha256:fe89295219b9c9e47780a0f1c75ca44211e706d1c598242249fe717af3385ec8" - ], - "version": "==1.67.0" + "sha256:01f616a964e540638af5130469451cf580ba8c7329f45ca998ab66e0c7dcdb04", + "sha256:0489063974d1452436139501bf6b180f63d4977223ee87488fe36858c5725292", + "sha256:0e6f255980afef598a9e64a24efce87b625e3e3c80a45162d111a461a9f92955", + "sha256:0f3e49c738396e93b7ba9016e153eb09e0778e776df6090c1b8c91877cc1c426", + "sha256:178f5db771c4f9a9facb2ab37a434c46cb9be1a75e820f187ee3d1e7805c4f65", + "sha256:1a65b503d008f066e994f34f456e0647e5ceb34cfcec5ad180b1b44020ad4970", + "sha256:1d7616d2ded471231c701489190379e0c311ee0a6c756f3c03e6a62b95a7146e", + "sha256:24e8a26dbfc5274d7474c27759b54486b8de23c709d76695237515bc8b5baeab", + "sha256:267d1745894200e4c604958da5f856da6293f063327cb049a51fe67348e4f953", + "sha256:299b3d8c4f790c6bcca485f9963b4846dd92cf6f1b65d3697145d005c80f9fe8", + "sha256:3b6c16489326d79ead41689c4b84bc40d522c9a7617219f4ad94bc7f448c5085", + "sha256:3dc2ed4cabea4dc14d5e708c2b426205956077cc5de419b4d4079315017e9732", + "sha256:43112046864317498a33bdc4797ae6a268c36345a910de9b9c17159d8346602f", + "sha256:4422581cdc628f77302270ff839a44f4c24fdc57887dc2a45b7e53d8fc2376af", + "sha256:4e7b904484a634a0fff132958dabdb10d63e0927398273917da3ee103e8d1f78", + "sha256:5721e66a594a6c4204458004852719b38f3d5522082be9061d6510b455c90afc", + "sha256:5db70d32d6703b89912af16d6d45d78406374a8b8ef0d28140351dd0ec610e98", + "sha256:5ed601c4c6008429e3d247ddb367fe8c7259c355757448d7c1ef7bd4a6739e8e", + "sha256:60336bff760fbb47d7e86165408126f1dded184448e9a4c892189eb7c9d3f90f", + "sha256:608d87d1bdabf9e2868b12338cd38a79969eaf920c89d698ead08f48de9c0f9e", + "sha256:60e6a4dcf5af7bbc36fd9f81c9f372e8ae580870a9e4b6eafe948cd334b81cf3", + "sha256:638354e698fd0c6c76b04540a850bf1db27b4d2515a19fcd5cf645c48d3eb1ed", + "sha256:699e964923b70f3101393710793289e42845791ea07565654ada0969522d0a38", + "sha256:7818c0454027ae3384235a65210bbf5464bd715450e30a3d40385453a85a70cb", + "sha256:786a5b18544622bfb1e25cc08402bd44ea83edfb04b93798d85dca4d1a0b5be5", + "sha256:804c6457c3cd3ec04fe6006c739579b8d35c86ae3298ffca8de57b493524b771", + "sha256:80b866f73224b0634f4312a4674c1be21b2b4afa73cb20953cbbb73a6b36c3cc", + "sha256:85f69fdc1d28ce7cff8de3f9c67db2b0ca9ba4449644488c1e0303c146135ddb", + "sha256:85f862069b86a305497e74d0dc43c02de3d1d184fc2c180993aa8aa86fbd19b8", + "sha256:8a00efecde9d6fcc3ab00c13f816313c040a28450e5e25739c24f432fc6d3c75", + "sha256:8a23cbcc5bb11ea7dc6163078be36c065db68d915c24f5faa4f872c573bb400f", + "sha256:8b0341d66a57f8a3119b77ab32207072be60c9bf79760fa609c5609f2deb1f3f", + "sha256:917e8d8994eed1d86b907ba2a61b9f0aef27a2155bca6cbb322430fc7135b7bb", + "sha256:95b5f2b857856ed78d72da93cd7d09b6db8ef30102e5e7fe0961fe4d9f7d48e8", + "sha256:9e838cad2176ebd5d4a8bb03955138d6589ce9e2ce5d51c3ada34396dbd2dba8", + "sha256:9fd042de4a82e3e7aca44008ee2fb5da01b3e5adb316348c21980f7f58adc311", + "sha256:a25bdea92b13ff4d7790962190bf6bf5c4639876e01c0f3dda70fc2769616335", + "sha256:a6703916c43b1d468d0756c8077b12017a9fcb6a1ef13faf49e67d20d7ebda62", + "sha256:a93deda571a1bf94ec1f6fcda2872dad3ae538700d94dc283c672a3b508ba3af", + "sha256:aa0162e56fd10a5547fac8774c4899fc3e18c1aa4a4759d0ce2cd00d3696ea6b", + "sha256:b49359977c6ec9f5d0573ea4e0071ad278ef905aa74e420acc73fd28ce39e9ce", + "sha256:beee96c8c0b1a75d556fe57b92b58b4347c77a65781ee2ac749d550f2a365dc1", + "sha256:c7a01337407dd89005527623a4a72c5c8e2894d22bead0895306b23c6695698f", + "sha256:c9b929f13677b10f63124c1a410994a401cdd85214ad83ab67cc077fc7e480f0", + "sha256:cdc491ae35a13535fd9196acb5afe1af37c8237df2e54427be3eecda3653127e", + "sha256:e279330bef1744040db8fc432becc8a727b84f456ab62b744d3fdb83f327e121", + "sha256:e29ca27bec8e163dca0c98084040edec3bc49afd10f18b412f483cc68c712744", + "sha256:e7d1797a8a3845437d327145959a2c0c47c05947c9eef5ff1a4c80e499dcc6fa", + "sha256:ea33986b70f83844cd00814cee4451055cd8cab36f00ac64a31f5bb09b31919e", + "sha256:ec74ef02010186185de82cc594058a3ccd8d86821842bbac9873fd4a2cf8be8d", + "sha256:f26b0b547eb8d00e195274cdfc63ce64c8fc2d3e2d00b12bf468ece41a0423a0", + "sha256:f5a27dddefe0e2357d3e617b9079b4bfdc91341a91565111a21ed6ebbc51b22d", + "sha256:f5b76ff64aaac53fede0cc93abf57894ab2a7362986ba22243d06218b93efe46", + "sha256:f9fff78ba10d4250bfc07a01bd6254a6d87dc67f9627adece85c0b2ed754fa96", + "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba" + ], + "version": "==1.67.1" }, "grpcio-status": { "hashes": [ - "sha256:0e79e2e01ba41a6ca6ed9d7a825323c511fe1653a646f8014c7e3c8132527acc", - "sha256:c3e5a86fa007e9e263cd5f988a8a907484da4caab582874ea2a4a6092734046b" + "sha256:16e6c085950bdacac97c779e6a502ea671232385e6e37f258884d6883392c2bd", + "sha256:2bf38395e028ceeecfd8866b081f61628114b384da7d51ae064ddc8d766a5d11" ], - "version": "==1.67.0" + "version": "==1.67.1" }, "httplib2": { "hashes": [ @@ -5281,6 +5287,103 @@ "markers": "python_version >= '3.7'", "version": "==3.1.4" }, + "langcodes": { + "hashes": [ + "sha256:68f686fc3d358f222674ecf697ddcee3ace3c2fe325083ecad2543fd28a20e77", + "sha256:a24879fed238013ac3af2424b9d1124e38b4a38b2044fd297c8ff38e5912e718" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.1" + }, + "language-data": { + "hashes": [ + "sha256:77d5cab917f91ee0b2f1aa7018443e911cf8985ef734ca2ba3940770f6a3816b", + "sha256:82a86050bbd677bfde87d97885b17566cfe75dad3ac4f5ce44b52c28f752e773" + ], + "version": "==1.2.0" + }, + "marisa-trie": { + "hashes": [ + "sha256:06b099dd743676dbcd8abd8465ceac8f6d97d8bfaabe2c83b965495523b4cef2", + "sha256:0ee6cf6a16d9c3d1c94e21c8e63c93d8b34bede170ca4e937e16e1c0700d399f", + "sha256:0fe69fb9ffb2767746181f7b3b29bbd3454d1d24717b5958e030494f3d3cddf3", + "sha256:1db3213b451bf058d558f6e619bceff09d1d130214448a207c55e1526e2773a1", + "sha256:20948e40ab2038e62b7000ca6b4a913bc16c91a2c2e6da501bd1f917eeb28d51", + "sha256:2428b495003c189695fb91ceeb499f9fcced3a2dce853e17fa475519433c67ff", + "sha256:24a81aa7566e4ec96fc4d934581fe26d62eac47fc02b35fa443a0bb718b471e8", + "sha256:25688f34cac3bec01b4f655ffdd6c599a01f0bd596b4a79cf56c6f01a7df3560", + "sha256:36aa4401a1180615f74d575571a6550081d84fc6461e9aefc0bb7b2427af098e", + "sha256:3a27c408e2aefc03e0f1d25b2ff2afb85aac3568f6fa2ae2a53b57a2e87ce29d", + "sha256:3ad356442c2fea4c2a6f514738ddf213d23930f942299a2b2c05df464a00848a", + "sha256:429858a0452a7bedcf67bc7bb34383d00f666c980cb75a31bcd31285fbdd4403", + "sha256:436f62d27714970b9cdd3b3c41bdad046f260e62ebb0daa38125ef70536fc73b", + "sha256:46e528ee71808c961baf8c3ce1c46a8337ec7a96cc55389d11baafe5b632f8e9", + "sha256:4728ed3ae372d1ea2cdbd5eaa27b8f20a10e415d1f9d153314831e67d963f281", + "sha256:536ea19ce6a2ce61c57fed4123ecd10d18d77a0db45cd2741afff2b8b68f15b3", + "sha256:5685a14b3099b1422c4f59fa38b0bf4b5342ee6cc38ae57df9666a0b28eeaad3", + "sha256:594f98491a96c7f1ffe13ce292cef1b4e63c028f0707effdea0f113364c1ae6c", + "sha256:5bd39a4e1cc839a88acca2889d17ebc3f202a5039cd6059a13148ce75c8a6244", + "sha256:5e43891a37b0d7f618819fea14bd951289a0a8e3dd0da50c596139ca83ebb9b1", + "sha256:5e649f3dc8ab5476732094f2828cc90cac3be7c79bc0c8318b6fda0c1d248db4", + "sha256:5fe5a286f997848a410eebe1c28657506adaeb405220ee1e16cfcfd10deb37f2", + "sha256:638506eacf20ca503fff72221a7e66a6eadbf28d6a4a6f949fcf5b1701bb05ec", + "sha256:6532615111eec2c79e711965ece0bc95adac1ff547a7fff5ffca525463116deb", + "sha256:66b23e5b35dd547f85bf98db7c749bc0ffc57916ade2534a6bbc32db9a4abc44", + "sha256:6704adf0247d2dda42e876b793be40775dff46624309ad99bc7537098bee106d", + "sha256:67f0c2ec82c20a02c16fc9ba81dee2586ef20270127c470cb1054767aa8ba310", + "sha256:6946100a43f933fad6bc458c502a59926d80b321d5ac1ed2ff9c56605360496f", + "sha256:6c50c861faad0a5c091bd763e0729f958c316e678dfa065d3984fbb9e4eacbcd", + "sha256:735c363d9aaac82eaf516a28f7c6b95084c2e176d8231c87328dc80e112a9afa", + "sha256:746a7c60a17fccd3cfcfd4326926f02ea4fcdfc25d513411a0c4fc8e4a1ca51f", + "sha256:7ac170d20b97beb75059ba65d1ccad6b434d777c8992ab41ffabdade3b06dd74", + "sha256:7cca7f96236ffdbf49be4b2e42c132e3df05968ac424544034767650913524de", + "sha256:7e7b1786e852e014d03e5f32dbd991f9a9eb223dd3fa9a2564108b807e4b7e1c", + "sha256:852d7bcf14b0c63404de26e7c4c8d5d65ecaeca935e93794331bc4e2f213660b", + "sha256:875a6248e60fbb48d947b574ffa4170f34981f9e579bde960d0f9a49ea393ecc", + "sha256:8951e7ce5d3167fbd085703b4cbb3f47948ed66826bef9a2173c379508776cf5", + "sha256:8cf4f25cf895692b232f49aa5397af6aba78bb679fb917a05fce8d3cb1ee446d", + "sha256:952af3a5859c3b20b15a00748c36e9eb8316eb2c70bd353ae1646da216322908", + "sha256:98042040d1d6085792e8d0f74004fc0f5f9ca6091c298f593dd81a22a4643854", + "sha256:9c9b32b14651a6dcf9e8857d2df5d29d322a1ea8c0be5c8ffb88f9841c4ec62b", + "sha256:9e956e6a46f604b17d570901e66f5214fb6f658c21e5e7665deace236793cef6", + "sha256:9f627f4e41be710b6cb6ed54b0128b229ac9d50e2054d9cde3af0fef277c23cf", + "sha256:a2eb41d2f9114d8b7bd66772c237111e00d2bae2260824560eaa0a1e291ce9e8", + "sha256:a3c98613180cf1730e221933ff74b454008161b1a82597e41054127719964188", + "sha256:a4177dc0bd1374e82be9b2ba4d0c2733b0a85b9d154ceeea83a5bee8c1e62fbf", + "sha256:a8443d116c612cfd1961fbf76769faf0561a46d8e317315dd13f9d9639ad500c", + "sha256:aa7cd17e1c690ce96c538b2f4aae003d9a498e65067dd433c52dd069009951d4", + "sha256:ad548117744b2bcf0e3d97374608be0a92d18c2af13d98b728d37cd06248e571", + "sha256:aefe0973cc4698e0907289dc0517ab0c7cdb13d588201932ff567d08a50b0e2e", + "sha256:b0ef26733d3c836be79e812071e1a431ce1f807955a27a981ebb7993d95f842b", + "sha256:b1ce340da608530500ab4f963f12d6bfc8d8680900919a60dbdc9b78c02060a4", + "sha256:b1ec93f0d1ee6d7ab680a6d8ea1a08bf264636358e92692072170032dda652ba", + "sha256:b2a7d00f53f4945320b551bccb826b3fb26948bde1a10d50bb9802fabb611b10", + "sha256:b2eacb84446543082ec50f2fb563f1a94c96804d4057b7da8ed815958d0cdfbe", + "sha256:b5ea16e69bfda0ac028c921b58de1a4aaf83d43934892977368579cd3c0a2554", + "sha256:bd45142501300e7538b2e544905580918b67b1c82abed1275fe4c682c95635fa", + "sha256:c0fe2ace0cb1806badbd1c551a8ec2f8d4cf97bf044313c082ef1acfe631ddca", + "sha256:c484410911182457a8a1a0249d0c09c01e2071b78a0a8538cd5f7fa45589b13a", + "sha256:ce37d8ca462bb64cc13f529b9ed92f7b21fe8d1f1679b62e29f9cb7d0e888b49", + "sha256:ce59bcd2cda9bb52b0e90cc7f36413cd86c3d0ce7224143447424aafb9f4aa48", + "sha256:d2a82eb21afdaf22b50d9b996472305c05ca67fc4ff5a026a220320c9c961db6", + "sha256:d5648c6dcc5dc9200297fb779b1663b8a4467bda034a3c69bd9c32d8afb33b1d", + "sha256:d659fda873d8dcb2c14c2c331de1dee21f5a902d7f2de7978b62c6431a8850ef", + "sha256:d7eb20bf0e8b55a58d2a9b518aabc4c18278787bdba476c551dd1c1ed109e509", + "sha256:da4e4facb79614cc4653cfd859f398e4db4ca9ab26270ff12610e50ed7f1f6c6", + "sha256:de1665eaafefa48a308e4753786519888021740501a15461c77bdfd57638e6b4", + "sha256:e2699255d7ac610dee26d4ae7bda5951d05c7d9123a22e1f7c6a6f1964e0a4e4", + "sha256:e58788004adda24c401d1751331618ed20c507ffc23bfd28d7c0661a1cf0ad16", + "sha256:e70869737cc0e5bd903f620667da6c330d6737048d1f44db792a6af68a1d35be", + "sha256:eba6ca45500ca1a042466a0684aacc9838e7f20fe2605521ee19f2853062798f", + "sha256:ed3fb4ed7f2084597e862bcd56c56c5529e773729a426c083238682dba540e98", + "sha256:f2806f75817392cedcacb24ac5d80b0350dde8d3861d67d045c1d9b109764114", + "sha256:f35c2603a6be168088ed1db6ad1704b078aa8f39974c60888fbbced95dcadad4", + "sha256:f4cd800704a5fc57e53c39c3a6b0c9b1519ebdbcb644ede3ee67a06eb542697d", + "sha256:f713af9b8aa66a34cd3a78c7d150a560a75734713abe818a69021fd269e927fa" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.1" + }, "markdown": { "hashes": [ "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", @@ -5771,11 +5874,11 @@ }, "pymdown-extensions": { "hashes": [ - "sha256:41cdde0a77290e480cf53892f5c5e50921a7ee3e5cd60ba91bf19837b33badcf", - "sha256:bc8847ecc9e784a098efd35e20cba772bc5a1b529dfcef9dc1972db9021a1049" + "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", + "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7" ], "markers": "python_version >= '3.8'", - "version": "==10.11.2" + "version": "==10.12" }, "pyparsing": { "hashes": [ @@ -5787,12 +5890,12 @@ }, "pyright": { "hashes": [ - "sha256:7071ac495593b2258ccdbbf495f1a5c0e5f27951f6b429bed4e8b296eb5cd21d", - "sha256:8e9975e34948ba5f8e07792a9c9d2bdceb2c6c0b61742b068d2229ca2bc4a9d9" + "sha256:577de60224f7fe36505d5b181231e3a395d427b7873be0bbcaa962a29ea93a60", + "sha256:6a1f495a261a72e12ad17e20d1ae3df4511223c773b19407cfa006229b1b08a5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.1.386" + "version": "==1.1.387" }, "pytest": { "hashes": [ @@ -5813,12 +5916,12 @@ }, "pytest-cov": { "hashes": [ - "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", - "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" + "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", + "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==5.0.0" + "markers": "python_version >= '3.9'", + "version": "==6.0.0" }, "pytest-gevent": { "hashes": [ @@ -5957,11 +6060,11 @@ }, "setuptools": { "hashes": [ - "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec", - "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8" + "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd", + "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686" ], "markers": "python_version >= '3.8'", - "version": "==75.2.0" + "version": "==75.3.0" }, "six": { "hashes": [ @@ -6037,11 +6140,11 @@ }, "virtualenv": { "hashes": [ - "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2", - "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655" + "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", + "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4" ], "markers": "python_version >= '3.8'", - "version": "==20.27.0" + "version": "==20.27.1" }, "watchdog": { "hashes": [ diff --git a/breathecode/activity/views.py b/breathecode/activity/views.py index d5928d2cd..b887ce3cd 100644 --- a/breathecode/activity/views.py +++ b/breathecode/activity/views.py @@ -1,5 +1,7 @@ import json +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import User from django.db.models import Avg, Count, Q, Sum from google.cloud import bigquery @@ -14,8 +16,6 @@ from breathecode.authenticate.actions import get_user_language from breathecode.services.google_cloud.big_query import BigQuery from breathecode.utils import HeaderLimitOffsetPagination, capable_of, getLogger -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .utils import ( generate_created_at, diff --git a/breathecode/admissions/views.py b/breathecode/admissions/views.py index e382ed02c..eeaa8fea8 100644 --- a/breathecode/admissions/views.py +++ b/breathecode/admissions/views.py @@ -2,16 +2,18 @@ import pytz from adrf.decorators import api_view +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import AnonymousUser, User from django.db.models import FloatField, Max, Q, Value from django.utils import timezone -from slugify import slugify from rest_framework import status from rest_framework.decorators import permission_classes from rest_framework.exceptions import ParseError, PermissionDenied, ValidationError from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from slugify import slugify from breathecode.admissions import tasks from breathecode.admissions.caches import CohortCache, CohortUserCache, SyllabusVersionCache, TeacherCache, UserCache @@ -26,8 +28,6 @@ localize_query, ) from breathecode.utils.find_by_full_name import query_like_by_full_name -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .actions import find_asset_on_json, test_syllabus, update_asset_on_json from .models import ( diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index c8f3f5a93..e3edfd3c7 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -1,3 +1,5 @@ +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import AnonymousUser from django.utils import timezone from rest_framework import serializers @@ -5,8 +7,6 @@ from breathecode.admissions.models import Academy from breathecode.utils import serpy from breathecode.utils.datetime_integer import duration_to_str, from_now -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import Answer, Assessment, Option, Question, UserAssessment diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index d498fce5b..87491598e 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -1,4 +1,7 @@ from datetime import datetime + +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.db.models import Q from django.utils import timezone from rest_framework import status @@ -12,8 +15,6 @@ from breathecode.marketing.serializers import FormEntryBigSerializer, PostFormEntrySerializer from breathecode.marketing.tasks import persist_single_lead from breathecode.utils import APIViewExtensions, GenerateLookupsMixin, capable_of -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import Answer, Assessment, AssessmentLayout, AssessmentThreshold, Option, Question, UserAssessment from .serializers import ( diff --git a/breathecode/assignments/permissions/consumers.py b/breathecode/assignments/permissions/consumers.py index 685ff2cd8..b0208c2dc 100644 --- a/breathecode/assignments/permissions/consumers.py +++ b/breathecode/assignments/permissions/consumers.py @@ -1,11 +1,12 @@ import logging +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import PaymentException + from breathecode.admissions.actions import is_no_saas_student_up_to_date_in_any_cohort from breathecode.authenticate.actions import get_user_language from breathecode.payments.models import Service from breathecode.utils.decorators import ServiceContext -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import PaymentException logger = logging.getLogger(__name__) diff --git a/breathecode/assignments/receivers.py b/breathecode/assignments/receivers.py index d692b22ac..0370ad3d0 100644 --- a/breathecode/assignments/receivers.py +++ b/breathecode/assignments/receivers.py @@ -1,6 +1,7 @@ import logging from typing import Any, Type +from capyc.core.i18n import translation from django.dispatch import receiver from breathecode.admissions.signals import syllabus_asset_slug_updated @@ -8,7 +9,6 @@ from breathecode.authenticate.actions import get_user_settings from breathecode.authenticate.models import CredentialsGithub from breathecode.notify.actions import send_email_message -from breathecode.utils.i18n import translation from .models import RepositoryDeletionOrder, Task from .signals import assignment_status_updated, status_updated diff --git a/breathecode/assignments/views.py b/breathecode/assignments/views.py index a04e4a210..408a50662 100644 --- a/breathecode/assignments/views.py +++ b/breathecode/assignments/views.py @@ -4,6 +4,7 @@ from adrf.views import APIView from asgiref.sync import sync_to_async +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError from django.contrib import messages @@ -30,7 +31,6 @@ from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import consume, has_permission from breathecode.utils.decorators.capable_of import acapable_of -from breathecode.utils.i18n import translation from breathecode.utils.multi_status_response import MultiStatusResponse from .actions import deliver_task, sync_cohort_tasks diff --git a/breathecode/authenticate/actions.py b/breathecode/authenticate/actions.py index 77bcbbf54..f1dbc261e 100644 --- a/breathecode/authenticate/actions.py +++ b/breathecode/authenticate/actions.py @@ -9,6 +9,8 @@ from adrf.requests import AsyncRequest from asgiref.sync import sync_to_async +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import User from django.core.handlers.wsgi import WSGIRequest from django.db.models import Q @@ -17,8 +19,6 @@ import breathecode.notify.actions as notify_actions from breathecode.admissions.models import Academy, CohortUser from breathecode.services.github import Github -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import ( AcademyAuthSettings, diff --git a/breathecode/authenticate/serializers.py b/breathecode/authenticate/serializers.py index 123f47966..62c93aafc 100644 --- a/breathecode/authenticate/serializers.py +++ b/breathecode/authenticate/serializers.py @@ -4,6 +4,8 @@ import random import urllib.parse +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import Permission, User from django.db import IntegrityError from django.db.models import Q @@ -18,8 +20,6 @@ from breathecode.events.models import Event from breathecode.registry.models import Asset from breathecode.utils import serpy, validate_conversion_info -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import ( AcademyAuthSettings, diff --git a/breathecode/authenticate/tasks.py b/breathecode/authenticate/tasks.py index 299968011..e7dbcbad6 100644 --- a/breathecode/authenticate/tasks.py +++ b/breathecode/authenticate/tasks.py @@ -1,6 +1,8 @@ import logging import os +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from celery import shared_task from django.contrib.auth.models import User from task_manager.core.exceptions import AbortTask, RetryTask @@ -10,8 +12,6 @@ from breathecode.marketing.actions import validate_email from breathecode.notify import actions as notify_actions from breathecode.utils.decorators import TaskPriority -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .actions import add_to_organization, get_user_settings, remove_from_organization, set_gitpod_user_expiration diff --git a/breathecode/authenticate/views.py b/breathecode/authenticate/views.py index 5af384284..0ab6bf13a 100644 --- a/breathecode/authenticate/views.py +++ b/breathecode/authenticate/views.py @@ -13,6 +13,7 @@ from adrf.decorators import api_view from adrf.views import APIView from asgiref.sync import sync_to_async +from capyc.core.i18n import translation from capyc.core.managers import feature from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError @@ -50,7 +51,6 @@ from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import has_permission from breathecode.utils.find_by_full_name import query_like_by_full_name -from breathecode.utils.i18n import translation from breathecode.utils.shorteners import C from breathecode.utils.views import private_view, render_message, set_query_parameter diff --git a/breathecode/events/permissions/consumers.py b/breathecode/events/permissions/consumers.py index 00eca1d45..fbf9cf682 100644 --- a/breathecode/events/permissions/consumers.py +++ b/breathecode/events/permissions/consumers.py @@ -1,5 +1,7 @@ import logging +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import PaymentException, ValidationException from django.db.models import Q from django.utils import timezone @@ -9,8 +11,6 @@ from breathecode.events.actions import get_my_event_types from breathecode.events.models import Event, LiveClass from breathecode.utils.decorators import ServiceContext -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import PaymentException, ValidationException logger = logging.getLogger(__name__) diff --git a/breathecode/events/serializers.py b/breathecode/events/serializers.py index 23e10e2da..d3d86812e 100644 --- a/breathecode/events/serializers.py +++ b/breathecode/events/serializers.py @@ -2,6 +2,8 @@ from datetime import timedelta from typing import Any +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.db.models.query_utils import Q from django.utils import timezone from rest_framework import serializers @@ -15,8 +17,6 @@ from breathecode.registry.models import Asset from breathecode.registry.serializers import AssetSmallSerializer from breathecode.utils import serpy -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import Event, EventbriteWebhook, EventCheckin, EventType, LiveClass, Organization diff --git a/breathecode/events/views.py b/breathecode/events/views.py index 8734e28e7..bf7625fcf 100644 --- a/breathecode/events/views.py +++ b/breathecode/events/views.py @@ -4,6 +4,8 @@ from datetime import datetime, timedelta import pytz +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.contrib.auth.models import User from django.db.models.query_utils import Q from django.http.response import HttpResponse @@ -36,10 +38,8 @@ ) from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import consume -from breathecode.utils.i18n import translation from breathecode.utils.multi_status_response import MultiStatusResponse from breathecode.utils.views import private_view, render_message -from capyc.rest_framework.exceptions import ValidationException from .actions import fix_datetime_weekday, get_my_event_types, update_timeslots_out_of_range from .models import ( diff --git a/breathecode/marketing/actions.py b/breathecode/marketing/actions.py index 87cd8e78f..e3fa8fdcb 100644 --- a/breathecode/marketing/actions.py +++ b/breathecode/marketing/actions.py @@ -6,6 +6,7 @@ import numpy as np import requests +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from django.db.models import Q from django.utils import timezone @@ -17,7 +18,6 @@ from breathecode.services.activecampaign import ACOldClient, ActiveCampaign, ActiveCampaignClient, acp_ids, map_ids from breathecode.services.brevo import Brevo from breathecode.utils import getLogger -from breathecode.utils.i18n import translation from .models import AcademyAlias, ActiveCampaignAcademy, Automation, FormEntry, Tag diff --git a/breathecode/marketing/views.py b/breathecode/marketing/views.py index 4f153596f..310f980a6 100644 --- a/breathecode/marketing/views.py +++ b/breathecode/marketing/views.py @@ -10,6 +10,7 @@ import pandas as pd import pytz +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError from django.contrib.auth.models import AnonymousUser @@ -37,7 +38,6 @@ from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import validate_captcha, validate_captcha_challenge from breathecode.utils.find_by_full_name import query_like_by_full_name -from breathecode.utils.i18n import translation from .actions import convert_data_frame, sync_automations, sync_tags, validate_email from .models import ( diff --git a/breathecode/media/settings.py b/breathecode/media/settings.py index fb1d946d0..4dc6650b8 100644 --- a/breathecode/media/settings.py +++ b/breathecode/media/settings.py @@ -3,6 +3,7 @@ from typing import Any, Awaitable, Callable, Literal, Optional, Type, TypedDict from adrf.requests import AsyncRequest +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile from PIL import Image @@ -11,7 +12,6 @@ from breathecode.media.models import Chunk, File from breathecode.notify.models import Notification from breathecode.services.google_cloud.storage import Storage -from breathecode.utils.i18n import translation type TypeValidator = Callable[[str, Any], None] type TypeValidatorWrapper = Callable[[Type[Any]], TypeValidator] diff --git a/breathecode/media/tasks.py b/breathecode/media/tasks.py index 4b7c30400..c7dc29bee 100644 --- a/breathecode/media/tasks.py +++ b/breathecode/media/tasks.py @@ -1,6 +1,7 @@ import logging from typing import Any, Optional +from capyc.core.i18n import translation from django.core.cache import cache from task_manager.core.exceptions import AbortTask, RetryTask from task_manager.django.decorators import task @@ -8,7 +9,6 @@ from breathecode.authenticate.actions import get_user_settings from breathecode.notify.models import Notification from breathecode.utils.decorators import TaskPriority -from breathecode.utils.i18n import translation from .models import File from .utils import media_settings diff --git a/breathecode/media/utils.py b/breathecode/media/utils.py index fad5387c1..e06e8335d 100644 --- a/breathecode/media/utils.py +++ b/breathecode/media/utils.py @@ -6,6 +6,7 @@ from typing import Any, Optional, Tuple, overload from adrf.views import APIView +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from rest_framework import status from rest_framework.response import Response @@ -15,7 +16,6 @@ from breathecode.media.models import Chunk, File from breathecode.media.signals import schedule_deletion from breathecode.services.google_cloud.storage import Storage -from breathecode.utils.i18n import translation from .settings import MEDIA_MIME_ALLOWED, MEDIA_SETTINGS, MediaSettings, Schema diff --git a/breathecode/media/views.py b/breathecode/media/views.py index d237d2ff7..ecfa64df0 100644 --- a/breathecode/media/views.py +++ b/breathecode/media/views.py @@ -7,6 +7,7 @@ import requests from adrf.views import APIView from adrf.viewsets import ViewSet +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError from django.db.models import Q @@ -35,7 +36,6 @@ from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import has_permission from breathecode.utils.decorators.capable_of import acapable_of -from breathecode.utils.i18n import translation logger = logging.getLogger(__name__) MIME_ALLOWED = [ diff --git a/breathecode/mentorship/permissions/consumers.py b/breathecode/mentorship/permissions/consumers.py index 24ee0abeb..d0d67532c 100644 --- a/breathecode/mentorship/permissions/consumers.py +++ b/breathecode/mentorship/permissions/consumers.py @@ -1,5 +1,6 @@ import logging +from capyc.core.i18n import translation from capyc.core.managers import feature from capyc.rest_framework.exceptions import PaymentException, ValidationException @@ -9,7 +10,6 @@ from breathecode.mentorship.models import MentorProfile, MentorshipService from breathecode.payments.models import Consumable, ConsumptionSession from breathecode.utils.decorators import ServiceContext -from breathecode.utils.i18n import translation logger = logging.getLogger(__name__) diff --git a/breathecode/mentorship/serializers.py b/breathecode/mentorship/serializers.py index 98468c6c9..957e6a710 100644 --- a/breathecode/mentorship/serializers.py +++ b/breathecode/mentorship/serializers.py @@ -1,3 +1,5 @@ +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from rest_framework import serializers import breathecode.activity.tasks as tasks_activity @@ -8,8 +10,6 @@ from breathecode.services.calendly import Calendly from breathecode.utils import serpy from breathecode.utils.datetime_integer import duration_to_str -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .actions import generate_mentor_bill from .models import CalendlyOrganization, MentorProfile, MentorshipBill, MentorshipService, MentorshipSession diff --git a/breathecode/mentorship/views.py b/breathecode/mentorship/views.py index cbdd28909..92f3c42cc 100644 --- a/breathecode/mentorship/views.py +++ b/breathecode/mentorship/views.py @@ -8,6 +8,7 @@ import urllib.parse import timeago +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from django.contrib import messages from django.contrib.auth.models import User @@ -36,7 +37,6 @@ from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import consume, has_permission from breathecode.utils.find_by_full_name import query_like_by_full_name -from breathecode.utils.i18n import translation from breathecode.utils.multi_status_response import MultiStatusResponse from breathecode.utils.views import private_view, render_message, set_query_parameter diff --git a/breathecode/monitoring/serializers.py b/breathecode/monitoring/serializers.py index 51594aee2..c3807bd58 100644 --- a/breathecode/monitoring/serializers.py +++ b/breathecode/monitoring/serializers.py @@ -1,5 +1,7 @@ import logging +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.core.validators import URLValidator from rest_framework import serializers @@ -8,8 +10,6 @@ from breathecode.monitoring.models import RepositorySubscription from breathecode.monitoring.tasks import async_subscribe_repo, async_unsubscribe_repo from breathecode.utils import serpy -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException logger = logging.getLogger(__name__) diff --git a/breathecode/monitoring/views.py b/breathecode/monitoring/views.py index eab4ab1bf..8308de41d 100644 --- a/breathecode/monitoring/views.py +++ b/breathecode/monitoring/views.py @@ -2,6 +2,8 @@ import os import stripe +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError from django.db.models import Q from django.http import StreamingHttpResponse @@ -17,8 +19,6 @@ from breathecode.monitoring import signals from breathecode.utils import GenerateLookupsMixin, capable_of from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .actions import add_github_webhook, add_stripe_webhook from .models import CSVDownload, CSVUpload, RepositorySubscription, RepositoryWebhook diff --git a/breathecode/payments/actions.py b/breathecode/payments/actions.py index d4d27bee6..2a212bbdd 100644 --- a/breathecode/payments/actions.py +++ b/breathecode/payments/actions.py @@ -5,6 +5,7 @@ from typing import Literal, Optional, Type, TypedDict from adrf.requests import AsyncRequest +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from dateutil.relativedelta import relativedelta from django.contrib.auth.models import User @@ -22,7 +23,6 @@ from breathecode.media.models import File from breathecode.payments import tasks from breathecode.utils import getLogger -from breathecode.utils.i18n import translation from breathecode.utils.validate_conversion_info import validate_conversion_info from .models import ( diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 67bd33422..d3a659bb7 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -7,6 +7,7 @@ from typing import Any, Optional from asgiref.sync import sync_to_async +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from currencies import Currency as CurrencyFormatter from django import forms @@ -23,7 +24,6 @@ from breathecode.events.models import EventType from breathecode.mentorship.models import MentorshipService from breathecode.payments import signals -from breathecode.utils.i18n import translation from breathecode.utils.validators.language import validate_language_code # https://devdocs.prestashop-project.org/1.7/webservice/resources/warehouses/ diff --git a/breathecode/payments/services/stripe.py b/breathecode/payments/services/stripe.py index de05eff02..56142e6dc 100644 --- a/breathecode/payments/services/stripe.py +++ b/breathecode/payments/services/stripe.py @@ -2,14 +2,14 @@ import os import stripe +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import PaymentException, ValidationException from django.contrib.auth.models import User from django.utils import timezone from breathecode.authenticate.models import UserSetting from breathecode.payments.models import Bag, Currency, FinancialReputation, Invoice, PaymentContact from breathecode.utils import getLogger -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import PaymentException, ValidationException logger = getLogger(__name__) diff --git a/breathecode/payments/tasks.py b/breathecode/payments/tasks.py index e8ae52645..08129c4b2 100644 --- a/breathecode/payments/tasks.py +++ b/breathecode/payments/tasks.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from typing import Any, Optional +from capyc.core.i18n import translation from dateutil.relativedelta import relativedelta from django.core.cache import cache from django.utils import timezone @@ -21,7 +22,6 @@ from breathecode.payments.signals import consume_service, reimburse_service_units from breathecode.services.google.google import Google from breathecode.utils.decorators import TaskPriority -from breathecode.utils.i18n import translation from breathecode.utils.redis import Lock from .models import ( diff --git a/breathecode/payments/views.py b/breathecode/payments/views.py index 0b1ef1e35..e7711db0e 100644 --- a/breathecode/payments/views.py +++ b/breathecode/payments/views.py @@ -1,5 +1,6 @@ from datetime import timedelta +from capyc.core.i18n import translation from capyc.core.shorteners import C from capyc.rest_framework.exceptions import PaymentException, ValidationException from django.core.cache import cache @@ -78,7 +79,6 @@ from breathecode.payments.signals import reimburse_service_units from breathecode.utils import APIViewExtensions, getLogger, validate_conversion_info from breathecode.utils.decorators.capable_of import capable_of -from breathecode.utils.i18n import translation from breathecode.utils.redis import Lock logger = getLogger(__name__) diff --git a/breathecode/provisioning/actions.py b/breathecode/provisioning/actions.py index aa06d12aa..278a49557 100644 --- a/breathecode/provisioning/actions.py +++ b/breathecode/provisioning/actions.py @@ -5,6 +5,8 @@ from decimal import Decimal, localcontext from typing import Optional, TypedDict +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from dateutil.relativedelta import relativedelta from django.db.models import Q, QuerySet from django.utils import timezone @@ -22,8 +24,6 @@ from breathecode.registry.models import Asset from breathecode.services.github import Github from breathecode.utils import getLogger -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import ( ProvisioningBill, diff --git a/breathecode/provisioning/serializers.py b/breathecode/provisioning/serializers.py index 7acb79afc..e4bea99b6 100644 --- a/breathecode/provisioning/serializers.py +++ b/breathecode/provisioning/serializers.py @@ -1,10 +1,10 @@ import re +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from rest_framework import serializers from breathecode.utils import serpy -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException from .models import ProvisioningBill, ProvisioningContainer diff --git a/breathecode/provisioning/views.py b/breathecode/provisioning/views.py index 6a4f32649..a981b058e 100644 --- a/breathecode/provisioning/views.py +++ b/breathecode/provisioning/views.py @@ -5,6 +5,7 @@ from urllib.parse import parse_qs, urlencode, urlparse, urlunparse import pandas as pd +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError from dateutil.relativedelta import relativedelta @@ -25,21 +26,20 @@ from breathecode.provisioning.serializers import ( GetProvisioningBillSerializer, GetProvisioningBillSmallSerializer, + GetProvisioningProfile, GetProvisioningUserConsumptionSerializer, ProvisioningBillHTMLSerializer, ProvisioningBillSerializer, ProvisioningUserConsumptionHTMLResumeSerializer, - GetProvisioningProfile, ) from breathecode.utils import capable_of, cut_csv from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions from breathecode.utils.decorators import has_permission -from breathecode.utils.i18n import translation from breathecode.utils.io.file import count_csv_rows from breathecode.utils.views import private_view, render_message from .actions import get_provisioning_vendor -from .models import BILL_STATUS, ProvisioningBill, ProvisioningUserConsumption, ProvisioningProfile +from .models import BILL_STATUS, ProvisioningBill, ProvisioningProfile, ProvisioningUserConsumption @private_view() diff --git a/breathecode/registry/permissions/consumers.py b/breathecode/registry/permissions/consumers.py index af3b73964..349b2e14b 100644 --- a/breathecode/registry/permissions/consumers.py +++ b/breathecode/registry/permissions/consumers.py @@ -1,5 +1,7 @@ import logging +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import PaymentException, ValidationException from django.db.models import Q from breathecode.admissions.actions import is_no_saas_student_up_to_date_in_any_cohort @@ -7,8 +9,6 @@ from breathecode.authenticate.actions import get_user_language from breathecode.registry.models import Asset from breathecode.utils.decorators import ServiceContext -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import PaymentException, ValidationException logger = logging.getLogger(__name__) diff --git a/breathecode/registry/tests/signals/tests_asset_saved.py b/breathecode/registry/tests/signals/tests_asset_saved.py index 6219a5f2f..9469aeeb0 100644 --- a/breathecode/registry/tests/signals/tests_asset_saved.py +++ b/breathecode/registry/tests/signals/tests_asset_saved.py @@ -3,10 +3,10 @@ """ import random +from typing import Optional import capyc.pytest as capy import pytest -from aiohttp_retry import Optional from breathecode.registry.models import Asset diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index 5a6cd5d34..dfb43058f 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -4,6 +4,8 @@ from pathlib import Path import requests +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from circuitbreaker import CircuitBreakerError from django.core.validators import URLValidator from django.db.models import Count, Q @@ -25,9 +27,7 @@ from breathecode.services.seo import SEOAnalyzer from breathecode.utils import GenerateLookupsMixin, capable_of, consume from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions -from breathecode.utils.i18n import translation from breathecode.utils.views import render_message -from capyc.rest_framework.exceptions import ValidationException from .actions import ( AssetThumbnailGenerator, @@ -43,11 +43,11 @@ AssetAlias, AssetCategory, AssetComment, + AssetContext, AssetErrorLog, AssetImage, AssetKeyword, AssetTechnology, - AssetContext, ContentVariable, KeywordCluster, OriginalityScan, @@ -61,6 +61,7 @@ AssetBigSerializer, AssetBigTechnologySerializer, AssetCategorySerializer, + AssetContextSerializer, AssetExpandableSerializer, AssetImageSmallSerializer, AssetKeywordBigSerializer, @@ -85,7 +86,6 @@ SEOReportSerializer, TechnologyPUTSerializer, VariableSmallSerializer, - AssetContextSerializer, ) from .tasks import async_pull_from_github from .utils import is_url diff --git a/breathecode/utils/__init__.py b/breathecode/utils/__init__.py index c3021d78a..08162392e 100644 --- a/breathecode/utils/__init__.py +++ b/breathecode/utils/__init__.py @@ -11,7 +11,6 @@ from .header_limit_offset_pagination import * # noqa: F401 # from .validators import * -from .i18n import * # noqa: F401 from .io import * # noqa: F401 from .localize_query import * # noqa: F401 from .multi_status_response import * # noqa: F401 diff --git a/breathecode/utils/api_view_extensions/extensions/lookup_extension.py b/breathecode/utils/api_view_extensions/extensions/lookup_extension.py index c76f1456d..192571d04 100644 --- a/breathecode/utils/api_view_extensions/extensions/lookup_extension.py +++ b/breathecode/utils/api_view_extensions/extensions/lookup_extension.py @@ -1,12 +1,12 @@ from functools import cache from typing import Any, Callable, Optional +from capyc.core.i18n import translation +from capyc.rest_framework.exceptions import ValidationException from django.db.models import Q from django.utils import dateparse from breathecode.utils.api_view_extensions.extension_base import ExtensionBase -from breathecode.utils.i18n import translation -from capyc.rest_framework.exceptions import ValidationException __all__ = ["LookupExtension"] diff --git a/breathecode/utils/i18n.py b/breathecode/utils/i18n.py deleted file mode 100644 index 166106b8e..000000000 --- a/breathecode/utils/i18n.py +++ /dev/null @@ -1,167 +0,0 @@ -import logging -import os -from datetime import date, datetime, time -from functools import cache -from typing import Optional - -import pytz -from babel.dates import format_date as babel_format_date -from babel.dates import format_datetime as babel_format_datetime -from babel.dates import format_time as babel_format_time -from babel.dates import format_timedelta as babel_format_timedelta - -from breathecode.utils.exceptions import MalformedLanguageCode - -__all__ = ["translation", "format_date", "format_datetime", "format_time", "format_timedelta"] - -IS_TEST_ENV = os.getenv("ENV") == "test" -logger = logging.getLogger(__name__) - - -def get_short_code(code: str) -> str: - return code[:2] - - -def format_and_assert_code(code: str, from_kwargs: bool = False) -> None: - # do not remove the assertions - - is_short = len(code) == 2 - - # first two character only with lowercase - if not code[:2].islower(): - raise MalformedLanguageCode("Lang code is not lowercase") - - # last two character only with lowercase - if not is_short and from_kwargs and not code[3:].islower(): - raise MalformedLanguageCode("Country code is not lowercase") - - # last two character only with uppercase - elif not is_short and not from_kwargs and not code[2:].isupper(): - raise MalformedLanguageCode("Country code is not uppercase") - - separator = "_" if from_kwargs else "-" - - # the format is en or en-US - if not (len(code) == 2 or (len(code) == 5 and code[2] == separator)): - raise MalformedLanguageCode("Code malformed") - - if not from_kwargs: - return code.replace(separator, "_") - - return code - - -# parse a date to a str with the local format -def format_date(code: Optional[str], date: date, format="medium"): - """Translate the date to the local language.""" - - if not code: - code = "en" - - code = format_and_assert_code(code) - return babel_format_date(date, locale=code, format=format) - - -# parse a date to a str with the local format -def format_datetime(code: Optional[str], date: datetime, tz: pytz.BaseTzInfo | str = pytz.UTC, format="medium"): - """Translate the datetime to the local language.""" - - if not code: - code = "en" - - code = format_and_assert_code(code) - - if isinstance(tz, str): - tz = pytz.timezone(tz) - - return babel_format_datetime(date, locale=code, tzinfo=tz, format=format) - - -def format_time(code: Optional[str], date: time, format="full", **kwargs: str): - """Translate the time to the local language.""" - - if not code: - code = "en" - - code = format_and_assert_code(code) - return babel_format_time(date, locale=code, format=format) - - -def format_timedelta(code: Optional[str], date: time): - """Translate the timedelta to the local language.""" - - if not code: - code = "en" - - code = format_and_assert_code(code) - return babel_format_timedelta(date, locale=code) - - -def format_languages(code: str) -> list: - """Translate the language to the local language.""" - - languages = set() - - code.replace(" ", "") - - codes = [x for x in code.split(",") if x] - - for code in codes: - priority = 1 - if ";q=" in code: - s = code.split(";q=") - code = s[0] - try: - priority = float(s[1]) - except Exception: - raise MalformedLanguageCode( - 'The priority is not a float, example: "en;q=0.5"', slug="malformed-quantity-language-code" - ) - - languages.add((priority, code)) - - return [x[1] for x in sorted(languages, key=lambda x: (x[0], "-" in x[1], x[1]), reverse=True)] - - -def try_to_translate(code, **kwargs: str) -> str | None: - is_short = len(code) == 2 - - if code.lower() in kwargs: - return kwargs[code.lower()] - - elif not is_short and (short_code := get_short_code(code)) in kwargs: - return kwargs[short_code] - - return None - - -@cache -def translation(code: Optional[str] = "en", slug: Optional[str] = None, **kwargs: str) -> str: - """Get the translation.""" - - if not code: - code = "en" - - languages = [format_and_assert_code(language) for language in format_languages(code)] - - # do the assertions - for key in kwargs: - format_and_assert_code(key, from_kwargs=True) - - # the english if mandatory - if not ("en" in kwargs or "en_us" in kwargs): - raise MalformedLanguageCode("The english translation is mandatory") - - if slug and IS_TEST_ENV: - return slug - - for language in languages: - v = try_to_translate(language, **kwargs) - - if v: - return v - - if "en_us" in kwargs: - return kwargs["en_us"] - - return kwargs["en"] diff --git a/breathecode/utils/tests/i18n/__init__.py b/breathecode/utils/tests/i18n/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/breathecode/utils/tests/i18n/tests_translation.py b/breathecode/utils/tests/i18n/tests_translation.py deleted file mode 100644 index 8a8a388dd..000000000 --- a/breathecode/utils/tests/i18n/tests_translation.py +++ /dev/null @@ -1,113 +0,0 @@ -from email import header -import json -import random -from wsgiref import headers -from rest_framework.views import APIView -from rest_framework.decorators import api_view, permission_classes -from rest_framework.response import Response -from rest_framework.test import APIRequestFactory, force_authenticate -from breathecode.authenticate.models import ProfileAcademy -import breathecode.utils.decorators as decorators -from rest_framework.permissions import AllowAny -from rest_framework import status - -from breathecode.utils.exceptions import MalformedLanguageCode -from ..mixins import UtilsTestCase -from ...i18n import translation - - -def randomLang(self, force_complete=True): - code = "en" - - # avoid choices english - while code == "en": - code = self.bc.random.string(lower=True, size=2) - - if force_complete or random.randint(0, 1): - code += f"-{self.bc.random.string(upper=True, size=2)}" - - return code - - -def langWithRandomCountry(self, code): - return f"{code}-{self.bc.random.string(upper=True, size=2)}" - - -def getLangParam(code: str): - return code.lower().replace("-", "_") - - -def randomBool(): - return bool(random.randint(0, 1)) - - -class TranslationTestSuite(UtilsTestCase): - """ - 🔽🔽🔽 Function get id - """ - - def test_Given_RandomLang_When_EnglishTranstalionIsNotGiven_Expect_Exception(self): - code = randomLang(self, randomBool()) - with self.assertRaisesMessage(MalformedLanguageCode, "The english translation is mandatory"): - translation(code) - - def test_Given_RandomLang_When_GeneralEnglishTranstalionAndUsaEnglishIsNotGiven_Expect_Exception(self): - code = randomLang(self, randomBool()) - with self.assertRaisesMessage(MalformedLanguageCode, "The english translation is mandatory"): - translation(code, en_au="Hello") - - def test_Given_RandomLang_When_LangCodeUppercase_Expect_Exception(self): - code = randomLang(self, randomBool()) - with self.assertRaisesMessage(MalformedLanguageCode, "Lang code is not lowercase"): - translation(code, EN_au="Hello") - - def test_Given_RandomLang_When_CountryCodeUppercase_Expect_Exception(self): - code = randomLang(self, randomBool()) - with self.assertRaisesMessage(MalformedLanguageCode, "Country code is not lowercase"): - translation(code, en_AU="Hello") - - def test_Given_RandomLang_When_SpanishTranslationIsNotGiven_Expect_GetEnglishTranslation(self): - code = randomLang(self, randomBool()) - - cases = ["en", "en_us"] - for case in cases: - kwargs = {case: "Hello"} - string = translation(code, **kwargs) - self.assertEqual(string, "Hello") - - def test_Given_None_When_EnglishTranslationIsGiven_Expect_GetEnglishTranslation(self): - string = translation(None, en="Hello") - self.assertEqual(string, "Hello") - - def test_Given_LangEs_When_SpanishTranslationIsGiven_Expect_GetGenericSpanishTranslation(self): - code = langWithRandomCountry(self, "es") - param = getLangParam(code) - kwargs = { - "en": "Hello", - "es": "Hola", - param: "Qué onda tío", - } - string = translation("es", **kwargs) - self.assertEqual(string, "Hola") - - def test_Given_LangEsWithCountry_When_SpanishTranslationIsGiven_Expect_GetGenericSpanishTranslation(self): - code = langWithRandomCountry(self, "es") - kwargs = { - "en": "Hello", - "es": "Hola", - } - string = translation(code, **kwargs) - self.assertEqual(string, "Hola") - - def test_Given_LangEsWithCountry_When_SpanishWithCountryTranslationIsGiven_Expect_GetSpanishTranslationOfThatCountry( - self, - ): - code = langWithRandomCountry(self, "es") - param = getLangParam(code) - kwargs = { - "en": "Hello", - "es": "Hola", - param: "Qué onda tío", - } - string = translation(code, **kwargs) - self.assertEqual(string, "Qué onda tío") diff --git a/breathecode/utils/validate_conversion_info.py b/breathecode/utils/validate_conversion_info.py index aeadce734..139138d10 100644 --- a/breathecode/utils/validate_conversion_info.py +++ b/breathecode/utils/validate_conversion_info.py @@ -1,4 +1,4 @@ -from breathecode.utils.i18n import translation +from capyc.core.i18n import translation from capyc.rest_framework.exceptions import ValidationException __all__ = ["validate_conversion_info"] diff --git a/staging/pytest/core/fixtures/http.py b/staging/pytest/core/fixtures/http.py index dd6957e66..40b67bfd2 100644 --- a/staging/pytest/core/fixtures/http.py +++ b/staging/pytest/core/fixtures/http.py @@ -1,6 +1,6 @@ -from aiohttp_retry import Optional +from typing import Generator, Optional, Self + import pytest -from typing import Generator, Self # not implemented yet supported_http_clients = [] From 5278c58a26cb3e338d0d3eb04960746539da147f Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 30 Oct 2024 20:56:39 -0500 Subject: [PATCH 12/23] update python runtime in heroku due to a security issue --- .github/workflows/checks.yml | 4 +--- .gitpod.Dockerfile | 2 +- runtime.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 461b4e924..f757c76b1 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -8,7 +8,7 @@ on: pull_request: {} env: - PYTHON_VERSION: 3.12.3 + PYTHON_VERSION: 3.12.7 PYTHONUNBUFFERED: 1 APP_URL: https://4geeks.com @@ -20,7 +20,6 @@ env: # |> naming-conventions |> linter # |> unexpected-behaviors |> pages - jobs: cache: runs-on: ubuntu-latest @@ -142,7 +141,6 @@ jobs: id: calculate-md5-2 run: pipenv run pip freeze -r requirements2.txt > requirements2.txt - - name: Check Pipfile.lock is up-to-date run: diff requirements.txt requirements2.txt diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index d38ec53fe..f4935f670 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -29,7 +29,7 @@ ENV PGHOSTADDR="127.0.0.1" ENV PGDATABASE="postgres" COPY --chown=gitpod:gitpod postgresql-hook.bash $HOME/.bashrc.d/200-postgresql-launch -# RUN pyenv install 3.12.3 && pyenv global 3.12.3 +# RUN pyenv install 3.12.7 && pyenv global 3.12.7 # RUN pip install pipenv USER gitpod diff --git a/runtime.txt b/runtime.txt index 4ddc7cd66..32905d6e0 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.12.3 +python-3.12.7 From b7fee234fc513d06c2c60bb5cb406b2e14c59e0f Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 30 Oct 2024 20:59:50 -0500 Subject: [PATCH 13/23] update deps --- Pipfile.lock | 113 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index ba3cf041e..b4862670c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -414,11 +414,11 @@ "django" ], "hashes": [ - "sha256:7cb8678ae393d78a299cf2f95cffb6118ae1f55bad9f691d3044b82c246fc694", - "sha256:9d7f86da5a36e2c19ba91bb126789608977b2e29ae5255622285a5c040e36244" + "sha256:5d02ead62715ab4061cd3b07562db898a976badc274d88743e6f00f6e3093e98", + "sha256:ad7a213739531a4493a5ea8190ad4752c42f228f47d86940133d7f63b168a299" ], "markers": "python_version >= '3.11'", - "version": "==1.1.0" + "version": "==1.1.1" }, "celery": { "hashes": [ @@ -1420,7 +1420,7 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "python_version >= '3.7'", + "markers": "platform_python_implementation == 'CPython'", "version": "==3.1.1" }, "grpcio": { @@ -1886,6 +1886,21 @@ "markers": "python_version >= '3.8'", "version": "==5.4.2" }, + "langcodes": { + "hashes": [ + "sha256:68f686fc3d358f222674ecf697ddcee3ace3c2fe325083ecad2543fd28a20e77", + "sha256:a24879fed238013ac3af2424b9d1124e38b4a38b2044fd297c8ff38e5912e718" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.1" + }, + "language-data": { + "hashes": [ + "sha256:77d5cab917f91ee0b2f1aa7018443e911cf8985ef734ca2ba3940770f6a3816b", + "sha256:82a86050bbd677bfde87d97885b17566cfe75dad3ac4f5ce44b52c28f752e773" + ], + "version": "==1.2.0" + }, "launchdarkly-eventsource": { "hashes": [ "sha256:8cb3301ec0daeb5e17eaa37b3b65f6660fab851b317e69271185ef2fb42c2fde", @@ -2061,6 +2076,88 @@ "markers": "python_version >= '3.6'", "version": "==5.3.0" }, + "marisa-trie": { + "hashes": [ + "sha256:06b099dd743676dbcd8abd8465ceac8f6d97d8bfaabe2c83b965495523b4cef2", + "sha256:0ee6cf6a16d9c3d1c94e21c8e63c93d8b34bede170ca4e937e16e1c0700d399f", + "sha256:0fe69fb9ffb2767746181f7b3b29bbd3454d1d24717b5958e030494f3d3cddf3", + "sha256:1db3213b451bf058d558f6e619bceff09d1d130214448a207c55e1526e2773a1", + "sha256:20948e40ab2038e62b7000ca6b4a913bc16c91a2c2e6da501bd1f917eeb28d51", + "sha256:2428b495003c189695fb91ceeb499f9fcced3a2dce853e17fa475519433c67ff", + "sha256:24a81aa7566e4ec96fc4d934581fe26d62eac47fc02b35fa443a0bb718b471e8", + "sha256:25688f34cac3bec01b4f655ffdd6c599a01f0bd596b4a79cf56c6f01a7df3560", + "sha256:36aa4401a1180615f74d575571a6550081d84fc6461e9aefc0bb7b2427af098e", + "sha256:3a27c408e2aefc03e0f1d25b2ff2afb85aac3568f6fa2ae2a53b57a2e87ce29d", + "sha256:3ad356442c2fea4c2a6f514738ddf213d23930f942299a2b2c05df464a00848a", + "sha256:429858a0452a7bedcf67bc7bb34383d00f666c980cb75a31bcd31285fbdd4403", + "sha256:436f62d27714970b9cdd3b3c41bdad046f260e62ebb0daa38125ef70536fc73b", + "sha256:46e528ee71808c961baf8c3ce1c46a8337ec7a96cc55389d11baafe5b632f8e9", + "sha256:4728ed3ae372d1ea2cdbd5eaa27b8f20a10e415d1f9d153314831e67d963f281", + "sha256:536ea19ce6a2ce61c57fed4123ecd10d18d77a0db45cd2741afff2b8b68f15b3", + "sha256:5685a14b3099b1422c4f59fa38b0bf4b5342ee6cc38ae57df9666a0b28eeaad3", + "sha256:594f98491a96c7f1ffe13ce292cef1b4e63c028f0707effdea0f113364c1ae6c", + "sha256:5bd39a4e1cc839a88acca2889d17ebc3f202a5039cd6059a13148ce75c8a6244", + "sha256:5e43891a37b0d7f618819fea14bd951289a0a8e3dd0da50c596139ca83ebb9b1", + "sha256:5e649f3dc8ab5476732094f2828cc90cac3be7c79bc0c8318b6fda0c1d248db4", + "sha256:5fe5a286f997848a410eebe1c28657506adaeb405220ee1e16cfcfd10deb37f2", + "sha256:638506eacf20ca503fff72221a7e66a6eadbf28d6a4a6f949fcf5b1701bb05ec", + "sha256:6532615111eec2c79e711965ece0bc95adac1ff547a7fff5ffca525463116deb", + "sha256:66b23e5b35dd547f85bf98db7c749bc0ffc57916ade2534a6bbc32db9a4abc44", + "sha256:6704adf0247d2dda42e876b793be40775dff46624309ad99bc7537098bee106d", + "sha256:67f0c2ec82c20a02c16fc9ba81dee2586ef20270127c470cb1054767aa8ba310", + "sha256:6946100a43f933fad6bc458c502a59926d80b321d5ac1ed2ff9c56605360496f", + "sha256:6c50c861faad0a5c091bd763e0729f958c316e678dfa065d3984fbb9e4eacbcd", + "sha256:735c363d9aaac82eaf516a28f7c6b95084c2e176d8231c87328dc80e112a9afa", + "sha256:746a7c60a17fccd3cfcfd4326926f02ea4fcdfc25d513411a0c4fc8e4a1ca51f", + "sha256:7ac170d20b97beb75059ba65d1ccad6b434d777c8992ab41ffabdade3b06dd74", + "sha256:7cca7f96236ffdbf49be4b2e42c132e3df05968ac424544034767650913524de", + "sha256:7e7b1786e852e014d03e5f32dbd991f9a9eb223dd3fa9a2564108b807e4b7e1c", + "sha256:852d7bcf14b0c63404de26e7c4c8d5d65ecaeca935e93794331bc4e2f213660b", + "sha256:875a6248e60fbb48d947b574ffa4170f34981f9e579bde960d0f9a49ea393ecc", + "sha256:8951e7ce5d3167fbd085703b4cbb3f47948ed66826bef9a2173c379508776cf5", + "sha256:8cf4f25cf895692b232f49aa5397af6aba78bb679fb917a05fce8d3cb1ee446d", + "sha256:952af3a5859c3b20b15a00748c36e9eb8316eb2c70bd353ae1646da216322908", + "sha256:98042040d1d6085792e8d0f74004fc0f5f9ca6091c298f593dd81a22a4643854", + "sha256:9c9b32b14651a6dcf9e8857d2df5d29d322a1ea8c0be5c8ffb88f9841c4ec62b", + "sha256:9e956e6a46f604b17d570901e66f5214fb6f658c21e5e7665deace236793cef6", + "sha256:9f627f4e41be710b6cb6ed54b0128b229ac9d50e2054d9cde3af0fef277c23cf", + "sha256:a2eb41d2f9114d8b7bd66772c237111e00d2bae2260824560eaa0a1e291ce9e8", + "sha256:a3c98613180cf1730e221933ff74b454008161b1a82597e41054127719964188", + "sha256:a4177dc0bd1374e82be9b2ba4d0c2733b0a85b9d154ceeea83a5bee8c1e62fbf", + "sha256:a8443d116c612cfd1961fbf76769faf0561a46d8e317315dd13f9d9639ad500c", + "sha256:aa7cd17e1c690ce96c538b2f4aae003d9a498e65067dd433c52dd069009951d4", + "sha256:ad548117744b2bcf0e3d97374608be0a92d18c2af13d98b728d37cd06248e571", + "sha256:aefe0973cc4698e0907289dc0517ab0c7cdb13d588201932ff567d08a50b0e2e", + "sha256:b0ef26733d3c836be79e812071e1a431ce1f807955a27a981ebb7993d95f842b", + "sha256:b1ce340da608530500ab4f963f12d6bfc8d8680900919a60dbdc9b78c02060a4", + "sha256:b1ec93f0d1ee6d7ab680a6d8ea1a08bf264636358e92692072170032dda652ba", + "sha256:b2a7d00f53f4945320b551bccb826b3fb26948bde1a10d50bb9802fabb611b10", + "sha256:b2eacb84446543082ec50f2fb563f1a94c96804d4057b7da8ed815958d0cdfbe", + "sha256:b5ea16e69bfda0ac028c921b58de1a4aaf83d43934892977368579cd3c0a2554", + "sha256:bd45142501300e7538b2e544905580918b67b1c82abed1275fe4c682c95635fa", + "sha256:c0fe2ace0cb1806badbd1c551a8ec2f8d4cf97bf044313c082ef1acfe631ddca", + "sha256:c484410911182457a8a1a0249d0c09c01e2071b78a0a8538cd5f7fa45589b13a", + "sha256:ce37d8ca462bb64cc13f529b9ed92f7b21fe8d1f1679b62e29f9cb7d0e888b49", + "sha256:ce59bcd2cda9bb52b0e90cc7f36413cd86c3d0ce7224143447424aafb9f4aa48", + "sha256:d2a82eb21afdaf22b50d9b996472305c05ca67fc4ff5a026a220320c9c961db6", + "sha256:d5648c6dcc5dc9200297fb779b1663b8a4467bda034a3c69bd9c32d8afb33b1d", + "sha256:d659fda873d8dcb2c14c2c331de1dee21f5a902d7f2de7978b62c6431a8850ef", + "sha256:d7eb20bf0e8b55a58d2a9b518aabc4c18278787bdba476c551dd1c1ed109e509", + "sha256:da4e4facb79614cc4653cfd859f398e4db4ca9ab26270ff12610e50ed7f1f6c6", + "sha256:de1665eaafefa48a308e4753786519888021740501a15461c77bdfd57638e6b4", + "sha256:e2699255d7ac610dee26d4ae7bda5951d05c7d9123a22e1f7c6a6f1964e0a4e4", + "sha256:e58788004adda24c401d1751331618ed20c507ffc23bfd28d7c0661a1cf0ad16", + "sha256:e70869737cc0e5bd903f620667da6c330d6737048d1f44db792a6af68a1d35be", + "sha256:eba6ca45500ca1a042466a0684aacc9838e7f20fe2605521ee19f2853062798f", + "sha256:ed3fb4ed7f2084597e862bcd56c56c5529e773729a426c083238682dba540e98", + "sha256:f2806f75817392cedcacb24ac5d80b0350dde8d3861d67d045c1d9b109764114", + "sha256:f35c2603a6be168088ed1db6ad1704b078aa8f39974c60888fbbced95dcadad4", + "sha256:f4cd800704a5fc57e53c39c3a6b0c9b1519ebdbcb644ede3ee67a06eb542697d", + "sha256:f713af9b8aa66a34cd3a78c7d150a560a75734713abe818a69021fd269e927fa" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.1" + }, "markdown": { "hashes": [ "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", @@ -4649,11 +4746,11 @@ "django" ], "hashes": [ - "sha256:7cb8678ae393d78a299cf2f95cffb6118ae1f55bad9f691d3044b82c246fc694", - "sha256:9d7f86da5a36e2c19ba91bb126789608977b2e29ae5255622285a5c040e36244" + "sha256:5d02ead62715ab4061cd3b07562db898a976badc274d88743e6f00f6e3093e98", + "sha256:ad7a213739531a4493a5ea8190ad4752c42f228f47d86940133d7f63b168a299" ], "markers": "python_version >= '3.11'", - "version": "==1.1.0" + "version": "==1.1.1" }, "certifi": { "hashes": [ @@ -5170,7 +5267,7 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "python_version >= '3.7'", + "markers": "platform_python_implementation == 'CPython'", "version": "==3.1.1" }, "griffe": { From 3992a791cacc751bece02ae49d820e1864ecdc78 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 30 Oct 2024 21:09:04 -0500 Subject: [PATCH 14/23] update a cond about payment endpoint --- breathecode/payments/actions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/breathecode/payments/actions.py b/breathecode/payments/actions.py index 2a212bbdd..8a3c89cc9 100644 --- a/breathecode/payments/actions.py +++ b/breathecode/payments/actions.py @@ -253,9 +253,8 @@ def ask_to_add_plan_and_charge_it_in_the_bag(plan: Plan, user: User, lang: str): price and plan.is_renewable and subscriptions.filter( - Q(Q(status="CANCELLED") | Q(status="DEPRECATED"), valid_until=None, next_payment_at__gte=utc_now) - | Q(valid_until__gte=utc_now) - ) + Q(valid_until=None, next_payment_at__gte=utc_now) | Q(valid_until__gte=utc_now) + ).exclude(status__in=["CANCELLED", "DEPRECATED"]) ): raise ValidationException( translation( From 5e3bc5f67de0a060b59fd3834b8a6fc97c5a0964 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:41:34 +0100 Subject: [PATCH 15/23] Update data.py --- breathecode/payments/data.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/breathecode/payments/data.py b/breathecode/payments/data.py index dd4e496dc..34b8762ca 100644 --- a/breathecode/payments/data.py +++ b/breathecode/payments/data.py @@ -9,13 +9,6 @@ def get_virtual_consumables() -> list[ConsumableType]: return [ consumable( - service_item=1, - cohort_set=1, - event_type_set=1, - mentorship_service_set=1, - ), - consumable( - service_item=service_item(service=76, unit_type="unit", how_many=10), - cohort_set=1, + service_item=service_item(service=76, unit_type="unit", how_many=-1), ), ] From 9a9f6bc4822ab277bdcc74a2f6b79a27cbe89844 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 31 Oct 2024 14:21:44 +0000 Subject: [PATCH 16/23] add changes --- .gitpod.Dockerfile | 2 +- Pipfile.lock | 122 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 20 deletions(-) diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index f05fe0974..fa798aff2 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -11,7 +11,7 @@ RUN sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release wget --quiet -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - && \ echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" | sudo tee /etc/apt/sources.list.d/llvm.list && \ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - && \ - sudo install-packages postgresql-16 postgresql-contrib-16 redis-server + sudo install-packages postgresql-16 postgresql-contrib-16 redis-server netcat # Setup PostgreSQL server for user gitpod ENV PATH="/usr/lib/postgresql/16/bin:$PATH" diff --git a/Pipfile.lock b/Pipfile.lock index b4862670c..9e06745d4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -4700,7 +4700,6 @@ "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==2.16.0" }, @@ -4743,7 +4742,7 @@ }, "capy-core": { "extras": [ - "django" + "pytest" ], "hashes": [ "sha256:5d02ead62715ab4061cd3b07562db898a976badc274d88743e6f00f6e3093e98", @@ -4757,7 +4756,6 @@ "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], - "index": "pypi", "markers": "python_version >= '3.6'", "version": "==2024.8.30" }, @@ -4989,7 +4987,6 @@ "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" ], - "index": "pypi", "markers": "python_version >= '3.10'", "version": "==5.1.2" }, @@ -5109,7 +5106,6 @@ "sha256:f4e526fdc279c655c1e809b0c34b45844182c2a6b219802da5e411bd2cf5a8ad", "sha256:f7f4f171d4d2018170454d84c934842e1b5f6ce7468ba298f6e7f7cff15000a3" ], - "index": "pypi", "markers": "python_version >= '3.9'", "version": "==24.10.3" }, @@ -5136,7 +5132,6 @@ "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==2.149.0" }, @@ -5267,7 +5262,7 @@ "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" ], - "markers": "platform_python_implementation == 'CPython'", + "markers": "python_version >= '3.7'", "version": "==3.1.1" }, "griffe": { @@ -5336,6 +5331,7 @@ "sha256:f9fff78ba10d4250bfc07a01bd6254a6d87dc67f9627adece85c0b2ed754fa96", "sha256:fa0c739ad8b1996bd24823950e3cb5152ae91fca1c09cc791190bf1627ffefba" ], + "markers": "python_version >= '3.8'", "version": "==1.67.1" }, "grpcio-status": { @@ -5343,6 +5339,7 @@ "sha256:16e6c085950bdacac97c779e6a502ea671232385e6e37f258884d6883392c2bd", "sha256:2bf38395e028ceeecfd8866b081f61628114b384da7d51ae064ddc8d766a5d11" ], + "markers": "python_version >= '3.8'", "version": "==1.67.1" }, "httplib2": { @@ -5366,6 +5363,7 @@ "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], + "markers": "python_version >= '3.6'", "version": "==3.10" }, "iniconfig": { @@ -5486,7 +5484,6 @@ "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==3.7" }, @@ -5601,12 +5598,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:452a7c5d21284b373f36b981a2cbebfff59263feebeede1bc28652e9c5bbe316", - "sha256:92779b5e9b5934540c574c11647131d217dc540dce72b05feeda088c8eb1b8f2" + "sha256:4aae0664c456fd12837a3192e0225c17960ba8bf55d7f0a7daef7e4b0b914a34", + "sha256:83be7ff30b65a1e4930dfa4ab911e75780a3afc9583d162692e434581cb46979" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.42" + "version": "==9.5.43" }, "mkdocs-material-extensions": { "hashes": [ @@ -5745,7 +5742,6 @@ "sha256:faa88bc527d0f097abdc2c663cddf37c05a1c2f113716601555249805cf573f1", "sha256:fc44e3c68ff00fd991b59092a54350e6e4911152682b4782f68070985aa9e648" ], - "index": "pypi", "markers": "python_version >= '3.10'", "version": "==2.1.2" }, @@ -5867,7 +5863,6 @@ "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" ], - "index": "pypi", "markers": "python_version >= '3.9'", "version": "==11.0.0" }, @@ -5982,7 +5977,7 @@ "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version > '3.0'", + "markers": "python_version >= '3.9'", "version": "==3.2.0" }, "pyright": { @@ -5999,6 +5994,7 @@ "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" ], + "index": "pypi", "markers": "python_version >= '3.8'", "version": "==8.3.3" }, @@ -6051,7 +6047,6 @@ "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" ], - "index": "pypi", "version": "==2024.2" }, "pyyaml": { @@ -6110,7 +6105,6 @@ "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==6.0.2" }, @@ -6124,8 +6118,100 @@ }, "regex": { "hashes": [ + "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", + "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", + "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", + "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", + "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", + "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", + "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", + "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", + "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", + "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", + "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", + "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", + "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", + "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", + "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", + "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", + "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", + "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", + "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", + "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", + "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", + "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", + "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", + "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", + "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", + "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", + "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", + "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", + "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", + "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", + "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", + "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", + "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", + "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", + "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", + "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", + "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", + "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", + "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", + "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", + "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", + "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", + "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a" + "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", + "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", + "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", + "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", + "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", + "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", + "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", + "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", + "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", + "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", + "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", + "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", + "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", + "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", + "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", + "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", + "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", + "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", + "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", + "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", + "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", + "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", + "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", + "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", + "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", + "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", + "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", + "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", + "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", + "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", + "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", + "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", + "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", + "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", + "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", + "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", + "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", + "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", + "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", + "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", + "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", + "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", + "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", + "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", + "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", + "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", + "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", + "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", + "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", + "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" ], "markers": "python_version >= '3.8'", "version": "==2024.9.11" @@ -6135,7 +6221,6 @@ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==2.32.3" }, @@ -6223,7 +6308,6 @@ "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" ], - "index": "pypi", "markers": "python_version >= '3.6'", "version": "==4.1.1" }, From 6972b373c6e5a8adeb2160ce2b16cb29455109ed Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 1 Nov 2024 15:34:13 -0400 Subject: [PATCH 17/23] Update models.py --- breathecode/assessment/models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index cc0fa6158..a9ecd2266 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -294,14 +294,20 @@ def __init__(self, *args, **kwargs): self._old_status = self.status def save(self, *args, **kwargs): + + is_creating = self.pk is None if not self.pk: self.token = binascii.hexlify(os.urandom(20)).decode() + _instance = super().save(*args, **kwargs) + # Answer is being closed - if self.status != self._old_status: + if is_creating or self.status != old_status: signals.userassessment_status_updated.send_robust(instance=self, sender=self.__class__) - return super().save(*args, **kwargs) + return _instance + + def get_score(self): From 14291ba34ae314e5c473ee3d89c1bb9d33ae3b58 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 1 Nov 2024 15:37:29 -0400 Subject: [PATCH 18/23] Update models.py --- breathecode/assessment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index a9ecd2266..0aa3e7d3a 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -305,7 +305,7 @@ def save(self, *args, **kwargs): if is_creating or self.status != old_status: signals.userassessment_status_updated.send_robust(instance=self, sender=self.__class__) - return _instance + return _instance From 189a054dbe23177c2b4f17fe49a5ce50149f6571 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 1 Nov 2024 22:46:35 -0400 Subject: [PATCH 19/23] Update models.py --- breathecode/assessment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 0aa3e7d3a..3bf4b332f 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -302,7 +302,7 @@ def save(self, *args, **kwargs): _instance = super().save(*args, **kwargs) # Answer is being closed - if is_creating or self.status != old_status: + if is_creating or self.status != self.old_status: signals.userassessment_status_updated.send_robust(instance=self, sender=self.__class__) return _instance From 560db3bc57908131080650a8c823290e708d1946 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 1 Nov 2024 23:00:14 -0400 Subject: [PATCH 20/23] Update models.py --- breathecode/assessment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 3bf4b332f..dd945236f 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -302,7 +302,7 @@ def save(self, *args, **kwargs): _instance = super().save(*args, **kwargs) # Answer is being closed - if is_creating or self.status != self.old_status: + if is_creating or self.status != self._old_status: signals.userassessment_status_updated.send_robust(instance=self, sender=self.__class__) return _instance From 9f4ec011117d984c735dcdc8045a45bf22961dbc Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 1 Nov 2024 23:18:21 -0400 Subject: [PATCH 21/23] Update serializers.py --- breathecode/assessment/serializers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index e7f46a9f2..d2acfacc9 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -162,7 +162,6 @@ class HookUserAssessmentSerializer(serpy.Serializer): status_text = serpy.Field() conversion_info = serpy.Field() - total_score = serpy.Field() comment = serpy.Field() started_at = serpy.Field() @@ -170,6 +169,16 @@ class HookUserAssessmentSerializer(serpy.Serializer): created_at = serpy.Field() + summary = serpy.MethodField() + def get_summary(self, obj): + total_score, last_one = obj.get_score() + + last_answer = None + if last_one is not None: + last_answer = AnswerSmallSerializer(last_one).data + + return {"last_answer": last_answer, "live_score": total_score} + class PublicUserAssessmentSerializer(serpy.Serializer): id = serpy.Field() From a0c72066fd80bdcfd83cdbf46179dc77d36dcd86 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 4 Nov 2024 15:13:59 -0500 Subject: [PATCH 22/23] Update views.py --- breathecode/admissions/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/admissions/views.py b/breathecode/admissions/views.py index e4bb9d3de..6a6762297 100644 --- a/breathecode/admissions/views.py +++ b/breathecode/admissions/views.py @@ -1856,7 +1856,7 @@ def get(self, request, syllabus_id, version, academy_id): day.get("teacher_instructions", ""), ] ) - cumulative_days += day["duration_in_days"] + cumulative_days += day["duration_in_days"] if "duration_in_days" in day else 1 return response From cdaa714f956effdd42247fb2e140f7b1b839725d Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 4 Nov 2024 15:53:22 -0500 Subject: [PATCH 23/23] Update views.py --- breathecode/admissions/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/breathecode/admissions/views.py b/breathecode/admissions/views.py index 6a6762297..01b8f557b 100644 --- a/breathecode/admissions/views.py +++ b/breathecode/admissions/views.py @@ -1834,6 +1834,9 @@ def get(self, request, syllabus_id, version, academy_id): # Write the data rows for each day for day in sorted(syllabus_version.json["days"], key=lambda x: x["position"]): week_number = math.ceil(cumulative_days / class_days_per_week) + if "technologies" not in day: + day["technologies"] = [] + if lang == "es": writer.writerow( [