From dd1c59566020b1a09bb5db2994071a5e8c0a7e1e Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 16 May 2024 14:29:08 -0500 Subject: [PATCH 01/40] add provisioning multiplier --- .gitignore | 1 + breathecode/provisioning/actions.py | 15 ++++++++-- breathecode/provisioning/tasks.py | 7 ++--- .../provisioning/tests/tasks/tests_upload.py | 28 +++++++++---------- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 6cfeaa413..c2eff4529 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,4 @@ dump.rdb *:Zone.Identifier node_modules/ +.env* diff --git a/breathecode/provisioning/actions.py b/breathecode/provisioning/actions.py index ab9c3b28b..f53b9d144 100644 --- a/breathecode/provisioning/actions.py +++ b/breathecode/provisioning/actions.py @@ -1,3 +1,4 @@ +import os import random import re from datetime import datetime @@ -242,6 +243,16 @@ def handle_pending_github_user(organization: str, username: str) -> list[Academy return [org.academy for org in orgs] +def get_multiplier() -> float: + try: + x = os.getenv('PROVISIONING_MULTIPLIER', '1.3').replace(',', '.') + x = float(x) + except Exception: + x = 1.3 + + return x + + def add_codespaces_activity(context: ActivityContext, field: dict, position: int) -> None: if isinstance(field['Username'], float): field['Username'] = '' @@ -357,7 +368,7 @@ def add_codespaces_activity(context: ActivityContext, field: dict, position: int price, _ = ProvisioningPrice.objects.get_or_create( currency=currency, unit_type=field['Unit Type'], - price_per_unit=field['Price Per Unit ($)'], + price_per_unit=field['Price Per Unit ($)'] * context['provisioning_multiplier'], multiplier=field['Multiplier'], ) @@ -495,7 +506,7 @@ def add_gitpod_activity(context: ActivityContext, field: dict, position: int): price, _ = ProvisioningPrice.objects.get_or_create( currency=currency, unit_type='Credits', - price_per_unit=0.036, + price_per_unit=0.036 * context['provisioning_multiplier'], multiplier=1, ) diff --git a/breathecode/provisioning/tasks.py b/breathecode/provisioning/tasks.py index 914a0215c..5d5e42a26 100644 --- a/breathecode/provisioning/tasks.py +++ b/breathecode/provisioning/tasks.py @@ -14,11 +14,7 @@ from breathecode.payments.services.stripe import Stripe from breathecode.provisioning import actions -from breathecode.provisioning.models import ( - ProvisioningBill, - ProvisioningConsumptionEvent, - ProvisioningUserConsumption, -) +from breathecode.provisioning.models import ProvisioningBill, ProvisioningConsumptionEvent, ProvisioningUserConsumption from breathecode.services.google_cloud.storage import Storage from breathecode.utils.decorators import TaskPriority from breathecode.utils.io.file import cut_csv @@ -158,6 +154,7 @@ def upload(hash: str, *, page: int = 0, force: bool = False, task_manager_id: in 'github_academy_user_logs': {}, 'provisioning_activity_prices': {}, 'provisioning_activity_kinds': {}, + 'provisioning_multiplier': actions.get_multiplier(), 'currencies': {}, 'profile_academies': {}, 'hash': hash, diff --git a/breathecode/provisioning/tests/tasks/tests_upload.py b/breathecode/provisioning/tests/tasks/tests_upload.py index b0550b45d..8a40e5a71 100644 --- a/breathecode/provisioning/tests/tasks/tests_upload.py +++ b/breathecode/provisioning/tests/tasks/tests_upload.py @@ -485,7 +485,7 @@ def test_users_not_found(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -610,7 +610,7 @@ def test_users_not_found__case1(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -760,7 +760,7 @@ def test_users_not_found__case2(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -902,7 +902,7 @@ def test_from_github_credentials__generate_anything(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -1014,7 +1014,7 @@ def test_pagination(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -1131,7 +1131,7 @@ def test_from_github_credentials__generate_anything__force(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -1240,7 +1240,7 @@ def test_from_github_credentials__generate_anything__case1(self): 'currency_id': 1, 'id': n + 1, 'multiplier': csv['Multiplier'][n], - 'price_per_unit': csv['Price Per Unit ($)'][n], + 'price_per_unit': csv['Price Per Unit ($)'][n] * 1.3, 'unit_type': csv['Unit Type'][n], }) for n in range(10) ]) @@ -1334,7 +1334,7 @@ def test_users_not_found(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) @@ -1441,7 +1441,7 @@ def test_from_github_credentials__vendor_not_found(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) @@ -1543,7 +1543,7 @@ def test_from_github_credentials__generate_anything(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) @@ -1653,7 +1653,7 @@ def test_pagination(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) @@ -1781,7 +1781,7 @@ def test_from_github_credentials__generate_anything__case1(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) @@ -1919,7 +1919,7 @@ def test_from_github_credentials__generate_anything__case2(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) @@ -2069,7 +2069,7 @@ def test_from_github_credentials__generate_anything__case3(self): 'currency_id': 1, 'id': 1, 'multiplier': 1.0, - 'price_per_unit': 0.036, + 'price_per_unit': 0.036 * 1.3, 'unit_type': 'Credits', }) ]) From 8c3a6480bf5c4361a8366804a360c487261927df Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 16 May 2024 14:31:51 -0500 Subject: [PATCH 02/40] update deps --- Pipfile.lock | 1070 ++++++++++++++++++++++++-------------------------- 1 file changed, 518 insertions(+), 552 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 96a1a09d1..666106a1a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -655,11 +655,11 @@ }, "cssutils": { "hashes": [ - "sha256:4ad7d2f29270b22cf199f65a6b5e795f2c3130f3b9fb50c3d45e5054ef86e41a", - "sha256:93cf92a350b1c123b17feff042e212f94d960975a3ed145743d84ebe8ccec7ab" + "sha256:220816dc6d413e81281bbd568c473a8ae28f73b1af008b1bacf3a7ebd21e0334", + "sha256:cd24a30b9a848ca92d80f0d1b362139c0b69de31394d585dbf1b17a5dc4aa627" ], "markers": "python_version >= '3.8'", - "version": "==2.10.2" + "version": "==2.11.0" }, "currencies": { "hashes": [ @@ -713,12 +713,12 @@ }, "django": { "hashes": [ - "sha256:8af4f166dc9a2bb822f9374cd78e34a10c286b402597fe2c7fb97c131656ba65", - "sha256:dc95c9cb2a37ba54599d9d1c8faf81609d36f3e74cd04395ce1300573e57baf9" + "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905", + "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.0.5" + "version": "==5.0.6" }, "django-cors-headers": { "hashes": [ @@ -1021,12 +1021,12 @@ }, "google-cloud-bigquery": { "hashes": [ - "sha256:80c8e31a23b68b7d3ae5d138c9a9edff69d100ee812db73a5e63c79a13a5063d", - "sha256:957591e6f948d7cb4aa0f7a8e4e47b4617cd7f0269e28a71c37953c39b6e8a4c" + "sha256:7ecdb207727d513b1bce1f213dbb926ed2e1d4f0122778de00f0e56d19d47a01", + "sha256:dc0a4a47ab541a34aa1dc1f48539d88c091adc0637da7744d7fab6f3bc8886d5" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.22.0" + "version": "==3.23.0" }, "google-cloud-bigquery-storage": { "hashes": [ @@ -1278,7 +1278,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", "version": "==3.0.3" }, "grpcio": { @@ -1647,164 +1647,148 @@ }, "lxml": { "hashes": [ - "sha256:04ab5415bf6c86e0518d57240a96c4d1fcfc3cb370bb2ac2a732b67f579e5a04", - "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0", - "sha256:058a1308914f20784c9f4674036527e7c04f7be6fb60f5d61353545aa7fcb739", - "sha256:08802f0c56ed150cc6885ae0788a321b73505d2263ee56dad84d200cab11c07a", - "sha256:0a15438253b34e6362b2dc41475e7f80de76320f335e70c5528b7148cac253a1", - "sha256:0c3f67e2aeda739d1cc0b1102c9a9129f7dc83901226cc24dd72ba275ced4218", - "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9", - "sha256:0ed777c1e8c99b63037b91f9d73a6aad20fd035d77ac84afcc205225f8f41188", - "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138", - "sha256:0f8c09ed18ecb4ebf23e02b8e7a22a05d6411911e6fabef3a36e4f371f4f2585", - "sha256:11a04306fcba10cd9637e669fd73aa274c1c09ca64af79c041aa820ea992b637", - "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe", - "sha256:1c5bb205e9212d0ebddf946bc07e73fa245c864a5f90f341d11ce7b0b854475d", - "sha256:1f7785f4f789fdb522729ae465adcaa099e2a3441519df750ebdccc481d961a1", - "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095", - "sha256:21c2e6b09565ba5b45ae161b438e033a86ad1736b8c838c766146eff8ceffff9", - "sha256:2213afee476546a7f37c7a9b4ad4d74b1e112a6fafffc9185d6d21f043128c81", - "sha256:27aa20d45c2e0b8cd05da6d4759649170e8dfc4f4e5ef33a34d06f2d79075d57", - "sha256:2a66bf12fbd4666dd023b6f51223aed3d9f3b40fef06ce404cb75bafd3d89536", - "sha256:2c9d147f754b1b0e723e6afb7ba1566ecb162fe4ea657f53d2139bbf894d050a", - "sha256:2ddfe41ddc81f29a4c44c8ce239eda5ade4e7fc305fb7311759dd6229a080052", - "sha256:31e9a882013c2f6bd2f2c974241bf4ba68c85eba943648ce88936d23209a2e01", - "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98", - "sha256:3545039fa4779be2df51d6395e91a810f57122290864918b172d5dc7ca5bb433", - "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1", - "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f", - "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4", - "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b", - "sha256:3dbe858ee582cbb2c6294dc85f55b5f19c918c2597855e950f34b660f1a5ede6", - "sha256:3dc773b2861b37b41a6136e0b72a1a44689a9c4c101e0cddb6b854016acc0aa8", - "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306", - "sha256:417d14450f06d51f363e41cace6488519038f940676ce9664b34ebf5653433a5", - "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f", - "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4", - "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be", - "sha256:52421b41ac99e9d91934e4d0d0fe7da9f02bfa7536bb4431b4c05c906c8c6919", - "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af", - "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66", - "sha256:5670fb70a828663cc37552a2a85bf2ac38475572b0e9b91283dc09efb52c41d1", - "sha256:56c22432809085b3f3ae04e6e7bdd36883d7258fcd90e53ba7b2e463efc7a6af", - "sha256:58278b29cb89f3e43ff3e0c756abbd1518f3ee6adad9e35b51fb101c1c1daaec", - "sha256:588008b8497667f1ddca7c99f2f85ce8511f8f7871b4a06ceede68ab62dff64b", - "sha256:59565f10607c244bc4c05c0c5fa0c190c990996e0c719d05deec7030c2aa8289", - "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a", - "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d", - "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102", - "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9", - "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc", - "sha256:5d5792e9b3fb8d16a19f46aa8208987cfeafe082363ee2745ea8b643d9cc5b45", - "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa", - "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a", - "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c", - "sha256:627402ad8dea044dde2eccde4370560a2b750ef894c9578e1d4f8ffd54000461", - "sha256:644df54d729ef810dcd0f7732e50e5ad1bd0a135278ed8d6bcb06f33b6b6f708", - "sha256:64641a6068a16201366476731301441ce93457eb8452056f570133a6ceb15fca", - "sha256:64c2baa7774bc22dd4474248ba16fe1a7f611c13ac6123408694d4cc93d66dbd", - "sha256:6588c459c5627fefa30139be4d2e28a2c2a1d0d1c265aad2ba1935a7863a4913", - "sha256:66bc5eb8a323ed9894f8fa0ee6cb3e3fb2403d99aee635078fd19a8bc7a5a5da", - "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0", - "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5", - "sha256:6992030d43b916407c9aa52e9673612ff39a575523c5f4cf72cdef75365709a5", - "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96", - "sha256:6ab833e4735a7e5533711a6ea2df26459b96f9eec36d23f74cafe03631647c41", - "sha256:6cc6ee342fb7fa2471bd9b6d6fdfc78925a697bf5c2bcd0a302e98b0d35bfad3", - "sha256:6cf58416653c5901e12624e4013708b6e11142956e7f35e7a83f1ab02f3fe456", - "sha256:70a9768e1b9d79edca17890175ba915654ee1725975d69ab64813dd785a2bd5c", - "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867", - "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0", - "sha256:7221d49259aa1e5a8f00d3d28b1e0b76031655ca74bb287123ef56c3db92f213", - "sha256:74b28c6334cca4dd704e8004cba1955af0b778cf449142e581e404bd211fb619", - "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240", - "sha256:78bfa756eab503673991bdcf464917ef7845a964903d3302c5f68417ecdc948c", - "sha256:794f04eec78f1d0e35d9e0c36cbbb22e42d370dda1609fb03bcd7aeb458c6377", - "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b", - "sha256:7a7efd5b6d3e30d81ec68ab8a88252d7c7c6f13aaa875009fe3097eb4e30b84c", - "sha256:7c17b64b0a6ef4e5affae6a3724010a7a66bda48a62cfe0674dabd46642e8b54", - "sha256:804f74efe22b6a227306dd890eecc4f8c59ff25ca35f1f14e7482bbce96ef10b", - "sha256:853e074d4931dbcba7480d4dcab23d5c56bd9607f92825ab80ee2bd916edea53", - "sha256:857500f88b17a6479202ff5fe5f580fc3404922cd02ab3716197adf1ef628029", - "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6", - "sha256:88e22fc0a6684337d25c994381ed8a1580a6f5ebebd5ad41f89f663ff4ec2885", - "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94", - "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134", - "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8", - "sha256:9123716666e25b7b71c4e1789ec829ed18663152008b58544d95b008ed9e21e9", - "sha256:958244ad566c3ffc385f47dddde4145088a0ab893504b54b52c041987a8c1863", - "sha256:96323338e6c14e958d775700ec8a88346014a85e5de73ac7967db0367582049b", - "sha256:9676bfc686fa6a3fa10cd4ae6b76cae8be26eb5ec6811d2a325636c460da1806", - "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11", - "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9", - "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817", - "sha256:9ca66b8e90daca431b7ca1408cae085d025326570e57749695d6a01454790e95", - "sha256:9e2addd2d1866fe112bc6f80117bcc6bc25191c5ed1bfbcf9f1386a884252ae8", - "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc", - "sha256:a2b44bec7adf3e9305ce6cbfa47a4395667e744097faed97abb4728748ba7d47", - "sha256:a2dfe7e2473f9b59496247aad6e23b405ddf2e12ef0765677b0081c02d6c2c0b", - "sha256:a55ee573116ba208932e2d1a037cc4b10d2c1cb264ced2184d00b18ce585b2c0", - "sha256:a7baf9ffc238e4bf401299f50e971a45bfcc10a785522541a6e3179c83eabf0a", - "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f", - "sha256:a91481dbcddf1736c98a80b122afa0f7296eeb80b72344d7f45dc9f781551f56", - "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef", - "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851", - "sha256:adfb84ca6b87e06bc6b146dc7da7623395db1e31621c4785ad0658c5028b37d7", - "sha256:afbbdb120d1e78d2ba8064a68058001b871154cc57787031b645c9142b937a62", - "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4", - "sha256:b070bbe8d3f0f6147689bed981d19bbb33070225373338df755a46893528104a", - "sha256:b0b58fbfa1bf7367dde8a557994e3b1637294be6cf2169810375caf8571a085c", - "sha256:b560e3aa4b1d49e0e6c847d72665384db35b2f5d45f8e6a5c0072e0283430533", - "sha256:b6241d4eee5f89453307c2f2bfa03b50362052ca0af1efecf9fef9a41a22bb4f", - "sha256:b6787b643356111dfd4032b5bffe26d2f8331556ecb79e15dacb9275da02866e", - "sha256:bcbf4af004f98793a95355980764b3d80d47117678118a44a80b721c9913436a", - "sha256:beb72935a941965c52990f3a32d7f07ce869fe21c6af8b34bf6a277b33a345d3", - "sha256:bf2e2458345d9bffb0d9ec16557d8858c9c88d2d11fed53998512504cd9df49b", - "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4", - "sha256:c38d7b9a690b090de999835f0443d8aa93ce5f2064035dfc48f27f02b4afc3d0", - "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d", - "sha256:c817d420c60a5183953c783b0547d9eb43b7b344a2c46f69513d5952a78cddf3", - "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5", - "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534", - "sha256:cc4691d60512798304acb9207987e7b2b7c44627ea88b9d77489bbe3e6cc3bd4", - "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144", - "sha256:cd53553ddad4a9c2f1f022756ae64abe16da1feb497edf4d9f87f99ec7cf86bd", - "sha256:cf22b41fdae514ee2f1691b6c3cdeae666d8b7fa9434de445f12bbeee0cf48dd", - "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860", - "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704", - "sha256:d4f2cc7060dc3646632d7f15fe68e2fa98f58e35dd5666cd525f3b35d3fed7f8", - "sha256:d7520db34088c96cc0e0a3ad51a4fd5b401f279ee112aa2b7f8f976d8582606d", - "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9", - "sha256:da052e7962ea2d5e5ef5bc0355d55007407087392cf465b7ad84ce5f3e25fe0f", - "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad", - "sha256:ddc678fb4c7e30cf830a2b5a8d869538bc55b28d6c68544d09c7d0d8f17694dc", - "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510", - "sha256:e02c5175f63effbd7c5e590399c118d5db6183bbfe8e0d118bdb5c2d1b48d937", - "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a", - "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460", - "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85", - "sha256:e3d30321949861404323c50aebeb1943461a67cd51d4200ab02babc58bd06a86", - "sha256:e89580a581bf478d8dcb97d9cd011d567768e8bc4095f8557b21c4d4c5fea7d0", - "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246", - "sha256:ec42088248c596dbd61d4ae8a5b004f97a4d91a9fd286f632e42e60b706718d7", - "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa", - "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08", - "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270", - "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a", - "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169", - "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e", - "sha256:f443cdef978430887ed55112b491f670bba6462cea7a7742ff8f14b7abb98d75", - "sha256:f51969bac61441fd31f028d7b3b45962f3ecebf691a510495e5d2cd8c8092dbd", - "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354", - "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c", - "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1", - "sha256:feaa45c0eae424d3e90d78823f3828e7dc42a42f21ed420db98da2c4ecf0a2cb", - "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f", - "sha256:ff46d772d5f6f73564979cd77a4fffe55c916a05f3cb70e7c9c0590059fb29ef" + "sha256:02437fb7308386867c8b7b0e5bc4cd4b04548b1c5d089ffb8e7b31009b961dc3", + "sha256:02f6a8eb6512fdc2fd4ca10a49c341c4e109aa6e9448cc4859af5b949622715a", + "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0", + "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b", + "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f", + "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6", + "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73", + "sha256:0a028b61a2e357ace98b1615fc03f76eb517cc028993964fe08ad514b1e8892d", + "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad", + "sha256:13e69be35391ce72712184f69000cda04fc89689429179bc4c0ae5f0b7a8c21b", + "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a", + "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5", + "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab", + "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316", + "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df", + "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca", + "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264", + "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8", + "sha256:23cfafd56887eaed93d07bc4547abd5e09d837a002b791e9767765492a75883f", + "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b", + "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3", + "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5", + "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed", + "sha256:339ee4a4704bc724757cd5dd9dc8cf4d00980f5d3e6e06d5847c1b594ace68ab", + "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5", + "sha256:343ab62e9ca78094f2306aefed67dcfad61c4683f87eee48ff2fd74902447726", + "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d", + "sha256:364d03207f3e603922d0d3932ef363d55bbf48e3647395765f9bfcbdf6d23632", + "sha256:38b67afb0a06b8575948641c1d6d68e41b83a3abeae2ca9eed2ac59892b36706", + "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8", + "sha256:3b019d4ee84b683342af793b56bb35034bd749e4cbdd3d33f7d1107790f8c472", + "sha256:3b6a30a9ab040b3f545b697cb3adbf3696c05a3a68aad172e3fd7ca73ab3c835", + "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf", + "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db", + "sha256:453d037e09a5176d92ec0fd282e934ed26d806331a8b70ab431a81e2fbabf56d", + "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545", + "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9", + "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be", + "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe", + "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905", + "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438", + "sha256:50127c186f191b8917ea2fb8b206fbebe87fd414a6084d15568c27d0a21d60db", + "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776", + "sha256:519895c99c815a1a24a926d5b60627ce5ea48e9f639a5cd328bda0515ea0f10c", + "sha256:54401c77a63cc7d6dc4b4e173bb484f28a5607f3df71484709fe037c92d4f0ed", + "sha256:546cf886f6242dff9ec206331209db9c8e1643ae642dea5fdbecae2453cb50fd", + "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484", + "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d", + "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6", + "sha256:5b8c041b6265e08eac8a724b74b655404070b636a8dd6d7a13c3adc07882ef30", + "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182", + "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61", + "sha256:610b5c77428a50269f38a534057444c249976433f40f53e3b47e68349cca1425", + "sha256:625e3ef310e7fa3a761d48ca7ea1f9d8718a32b1542e727d584d82f4453d5eeb", + "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1", + "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511", + "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e", + "sha256:6d68ce8e7b2075390e8ac1e1d3a99e8b6372c694bbe612632606d1d546794207", + "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b", + "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585", + "sha256:739e36ef7412b2bd940f75b278749106e6d025e40027c0b94a17ef7968d55d56", + "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391", + "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85", + "sha256:74e4f025ef3db1c6da4460dd27c118d8cd136d0391da4e387a15e48e5c975147", + "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18", + "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1", + "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa", + "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48", + "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3", + "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67", + "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7", + "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34", + "sha256:8a7e24cb69ee5f32e003f50e016d5fde438010c1022c96738b04fc2423e61706", + "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8", + "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c", + "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115", + "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009", + "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466", + "sha256:96e85aa09274955bb6bd483eaf5b12abadade01010478154b0ec70284c1b1526", + "sha256:981a06a3076997adf7c743dcd0d7a0415582661e2517c7d961493572e909aa1d", + "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525", + "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14", + "sha256:a02d3c48f9bb1e10c7788d92c0c7db6f2002d024ab6e74d6f45ae33e3d0288a3", + "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0", + "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b", + "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1", + "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf", + "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf", + "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0", + "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b", + "sha256:aef5474d913d3b05e613906ba4090433c515e13ea49c837aca18bde190853dff", + "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88", + "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2", + "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40", + "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716", + "sha256:b47633251727c8fe279f34025844b3b3a3e40cd1b198356d003aa146258d13a2", + "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2", + "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a", + "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734", + "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87", + "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48", + "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36", + "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b", + "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07", + "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573", + "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001", + "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9", + "sha256:cd9e78285da6c9ba2d5c769628f43ef66d96ac3085e59b10ad4f3707980710d3", + "sha256:cf2a978c795b54c539f47964ec05e35c05bd045db5ca1e8366988c7f2fe6b3ce", + "sha256:d14a0d029a4e176795cef99c056d58067c06195e0c7e2dbb293bf95c08f772a3", + "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04", + "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927", + "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083", + "sha256:d4ed0c7cbecde7194cd3228c044e86bf73e30a23505af852857c09c24e77ec5d", + "sha256:d83e2d94b69bf31ead2fa45f0acdef0757fa0458a129734f59f67f3d2eb7ef32", + "sha256:d8bbcd21769594dbba9c37d3c819e2d5847656ca99c747ddb31ac1701d0c0ed9", + "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f", + "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2", + "sha256:dfa7c241073d8f2b8e8dbc7803c434f57dbb83ae2a3d7892dd068d99e96efe2c", + "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d", + "sha256:e290d79a4107d7d794634ce3e985b9ae4f920380a813717adf61804904dc4393", + "sha256:e3d9d13603410b72787579769469af730c38f2f25505573a5888a94b62b920f8", + "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6", + "sha256:e49b052b768bb74f58c7dda4e0bdf7b79d43a9204ca584ffe1fb48a6f3c84c66", + "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5", + "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97", + "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196", + "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836", + "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae", + "sha256:f5b65529bb2f21ac7861a0e94fdbf5dc0daab41497d18223b46ee8515e5ad297", + "sha256:f60fdd125d85bf9c279ffb8e94c78c51b3b6a37711464e1f5f31078b45002421", + "sha256:f61efaf4bed1cc0860e567d2ecb2363974d414f7f1f124b1df368bbf183453a6", + "sha256:f90e552ecbad426eab352e7b2933091f2be77115bb16f09f78404861c8322981", + "sha256:f956196ef61369f1685d14dad80611488d8dc1ef00be57c0c5a03064005b0f30", + "sha256:fb91819461b1b56d06fa4bcf86617fac795f6a99d12239fb0c68dbeba41a0a30", + "sha256:fbc9d316552f9ef7bba39f4edfad4a734d3d6f93341232a9dddadec4f15d425f", + "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324", + "sha256:ffb2be176fed4457e445fe540617f0252a72a8bc56208fd65a690fdb1f57660b" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==5.2.1" + "version": "==5.2.2" }, "markdown": { "hashes": [ @@ -2114,39 +2098,39 @@ }, "newrelic": { "hashes": [ - "sha256:04cd3fc7087513a4786908a9b0a7475db154c888ac9d2de251f8abb93353a4a7", - "sha256:1743df0e72bf559b61112763a71c35e5d456a509ba4dde2bdbaa88d894f1812a", - "sha256:2182673a01f04a0ed4a0bb3f49e8fa869044c37558c8f409c96de13105f58a57", - "sha256:26713f779cf23bb29c6b408436167059d0c8ee1475810dc1b0efe858fe578f25", - "sha256:2ffcbdb706de1bbaa36acd0c9b487a08895a420020bcf775be2d80c7df29b56c", - "sha256:4356690cbc9e5e662defa2af15aba05901cf9b285a8d02aeb90718e84dd6d779", - "sha256:47efe8fc4dc14b0f265d635639f94ef5a071b5e5ebbf41ecf0946fce071c49e6", - "sha256:4cf5d85a4a8e8de6e0aeb7a76afad9264d0c0dc459bc3f1a8b02a0e48a9a26da", - "sha256:57451807f600331a94ad1ec66e3981523b0516d5b2dd9fd078e7f3d6c9228913", - "sha256:5b40155f9712e75c00d03cdec8272f6cf8eaa05ea2ed22bb5ecc96ed86017b47", - "sha256:63b230dd5d093874c0137eddc738cb028e17326d2a8a98cbc12c665bbdf6ec67", - "sha256:834ce8de7550bc444aed6c2afc1436c04485998e46f429e41b89d66ab85f0fbb", - "sha256:9dbf35914d0bbf1294d8eb6fa5357d072238c6c722726c2ee20b9c1e35b8253d", - "sha256:a257995d832858cf7c56bcfb1911f3379f9d3e795d7357f56f035f1b60339ea0", - "sha256:a57ff176818037983589c15b6dca03841fcef1429c279f5948800caa333fb476", - "sha256:a91dea75f8c202a6a553339a1997983224465555a3f8d7294b24de1e2bee5f05", - "sha256:b60f66132a42ec8c67fd26b8082cc3a0626192283dc9b5716a66203a58f10d30", - "sha256:b64a61f2f228b70f91c06a0bd82e2645c6b75ddbd50587f94a67c89ef6d5d854", - "sha256:b773ee74d869bf632ce1e12903cc8e7ae8b5697ef9ae97169ed263a5d3a87f76", - "sha256:c4e12ead3602ca2c188528fde444f8ab953b504b095d70265303bbf132908eb7", - "sha256:cf3c13d264cd089d467e9848fb6875907940202d22475b506a70683f04ef82af", - "sha256:d8304317ff27bb50fd94f1e6e8c3ae0c59151ee85de2ea0269dbe7e982512c45", - "sha256:dac3b74bd801513e8221f05a01a294405eda7f4922fce5b174e5e33c222ae09d", - "sha256:db32fa04d69bbb742401c124a6cec158e6237a21af4602dbf53e4630ea9dd068", - "sha256:de2ac509f8730fc6f6819f13a9ebbe52865397d526ca4dbe963a0e9865bb0500", - "sha256:df6198259dae01212b39079add58e0ef7311cf01734adea51fec4d2f7a9fafec", - "sha256:e6cb86aa2f7230ee9dcb5f9f8821c7090566419def5537a44240f978b680c4f7", - "sha256:f0d8c8f66aba3629f0f17a1d2314beb2984ad7c485dd318ef2d5f257c040981d", - "sha256:f48898e268dcaa14aa1b6d5c8b8d10f3f4396589a37be10a06bb5ba262ef0541" + "sha256:0d6feba8968662c7a84ee6fe837d3be8c53a7126398ded3283634bb51dc43e94", + "sha256:1e613f1ffd0d35b1f866382eeee52d8aa9576d82f3de818a84aa2e56c08f1868", + "sha256:21e280c027835062f54be2df48f32834dcc98f382b049c14ee35b80aa7b48ea0", + "sha256:2b165328c05fd2c006cf1f476bebb281579944418a13903e802344660b13332c", + "sha256:303117d3402659afac45174dfe7c595b7d4b3c0812a76b712c251c91ef95c430", + "sha256:3c99cc368a3cfd9ce40ca4bbe2fe3bdd5f7d37865ea5e4bf811ba6fd0d00152d", + "sha256:3ef567a779b068297c040f7410153135fb12e51e4a82084675b0cf142c407551", + "sha256:40820a3dff89cc8e242f0543fabd1692333458f627ebad6f2e56f6c9db7d2efe", + "sha256:474499f482da7f58b5039f2c42dea2880d878b30729ae563bb1498a0bb30be44", + "sha256:5710910ceb847f8806540e6934764fff6823d7dcc6d30955e9ecb012e20efbfd", + "sha256:5c813e9c7bdb1381cb0eda4925e07aa8ee21e111b5025d02261605eaabb129f1", + "sha256:673ed069516fa4d168cd12b7319bcadf75fbc9f0ebcd147916e281b2bc16c551", + "sha256:763faab4868b0226906c17ef0419dab527964f489cb2e3818d57d0484762cb2e", + "sha256:7aa1be0d0530d0c566dee2c4d43765aba9fc5fae256fac110ba57aae6ae8d8c4", + "sha256:7c6361af2a60ab60a5757b13ce0b9b4efeee577a228637b9b8b449d47ec81fdd", + "sha256:7f41343548aad28b7722c85d00079b4e61ef48d5a6bdf757c458a5fe860bb099", + "sha256:8ad34b8eb60f33b0eab9ed7727cdb9452ad7d4381a2c5397e6ed3d4895833fd1", + "sha256:8fb0e56324df855c3079d7d86fd6b35e79727759de8c8517be9c06d482092c3b", + "sha256:aefa66f59d62ec22a6d347afa73c24bd723521c4cc0fdce7f51c71bfe85c42bc", + "sha256:afdb30c4f89d0f089ac05ca50a383f94cfcdb07aab0b9722d2d5af09626ab304", + "sha256:c3264e305ae0e973f3a02f7394460f4c7366822e8a3509cd08b2093f9cb5def5", + "sha256:c43a14c48dd8f752da348c3ec80cb500b9ead12abcd40d29d39a0bb8a62a3a0d", + "sha256:d50fa347584967c15e574a2503fdcafcd13c86c17e589021eae5432d4aad1cca", + "sha256:ddb2d4a2fc3f88c5d1c0b4dec2f8eb89907541501f2ec7ac14e5506ea702e0f5", + "sha256:e3226ac2c0c57955a00a11f6cf982dd6747490254ed322d6fcf36077bfc37386", + "sha256:e49c734058c7b6a6c199e8c2657187143061a6eda92cc8ba67739de88a9e203d", + "sha256:e5d688917307d083d7fa6f3b31eec40c5a3782b160383230f5f644e2d4ae2a26", + "sha256:eec85620708aea387b602db61fb43504efc5b5fcb7b627d2cbe0a33c3fe10ab9", + "sha256:fbca7a8749eadb05eacdfb68af938dc1045c6be8bcc83375d15a840172b5f40e" ], "index": "pypi", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==9.9.0" + "version": "==9.9.1" }, "numpy": { "hashes": [ @@ -2193,12 +2177,12 @@ }, "openai": { "hashes": [ - "sha256:642e857b60855702ee6ff665e8fa80946164f77b92e58fd24e01b545685b8405", - "sha256:884ced523fb0225780f8b0e0ed6f7e014049c32d049a41ad0ac962869f1055d1" + "sha256:4f85190e577cba0b066e1950b8eb9b11d25bc7ebcc43a86b326ce1bfa564ec74", + "sha256:c9fb3c3545c118bbce8deb824397b9433a66d0d0ede6a96f7009c95b76de4a46" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.26.0" + "version": "==1.30.1" }, "packaging": { "hashes": [ @@ -2346,11 +2330,11 @@ }, "platformdirs": { "hashes": [ - "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf", - "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.2.1" + "version": "==4.2.2" }, "pluggy": { "hashes": [ @@ -2420,88 +2404,86 @@ "pool" ], "hashes": [ - "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b", - "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e" + "sha256:92d7b78ad82426cdcf1a0440678209faa890c6e1721361c2f8901f0dccd62961", + "sha256:dca5e5521c859f6606686432ae1c94e8766d29cc91f2ee595378c510cc5b0731" ], "markers": "python_version >= '3.7'", - "version": "==3.1.18" + "version": "==3.1.19" }, "psycopg-binary": { "hashes": [ - "sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7", - "sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c", - "sha256:13bcd3742112446037d15e360b27a03af4b5afcf767f5ee374ef8f5dd7571b31", - "sha256:1729d0e3dfe2546d823841eb7a3d003144189d6f5e138ee63e5227f8b75276a5", - "sha256:1859aeb2133f5ecdd9cbcee155f5e38699afc06a365f903b1512c765fd8d457e", - "sha256:1c9b6bd7fb5c6638cb32469674707649b526acfe786ba6d5a78ca4293d87bae4", - "sha256:247474af262bdd5559ee6e669926c4f23e9cf53dae2d34c4d991723c72196404", - "sha256:258d2f0cb45e4574f8b2fe7c6d0a0e2eb58903a4fd1fbaf60954fba82d595ab7", - "sha256:2e2484ae835dedc80cdc7f1b1a939377dc967fed862262cfd097aa9f50cade46", - "sha256:320047e3d3554b857e16c2b6b615a85e0db6a02426f4d203a4594a2f125dfe57", - "sha256:39242546383f6b97032de7af30edb483d237a0616f6050512eee7b218a2aa8ee", - "sha256:3c2b039ae0c45eee4cd85300ef802c0f97d0afc78350946a5d0ec77dd2d7e834", - "sha256:3c7afcd6f1d55992f26d9ff7b0bd4ee6b475eb43aa3f054d67d32e09f18b0065", - "sha256:3e4b0bb91da6f2238dbd4fbb4afc40dfb4f045bb611b92fce4d381b26413c686", - "sha256:3e7ce4d988112ca6c75765c7f24c83bdc476a6a5ce00878df6c140ca32c3e16d", - "sha256:4085f56a8d4fc8b455e8f44380705c7795be5317419aa5f8214f315e4205d804", - "sha256:4575da95fc441244a0e2ebaf33a2b2f74164603341d2046b5cde0a9aa86aa7e2", - "sha256:489aa4fe5a0b653b68341e9e44af247dedbbc655326854aa34c163ef1bcb3143", - "sha256:4e4de16a637ec190cbee82e0c2dc4860fed17a23a35f7a1e6dc479a5c6876722", - "sha256:531381f6647fc267383dca88dbe8a70d0feff433a8e3d0c4939201fea7ae1b82", - "sha256:55ff0948457bfa8c0d35c46e3a75193906d1c275538877ba65907fd67aa059ad", - "sha256:59701118c7d8842e451f1e562d08e8708b3f5d14974eefbce9374badd723c4ae", - "sha256:5c323103dfa663b88204cf5f028e83c77d7a715f9b6f51d2bbc8184b99ddd90a", - "sha256:5d6e860edf877d4413e4a807e837d55e3a7c7df701e9d6943c06e460fa6c058f", - "sha256:639dd78ac09b144b0119076783cb64e1128cc8612243e9701d1503c816750b2e", - "sha256:6432047b8b24ef97e3fbee1d1593a0faaa9544c7a41a2c67d1f10e7621374c83", - "sha256:67284e2e450dc7a9e4d76e78c0bd357dc946334a3d410defaeb2635607f632cd", - "sha256:6ebecbf2406cd6875bdd2453e31067d1bd8efe96705a9489ef37e93b50dc6f09", - "sha256:7121acc783c4e86d2d320a7fb803460fab158a7f0a04c5e8c5d49065118c1e73", - "sha256:74e498586b72fb819ca8ea82107747d0cb6e00ae685ea6d1ab3f929318a8ce2d", - "sha256:780a90bcb69bf27a8b08bc35b958e974cb6ea7a04cdec69e737f66378a344d68", - "sha256:7ac1785d67241d5074f8086705fa68e046becea27964267ab3abd392481d7773", - "sha256:812726266ab96de681f2c7dbd6b734d327f493a78357fcc16b2ac86ff4f4e080", - "sha256:824a1bfd0db96cc6bef2d1e52d9e0963f5bf653dd5bc3ab519a38f5e6f21c299", - "sha256:87dd9154b757a5fbf6d590f6f6ea75f4ad7b764a813ae04b1d91a70713f414a1", - "sha256:887f8d856c91510148be942c7acd702ccf761a05f59f8abc123c22ab77b5a16c", - "sha256:888a72c2aca4316ca6d4a619291b805677bae99bba2f6e31a3c18424a48c7e4d", - "sha256:8f54978c4b646dec77fefd8485fa82ec1a87807f334004372af1aaa6de9539a5", - "sha256:91074f78a9f890af5f2c786691575b6b93a4967ad6b8c5a90101f7b8c1a91d9c", - "sha256:9d684227ef8212e27da5f2aff9d4d303cc30b27ac1702d4f6881935549486dd5", - "sha256:9e24e7b6a68a51cc3b162d0339ae4e1263b253e887987d5c759652f5692b5efe", - "sha256:9ffcbbd389e486d3fd83d30107bbf8b27845a295051ccabde240f235d04ed921", - "sha256:a87e9eeb80ce8ec8c2783f29bce9a50bbcd2e2342a340f159c3326bf4697afa1", - "sha256:ad35ac7fd989184bf4d38a87decfb5a262b419e8ba8dcaeec97848817412c64a", - "sha256:b15e3653c82384b043d820fc637199b5c6a36b37fa4a4943e0652785bb2bad5d", - "sha256:b293e01057e63c3ac0002aa132a1071ce0fdb13b9ee2b6b45d3abdb3525c597d", - "sha256:b2f7f95746efd1be2dc240248cc157f4315db3fd09fef2adfcc2a76e24aa5741", - "sha256:bd27f713f2e5ef3fd6796e66c1a5203a27a30ecb847be27a78e1df8a9a5ae68c", - "sha256:c38a4796abf7380f83b1653c2711cb2449dd0b2e5aca1caa75447d6fa5179c69", - "sha256:c76659ae29a84f2c14f56aad305dd00eb685bd88f8c0a3281a9a4bc6bd7d2aa7", - "sha256:c84a0174109f329eeda169004c7b7ca2e884a6305acab4a39600be67f915ed38", - "sha256:cd2a9f7f0d4dacc5b9ce7f0e767ae6cc64153264151f50698898c42cabffec0c", - "sha256:d322ba72cde4ca2eefc2196dad9ad7e52451acd2f04e3688d590290625d0c970", - "sha256:d4422af5232699f14b7266a754da49dc9bcd45eba244cf3812307934cd5d6679", - "sha256:d46ae44d66bf6058a812467f6ae84e4e157dee281bfb1cfaeca07dee07452e85", - "sha256:da917f6df8c6b2002043193cb0d74cc173b3af7eb5800ad69c4e1fbac2a71c30", - "sha256:dea4a59da7850192fdead9da888e6b96166e90608cf39e17b503f45826b16f84", - "sha256:e05f6825f8db4428782135e6986fec79b139210398f3710ed4aa6ef41473c008", - "sha256:e1cf59e0bb12e031a48bb628aae32df3d0c98fd6c759cb89f464b1047f0ca9c8", - "sha256:e252d66276c992319ed6cd69a3ffa17538943954075051e992143ccbf6dc3d3e", - "sha256:e262398e5d51563093edf30612cd1e20fedd932ad0994697d7781ca4880cdc3d", - "sha256:e28ff8f3de7b56588c2a398dc135fd9f157d12c612bd3daa7e6ba9872337f6f5", - "sha256:eea5f14933177ffe5c40b200f04f814258cc14b14a71024ad109f308e8bad414", - "sha256:f876ebbf92db70125f6375f91ab4bc6b27648aa68f90d661b1fc5affb4c9731c", - "sha256:f8ff3bc08b43f36fdc24fedb86d42749298a458c4724fb588c4d76823ac39f54" - ], - "version": "==3.1.18" + "sha256:00879d4c6be4b3afc510073f48a5e960f797200e261ab3d9bd9b7746a08c669d", + "sha256:0106e42b481677c41caa69474fe530f786dcef88b11b70000f0e45a03534bc8f", + "sha256:017518bd2de4851adc826a224fb105411e148ad845e11355edd6786ba3dfedf5", + "sha256:03354a9db667c27946e70162cb0042c3929154167f3678a30d23cebfe0ad55b5", + "sha256:052f5193304066318853b4b2e248f523c8f52b371fc4e95d4ef63baee3f30955", + "sha256:0e991632777e217953ac960726158987da684086dd813ac85038c595e7382c91", + "sha256:1285aa54449e362b1d30d92b2dc042ad3ee80f479cc4e323448d0a0a8a1641fa", + "sha256:1622ca27d5a7a98f7d8f35e8b146dc7efda4a4b6241d2edf7e076bd6bcecbeb4", + "sha256:1cf49e91dcf699b8a449944ed898ef1466b39b92720613838791a551bc8f587a", + "sha256:1d87484dd42c8783c44a30400949efb3d81ef2487eaa7d64d1c54df90cf8b97a", + "sha256:29008f3f8977f600b8a7fb07c2e041b01645b08121760609cc45e861a0364dc9", + "sha256:321814a9a3ad785855a821b842aba08ca1b7de7dfb2979a2f0492dca9ec4ae70", + "sha256:3433924e1b14074798331dc2bfae2af452ed7888067f2fc145835704d8981b15", + "sha256:34a6997c80f86d3dd80a4f078bb3b200079c47eeda4fd409d8899b883c90d2ac", + "sha256:38ed45ec9673709bfa5bc17f140e71dd4cca56d4e58ef7fd50d5a5043a4f55c6", + "sha256:433f1c256108f9e26f480a8cd6ddb0fb37dbc87d7f5a97e4540a9da9b881f23f", + "sha256:469424e354ebcec949aa6aa30e5a9edc352a899d9a68ad7a48f97df83cc914cf", + "sha256:46e50c05952b59a214e27d3606f6d510aaa429daed898e16b8a37bfbacc81acc", + "sha256:49cd7af7d49e438a39593d1dd8cab106a1912536c2b78a4d814ebdff2786094e", + "sha256:4aa0ca13bb8a725bb6d12c13999217fd5bc8b86a12589f28a74b93e076fbb959", + "sha256:4ae8109ff9fdf1fa0cb87ab6645298693fdd2666a7f5f85660df88f6965e0bb7", + "sha256:5c6956808fd5cf0576de5a602243af8e04594b25b9a28675feddc71c5526410a", + "sha256:621a814e60825162d38760c66351b4df679fd422c848b7c2f86ad399bff27145", + "sha256:6469ebd9e93327e9f5f36dcf8692fb1e7aeaf70087c1c15d4f2c020e0be3a891", + "sha256:6cff31af8155dc9ee364098a328bab688c887c732c66b8d027e5b03818ca0287", + "sha256:6d4e67fd86758dbeac85641419a54f84d74495a8683b58ad5dfad08b7fc37a8f", + "sha256:703c2f3b79037581afec7baa2bdbcb0a1787f1758744a7662099b0eca2d721cb", + "sha256:7204818f05151dd08f8f851defb01972ec9d2cc925608eb0de232563f203f354", + "sha256:738c34657305b5973af6dbb6711b07b179dfdd21196d60039ca30a74bafe9648", + "sha256:76fcd33342f38e35cd6b5408f1bc117d55ab8b16e5019d99b6d3ce0356c51717", + "sha256:7c6a9a651a08d876303ed059c9553df18b3c13c3406584a70a8f37f1a1fe2709", + "sha256:81efe09ba27533e35709905c3061db4dc9fb814f637360578d065e2061fbb116", + "sha256:85bca9765c04b6be90cb46e7566ffe0faa2d7480ff5c8d5e055ac427f039fd24", + "sha256:866db42f986298f0cf15d805225eb8df2228bf19f7997d7f1cb5f388cbfc6a0f", + "sha256:8a732610a5a6b4f06dadcf9288688a8ff202fd556d971436a123b7adb85596e2", + "sha256:91a645e6468c4f064b7f4f3b81074bdd68fe5aa2b8c5107de15dcd85ba6141be", + "sha256:955ca8905c0251fc4af7ce0a20999e824a25652f53a558ab548b60969f1f368e", + "sha256:959feabddc7fffac89b054d6f23f3b3c62d7d3c90cd414a02e3747495597f150", + "sha256:95f16ae82bc242b76cd3c3e5156441e2bd85ff9ec3a9869d750aad443e46073c", + "sha256:964c307e400c5f33fa762ba1e19853e048814fcfbd9679cc923431adb7a2ead2", + "sha256:9d39d5ffc151fb33bcd55b99b0e8957299c0b1b3e5a1a5f4399c1287ef0051a9", + "sha256:a100482950a55228f648bd382bb71bfaff520002f29845274fccbbf02e28bd52", + "sha256:a53809ee02e3952fae7977c19b30fd828bd117b8f5edf17a3a94212feb57faaf", + "sha256:a836610d5c75e9cff98b9fdb3559c007c785c09eaa84a60d5d10ef6f85f671e8", + "sha256:aebd1e98e865e9a28ce0cb2c25b7dfd752f0d1f0a423165b55cd32a431dcc0f4", + "sha256:affebd61aa3b7a8880fd4ac3ee94722940125ff83ff485e1a7c76be9adaabb38", + "sha256:b04f5349313529ae1f1c42fe1aa0443faaf50fdf12d13866c2cc49683bfa53d0", + "sha256:bfd2c734da9950f7afaad5f132088e0e1478f32f042881fca6651bb0c8d14206", + "sha256:c1823221a6b96e38b15686170d4fc5b36073efcb87cce7d3da660440b50077f6", + "sha256:c35fd811f339a3cbe7f9b54b2d9a5e592e57426c6cc1051632a62c59c4810208", + "sha256:c50592bc8517092f40979e4a5d934f96a1737a77724bb1d121eb78b614b30fc8", + "sha256:cd88c5cea4efe614d5004fb5f5dcdea3d7d59422be796689e779e03363102d24", + "sha256:d1bac282f140fa092f2bbb6c36ed82270b4a21a6fc55d4b16748ed9f55e50fdb", + "sha256:d1d1723d7449c12bb61aca7eb6e0c6ab2863cd8dc0019273cc4d4a1982f84bdb", + "sha256:d312d6dddc18d9c164e1893706269c293cba1923118349d375962b1188dafb01", + "sha256:d9b689c4a17dd3130791dcbb8c30dbf05602f7c2d56c792e193fb49adc7bf5f8", + "sha256:e12173e34b176e93ad2da913de30f774d5119c2d4d4640c6858d2d77dfa6c9bf", + "sha256:e14bc8250000921fcccd53722f86b3b3d1b57db901e206e49e2ab2afc5919c2d", + "sha256:e538a8671005641fa195eab962f85cf0504defbd3b548c4c8fc27102a59f687b", + "sha256:e9da624a6ca4bc5f7fa1f03f8485446b5b81d5787b6beea2b4f8d9dbef878ad7", + "sha256:ed61e43bf5dc8d0936daf03a19fef3168d64191dbe66483f7ad08c4cea0bc36b", + "sha256:ef8de7a1d9fb3518cc6b58e3c80b75a824209ad52b90c542686c912db8553dad", + "sha256:fb9758473200384a04374d0e0cac6f451218ff6945a024f65a1526802c34e56e" + ], + "version": "==3.1.19" }, "psycopg-pool": { "hashes": [ - "sha256:060b551d1b97a8d358c668be58b637780b884de14d861f4f5ecc48b7563aafb7", - "sha256:6509a75c073590952915eddbba7ce8b8332a440a31e77bba69561483492829ad" + "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153", + "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c" ], - "version": "==3.2.1" + "version": "==3.2.2" }, "psycopg2": { "hashes": [ @@ -2524,44 +2506,44 @@ }, "pyarrow": { "hashes": [ - "sha256:00a1dcb22ad4ceb8af87f7bd30cc3354788776c417f493089e0a0af981bc8d80", - "sha256:1ab8b9050752b16a8b53fcd9853bf07d8daf19093533e990085168f40c64d978", - "sha256:20ce707d9aa390593ea93218b19d0eadab56390311cb87aad32c9a869b0e958c", - "sha256:22a1fdb1254e5095d629e29cd1ea98ed04b4bbfd8e42cc670a6b639ccc208b60", - "sha256:266ddb7e823f03733c15adc8b5078db2df6980f9aa93d6bb57ece615df4e0ba7", - "sha256:2a7abdee4a4a7cfa239e2e8d721224c4b34ffe69a0ca7981354fe03c1328789b", - "sha256:35692ce8ad0b8c666aa60f83950957096d92f2a9d8d7deda93fb835e6053307e", - "sha256:3c2f5e239db7ed43e0ad2baf46a6465f89c824cc703f38ef0fde927d8e0955f7", - "sha256:42e56557bc7c5c10d3e42c3b32f6cff649a29d637e8f4e8b311d334cc4326730", - "sha256:5448564754c154997bc09e95a44b81b9e31ae918a86c0fcb35c4aa4922756f55", - "sha256:56850a0afe9ef37249d5387355449c0f94d12ff7994af88f16803a26d38f2016", - "sha256:574a00260a4ed9d118a14770edbd440b848fcae5a3024128be9d0274dbcaf858", - "sha256:5823275c8addbbb50cd4e6a6839952682a33255b447277e37a6f518d6972f4e1", - "sha256:59bb1f1edbbf4114c72415f039f1359f1a57d166a331c3229788ccbfbb31689a", - "sha256:5cc23090224b6594f5a92d26ad47465af47c1d9c079dd4a0061ae39551889efe", - "sha256:705db70d3e2293c2f6f8e84874b5b775f690465798f66e94bb2c07bab0a6bb55", - "sha256:71d52561cd7aefd22cf52538f262850b0cc9e4ec50af2aaa601da3a16ef48877", - "sha256:729f7b262aa620c9df8b9967db96c1575e4cfc8c25d078a06968e527b8d6ec05", - "sha256:91d28f9a40f1264eab2af7905a4d95320ac2f287891e9c8b0035f264fe3c3a4b", - "sha256:99af421ee451a78884d7faea23816c429e263bd3618b22d38e7992c9ce2a7ad9", - "sha256:9dd3151d098e56f16a8389c1247137f9e4c22720b01c6f3aa6dec29a99b74d80", - "sha256:b93c9a50b965ee0bf4fef65e53b758a7e8dcc0c2d86cebcc037aaaf1b306ecc0", - "sha256:bd40467bdb3cbaf2044ed7a6f7f251c8f941c8b31275aaaf88e746c4f3ca4a7a", - "sha256:c0815d0ddb733b8c1b53a05827a91f1b8bde6240f3b20bf9ba5d650eb9b89cdf", - "sha256:cc8814310486f2a73c661ba8354540f17eef51e1b6dd090b93e3419d3a097b3a", - "sha256:d22d0941e6c7bafddf5f4c0662e46f2075850f1c044bf1a03150dd9e189427ce", - "sha256:d831690844706e374c455fba2fb8cfcb7b797bfe53ceda4b54334316e1ac4fa4", - "sha256:d91073d1e2fef2c121154680e2ba7e35ecf8d4969cc0af1fa6f14a8675858159", - "sha256:dd9334a07b6dc21afe0857aa31842365a62eca664e415a3f9536e3a8bb832c07", - "sha256:df0080339387b5d30de31e0a149c0c11a827a10c82f0c67d9afae3981d1aabb7", - "sha256:ed66e5217b4526fa3585b5e39b0b82f501b88a10d36bd0d2a4d8aa7b5a48e2df", - "sha256:edf38cce0bf0dcf726e074159c60516447e4474904c0033f018c1f33d7dac6c5", - "sha256:ef2f309b68396bcc5a354106741d333494d6a0d3e1951271849787109f0229a6", - "sha256:f293e92d1db251447cb028ae12f7bc47526e4649c3a9924c8376cab4ad6b98bd", - "sha256:fb8065dbc0d051bf2ae2453af0484d99a43135cadabacf0af588a3be81fbbb9b", - "sha256:fda9a7cebd1b1d46c97b511f60f73a5b766a6de4c5236f144f41a5d5afec1f35" - ], - "version": "==16.0.0" + "sha256:06ebccb6f8cb7357de85f60d5da50e83507954af617d7b05f48af1621d331c9a", + "sha256:0d07de3ee730647a600037bc1d7b7994067ed64d0eba797ac74b2bc77384f4c2", + "sha256:0d27bf89dfc2576f6206e9cd6cf7a107c9c06dc13d53bbc25b0bd4556f19cf5f", + "sha256:0d32000693deff8dc5df444b032b5985a48592c0697cb6e3071a5d59888714e2", + "sha256:15fbb22ea96d11f0b5768504a3f961edab25eaf4197c341720c4a387f6c60315", + "sha256:17e23b9a65a70cc733d8b738baa6ad3722298fa0c81d88f63ff94bf25eaa77b9", + "sha256:185d121b50836379fe012753cf15c4ba9638bda9645183ab36246923875f8d1b", + "sha256:18da9b76a36a954665ccca8aa6bd9f46c1145f79c0bb8f4f244f5f8e799bca55", + "sha256:19741c4dbbbc986d38856ee7ddfdd6a00fc3b0fc2d928795b95410d38bb97d15", + "sha256:25233642583bf658f629eb230b9bb79d9af4d9f9229890b3c878699c82f7d11e", + "sha256:2e51ca1d6ed7f2e9d5c3c83decf27b0d17bb207a7dea986e8dc3e24f80ff7d6f", + "sha256:2e73cfc4a99e796727919c5541c65bb88b973377501e39b9842ea71401ca6c1c", + "sha256:31a1851751433d89a986616015841977e0a188662fcffd1a5677453f1df2de0a", + "sha256:3b20bd67c94b3a2ea0a749d2a5712fc845a69cb5d52e78e6449bbd295611f3aa", + "sha256:4740cc41e2ba5d641071d0ab5e9ef9b5e6e8c7611351a5cb7c1d175eaf43674a", + "sha256:48be160782c0556156d91adbdd5a4a7e719f8d407cb46ae3bb4eaee09b3111bd", + "sha256:8785bb10d5d6fd5e15d718ee1d1f914fe768bf8b4d1e5e9bf253de8a26cb1628", + "sha256:98100e0268d04e0eec47b73f20b39c45b4006f3c4233719c3848aa27a03c1aef", + "sha256:99f7549779b6e434467d2aa43ab2b7224dd9e41bdde486020bae198978c9e05e", + "sha256:9cf389d444b0f41d9fe1444b70650fea31e9d52cfcb5f818b7888b91b586efff", + "sha256:a33a64576fddfbec0a44112eaf844c20853647ca833e9a647bfae0582b2ff94b", + "sha256:a8914cd176f448e09746037b0c6b3a9d7688cef451ec5735094055116857580c", + "sha256:b04707f1979815f5e49824ce52d1dceb46e2f12909a48a6a753fe7cafbc44a0c", + "sha256:b5f5705ab977947a43ac83b52ade3b881eb6e95fcc02d76f501d549a210ba77f", + "sha256:ba8ac20693c0bb0bf4b238751d4409e62852004a8cf031c73b0e0962b03e45e3", + "sha256:bf9251264247ecfe93e5f5a0cd43b8ae834f1e61d1abca22da55b20c788417f6", + "sha256:d0ebea336b535b37eee9eee31761813086d33ed06de9ab6fc6aaa0bace7b250c", + "sha256:ddf5aace92d520d3d2a20031d8b0ec27b4395cab9f74e07cc95edf42a5cc0147", + "sha256:ddfe389a08ea374972bd4065d5f25d14e36b43ebc22fc75f7b951f24378bf0b5", + "sha256:e1369af39587b794873b8a307cc6623a3b1194e69399af0efd05bb202195a5a7", + "sha256:e6b6d3cd35fbb93b70ade1336022cc1147b95ec6af7d36906ca7fe432eb09710", + "sha256:f07fdffe4fd5b15f5ec15c8b64584868d063bc22b86b46c9695624ca3505b7b4", + "sha256:f2c5fb249caa17b94e2b9278b36a05ce03d3180e6da0c4c3b3ce5b2788f30eed", + "sha256:f68f409e7b283c085f2da014f9ef81e885d90dcd733bd648cfba3ef265961848", + "sha256:fbef391b63f708e103df99fbaa3acf9f671d77a183a07546ba2f2c297b361e83", + "sha256:febde33305f1498f6df85e8020bca496d0e9ebf2093bab9e0f65e2b4ae2b3444" + ], + "version": "==16.1.0" }, "pyasn1": { "hashes": [ @@ -3300,12 +3282,12 @@ }, "stripe": { "hashes": [ - "sha256:ace24a3ed07cff3b8efe0e23a72e02372f26124b034d1d3b78f58afdcd5bd390", - "sha256:cd38369641ae6140e1ce9edcd5400aecdb1a10ba704e653a2f2a7550cb8f277e" + "sha256:9305d849cea715dc59c5e39d01891475b82e10edb9d95ee1d8189457e5de792f", + "sha256:f519f6810ac7f6e096b4faf562c44b1f8e365138441548e4ab0bc93f86368ad7" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==9.5.0" + "version": "==9.6.0" }, "text-unidecode": { "hashes": [ @@ -3645,48 +3627,6 @@ "markers": "python_version >= '3.7'", "version": "==1.9.4" }, - "zope-interface": { - "hashes": [ - "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e", - "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf", - "sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130", - "sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86", - "sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1", - "sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e", - "sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5", - "sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c", - "sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92", - "sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021", - "sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c", - "sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10", - "sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83", - "sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb", - "sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920", - "sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299", - "sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e", - "sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af", - "sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39", - "sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21", - "sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061", - "sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b", - "sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5", - "sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0", - "sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6", - "sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85", - "sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5", - "sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a", - "sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9", - "sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1", - "sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12", - "sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e", - "sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785", - "sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91", - "sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a", - "sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d" - ], - "markers": "python_version >= '3.7'", - "version": "==6.3" - }, "zope.event": { "hashes": [ "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", @@ -3695,6 +3635,40 @@ "markers": "python_version >= '3.7'", "version": "==5.0" }, + "zope.interface": { + "hashes": [ + "sha256:21732994aa3ca43bbb6b36335c288023428a3c5b7322b637c7b0a03053937578", + "sha256:36ee6e507a9fd4f1f0aab8e8dfc801d162e7211c27503cbfb47e1d558941a7fa", + "sha256:3945f4fda92c1b6fb0cb6eaaaf72599e5c2c2059654bdc42bc09c6e711c214c8", + "sha256:414e6dccdf4a5c96c0c98da68ba040dbf9ba7511b61b34e228f11b0ed90c439d", + "sha256:4782e173c2fde4f649c2a9a68082445bc1f2c27f41907de06bf1ba82585847f2", + "sha256:4cd56eb9a23767958c9a0654306b9a4a74def485f645b3a7378cc6ab661ef31c", + "sha256:502d2c9c4231d022b20225dba5c6c736236ed65e1d7e2f6f402b5aa6a7040ec9", + "sha256:57f34b7997f8de7d2db08363eaccd05dad20f106e39efe95bed4fac84af2d022", + "sha256:5fbbb290751f5c4ed81e54ae73fe8557c4a85973f5ab019edbb0f746244ecea6", + "sha256:604fa920478dfc0c76cdb7c203572400a8317ffcdac288245c408b42b3d9aee9", + "sha256:62e6b756663deade5270f67899753437b39d970f9eecd49e19fae3b880310cf0", + "sha256:646cd83d24065d074f22f61fe101d20dbf4b729ca7831cc782ec986eb9156f93", + "sha256:6494dc0314e782ce4fb0e624b4ce2458f54d074382f50a920c7700c05cbcef28", + "sha256:6e4cc017206c1429a6d8fdd8a25c6efc15512065eec0a8d45c350df96a0911ed", + "sha256:72faa868fcfde49a29d287dce3c83180322467eecd725dd351098efe96e8d4bb", + "sha256:7cda82ab32f984985f09e4ec20a4f9665b26779a1b8e443b34a148de256f2052", + "sha256:855b7233fa5d0d1f3be8c14fadf4718dee1c928e1d75f1584bea6ecec6dcc4af", + "sha256:86e85eada0eb551950df05d72dc0e892320f14daa78bc434059e834d4b1f9300", + "sha256:8e246357f52952ae5fa950d19eda8572594c49e6cb1e5462508e6cec561a37de", + "sha256:93f28d84517dcd6c240979bd9b2f262a373832baef856fe663a24b9171d7f04d", + "sha256:b0f61ccbc26e08031d0e72b6a0cbf9b4030f035913cb2b39f940aa42eb8e0063", + "sha256:b11f2b67ccc990a1522fa8cd3f5d185a068459f944ab2d0e7a1b15d31bcb4af4", + "sha256:c04bd4ee4766d285e83c6d8c042663a98efb934389e05ccd643fefb066c88a9d", + "sha256:ee1e3ca6c98efe213a96dece89100a8aa52e210ac354861d8039d69bd1d6e5ff", + "sha256:f33af86ed460eb28dc9da1de1f3305795271a19c665161c1d973a737596b2081", + "sha256:f5092f2712e1fd07579fc3101b18e9c95857c853e836847598bf992c8e672434", + "sha256:f78e1eac48c4f4e0168a91cabcd8d1aedb972836df5c8769071fc6173294a0a3", + "sha256:fe636b49c333bfc5b0913590e36a2f151167c462fb36d9f4acc66029e45c974b" + ], + "markers": "python_version >= '3.7'", + "version": "==6.4" + }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", @@ -3909,71 +3883,71 @@ "toml" ], "hashes": [ - "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c", - "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63", - "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7", - "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f", - "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8", - "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf", - "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0", - "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384", - "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76", - "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7", - "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d", - "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70", - "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f", - "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818", - "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b", - "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d", - "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec", - "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083", - "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2", - "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9", - "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd", - "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade", - "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e", - "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a", - "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227", - "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87", - "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c", - "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e", - "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c", - "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e", - "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd", - "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec", - "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562", - "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8", - "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677", - "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357", - "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c", - "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd", - "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49", - "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286", - "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1", - "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf", - "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51", - "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409", - "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384", - "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e", - "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978", - "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57", - "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e", - "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2", - "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48", - "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4" + "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de", + "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661", + "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26", + "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41", + "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d", + "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981", + "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2", + "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34", + "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f", + "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a", + "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35", + "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223", + "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1", + "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746", + "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90", + "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c", + "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca", + "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8", + "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596", + "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e", + "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd", + "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e", + "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3", + "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e", + "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312", + "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7", + "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572", + "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428", + "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f", + "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07", + "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e", + "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4", + "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136", + "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5", + "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8", + "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d", + "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228", + "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206", + "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa", + "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e", + "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be", + "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5", + "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668", + "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601", + "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057", + "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146", + "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f", + "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8", + "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7", + "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987", + "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19", + "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==7.4.4" + "version": "==7.5.1" }, "coveralls": { "hashes": [ - "sha256:401715d244a27d5da03eb1ac614aa585cc7e4dd5b0d4c035113b6349da4e6161", - "sha256:9486f353176d309066053d38edbade3aad6346c5eb8a5edde7090d3116219414" + "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", + "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69" ], "index": "pypi", "markers": "python_version < '3.13' and python_version >= '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "distlib": { "hashes": [ @@ -4200,16 +4174,16 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", "version": "==3.0.3" }, "griffe": { "hashes": [ - "sha256:34aee1571042f9bf00529bc715de4516fb6f482b164e90d030300601009e0223", - "sha256:8a4471c469ba980b87c843f1168850ce39d0c1d0c7be140dca2480f76c8e5446" + "sha256:85cb2868d026ea51c89bdd589ad3ccc94abc5bd8d5d948e3d4450778a2a05b4a", + "sha256:90fe5c90e1b0ca7dd6fee78f9009f4e01b37dbc9ab484a9b2c1578915db1e571" ], "markers": "python_version >= '3.8'", - "version": "==0.44.0" + "version": "==0.45.0" }, "grpcio": { "hashes": [ @@ -4436,12 +4410,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:049f82770f40559d3c2aa2259c562ea7257dbb4aaa9624323b5ef27b2d95a450", - "sha256:210e1f179682cd4be17d5c641b2f4559574b9dea2f589c3f0e7c17c5bd1959bc" + "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288", + "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.21" + "version": "==9.5.23" }, "mkdocs-material-extensions": { "hashes": [ @@ -4462,12 +4436,12 @@ }, "mkdocstrings-python": { "hashes": [ - "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828", - "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc" + "sha256:38a4fd41953defb458a107033440c229c7e9f98f35a24e84d888789c97da5a63", + "sha256:e8e596b37f45c09b67bec253e035fe18988af5bbbbf44e0ccd711742eed750e5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.10.0" + "version": "==1.10.2" }, "nodeenv": { "hashes": [ @@ -4518,11 +4492,11 @@ }, "platformdirs": { "hashes": [ - "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf", - "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1" + "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", + "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" ], "markers": "python_version >= '3.8'", - "version": "==4.2.1" + "version": "==4.2.2" }, "pluggy": { "hashes": [ @@ -4534,12 +4508,12 @@ }, "pre-commit": { "hashes": [ - "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab", - "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060" + "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a", + "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.7.0" + "version": "==3.7.1" }, "proto-plus": { "hashes": [ @@ -4750,88 +4724,88 @@ }, "regex": { "hashes": [ - "sha256:05d9b6578a22db7dedb4df81451f360395828b04f4513980b6bd7a1412c679cc", - "sha256:08a1749f04fee2811c7617fdd46d2e46d09106fa8f475c884b65c01326eb15c5", - "sha256:0940038bec2fe9e26b203d636c44d31dd8766abc1fe66262da6484bd82461ccf", - "sha256:0a2a512d623f1f2d01d881513af9fc6a7c46e5cfffb7dc50c38ce959f9246c94", - "sha256:0a54a047b607fd2d2d52a05e6ad294602f1e0dec2291152b745870afc47c1397", - "sha256:0dd3f69098511e71880fb00f5815db9ed0ef62c05775395968299cb400aeab82", - "sha256:1031a5e7b048ee371ab3653aad3030ecfad6ee9ecdc85f0242c57751a05b0ac4", - "sha256:108e2dcf0b53a7c4ab8986842a8edcb8ab2e59919a74ff51c296772e8e74d0ae", - "sha256:144a1fc54765f5c5c36d6d4b073299832aa1ec6a746a6452c3ee7b46b3d3b11d", - "sha256:19d6c11bf35a6ad077eb23852827f91c804eeb71ecb85db4ee1386825b9dc4db", - "sha256:1f687a28640f763f23f8a9801fe9e1b37338bb1ca5d564ddd41619458f1f22d1", - "sha256:224803b74aab56aa7be313f92a8d9911dcade37e5f167db62a738d0c85fdac4b", - "sha256:23a412b7b1a7063f81a742463f38821097b6a37ce1e5b89dd8e871d14dbfd86b", - "sha256:25f87ae6b96374db20f180eab083aafe419b194e96e4f282c40191e71980c666", - "sha256:2630ca4e152c221072fd4a56d4622b5ada876f668ecd24d5ab62544ae6793ed6", - "sha256:28e1f28d07220c0f3da0e8fcd5a115bbb53f8b55cecf9bec0c946eb9a059a94c", - "sha256:2b51739ddfd013c6f657b55a508de8b9ea78b56d22b236052c3a85a675102dc6", - "sha256:2cc1b87bba1dd1a898e664a31012725e48af826bf3971e786c53e32e02adae6c", - "sha256:2fef0b38c34ae675fcbb1b5db760d40c3fc3612cfa186e9e50df5782cac02bcd", - "sha256:36f392dc7763fe7924575475736bddf9ab9f7a66b920932d0ea50c2ded2f5636", - "sha256:374f690e1dd0dbdcddea4a5c9bdd97632cf656c69113f7cd6a361f2a67221cb6", - "sha256:3986217ec830c2109875be740531feb8ddafe0dfa49767cdcd072ed7e8927962", - "sha256:39fb166d2196413bead229cd64a2ffd6ec78ebab83fff7d2701103cf9f4dfd26", - "sha256:4290035b169578ffbbfa50d904d26bec16a94526071ebec3dadbebf67a26b25e", - "sha256:43548ad74ea50456e1c68d3c67fff3de64c6edb85bcd511d1136f9b5376fc9d1", - "sha256:44a22ae1cfd82e4ffa2066eb3390777dc79468f866f0625261a93e44cdf6482b", - "sha256:457c2cd5a646dd4ed536c92b535d73548fb8e216ebee602aa9f48e068fc393f3", - "sha256:459226445c7d7454981c4c0ce0ad1a72e1e751c3e417f305722bbcee6697e06a", - "sha256:47af45b6153522733aa6e92543938e97a70ce0900649ba626cf5aad290b737b6", - "sha256:499334ad139557de97cbc4347ee921c0e2b5e9c0f009859e74f3f77918339257", - "sha256:57ba112e5530530fd175ed550373eb263db4ca98b5f00694d73b18b9a02e7185", - "sha256:5ce479ecc068bc2a74cb98dd8dba99e070d1b2f4a8371a7dfe631f85db70fe6e", - "sha256:5dbc1bcc7413eebe5f18196e22804a3be1bfdfc7e2afd415e12c068624d48247", - "sha256:6277d426e2f31bdbacb377d17a7475e32b2d7d1f02faaecc48d8e370c6a3ff31", - "sha256:66372c2a01782c5fe8e04bff4a2a0121a9897e19223d9eab30c54c50b2ebeb7f", - "sha256:670fa596984b08a4a769491cbdf22350431970d0112e03d7e4eeaecaafcd0fec", - "sha256:6f435946b7bf7a1b438b4e6b149b947c837cb23c704e780c19ba3e6855dbbdd3", - "sha256:7413167c507a768eafb5424413c5b2f515c606be5bb4ef8c5dee43925aa5718b", - "sha256:7c3d389e8d76a49923683123730c33e9553063d9041658f23897f0b396b2386f", - "sha256:7d77b6f63f806578c604dca209280e4c54f0fa9a8128bb8d2cc5fb6f99da4150", - "sha256:7e76b9cfbf5ced1aca15a0e5b6f229344d9b3123439ffce552b11faab0114a02", - "sha256:7f3502f03b4da52bbe8ba962621daa846f38489cae5c4a7b5d738f15f6443d17", - "sha256:7fe9739a686dc44733d52d6e4f7b9c77b285e49edf8570754b322bca6b85b4cc", - "sha256:83ab366777ea45d58f72593adf35d36ca911ea8bd838483c1823b883a121b0e4", - "sha256:84077821c85f222362b72fdc44f7a3a13587a013a45cf14534df1cbbdc9a6796", - "sha256:8bb381f777351bd534462f63e1c6afb10a7caa9fa2a421ae22c26e796fe31b1f", - "sha256:92da587eee39a52c91aebea8b850e4e4f095fe5928d415cb7ed656b3460ae79a", - "sha256:9301cc6db4d83d2c0719f7fcda37229691745168bf6ae849bea2e85fc769175d", - "sha256:965fd0cf4694d76f6564896b422724ec7b959ef927a7cb187fc6b3f4e4f59833", - "sha256:99d6a550425cc51c656331af0e2b1651e90eaaa23fb4acde577cf15068e2e20f", - "sha256:99ef6289b62042500d581170d06e17f5353b111a15aa6b25b05b91c6886df8fc", - "sha256:a1409c4eccb6981c7baabc8888d3550df518add6e06fe74fa1d9312c1838652d", - "sha256:a74fcf77d979364f9b69fcf8200849ca29a374973dc193a7317698aa37d8b01c", - "sha256:aaa179975a64790c1f2701ac562b5eeb733946eeb036b5bcca05c8d928a62f10", - "sha256:ac69b394764bb857429b031d29d9604842bc4cbfd964d764b1af1868eeebc4f0", - "sha256:b45d4503de8f4f3dc02f1d28a9b039e5504a02cc18906cfe744c11def942e9eb", - "sha256:b7d893c8cf0e2429b823ef1a1d360a25950ed11f0e2a9df2b5198821832e1947", - "sha256:b8eb28995771c087a73338f695a08c9abfdf723d185e57b97f6175c5051ff1ae", - "sha256:b91d529b47798c016d4b4c1d06cc826ac40d196da54f0de3c519f5a297c5076a", - "sha256:bc365ce25f6c7c5ed70e4bc674f9137f52b7dd6a125037f9132a7be52b8a252f", - "sha256:bf29304a8011feb58913c382902fde3395957a47645bf848eea695839aa101b7", - "sha256:c06bf3f38f0707592898428636cbb75d0a846651b053a1cf748763e3063a6925", - "sha256:c77d10ec3c1cf328b2f501ca32583625987ea0f23a0c2a49b37a39ee5c4c4630", - "sha256:cd196d056b40af073d95a2879678585f0b74ad35190fac04ca67954c582c6b61", - "sha256:d7a353ebfa7154c871a35caca7bfd8f9e18666829a1dc187115b80e35a29393e", - "sha256:d84308f097d7a513359757c69707ad339da799e53b7393819ec2ea36bc4beb58", - "sha256:dd7ef715ccb8040954d44cfeff17e6b8e9f79c8019daae2fd30a8806ef5435c0", - "sha256:e672cf9caaf669053121f1766d659a8813bd547edef6e009205378faf45c67b8", - "sha256:ecc6148228c9ae25ce403eade13a0961de1cb016bdb35c6eafd8e7b87ad028b1", - "sha256:f1c5742c31ba7d72f2dedf7968998730664b45e38827637e0f04a2ac7de2f5f1", - "sha256:f1d6e4b7b2ae3a6a9df53efbf199e4bfcff0959dbdb5fd9ced34d4407348e39a", - "sha256:f2fc053228a6bd3a17a9b0a3f15c3ab3cf95727b00557e92e1cfe094b88cc662", - "sha256:f57515750d07e14743db55d59759893fdb21d2668f39e549a7d6cad5d70f9fea", - "sha256:f85151ec5a232335f1be022b09fbbe459042ea1951d8a48fef251223fc67eee1", - "sha256:fb0315a2b26fde4005a7c401707c5352df274460f2f85b209cf6024271373013", - "sha256:fc0916c4295c64d6890a46e02d4482bb5ccf33bf1a824c0eaa9e83b148291f90", - "sha256:fd24fd140b69f0b0bcc9165c397e9b2e89ecbeda83303abf2a072609f60239e2", - "sha256:fdae0120cddc839eb8e3c15faa8ad541cc6d906d3eb24d82fb041cfe2807bc1e", - "sha256:fe00f4fe11c8a521b173e6324d862ee7ee3412bf7107570c9b564fe1119b56fb" + "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649", + "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35", + "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb", + "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68", + "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5", + "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133", + "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0", + "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d", + "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da", + "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f", + "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d", + "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53", + "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa", + "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a", + "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890", + "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67", + "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c", + "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2", + "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced", + "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741", + "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f", + "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa", + "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf", + "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4", + "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5", + "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2", + "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384", + "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7", + "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014", + "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704", + "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5", + "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2", + "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49", + "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1", + "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694", + "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629", + "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6", + "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435", + "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c", + "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835", + "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e", + "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201", + "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62", + "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5", + "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16", + "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f", + "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1", + "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f", + "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f", + "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145", + "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3", + "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed", + "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143", + "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca", + "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9", + "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa", + "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850", + "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80", + "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe", + "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656", + "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388", + "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1", + "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294", + "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3", + "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d", + "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b", + "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40", + "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600", + "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c", + "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569", + "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456", + "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9", + "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb", + "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e", + "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f", + "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d", + "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a", + "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a", + "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796" ], "markers": "python_version >= '3.8'", - "version": "==2024.4.28" + "version": "==2024.5.15" }, "requests": { "hashes": [ @@ -4899,11 +4873,11 @@ }, "virtualenv": { "hashes": [ - "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b", - "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75" + "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c", + "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b" ], "markers": "python_version >= '3.7'", - "version": "==20.26.1" + "version": "==20.26.2" }, "watchdog": { "hashes": [ @@ -4951,11 +4925,11 @@ }, "zipp": { "hashes": [ - "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", - "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059", + "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e" ], "markers": "python_version >= '3.8'", - "version": "==3.18.1" + "version": "==3.18.2" }, "zope.event": { "hashes": [ @@ -4967,45 +4941,37 @@ }, "zope.interface": { "hashes": [ - "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e", - "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf", - "sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130", - "sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86", - "sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1", - "sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e", - "sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5", - "sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c", - "sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92", - "sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021", - "sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c", - "sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10", - "sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83", - "sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb", - "sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920", - "sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299", - "sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e", - "sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af", - "sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39", - "sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21", - "sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061", - "sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b", - "sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5", - "sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0", - "sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6", - "sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85", - "sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5", - "sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a", - "sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9", - "sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1", - "sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12", - "sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e", - "sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785", - "sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91", - "sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a", - "sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d" + "sha256:21732994aa3ca43bbb6b36335c288023428a3c5b7322b637c7b0a03053937578", + "sha256:36ee6e507a9fd4f1f0aab8e8dfc801d162e7211c27503cbfb47e1d558941a7fa", + "sha256:3945f4fda92c1b6fb0cb6eaaaf72599e5c2c2059654bdc42bc09c6e711c214c8", + "sha256:414e6dccdf4a5c96c0c98da68ba040dbf9ba7511b61b34e228f11b0ed90c439d", + "sha256:4782e173c2fde4f649c2a9a68082445bc1f2c27f41907de06bf1ba82585847f2", + "sha256:4cd56eb9a23767958c9a0654306b9a4a74def485f645b3a7378cc6ab661ef31c", + "sha256:502d2c9c4231d022b20225dba5c6c736236ed65e1d7e2f6f402b5aa6a7040ec9", + "sha256:57f34b7997f8de7d2db08363eaccd05dad20f106e39efe95bed4fac84af2d022", + "sha256:5fbbb290751f5c4ed81e54ae73fe8557c4a85973f5ab019edbb0f746244ecea6", + "sha256:604fa920478dfc0c76cdb7c203572400a8317ffcdac288245c408b42b3d9aee9", + "sha256:62e6b756663deade5270f67899753437b39d970f9eecd49e19fae3b880310cf0", + "sha256:646cd83d24065d074f22f61fe101d20dbf4b729ca7831cc782ec986eb9156f93", + "sha256:6494dc0314e782ce4fb0e624b4ce2458f54d074382f50a920c7700c05cbcef28", + "sha256:6e4cc017206c1429a6d8fdd8a25c6efc15512065eec0a8d45c350df96a0911ed", + "sha256:72faa868fcfde49a29d287dce3c83180322467eecd725dd351098efe96e8d4bb", + "sha256:7cda82ab32f984985f09e4ec20a4f9665b26779a1b8e443b34a148de256f2052", + "sha256:855b7233fa5d0d1f3be8c14fadf4718dee1c928e1d75f1584bea6ecec6dcc4af", + "sha256:86e85eada0eb551950df05d72dc0e892320f14daa78bc434059e834d4b1f9300", + "sha256:8e246357f52952ae5fa950d19eda8572594c49e6cb1e5462508e6cec561a37de", + "sha256:93f28d84517dcd6c240979bd9b2f262a373832baef856fe663a24b9171d7f04d", + "sha256:b0f61ccbc26e08031d0e72b6a0cbf9b4030f035913cb2b39f940aa42eb8e0063", + "sha256:b11f2b67ccc990a1522fa8cd3f5d185a068459f944ab2d0e7a1b15d31bcb4af4", + "sha256:c04bd4ee4766d285e83c6d8c042663a98efb934389e05ccd643fefb066c88a9d", + "sha256:ee1e3ca6c98efe213a96dece89100a8aa52e210ac354861d8039d69bd1d6e5ff", + "sha256:f33af86ed460eb28dc9da1de1f3305795271a19c665161c1d973a737596b2081", + "sha256:f5092f2712e1fd07579fc3101b18e9c95857c853e836847598bf992c8e672434", + "sha256:f78e1eac48c4f4e0168a91cabcd8d1aedb972836df5c8769071fc6173294a0a3", + "sha256:fe636b49c333bfc5b0913590e36a2f151167c462fb36d9f4acc66029e45c974b" ], "markers": "python_version >= '3.7'", - "version": "==6.3" + "version": "==6.4" } } } From a2e9837365808c05984fef913ad499e4315a6b94 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Thu, 16 May 2024 15:38:01 -0400 Subject: [PATCH 03/40] Update actions.py --- breathecode/certificate/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/certificate/actions.py b/breathecode/certificate/actions.py index 33623f90a..fa1005301 100644 --- a/breathecode/certificate/actions.py +++ b/breathecode/certificate/actions.py @@ -201,7 +201,7 @@ def generate_certificate(user, cohort=None, layout=None): task_types=['PROJECT'], only_mandatory=True) - if pending_tasks: + if pending_tasks and pending_tasks > 0: raise ValidationException(f'The student has {pending_tasks} pending tasks', slug=f'with-pending-tasks-{pending_tasks}') From dda571d50e8a0d6adb8811df6dd931a26ceedafc Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 21 May 2024 13:21:48 -0400 Subject: [PATCH 04/40] Update provisioning_invoice.html --- breathecode/provisioning/templates/provisioning_invoice.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/provisioning/templates/provisioning_invoice.html b/breathecode/provisioning/templates/provisioning_invoice.html index c1e903e5a..369fca1c6 100644 --- a/breathecode/provisioning/templates/provisioning_invoice.html +++ b/breathecode/provisioning/templates/provisioning_invoice.html @@ -187,7 +187,7 @@
- {{ consumption.kind.product_name }} + {{ consumption.kind.product_name }} ({{ consumption.kind.sku }}) {% if consumption.status_text %} - From 1757ffc1d68319ac18b153a1889f0423f023b90f Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 16:10:17 -0500 Subject: [PATCH 05/40] add rigobot csv upload --- Pipfile.lock | 44 +- breathecode/provisioning/actions.py | 201 +++- breathecode/provisioning/models.py | 14 +- breathecode/provisioning/tasks.py | 10 + .../provisioning/tests/tasks/tests_upload.py | 1030 ++++++++++++++++- 5 files changed, 1269 insertions(+), 30 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 666106a1a..bf215bb7c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1639,11 +1639,11 @@ "requests" ], "hashes": [ - "sha256:0c71f043805e0f32a3f17ed4129f76ad5ba4710f68b092de210939d0d290a8f2", - "sha256:eba1d993a8568ecccc1b9d2f0427ba59df6f8d99575119ba2dc1f609d65fd120" + "sha256:61d05ddb69044958c041238d4deb593e2ce6c82e87dab5b0d2a905f51b5fa6e2", + "sha256:778db540c47dd580cbd52af63ab14549392d50407790a2f39939743d64cf7d65" ], "markers": "python_version >= '3.10'", - "version": "==1.2.1" + "version": "==1.2.2" }, "lxml": { "hashes": [ @@ -2238,10 +2238,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:d0e5fdc6c09519e53a331003db7203c9e82bd39ac676d584e297e6523d334448", - "sha256:e9d7b1220a2f376d35b0b7a305fe76fe5ef077e580f7840869dbd29d9c9f74a8" + "sha256:6b28e38ea2bc0d809c1f933f7fccb860d780bc62b9456a2754cb778a620dca76", + "sha256:7cdc76625e0879071ad31f4066867adbc6779ac37d574957c64a72b59b8bc82d" ], - "version": "==8.13.36" + "version": "==8.13.37" }, "pillow": { "hashes": [ @@ -3282,12 +3282,12 @@ }, "stripe": { "hashes": [ - "sha256:9305d849cea715dc59c5e39d01891475b82e10edb9d95ee1d8189457e5de792f", - "sha256:f519f6810ac7f6e096b4faf562c44b1f8e365138441548e4ab0bc93f86368ad7" + "sha256:266af2f4ff23ca3cdc73c332cf2776fd2e1084b1e9379b03fb981b1040ed69f0", + "sha256:af694723bf7968cea18a956641dcef786ec1f72de8022718fa3ef5f7868bd430" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==9.6.0" + "version": "==9.7.0" }, "text-unidecode": { "hashes": [ @@ -3627,15 +3627,7 @@ "markers": "python_version >= '3.7'", "version": "==1.9.4" }, - "zope.event": { - "hashes": [ - "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", - "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0" - }, - "zope.interface": { + "zope-interface": { "hashes": [ "sha256:21732994aa3ca43bbb6b36335c288023428a3c5b7322b637c7b0a03053937578", "sha256:36ee6e507a9fd4f1f0aab8e8dfc801d162e7211c27503cbfb47e1d558941a7fa", @@ -3669,6 +3661,14 @@ "markers": "python_version >= '3.7'", "version": "==6.4" }, + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", @@ -4483,12 +4483,12 @@ }, "pep8-naming": { "hashes": [ - "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971", - "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80" + "sha256:1ef228ae80875557eb6c1549deafed4dabbf3261cfcafa12f773fe0db9be8a36", + "sha256:63f514fc777d715f935faf185dedd679ab99526a7f2f503abb61587877f7b1c5" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==0.13.3" + "markers": "python_version >= '3.8'", + "version": "==0.14.1" }, "platformdirs": { "hashes": [ diff --git a/breathecode/provisioning/actions.py b/breathecode/provisioning/actions.py index f53b9d144..4ccf509ee 100644 --- a/breathecode/provisioning/actions.py +++ b/breathecode/provisioning/actions.py @@ -2,12 +2,15 @@ import random import re from datetime import datetime +from decimal import Decimal, localcontext from typing import TypedDict import pytz from dateutil.relativedelta import relativedelta +from django.contrib.auth.models import User from django.db.models import Q, QuerySet from django.utils import timezone +from linked_services.django.actions import get_user from breathecode.admissions.models import Academy, CohortUser from breathecode.authenticate.models import ( @@ -287,7 +290,7 @@ def add_codespaces_activity(context: ActivityContext, field: dict, position: int if not academies and not GithubAcademyUser.objects.filter(username=field['Username']).count(): academies = handle_pending_github_user(field['Owner'], field['Username']) - if not not_found: + if not not_found and academies: academies = random.choices(academies, k=1) errors = [] @@ -543,3 +546,199 @@ def add_gitpod_activity(context: ActivityContext, field: dict, position: int): pa.bills.add(provisioning_bill) pa.events.add(item) + + +def add_rigobot_activity(context: ActivityContext, field: dict, position: int) -> None: + errors = [] + ignores = [] + + if field['organization'] != '4Geeks': + return + + user = get_user(app='rigobot', sub=field['user_id']) + if user is None: + user = User.objects.filter(email=field['email']).first() + + if user is None: + return + + if field['billing_status'] != 'OPEN': + return + + github_academy_user_log = context['github_academy_user_logs'].get(user.id, None) + academies = [] + found_at_github_log = False + + if github_academy_user_log is None: + # make a function that calculate the user activity in the academies by percentage + github_academy_user_log = GithubAcademyUserLog.objects.filter( + Q(valid_until__isnull=True) + | Q(valid_until__gte=context['limit'] - relativedelta(months=1, weeks=1)), + created_at__lte=context['limit'], + academy_user__user=user, + storage_status='SYNCHED', + storage_action='ADD').order_by('-created_at') + + context['github_academy_user_logs'][user.id] = github_academy_user_log + + if github_academy_user_log: + found_at_github_log = True + academies = [x.academy_user.academy for x in github_academy_user_log] + + # not implemented yet + # not_found = bool(academies) + date = datetime.fromisoformat(field['consumption_period_start']) + end = datetime.fromisoformat(field['consumption_period_end']) + + if not academies: + profile_academies = context['profile_academies'].get(field['github_username'], None) + if profile_academies is None: + profile_academies = ProfileAcademy.objects.filter( + user__credentialsgithub__username=field['github_username'], status='ACTIVE') + + context['profile_academies'][field['github_username']] = profile_academies + + if profile_academies: + academies = sorted(list({profile.academy for profile in profile_academies}), key=lambda x: x.id) + + if not found_at_github_log and len(academies) > 1: + cohort_users = CohortUser.objects.filter( + Q(cohort__ending_date__lte=end) | Q(cohort__never_ends=True), + cohort__kickoff_date__gte=date, + user__credentialsgithub__username=field['github_username']).order_by('-created_at') + + if cohort_users: + academies = sorted(list({cohort_user.cohort.academy for cohort_user in cohort_users}), key=lambda x: x.id) + + if not academies: + if 'academies' not in context: + context['academies'] = Academy.objects.filter() + academies = list(context['academies']) + + if not found_at_github_log and academies: + academies = random.choices(academies, k=1) + + logs = {} + provisioning_bills = {} + provisioning_vendor = None + + provisioning_vendor = context['provisioning_vendors'].get('Rigobot', None) + if not provisioning_vendor: + provisioning_vendor = ProvisioningVendor.objects.filter(name='Rigobot').first() + context['provisioning_vendors']['Rigobot'] = provisioning_vendor + + if not provisioning_vendor: + errors.append('Provisioning vendor Rigobot not found') + + for academy in academies: + ls = context['logs'].get((field['github_username'], academy.id), None) + if ls is None: + ls = get_github_academy_user_logs(academy, field['github_username'], context['limit']) + context['logs'][(field['github_username'], academy.id)] = ls + logs[academy.id] = ls + + provisioning_bill = context['provisioning_bills'].get(academy.id, None) + if not provisioning_bill and (provisioning_bill := ProvisioningBill.objects.filter( + academy=academy, status='PENDING', hash=context['hash']).first()): + context['provisioning_bills'][academy.id] = provisioning_bill + provisioning_bills[academy.id] = provisioning_bill + + if not provisioning_bill: + provisioning_bill = ProvisioningBill() + provisioning_bill.academy = academy + provisioning_bill.vendor = provisioning_vendor + provisioning_bill.status = 'PENDING' + provisioning_bill.hash = context['hash'] + provisioning_bill.save() + + context['provisioning_bills'][academy.id] = provisioning_bill + provisioning_bills[academy.id] = provisioning_bill + + for academy_id in logs.keys(): + for log in logs[academy_id]: + if (log['storage_action'] == 'DELETE' and log['storage_status'] == 'SYNCHED' + and log['starting_at'] <= pytz.utc.localize(date) <= log['ending_at']): + provisioning_bills.pop(academy_id, None) + ignores.append( + f'User {field["github_username"]} was deleted from the academy during this event at {date}') + + if not provisioning_bills: + for academy_id in logs.keys(): + cohort_user = CohortUser.objects.filter( + Q(cohort__ending_date__lte=date) | Q(cohort__never_ends=True), + cohort__kickoff_date__gte=date, + cohort__academy__id=academy_id, + user__credentialsgithub__username=field['github_username']).order_by('-created_at').first() + + if cohort_user: + errors.append('We found activity from this user while he was studying at one of your cohort ' + f'{cohort_user.cohort.slug}') + + # not implemented yet + # if not_found: + # errors.append(f'We could not find enough information about {field["github_username"]}, mark this user user as ' + # 'deleted if you don\'t recognize it') + + s_slug = f'{field["purpose_slug"] or "no-provided"}--{field["pricing_type"].lower()}--{field["model"].lower()}' + s_name = f'{field["purpose"]} (type: {field["pricing_type"]}, model: {field["model"]})' + if not (kind := context['provisioning_activity_kinds'].get((s_name, s_slug), None)): + kind, _ = ProvisioningConsumptionKind.objects.get_or_create( + product_name=s_name, + sku=s_slug, + ) + context['provisioning_activity_kinds'][(s_name, s_slug)] = kind + + if not (currency := context['currencies'].get('USD', None)): + currency, _ = Currency.objects.get_or_create(code='USD', name='US Dollar', decimals=2) + context['currencies']['USD'] = currency + + if not (price := context['provisioning_activity_prices'].get((field['total_spent'], field['total_tokens']), None)): + with localcontext(prec=10): + price, _ = ProvisioningPrice.objects.get_or_create( + currency=currency, + unit_type='Tokens', + price_per_unit=Decimal(field['total_spent']) / Decimal(field['total_tokens']), + multiplier=context['provisioning_multiplier'], + ) + + context['provisioning_activity_prices'][(field['total_spent'], field['total_tokens'])] = price + + pa, _ = ProvisioningUserConsumption.objects.get_or_create(username=field['github_username'], + hash=context['hash'], + kind=kind, + defaults={'processed_at': timezone.now()}) + + item, _ = ProvisioningConsumptionEvent.objects.get_or_create( + vendor=provisioning_vendor, + price=price, + registered_at=date, + external_pk=field['consumption_item_id'], + quantity=field['total_tokens'], + repository_url=None, + task_associated_slug=None, + csv_row=position, + ) + + # if errors and not (len(errors) == 1 and not_found): + if errors: + pa.status = 'ERROR' + pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors + ignores) + + elif pa.status != 'ERROR' and ignores and not provisioning_bills: + pa.status = 'IGNORED' + pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(ignores) + + else: + pa.status = 'PERSISTED' + pa.status_text = pa.status_text + (', ' if pa.status_text else '') + ', '.join(errors + ignores) + + pa.status_text = ', '.join(sorted(set(pa.status_text.split(', ')))) + pa.status_text = pa.status_text[:255] + pa.save() + + current_bills = pa.bills.all() + for provisioning_bill in provisioning_bills.values(): + if provisioning_bill not in current_bills: + pa.bills.add(provisioning_bill) + + pa.events.add(item) diff --git a/breathecode/provisioning/models.py b/breathecode/provisioning/models.py index 564ce778b..3a2a8a17b 100644 --- a/breathecode/provisioning/models.py +++ b/breathecode/provisioning/models.py @@ -1,10 +1,11 @@ import logging -from django.db import models + from django.contrib.auth.models import User -from breathecode.admissions.models import Academy, Cohort -from breathecode.authenticate.models import ProfileAcademy +from django.db import models from django.utils import timezone +from breathecode.admissions.models import Academy, Cohort +from breathecode.authenticate.models import ProfileAcademy from breathecode.payments.models import Currency logger = logging.getLogger(__name__) @@ -180,9 +181,12 @@ class ProvisioningConsumptionEvent(models.Model): quantity = models.FloatField() price = models.ForeignKey(ProvisioningPrice, on_delete=models.CASCADE) - repository_url = models.URLField() + repository_url = models.URLField(null=True, blank=False) task_associated_slug = models.SlugField( - max_length=100, help_text='What assignment was the the student trying to complete with this') + max_length=100, + null=True, + blank=False, + help_text='What assignment was the the student trying to complete with this') def __str__(self): return str(self.quantity) + ' - ' + self.task_associated_slug diff --git a/breathecode/provisioning/tasks.py b/breathecode/provisioning/tasks.py index 5d5e42a26..3101bdc39 100644 --- a/breathecode/provisioning/tasks.py +++ b/breathecode/provisioning/tasks.py @@ -202,6 +202,16 @@ def upload(hash: str, *, page: int = 0, force: bool = False, task_manager_id: in if not handler and len(df.keys().intersection(fields)) == len(fields): handler = actions.add_codespaces_activity + if not handler: + fields = [ + 'organization', 'consumption_period_id', 'consumption_period_start', 'consumption_period_end', + 'billing_status', 'total_spent_period', 'consumption_item_id', 'user_id', 'email', 'consumption_type', + 'pricing_type', 'total_spent', 'total_tokens', 'model', 'purpose_id', 'purpose_slug', 'purpose', + 'created_at' + ] + if not handler and len(df.keys().intersection(fields)) == len(fields): + handler = actions.add_rigobot_activity + if not handler: raise AbortTask(f'File {hash} has an unsupported origin or the provider had changed the file format') diff --git a/breathecode/provisioning/tests/tasks/tests_upload.py b/breathecode/provisioning/tests/tasks/tests_upload.py index 8a40e5a71..0e4738d71 100644 --- a/breathecode/provisioning/tests/tasks/tests_upload.py +++ b/breathecode/provisioning/tests/tasks/tests_upload.py @@ -8,10 +8,12 @@ import re import string from datetime import datetime, timedelta +from decimal import Decimal, localcontext from random import choices from unittest.mock import MagicMock, PropertyMock, call, patch import pandas as pd +import pytest import pytz from django.utils import timezone from faker import Faker @@ -36,6 +38,11 @@ RANDOM_ACADEMIES = [random.randint(0, 2) for _ in range(10)] +@pytest.fixture(autouse=True) +def setup(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr('linked_services.django.tasks.check_credentials.delay', MagicMock()) + + def parse(s): return json.loads(s) @@ -130,6 +137,73 @@ def gitpod_csv(lines=1, data={}): } +def datetime_to_date_str(date: datetime) -> str: + return date.strftime('%Y-%m-%d') + + +def rigobot_csv(lines=1, data={}): + organizations = ['4Geeks' for _ in range(lines)] + consumption_period_ids = [random.randint(1, 10) for _ in range(lines)] + times = [datetime_to_iso(timezone.now()) for _ in range(lines)] + billing_statuses = ['OPEN' for _ in range(lines)] + total_spent_periods = [(random.random() * 30) + 0.01 for _ in range(lines)] + consumption_item_ids = [random.randint(1, 10) for _ in range(lines)] + user_ids = [10 for _ in range(lines)] + emails = [fake.email() for _ in range(lines)] + consumption_types = ['MESSAGE' for _ in range(lines)] + pricing_types = [random.choice(['INPUT', 'OUTPUT']) for _ in range(lines)] + total_tokens = [random.randint(1, 100) for _ in range(lines)] + total_spents = [] + res = [] + for i in range(lines): + total_token = total_tokens[i] + pricing_type = pricing_types[i] + price = 0.04 if pricing_type == 'OUTPUT' else 0.02 + total_spent = price * total_token + while total_spent in res: + total_tokens[i] = random.randint(1, 100) + total_token = total_tokens[i] + total_spent = price * total_token + + total_spents.append(total_spent) + res.append(total_spent) + + models = [ + random.choice(['gpt-4-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-3.5-turbo', 'gpt-3.5']) + for _ in range(lines) + ] + purpose_ids = [random.randint(1, 10) for _ in range(lines)] + purpose_slugs = [fake.slug() for _ in range(lines)] + purposes = [' '.join(fake.words()) for _ in range(lines)] + github_usernames = [fake.user_name() for _ in range(lines)] + + created_ats = [datetime_to_iso(timezone.now()) for _ in range(lines)] + + # dictionary of lists + return { + 'organization': organizations, + 'consumption_period_id': consumption_period_ids, + 'consumption_period_start': times, + 'consumption_period_end': times, + 'billing_status': billing_statuses, + 'total_spent_period': total_spent_periods, + 'consumption_item_id': consumption_item_ids, + 'user_id': user_ids, + 'email': emails, + 'consumption_type': consumption_types, + 'pricing_type': pricing_types, + 'total_spent': total_spents, + 'total_tokens': total_tokens, + 'model': models, + 'purpose_id': purpose_ids, + 'purpose_slug': purpose_slugs, + 'purpose': purposes, + 'created_at': created_ats, + 'github_username': github_usernames, + **data, + } + + def csv_file_mock(obj): df = pd.DataFrame.from_dict(obj) @@ -177,8 +251,8 @@ def provisioning_activity_item_data(data={}): 'price_id': 1, 'quantity': 0.0, 'registered_at': ..., - 'repository_url': '', - 'task_associated_slug': '', + 'repository_url': None, + 'task_associated_slug': None, 'vendor_id': None, 'csv_row': 0, **data, @@ -2105,3 +2179,955 @@ def test_from_github_credentials__generate_anything__case3(self): self.bc.check.calls(tasks.upload.delay.call_args_list, []) self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) + + +class RigobotTestSuite(ProvisioningTestCase): + + # Given: a csv with codespaces data + # When: users does not exist + # Then: the task should not create any bill, create an activity with wrong status + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + def test_users_not_found(self): + csv = rigobot_csv(10) + + self.bc.database.create(app={'slug': 'rigobot'}, first_party_credentials={'app': {'rigobot': 10}}) + logging.Logger.info.call_args_list = [] + + slug = self.bc.fake.slug() + with patch('requests.get', response_mock(content=[{'id': 1} for _ in range(10)])): + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), []) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + }) for n in range(10) + ]) + self.assertEqual( + self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), + [ + provisioning_activity_data({ + 'id': + n + 1, + 'kind_id': + n + 1, + 'hash': + slug, + 'username': + csv['github_username'][n], + 'processed_at': + UTC_NOW, + 'status': + 'ERROR', + 'status_text': + ', '.join([ + 'Provisioning vendor Rigobot not found', + # not implemented yet, + # f"We could not find enough information about {csv['github_username'][n]}, mark this user user " + # "as deleted if you don't recognize it", + ]), + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), []) + + self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, []) + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, []) + + # Given: a csv with codespaces data and 10 User, 10 GithubAcademyUser and 10 GithubAcademyUserLog + # When: vendor not found + # Then: the task should not create any bill or activity + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + def test_from_github_credentials__vendor_not_found(self): + csv = rigobot_csv(10) + + github_academy_users = [{ + 'username': username, + } for username in csv['github_username']] + github_academy_user_logs = [{ + 'storage_status': 'SYNCHED', + 'storage_action': 'ADD', + 'academy_user_id': n + 1, + } for n in range(10)] + model = self.bc.database.create(user=10, + app={'slug': 'rigobot'}, + first_party_credentials={'app': { + 'rigobot': 10 + }}, + github_academy_user=github_academy_users, + github_academy_user_log=github_academy_user_logs) + + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + slug = self.bc.fake.slug() + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ + provisioning_bill_data({'hash': slug}), + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': n + 1, + 'kind_id': n + 1, + 'hash': slug, + 'username': csv['github_username'][n], + 'processed_at': UTC_NOW, + 'status': 'ERROR', + 'status_text': ', '.join(['Provisioning vendor Rigobot not found']), + }) for n in range(10) + ]) + + self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, []) + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, []) + + # Given: a csv with codespaces data and 10 User, 10 GithubAcademyUser, 10 GithubAcademyUserLog + # -> and 1 ProvisioningVendor of type codespaces + # When: all the data is correct + # Then: the task should create 1 bills and 10 activities + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + def test_from_github_credentials__generate_anything(self): + csv = rigobot_csv(10) + + github_academy_users = [{ + 'username': username, + } for username in csv['github_username']] + github_academy_user_logs = [{ + 'storage_status': 'SYNCHED', + 'storage_action': 'ADD', + 'academy_user_id': n + 1, + } for n in range(10)] + provisioning_vendor = {'name': 'Rigobot'} + model = self.bc.database.create(user=10, + app={'slug': 'rigobot'}, + first_party_credentials={'app': { + 'rigobot': 10 + }}, + github_academy_user=github_academy_users, + github_academy_user_log=github_academy_user_logs, + provisioning_vendor=provisioning_vendor) + + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + slug = self.bc.fake.slug() + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ + provisioning_bill_data({ + 'hash': slug, + 'vendor_id': 1, + }), + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data({ + 'id': n + 1, + 'product_name': csv['kind'][n], + 'sku': str(csv['kind'][n]), + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + 'vendor_id': + 1, + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': n + 1, + 'kind_id': n + 1, + 'hash': slug, + 'username': csv['userName'][n], + 'processed_at': UTC_NOW, + 'status': 'PERSISTED', + }) for n in range(10) + ]) + + self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), + self.bc.format.to_dict(model.github_academy_user)) + + self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, []) + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) + + # Given: a csv with codespaces data and 10 User, 10 GithubAcademyUser, 10 GithubAcademyUserLog + # -> and 1 ProvisioningVendor of type codespaces + # When: all the data is correct, and the amount of rows is greater than the limit + # Then: the task should create 1 bills and 10 activities + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + @patch('breathecode.provisioning.tasks.PANDAS_ROWS_LIMIT', PropertyMock(return_value=3)) + def test_pagination(self): + csv = rigobot_csv(10) + + limit = tasks.PANDAS_ROWS_LIMIT + tasks.PANDAS_ROWS_LIMIT = 3 + + provisioning_vendor = {'name': 'Rigobot'} + github_academy_users = [{ + 'username': username, + } for username in csv['github_username']] + github_academy_user_logs = [{ + 'storage_status': 'SYNCHED', + 'storage_action': 'ADD', + 'academy_user_id': n + 1, + } for n in range(10)] + model = self.bc.database.create(user=10, + app={'slug': 'rigobot'}, + first_party_credentials={'app': { + 'rigobot': 10 + }}, + github_academy_user=github_academy_users, + github_academy_user_log=github_academy_user_logs, + provisioning_vendor=provisioning_vendor) + + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + task_manager_id = get_last_task_manager_id(self.bc) + 1 + + slug = self.bc.fake.slug() + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ + provisioning_bill_data({ + 'hash': slug, + 'vendor_id': 1, + }), + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + 'vendor_id': + 1, + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': n + 1, + 'kind_id': n + 1, + 'hash': slug, + 'username': csv['github_username'][n], + 'processed_at': UTC_NOW, + 'status': 'PERSISTED', + }) for n in range(10) + ]) + + self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), + self.bc.format.to_dict(model.github_academy_user)) + + self.bc.check.calls(logging.Logger.info.call_args_list, + [call(f'Starting upload for hash {slug}') for _ in range(4)]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, [ + call(slug, page=1, task_manager_id=task_manager_id), + call(slug, page=2, task_manager_id=task_manager_id), + call(slug, page=3, task_manager_id=task_manager_id), + ]) + + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) + + tasks.PANDAS_ROWS_LIMIT = limit + + # Given: a csv with codespaces data and 10 User, 10 GithubAcademyUser, 10 GithubAcademyUserLog + # -> and 1 ProvisioningVendor of type codespaces + # When: all the data is correct, without ProfileAcademy + # Then: the task should create 1 bills and 10 activities per academy + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + def test_from_github_credentials__generate_anything__case1(self): + csv = rigobot_csv(10) + + github_academy_users = [{ + 'username': username, + } for username in csv['github_username']] + github_academy_user_logs = [{ + 'storage_status': 'SYNCHED', + 'storage_action': 'ADD', + 'academy_user_id': n + 1, + } for n in range(10)] + provisioning_vendor = {'name': 'Rigobot'} + + model = self.bc.database.create(user=10, + academy=3, + app={'slug': 'rigobot'}, + first_party_credentials={'app': { + 'rigobot': 10 + }}, + github_academy_user=github_academy_users, + github_academy_user_log=github_academy_user_logs, + provisioning_vendor=provisioning_vendor) + + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + slug = self.bc.fake.slug() + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ + provisioning_bill_data({ + 'id': 1, + 'academy_id': 1, + 'vendor_id': 1, + 'hash': slug, + }), + provisioning_bill_data({ + 'id': 2, + 'academy_id': 2, + 'vendor_id': 1, + 'hash': slug, + }), + provisioning_bill_data({ + 'id': 3, + 'academy_id': 3, + 'vendor_id': 1, + 'hash': slug, + }), + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + 'vendor_id': + 1, + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': n + 1, + 'kind_id': n + 1, + 'hash': slug, + 'username': csv['userName'][n], + 'processed_at': UTC_NOW, + 'status': 'PERSISTED', + }) for n in range(10) + ]) + + self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), + self.bc.format.to_dict(model.github_academy_user)) + + self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, []) + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) + + # Given: a csv with codespaces data and 10 User, 10 GithubAcademyUser, 10 GithubAcademyUserLog + # -> and 1 ProvisioningVendor of type codespaces + # When: all the data is correct, with ProfileAcademy + # Then: the task should create 1 bills and 10 activities per user's ProfileAcademy + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + @patch('breathecode.authenticate.signals.academy_invite_accepted.send', MagicMock()) + def test_from_github_credentials__generate_anything__case2(self): + csv = rigobot_csv(10) + + github_academy_users = [{ + 'username': username, + } for username in csv['github_username']] + github_academy_user_logs = [{ + 'storage_status': 'SYNCHED', + 'storage_action': 'ADD', + 'academy_user_id': n + 1, + } for n in range(10)] + provisioning_vendor = {'name': 'Rigobot'} + profile_academies = [] + + for user_n in range(10): + for academy_n in range(3): + profile_academies.append({ + 'academy_id': academy_n + 1, + 'user_id': user_n + 1, + 'status': 'ACTIVE', + }) + + credentials_github = [{ + 'username': csv['github_username'][n], + 'user_id': n + 1, + } for n in range(10)] + + model = self.bc.database.create(user=10, + credentials_github=credentials_github, + app={'slug': 'rigobot'}, + first_party_credentials={'app': { + 'rigobot': 10 + }}, + academy=3, + profile_academy=profile_academies, + github_academy_user=github_academy_users, + github_academy_user_log=github_academy_user_logs, + provisioning_vendor=provisioning_vendor) + + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + slug = self.bc.fake.slug() + + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ + provisioning_bill_data({ + 'id': 1, + 'academy_id': 1, + 'hash': slug, + 'vendor_id': 1, + }), + provisioning_bill_data({ + 'id': 2, + 'academy_id': 2, + 'hash': slug, + 'vendor_id': 1, + }), + provisioning_bill_data({ + 'id': 3, + 'academy_id': 3, + 'hash': slug, + 'vendor_id': 1, + }), + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + 'vendor_id': + 1, + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': n + 1, + 'kind_id': n + 1, + 'hash': slug, + 'username': csv['userName'][n], + 'processed_at': UTC_NOW, + 'status': 'PERSISTED', + }) for n in range(10) + ]) + + self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), + self.bc.format.to_dict(model.github_academy_user)) + + self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, []) + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) + + # Given: a csv with codespaces data and 10 User, 10 GithubAcademyUser, 10 GithubAcademyUserLog + # -> and 1 ProvisioningVendor of type codespaces + # When: all the data is correct, with ProfileAcademy + # Then: the task should create 1 bills and 10 activities per user's ProfileAcademy + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock(wraps=upload.delay)) + @patch('breathecode.provisioning.tasks.calculate_bill_amounts.delay', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.admissions.signals.student_edu_status_updated.send', MagicMock()) + @patch('breathecode.notify.utils.hook_manager.HookManagerClass.process_model_event', MagicMock()) + @patch('breathecode.authenticate.signals.academy_invite_accepted.send', MagicMock()) + def test_from_github_credentials__generate_anything__case3(self): + csv = rigobot_csv(10) + + github_academy_users = [{ + 'username': username, + } for username in csv['github_username']] + github_academy_user_logs = [{ + 'storage_status': 'SYNCHED', + 'storage_action': 'ADD', + 'academy_user_id': n + 1, + } for n in range(10)] + provisioning_vendor = {'name': 'Rigobot'} + profile_academies = [] + + for user_n in range(10): + for academy_n in range(3): + profile_academies.append({ + 'academy_id': academy_n + 1, + 'user_id': user_n + 1, + 'status': 'ACTIVE', + }) + + credentials_github = [{ + 'username': csv['github_username'][n], + 'user_id': n + 1, + } for n in range(10)] + + cohort_users = [{ + 'user_id': n + 1, + 'cohort_id': 1, + } for n in range(10)] + + cohort = { + 'academy_id': 1, + 'kickoff_date': timezone.now() + timedelta(days=1), + 'ending_date': timezone.now() - timedelta(days=1), + } + + model = self.bc.database.create(user=10, + app={'slug': 'rigobot'}, + first_party_credentials={'app': { + 'rigobot': 10 + }}, + credentials_github=credentials_github, + academy=3, + cohort=cohort, + cohort_user=cohort_users, + profile_academy=profile_academies, + github_academy_user=github_academy_users, + github_academy_user_log=github_academy_user_logs, + provisioning_vendor=provisioning_vendor) + + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + slug = self.bc.fake.slug() + + y = [[model.academy[RANDOM_ACADEMIES[x]]] for x in range(10)] + + with patch('random.choices', MagicMock(side_effect=y)): + with patch('breathecode.services.google_cloud.File.download', MagicMock(side_effect=csv_file_mock(csv))): + + upload(slug) + + academies = [] + + for n in RANDOM_ACADEMIES: + if n not in academies: + academies.append(n) + + academies = list(academies) + + self.assertEqual(self.bc.database.list_of('payments.Currency'), [currency_data()]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ + provisioning_bill_data({ + 'id': 1, + 'academy_id': 1, + 'hash': slug, + 'vendor_id': 1, + }), + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 1, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] == 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + provisioning_activity_price_data({ + 'currency_id': 1, + 'id': 2, + 'multiplier': 1.3, + 'price_per_unit': 0.04 if csv['pricing_type'][0] != 'OUTPUT' else 0.02, + 'unit_type': 'Tokens', + }), + ]) + output_was_first = csv['pricing_type'][0] == 'OUTPUT' + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionEvent'), [ + provisioning_activity_item_data({ + 'id': + n + 1, + 'price_id': (1 if output_was_first else 2) if csv['pricing_type'][n] == 'OUTPUT' else + (2 if output_was_first else 1), + 'quantity': + float(csv['total_tokens'][n]), + 'external_pk': + str(csv['consumption_item_id'][n]), + 'registered_at': + self.bc.datetime.from_iso_string(csv['consumption_period_start'][n]), + 'csv_row': + n, + 'vendor_id': + 1, + }) for n in range(10) + ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': + n + 1, + 'kind_id': + n + 1, + 'hash': + slug, + 'username': + csv['github_username'][n], + 'processed_at': + UTC_NOW, + 'status': + 'ERROR', + 'status_text': + 'We found activity from this user while he was studying at ' + f'one of your cohort {model.cohort.slug}', + }) for n in range(10) + ]) + + self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), + self.bc.format.to_dict(model.github_academy_user)) + + self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) + self.bc.check.calls(logging.Logger.error.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, []) + self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, [call(slug)]) From 9c95f01d25a913e195a5c651cf829f96e3703178 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 16:38:10 -0500 Subject: [PATCH 06/40] update deps --- Pipfile.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index bb4197d61..d5492d96e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1278,7 +1278,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", "version": "==3.0.3" }, "grpcio": { @@ -1638,11 +1638,11 @@ "requests" ], "hashes": [ - "sha256:61d05ddb69044958c041238d4deb593e2ce6c82e87dab5b0d2a905f51b5fa6e2", - "sha256:778db540c47dd580cbd52af63ab14549392d50407790a2f39939743d64cf7d65" + "sha256:175e677ffc069148ac3674d60482327a3c1ab0f00f049a6002970b64b861e9cc", + "sha256:9e92fb86d0efc11c2d19948ca961f74cf6576b0e1d942e381d219361ca83801a" ], "markers": "python_version >= '3.10'", - "version": "==1.2.2" + "version": "==1.2.3" }, "lxml": { "hashes": [ @@ -2827,7 +2827,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "python-frontmatter": { @@ -3192,7 +3192,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "sniffio": { @@ -4177,7 +4177,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", "version": "==3.0.3" }, "griffe": { @@ -4603,7 +4603,7 @@ "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" ], - "markers": "python_version > '3.0'", + "markers": "python_version >= '3.1'", "version": "==3.1.2" }, "pytest": { @@ -4655,7 +4655,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.0.post0" }, "pyyaml": { @@ -4847,7 +4847,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.16.0" }, "snowballstemmer": { From 639e863eee884eeecf55ce8ce1b1a8b2feb81d9f Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 16:42:04 -0500 Subject: [PATCH 07/40] change lock file --- Pipfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index d5492d96e..d2090a4fc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -3630,7 +3630,15 @@ "markers": "python_version >= '3.7'", "version": "==1.9.4" }, - "zope-interface": { + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, + "zope.interface": { "hashes": [ "sha256:21732994aa3ca43bbb6b36335c288023428a3c5b7322b637c7b0a03053937578", "sha256:36ee6e507a9fd4f1f0aab8e8dfc801d162e7211c27503cbfb47e1d558941a7fa", @@ -3664,14 +3672,6 @@ "markers": "python_version >= '3.7'", "version": "==6.4" }, - "zope.event": { - "hashes": [ - "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", - "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0" - }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", From a735555f9c259a081aee6fbf5af4be5ad1501a2a Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 16:52:43 -0500 Subject: [PATCH 08/40] add migration --- ...onsumptionevent_repository_url_and_more.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 breathecode/provisioning/migrations/0016_alter_provisioningconsumptionevent_repository_url_and_more.py diff --git a/breathecode/provisioning/migrations/0016_alter_provisioningconsumptionevent_repository_url_and_more.py b/breathecode/provisioning/migrations/0016_alter_provisioningconsumptionevent_repository_url_and_more.py new file mode 100644 index 000000000..0004573cb --- /dev/null +++ b/breathecode/provisioning/migrations/0016_alter_provisioningconsumptionevent_repository_url_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.6 on 2024-05-21 21:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('provisioning', '0015_auto_20230811_0645'), + ] + + operations = [ + migrations.AlterField( + model_name='provisioningconsumptionevent', + name='repository_url', + field=models.URLField(null=True), + ), + migrations.AlterField( + model_name='provisioningconsumptionevent', + name='task_associated_slug', + field=models.SlugField(help_text='What assignment was the the student trying to complete with this', + max_length=100, + null=True), + ), + ] From 3a6869e01562a03c17061263f3e96dfbbab12f73 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 16:58:04 -0500 Subject: [PATCH 09/40] changes in checks.yml --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b4fe0fc38..729ac21d6 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -318,7 +318,7 @@ jobs: run: pipenv run flake8 --select=B tests: - needs: [migrations, dependencies] + needs: [migrations] runs-on: ubuntu-latest steps: From 3f4ba7aa87a74d75f53dd8ecf1feb24d44f2050f Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 17:01:34 -0500 Subject: [PATCH 10/40] add new dep --- Pipfile | 1 + Pipfile.lock | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index c7e9a5677..178413e22 100644 --- a/Pipfile +++ b/Pipfile @@ -140,3 +140,4 @@ eventlet = "*" linked-services = {extras = ["django", "aiohttp", "requests"], version = "*"} celery-task-manager = {extras = ["django"], version = "*"} django-sql-explorer = {extras = ["xls"], version = "==4.0.2"} +contextlib2 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d2090a4fc..38e12d0aa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a60a902b22af50adb3b38a8edc92ac6c17f76389a0f83b2da557e9411e13f709" + "sha256": "66365e8a3a48d8089abc905c1d2d55c1eb29099bc909196dac809734b156084b" }, "pipfile-spec": 6, "requires": {}, @@ -597,6 +597,15 @@ "markers": "python_version >= '3.8'", "version": "==23.10.4" }, + "contextlib2": { + "hashes": [ + "sha256:3fbdb64466afd23abaf6c977627b75b6139a5a3e8ce38405c5b413aed7a0471f", + "sha256:ab1e2bfe1d01d968e1b7e8d9023bc51ef3509bba217bb730cee3827e1ee82869" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==21.6.0" + }, "coralogix-logger": { "hashes": [ "sha256:7baccb1054a282b681f821e487e6adfc1fc171b3b5d6d987c1c41edae00403ce", @@ -1278,7 +1287,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", + "markers": "python_version >= '3.7'", "version": "==3.0.3" }, "grpcio": { @@ -1532,6 +1541,7 @@ "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], + "markers": "python_version >= '3.5'", "version": "==3.7" }, "incremental": { @@ -3381,7 +3391,7 @@ "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], - "markers": "python_version >= '3.8'", + "markers": "python_version >= '3.7'", "version": "==4.11.0" }, "tzdata": { @@ -4177,7 +4187,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.11' and platform_python_implementation == 'CPython'", + "markers": "python_version >= '3.7'", "version": "==3.0.3" }, "griffe": { @@ -4267,6 +4277,7 @@ "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], + "markers": "python_version >= '3.5'", "version": "==3.7" }, "importlib-metadata": { From b25dc1cd8cac4efafb8081e976c0e9e954531de5 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 17:14:08 -0500 Subject: [PATCH 11/40] remove deps as a required step --- .github/workflows/checks.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 729ac21d6..c8b07c647 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,13 +13,12 @@ env: APP_URL: https://4geeks.com # cache ------------------------------------ -# | |> migrations -------- -# | |> migrations -------- -# |> undefined-and-unused-variables |> dependencies ------ -# |> bad-docstrings | -# |> code-complexity |> tests -------|> dockerhub -# |> naming-conventions |> linter -# |> unexpected-behaviors |> pages +# | |> dependencies +# |> undefined-and-unused-variables |> migrations +# |> bad-docstrings | +# |> code-complexity |> tests -------|> dockerhub +# |> naming-conventions |> linter +# |> unexpected-behaviors |> pages jobs: @@ -370,7 +369,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} linter: - needs: [migrations, dependencies] + needs: [migrations] runs-on: ubuntu-latest continue-on-error: true @@ -406,7 +405,7 @@ jobs: pipenv run format pages: - needs: [migrations, dependencies] + needs: [migrations] if: >- github.repository == 'breatheco-de/apiv2' && github.event_name == 'push' && From 6c5432d5ab8b66d1c50f97dc1508b6fb02793f9b Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 23 May 2024 01:23:48 -0500 Subject: [PATCH 12/40] add rigobot billing --- .../tests/urls/tests_user_me_task.py | 30 ---- breathecode/provisioning/actions.py | 98 +++++++------ .../provisioning/tests/tasks/tests_upload.py | 137 ++++++++---------- 3 files changed, 114 insertions(+), 151 deletions(-) diff --git a/breathecode/assignments/tests/urls/tests_user_me_task.py b/breathecode/assignments/tests/urls/tests_user_me_task.py index 64a7cdb00..feed85959 100644 --- a/breathecode/assignments/tests/urls/tests_user_me_task.py +++ b/breathecode/assignments/tests/urls/tests_user_me_task.py @@ -739,36 +739,6 @@ def test_post__no_required_fields(client: capy.Client, database: capy.Database): assert database.list_of('assignments.Task') == [] -@pytest.mark.parametrize('task_type', [ - 'PROJECT', - 'QUIZ', - 'LESSON', - 'EXERCISE', -]) -def test_post__no_cohort(client: capy.Client, database: capy.Database, fake: capy.Fake, task_type: str): - url = reverse_lazy('assignments:user_me_task') - - model = database.create(user=1) - client.force_authenticate(model.user) - - data = { - 'associated_slug': fake.slug(), - 'title': fake.name(), - 'task_type': task_type, - } - response = client.post(url, data, format='json') - - json = response.json() - expected = { - 'detail': 'Cohort is required.', - 'status_code': 400, - } - - assert json == expected - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert database.list_of('assignments.Task') == [] - - @pytest.mark.parametrize('task_type', [ 'PROJECT', 'QUIZ', diff --git a/breathecode/provisioning/actions.py b/breathecode/provisioning/actions.py index 4ccf509ee..e7d78df2e 100644 --- a/breathecode/provisioning/actions.py +++ b/breathecode/provisioning/actions.py @@ -3,7 +3,7 @@ import re from datetime import datetime from decimal import Decimal, localcontext -from typing import TypedDict +from typing import Optional, TypedDict import pytz from dateutil.relativedelta import relativedelta @@ -209,17 +209,23 @@ class ActivityContext(TypedDict): profile_academies: dict[str, QuerySet[ProfileAcademy]] -def handle_pending_github_user(organization: str, username: str) -> list[Academy]: +def handle_pending_github_user(organization: str, username: str, starts: Optional[datetime] = None) -> list[Academy]: orgs = AcademyAuthSettings.objects.filter(github_username__iexact=organization) orgs = [ x for x in orgs if GithubAcademyUser.objects.filter(academy=x.academy, storage_action='ADD', storage_status='SYNCHED').count() ] - if not orgs: + if not orgs and organization: logger.error(f'Organization {organization} not found') return [] + if not orgs and organization is None: + logger.error(f'Organization not provided, in this case, all organizations will be used') + + if not orgs: + orgs = AcademyAuthSettings.objects.filter() + user = None credentials = None @@ -229,6 +235,22 @@ def handle_pending_github_user(organization: str, username: str) -> list[Academy if credentials: user = credentials.user + if starts and organization is None: + new_orgs = [] + for org in orgs: + + has_any_cohort_user = CohortUser.objects.filter( + Q(cohort__ending_date__lte=starts) | Q(cohort__never_ends=True), + cohort__kickoff_date__gte=starts, + cohort__academy__id=org.academy.id, + user__credentialsgithub__username=username).order_by('-created_at').exists() + + if has_any_cohort_user: + new_orgs.append(org) + + if new_orgs: + org = new_orgs + for org in orgs: pending, created = GithubAcademyUser.objects.get_or_create(username=username, academy=org.academy, @@ -238,7 +260,7 @@ def handle_pending_github_user(organization: str, username: str) -> list[Academy 'storage_action': 'IGNORE', }) - if not created: + if not created and pending.storage_action not in ['ADD', 'DELETE']: pending.storage_status = 'PAYMENT_CONFLICT' pending.storage_action = 'IGNORE' pending.save() @@ -556,18 +578,18 @@ def add_rigobot_activity(context: ActivityContext, field: dict, position: int) - return user = get_user(app='rigobot', sub=field['user_id']) - if user is None: - user = User.objects.filter(email=field['email']).first() if user is None: + logger.error(f'User {field["user_id"]} not found') return if field['billing_status'] != 'OPEN': return github_academy_user_log = context['github_academy_user_logs'].get(user.id, None) + date = datetime.fromisoformat(field['consumption_period_start']) academies = [] - found_at_github_log = False + not_found = False if github_academy_user_log is None: # make a function that calculate the user activity in the academies by percentage @@ -576,46 +598,27 @@ def add_rigobot_activity(context: ActivityContext, field: dict, position: int) - | Q(valid_until__gte=context['limit'] - relativedelta(months=1, weeks=1)), created_at__lte=context['limit'], academy_user__user=user, + academy_user__username=field['github_username'], storage_status='SYNCHED', storage_action='ADD').order_by('-created_at') context['github_academy_user_logs'][user.id] = github_academy_user_log if github_academy_user_log: - found_at_github_log = True academies = [x.academy_user.academy for x in github_academy_user_log] - # not implemented yet - # not_found = bool(academies) - date = datetime.fromisoformat(field['consumption_period_start']) - end = datetime.fromisoformat(field['consumption_period_end']) - if not academies: - profile_academies = context['profile_academies'].get(field['github_username'], None) - if profile_academies is None: - profile_academies = ProfileAcademy.objects.filter( - user__credentialsgithub__username=field['github_username'], status='ACTIVE') - - context['profile_academies'][field['github_username']] = profile_academies - - if profile_academies: - academies = sorted(list({profile.academy for profile in profile_academies}), key=lambda x: x.id) - - if not found_at_github_log and len(academies) > 1: - cohort_users = CohortUser.objects.filter( - Q(cohort__ending_date__lte=end) | Q(cohort__never_ends=True), - cohort__kickoff_date__gte=date, - user__credentialsgithub__username=field['github_username']).order_by('-created_at') + not_found = True + github_academy_users = GithubAcademyUser.objects.filter(username=field['github_username'], + storage_status='PAYMENT_CONFLICT', + storage_action='IGNORE') - if cohort_users: - academies = sorted(list({cohort_user.cohort.academy for cohort_user in cohort_users}), key=lambda x: x.id) + academies = [x.academy for x in github_academy_users] if not academies: - if 'academies' not in context: - context['academies'] = Academy.objects.filter() - academies = list(context['academies']) + academies = handle_pending_github_user(None, field['github_username'], date) - if not found_at_github_log and academies: + if not_found is False and academies: academies = random.choices(academies, k=1) logs = {} @@ -662,22 +665,23 @@ def add_rigobot_activity(context: ActivityContext, field: dict, position: int) - ignores.append( f'User {field["github_username"]} was deleted from the academy during this event at {date}') - if not provisioning_bills: - for academy_id in logs.keys(): - cohort_user = CohortUser.objects.filter( - Q(cohort__ending_date__lte=date) | Q(cohort__never_ends=True), - cohort__kickoff_date__gte=date, - cohort__academy__id=academy_id, - user__credentialsgithub__username=field['github_username']).order_by('-created_at').first() + # disabled because rigobot doesn't have the organization configured yet. + # if not provisioning_bills: + # for academy_id in logs.keys(): + # cohort_user = CohortUser.objects.filter( + # Q(cohort__ending_date__lte=date) | Q(cohort__never_ends=True), + # cohort__kickoff_date__gte=date, + # cohort__academy__id=academy_id, + # user__credentialsgithub__username=field['github_username']).order_by('-created_at').first() - if cohort_user: - errors.append('We found activity from this user while he was studying at one of your cohort ' - f'{cohort_user.cohort.slug}') + # if cohort_user: + # errors.append('We found activity from this user while he was studying at one of your cohort ' + # f'{cohort_user.cohort.slug}') # not implemented yet - # if not_found: - # errors.append(f'We could not find enough information about {field["github_username"]}, mark this user user as ' - # 'deleted if you don\'t recognize it') + if not_found: + errors.append(f'We could not find enough information about {field["github_username"]}, mark this user user as ' + 'deleted if you don\'t recognize it') s_slug = f'{field["purpose_slug"] or "no-provided"}--{field["pricing_type"].lower()}--{field["model"].lower()}' s_name = f'{field["purpose"]} (type: {field["pricing_type"]}, model: {field["model"]})' diff --git a/breathecode/provisioning/tests/tasks/tests_upload.py b/breathecode/provisioning/tests/tasks/tests_upload.py index 0e4738d71..af05b61ab 100644 --- a/breathecode/provisioning/tests/tasks/tests_upload.py +++ b/breathecode/provisioning/tests/tasks/tests_upload.py @@ -2259,35 +2259,34 @@ def test_users_not_found(self): n, }) for n in range(10) ]) - self.assertEqual( - self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), - [ - provisioning_activity_data({ - 'id': - n + 1, - 'kind_id': - n + 1, - 'hash': - slug, - 'username': - csv['github_username'][n], - 'processed_at': - UTC_NOW, - 'status': - 'ERROR', - 'status_text': - ', '.join([ - 'Provisioning vendor Rigobot not found', - # not implemented yet, - # f"We could not find enough information about {csv['github_username'][n]}, mark this user user " - # "as deleted if you don't recognize it", - ]), - }) for n in range(10) - ]) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ + provisioning_activity_data({ + 'id': + n + 1, + 'kind_id': + n + 1, + 'hash': + slug, + 'username': + csv['github_username'][n], + 'processed_at': + UTC_NOW, + 'status': + 'ERROR', + 'status_text': + ', '.join([ + 'Provisioning vendor Rigobot not found', + f"We could not find enough information about {csv['github_username'][n]}, mark this user user " + "as deleted if you don't recognize it", + ]), + }) for n in range(10) + ]) self.assertEqual(self.bc.database.list_of('authenticate.GithubAcademyUser'), []) self.bc.check.calls(logging.Logger.info.call_args_list, [call(f'Starting upload for hash {slug}')]) - self.bc.check.calls(logging.Logger.error.call_args_list, []) + self.bc.check.calls( + logging.Logger.error.call_args_list, + [call('Organization not provided, in this case, all organizations will be used') for _ in range(10)]) self.bc.check.calls(tasks.upload.delay.call_args_list, []) self.bc.check.calls(tasks.calculate_bill_amounts.delay.call_args_list, []) @@ -2464,11 +2463,12 @@ def test_from_github_credentials__generate_anything(self): }), ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ - provisioning_activity_kind_data({ - 'id': n + 1, - 'product_name': csv['kind'][n], - 'sku': str(csv['kind'][n]), - }) for n in range(10) + provisioning_activity_kind_data( + { + 'id': n + 1, + 'product_name': f'{csv["purpose"][n]} (type: {csv["pricing_type"][n]}, model: {csv["model"][n]})', + 'sku': f'{csv["purpose_slug"][n]}--{csv["pricing_type"][n].lower()}--{csv["model"][n].lower()}', + }) for n in range(10) ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningPrice'), [ provisioning_activity_price_data({ @@ -2510,7 +2510,7 @@ def test_from_github_credentials__generate_anything(self): 'id': n + 1, 'kind_id': n + 1, 'hash': slug, - 'username': csv['userName'][n], + 'username': csv['github_username'][n], 'processed_at': UTC_NOW, 'status': 'PERSISTED', }) for n in range(10) @@ -2698,6 +2698,9 @@ def test_from_github_credentials__generate_anything__case1(self): provisioning_vendor = {'name': 'Rigobot'} model = self.bc.database.create(user=10, + academy_auth_settings=[{ + 'academy_id': n + 1 + } for n in range(3)], academy=3, app={'slug': 'rigobot'}, first_party_credentials={'app': { @@ -2723,18 +2726,6 @@ def test_from_github_credentials__generate_anything__case1(self): 'vendor_id': 1, 'hash': slug, }), - provisioning_bill_data({ - 'id': 2, - 'academy_id': 2, - 'vendor_id': 1, - 'hash': slug, - }), - provisioning_bill_data({ - 'id': 3, - 'academy_id': 3, - 'vendor_id': 1, - 'hash': slug, - }), ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ provisioning_activity_kind_data( @@ -2784,7 +2775,7 @@ def test_from_github_credentials__generate_anything__case1(self): 'id': n + 1, 'kind_id': n + 1, 'hash': slug, - 'username': csv['userName'][n], + 'username': csv['github_username'][n], 'processed_at': UTC_NOW, 'status': 'PERSISTED', }) for n in range(10) @@ -2851,6 +2842,9 @@ def test_from_github_credentials__generate_anything__case2(self): } for n in range(10)] model = self.bc.database.create(user=10, + academy_auth_settings=[{ + 'academy_id': n + 1 + } for n in range(3)], credentials_github=credentials_github, app={'slug': 'rigobot'}, first_party_credentials={'app': { @@ -2879,18 +2873,6 @@ def test_from_github_credentials__generate_anything__case2(self): 'hash': slug, 'vendor_id': 1, }), - provisioning_bill_data({ - 'id': 2, - 'academy_id': 2, - 'hash': slug, - 'vendor_id': 1, - }), - provisioning_bill_data({ - 'id': 3, - 'academy_id': 3, - 'hash': slug, - 'vendor_id': 1, - }), ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningConsumptionKind'), [ provisioning_activity_kind_data( @@ -2940,7 +2922,7 @@ def test_from_github_credentials__generate_anything__case2(self): 'id': n + 1, 'kind_id': n + 1, 'hash': slug, - 'username': csv['userName'][n], + 'username': csv['github_username'][n], 'processed_at': UTC_NOW, 'status': 'PERSISTED', }) for n in range(10) @@ -3018,6 +3000,9 @@ def test_from_github_credentials__generate_anything__case3(self): } model = self.bc.database.create(user=10, + academy_auth_settings=[{ + 'academy_id': n + 1 + } for n in range(3)], app={'slug': 'rigobot'}, first_party_credentials={'app': { 'rigobot': 10 @@ -3055,7 +3040,19 @@ def test_from_github_credentials__generate_anything__case3(self): self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), [ provisioning_bill_data({ 'id': 1, - 'academy_id': 1, + 'academy_id': RANDOM_ACADEMIES[0] + 1, + 'hash': slug, + 'vendor_id': 1, + }), + provisioning_bill_data({ + 'id': 2, + 'academy_id': RANDOM_ACADEMIES[1] + 1, + 'hash': slug, + 'vendor_id': 1, + }), + provisioning_bill_data({ + 'id': 3, + 'academy_id': RANDOM_ACADEMIES[2] + 1, 'hash': slug, 'vendor_id': 1, }), @@ -3105,21 +3102,13 @@ def test_from_github_credentials__generate_anything__case3(self): ]) self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), [ provisioning_activity_data({ - 'id': - n + 1, - 'kind_id': - n + 1, - 'hash': - slug, - 'username': - csv['github_username'][n], - 'processed_at': - UTC_NOW, - 'status': - 'ERROR', - 'status_text': - 'We found activity from this user while he was studying at ' - f'one of your cohort {model.cohort.slug}', + 'id': n + 1, + 'kind_id': n + 1, + 'hash': slug, + 'username': csv['github_username'][n], + 'processed_at': UTC_NOW, + 'status': 'PERSISTED', + 'status_text': '', }) for n in range(10) ]) From ff6df62d140eab1d65e84553b86286559c1dc919 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 15:08:46 -0400 Subject: [PATCH 13/40] Update choose_vendor.html --- breathecode/provisioning/templates/choose_vendor.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/breathecode/provisioning/templates/choose_vendor.html b/breathecode/provisioning/templates/choose_vendor.html index 61f5c884b..261e3f1b7 100644 --- a/breathecode/provisioning/templates/choose_vendor.html +++ b/breathecode/provisioning/templates/choose_vendor.html @@ -38,8 +38,7 @@
-

No configuration.

-

Straight to coding learning instead!

+

Start working immediately

Now, you can instantly open this project on VSCode, with no installations and instant feedback from Rigobot (our internally developed AI). Choose one of the following vendors:

From a53a3b62ca660e9af27dd19abacea307e4a31ff3 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 19:59:12 +0000 Subject: [PATCH 14/40] better error reporting --- breathecode/monitoring/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/monitoring/decorators.py b/breathecode/monitoring/decorators.py index 93c8c7e79..5220d4467 100644 --- a/breathecode/monitoring/decorators.py +++ b/breathecode/monitoring/decorators.py @@ -33,7 +33,7 @@ def __call__(self, *args, **kwargs): webhook = _webhook webhook.status = 'DONE' else: - raise Exception('Error while running async webhook task') + raise Exception('Error while running async webhook task: type != ' + str(type(_webhook))) except Exception as ex: webhook.status = 'ERROR' webhook.status_text = str(ex)[:255] From 2a6fed0656303c74e059c17c37ed4ff9170559bf Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 20:25:15 +0000 Subject: [PATCH 15/40] fixed task that syncs github repos --- breathecode/registry/tasks.py | 8 +++----- breathecode/utils/decorators/task.py | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 6c5c39137..94a58ea85 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -431,18 +431,16 @@ def async_resize_asset_thumbnail(media_id: int, width: Optional[int] = 0, height resolution.save() -@shared_task(bind=True, base=WebhookTask, priority=TaskPriority.ACADEMY.value) +@task(bind=True, base=WebhookTask, priority=TaskPriority.CONTENT.value) def async_synchonize_repository_content(self, webhook): logger.debug('async_synchonize_repository_content') payload = webhook.get_payload() if 'commits' not in payload: - logger.debug('No commits found on the push object') - return False + raise AbortTask('No commits found on the push object') if 'repository' not in payload: - logger.debug('Missing repository information') - return False + raise AbortTask('Missing repository information') base_repo_url = payload['repository']['url'] default_branch = payload['repository']['default_branch'] diff --git a/breathecode/utils/decorators/task.py b/breathecode/utils/decorators/task.py index 1c3f4460c..3d48a3512 100644 --- a/breathecode/utils/decorators/task.py +++ b/breathecode/utils/decorators/task.py @@ -19,6 +19,7 @@ class TaskPriority(Enum): NOTIFICATION = 1 # non realtime notifications MONITORING = 2 # monitoring tasks ACTIVITY = 2 # user activity + CONTENT = 2 # related to the registry BILL = 2 # postpaid billing ASSESSMENT = 2 # user assessment CACHE = 3 # cache From 17d9ab8b85973b3fc5e23cf153d4b2d58e7a9bf9 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 20:53:18 +0000 Subject: [PATCH 16/40] better error reporting --- breathecode/registry/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 94a58ea85..9348bf625 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -431,7 +431,7 @@ def async_resize_asset_thumbnail(media_id: int, width: Optional[int] = 0, height resolution.save() -@task(bind=True, base=WebhookTask, priority=TaskPriority.CONTENT.value) +@shared_task(bind=True, base=WebhookTask, priority=TaskPriority.CONTENT.value) def async_synchonize_repository_content(self, webhook): logger.debug('async_synchonize_repository_content') From 23e10b8097d0e9734620297990bba1fc656c591b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 21:07:21 +0000 Subject: [PATCH 17/40] fixing asset sync when nested payload property exists --- breathecode/registry/models.py | 8 ++++++-- breathecode/registry/serializers.py | 1 + breathecode/registry/tasks.py | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index edb386986..902c576e9 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -337,11 +337,15 @@ def __init__(self, *args, **kwargs): 'External assets will open in a new window, they are not built using breathecode or learnpack tecnology', db_index=True) - interactive = models.BooleanField(default=False, db_index=True) + enable_table_of_content = models.BooleanField( + default=True, help_text='If true, it shows a tabled on contents on top of the lesson') + interactive = models.BooleanField(default=False, db_index=True, help_text='If true, it means is learnpack enabled') with_solutions = models.BooleanField(default=False, db_index=True) with_video = models.BooleanField(default=False, db_index=True) graded = models.BooleanField(default=False, db_index=True) - gitpod = models.BooleanField(default=False) + gitpod = models.BooleanField( + default=False, + help_text='If true, it means it can be opened on cloud provisioning vendors like Gitpod or Codespaces') duration = models.IntegerField(null=True, blank=True, default=None, help_text='In hours') difficulty = models.CharField(max_length=20, choices=DIFFICULTY, default=None, null=True, blank=True) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index 4a47546a5..a641125bb 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -324,6 +324,7 @@ class AssetBigSerializer(AssetMidSerializer): last_synch_at = serpy.Field() status_text = serpy.Field() published_at = serpy.Field() + enable_table_of_content = serpy.Field() delivery_instructions = serpy.Field() delivery_formats = serpy.Field() diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 9348bf625..0e103bbdb 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -436,6 +436,10 @@ def async_synchonize_repository_content(self, webhook): logger.debug('async_synchonize_repository_content') payload = webhook.get_payload() + + # some times the json contains a nested payload property + if 'payload' in payload: payload = payload['payload'] + if 'commits' not in payload: raise AbortTask('No commits found on the push object') From 11ebd22034cfac4e13be34184ee75877e180801a Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 21:24:45 +0000 Subject: [PATCH 18/40] fixing asset sync when nested payload property exists --- ..._of_content_alter_asset_gitpod_and_more.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 breathecode/registry/migrations/0042_asset_enable_table_of_content_alter_asset_gitpod_and_more.py diff --git a/breathecode/registry/migrations/0042_asset_enable_table_of_content_alter_asset_gitpod_and_more.py b/breathecode/registry/migrations/0042_asset_enable_table_of_content_alter_asset_gitpod_and_more.py new file mode 100644 index 000000000..12d4fccc2 --- /dev/null +++ b/breathecode/registry/migrations/0042_asset_enable_table_of_content_alter_asset_gitpod_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.3 on 2024-05-28 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registry', '0041_asset_is_auto_subscribed'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='enable_table_of_content', + field=models.BooleanField(default=True, + help_text='If true, it shows a tabled on contents on top of the lesson'), + ), + migrations.AlterField( + model_name='asset', + name='gitpod', + field=models.BooleanField( + default=False, + help_text='If true, it means it can be opened on cloud provisioning vendors like Gitpod or Codespaces'), + ), + migrations.AlterField( + model_name='asset', + name='interactive', + field=models.BooleanField(db_index=True, default=False, help_text='If true, it means is learnpack enabled'), + ), + ] From 16d38f263090630c55525768652fef2ae21f76b8 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 21:31:13 +0000 Subject: [PATCH 19/40] printing error happening on webhook task --- breathecode/monitoring/decorators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breathecode/monitoring/decorators.py b/breathecode/monitoring/decorators.py index 5220d4467..b0a1c1596 100644 --- a/breathecode/monitoring/decorators.py +++ b/breathecode/monitoring/decorators.py @@ -36,6 +36,8 @@ def __call__(self, *args, **kwargs): raise Exception('Error while running async webhook task: type != ' + str(type(_webhook))) except Exception as ex: webhook.status = 'ERROR' + print('Error ejejeje') + print(ex) webhook.status_text = str(ex)[:255] logger.debug(ex) From 45ff2094ab79b0ba6636ce8773dee937d9ea90a9 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 21:39:46 +0000 Subject: [PATCH 20/40] printing error happening on webhook task --- breathecode/monitoring/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/monitoring/decorators.py b/breathecode/monitoring/decorators.py index b0a1c1596..4373ce511 100644 --- a/breathecode/monitoring/decorators.py +++ b/breathecode/monitoring/decorators.py @@ -37,7 +37,7 @@ def __call__(self, *args, **kwargs): except Exception as ex: webhook.status = 'ERROR' print('Error ejejeje') - print(ex) + print(str(ex)) webhook.status_text = str(ex)[:255] logger.debug(ex) From ee20b20707defa108212f32e31cb9da5d8d29c49 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 21:47:24 +0000 Subject: [PATCH 21/40] printing error happening on webhook task --- breathecode/monitoring/decorators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/breathecode/monitoring/decorators.py b/breathecode/monitoring/decorators.py index 4373ce511..ec859fadc 100644 --- a/breathecode/monitoring/decorators.py +++ b/breathecode/monitoring/decorators.py @@ -1,4 +1,5 @@ import logging +import traceback from celery import Task from .models import RepositoryWebhook from django.utils import timezone @@ -37,9 +38,9 @@ def __call__(self, *args, **kwargs): except Exception as ex: webhook.status = 'ERROR' print('Error ejejeje') - print(str(ex)) + traceback.print_exc() webhook.status_text = str(ex)[:255] - logger.debug(ex) + logger.exception(ex) webhook.run_at = timezone.now() if webhook.status_text == self.pending_status: From 9118fcb6b8c85415fdcb3c5c110c41c07a87bb99 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 22:20:43 +0000 Subject: [PATCH 22/40] printing error happening on webhook task --- breathecode/monitoring/decorators.py | 2 -- breathecode/registry/tasks.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/monitoring/decorators.py b/breathecode/monitoring/decorators.py index ec859fadc..1c2e0aa34 100644 --- a/breathecode/monitoring/decorators.py +++ b/breathecode/monitoring/decorators.py @@ -37,8 +37,6 @@ def __call__(self, *args, **kwargs): raise Exception('Error while running async webhook task: type != ' + str(type(_webhook))) except Exception as ex: webhook.status = 'ERROR' - print('Error ejejeje') - traceback.print_exc() webhook.status_text = str(ex)[:255] logger.exception(ex) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 0e103bbdb..7c7235335 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -445,6 +445,8 @@ def async_synchonize_repository_content(self, webhook): if 'repository' not in payload: raise AbortTask('Missing repository information') + elif 'url' not in payload['repository']: + raise AbortTask('Repository payload is invalid, expecting an object with "url" key') base_repo_url = payload['repository']['url'] default_branch = payload['repository']['default_branch'] From 7cfb649f5b2ab85e7dd6e3e1bf0a82178db94168 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 18:56:01 -0400 Subject: [PATCH 23/40] Update subscribe_asset_repos.py --- .../registry/management/commands/subscribe_asset_repos.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/breathecode/registry/management/commands/subscribe_asset_repos.py b/breathecode/registry/management/commands/subscribe_asset_repos.py index aecf64435..3eff43bb9 100644 --- a/breathecode/registry/management/commands/subscribe_asset_repos.py +++ b/breathecode/registry/management/commands/subscribe_asset_repos.py @@ -21,9 +21,6 @@ def handle(self, *args, **options): repo_url = f'https://github.com/{username}/{repo_name}' subs = RepositorySubscription.objects.filter(repository=repo_url).first() if subs is None: - if not a.is_auto_subscribed: - logger.debug(f'Skipping asset {a.slug}, auto_subscribe is deactivated') - continue if academy_id not in settings: settings[academy_id] = AcademyAuthSettings.objects.filter(academy__id=a.academy.id).first() @@ -49,3 +46,8 @@ def handle(self, *args, **options): subs.save() else: logger.debug(f'Already subscribed to asset {a.slug} thru repo {repo_url}') + + if not a.is_auto_subscribed: + logger.debug(f'Disabling asset {a.slug}, subscription because auto_subscribe is deactivated') + subs.status = 'DISABLED' + continue From 3214a2313108fc46c50d22fb41320a08010adf9d Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 29 May 2024 10:29:10 -0400 Subject: [PATCH 24/40] Update actions.py --- breathecode/registry/actions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 9e66babee..f88a26239 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -654,6 +654,10 @@ def process_asset_config(asset, config): if not config: raise Exception('No configuration json found') + + if asset.asset_type in ["QUIZ"]: + raise Exception('Can only process exercise and project config objects') + # only replace title and description of English language if 'title' in config: if isinstance(config['title'], str): From 452c4dccda82c7ff69a08e275ef07cddcbfa4a1e Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 29 May 2024 11:09:36 -0400 Subject: [PATCH 25/40] Update serializers.py --- breathecode/registry/serializers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index a641125bb..566988a9d 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -363,6 +363,8 @@ def get_technologies(self, obj): # the admin.4geeks.com will use another one class AssetBigAndTechnologyPublishedSerializer(AssetBigSerializer): + assessment = AssessmentSmallSerializer(required=False) + technologies = serpy.MethodField() translations = serpy.MethodField() From bc26a1b4d90f4b79260872ab79cc24f428b74966 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 29 May 2024 15:51:08 -0400 Subject: [PATCH 26/40] Update views.py --- breathecode/registry/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index cc089947e..4ce84fd67 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -905,8 +905,10 @@ def get(self, request, asset_slug=None, academy_id=None): else: lookup['slug'] = param - if 'language' in self.request.GET: + if 'language' in self.request.GET or 'lang' in self.request.GET: param = self.request.GET.get('language') + if not param: param = self.request.GET.get('lang') + if param == 'en': param = 'us' lookup['lang'] = param From 834773ff1605ea2e6ee285ac90c0a19591fba070 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 29 May 2024 21:11:55 -0500 Subject: [PATCH 27/40] add support to rigobot billing in the upload endpoint --- breathecode/provisioning/tasks.py | 2 +- .../tests/urls/tests_admin_upload.py | 268 ++++++++++++++++-- breathecode/provisioning/views.py | 36 ++- 3 files changed, 286 insertions(+), 20 deletions(-) diff --git a/breathecode/provisioning/tasks.py b/breathecode/provisioning/tasks.py index 3101bdc39..46d1ac8d6 100644 --- a/breathecode/provisioning/tasks.py +++ b/breathecode/provisioning/tasks.py @@ -207,7 +207,7 @@ def upload(hash: str, *, page: int = 0, force: bool = False, task_manager_id: in 'organization', 'consumption_period_id', 'consumption_period_start', 'consumption_period_end', 'billing_status', 'total_spent_period', 'consumption_item_id', 'user_id', 'email', 'consumption_type', 'pricing_type', 'total_spent', 'total_tokens', 'model', 'purpose_id', 'purpose_slug', 'purpose', - 'created_at' + 'created_at', 'github_username' ] if not handler and len(df.keys().intersection(fields)) == len(fields): handler = actions.add_rigobot_activity diff --git a/breathecode/provisioning/tests/urls/tests_admin_upload.py b/breathecode/provisioning/tests/urls/tests_admin_upload.py index b4b77fd8c..dd47598b5 100644 --- a/breathecode/provisioning/tests/urls/tests_admin_upload.py +++ b/breathecode/provisioning/tests/urls/tests_admin_upload.py @@ -1,23 +1,95 @@ """ Test /v1/marketing/upload """ -import csv -import json +import hashlib +import os import random +import re import tempfile -import os -import hashlib -from unittest.mock import MagicMock, Mock, PropertyMock, call, patch +from unittest.mock import MagicMock, PropertyMock, call, patch + +import pandas as pd from django.urls.base import reverse_lazy +from django.utils import timezone +from faker import Faker +from pytz import UTC from rest_framework import status -from ..mixins import ProvisioningTestCase -from breathecode.marketing.views import MIME_ALLOW -import pandas as pd -from django.utils import timezone, dateparse + from breathecode.provisioning import tasks +from ..mixins import ProvisioningTestCase + UTC_NOW = timezone.now() +fake = Faker() + + +def datetime_to_iso(date) -> str: + return re.sub(r'\+00:00$', 'Z', date.replace(tzinfo=UTC).isoformat()) + + +def rigobot_csv(lines=1, data={}): + organizations = ['4Geeks' for _ in range(lines)] + consumption_period_ids = [random.randint(1, 10) for _ in range(lines)] + times = [datetime_to_iso(timezone.now()) for _ in range(lines)] + billing_statuses = ['OPEN' for _ in range(lines)] + total_spent_periods = [(random.random() * 30) + 0.01 for _ in range(lines)] + consumption_item_ids = [random.randint(1, 10) for _ in range(lines)] + user_ids = [10 for _ in range(lines)] + emails = [fake.email() for _ in range(lines)] + consumption_types = ['MESSAGE' for _ in range(lines)] + pricing_types = [random.choice(['INPUT', 'OUTPUT']) for _ in range(lines)] + total_tokens = [random.randint(1, 100) for _ in range(lines)] + total_spents = [] + res = [] + for i in range(lines): + total_token = total_tokens[i] + pricing_type = pricing_types[i] + price = 0.04 if pricing_type == 'OUTPUT' else 0.02 + total_spent = price * total_token + while total_spent in res: + total_tokens[i] = random.randint(1, 100) + total_token = total_tokens[i] + total_spent = price * total_token + + total_spents.append(total_spent) + res.append(total_spent) + + models = [ + random.choice(['gpt-4-turbo', 'gpt-4', 'gpt-4-turbo', 'gpt-4o', 'gpt-3.5-turbo', 'gpt-3.5']) + for _ in range(lines) + ] + purpose_ids = [random.randint(1, 10) for _ in range(lines)] + purpose_slugs = [fake.slug() for _ in range(lines)] + purposes = [' '.join(fake.words()) for _ in range(lines)] + github_usernames = [fake.user_name() for _ in range(lines)] + + created_ats = [datetime_to_iso(timezone.now()) for _ in range(lines)] + + # dictionary of lists + return { + 'organization': organizations, + 'consumption_period_id': consumption_period_ids, + 'consumption_period_start': times, + 'consumption_period_end': times, + 'billing_status': billing_statuses, + 'total_spent_period': total_spent_periods, + 'consumption_item_id': consumption_item_ids, + 'user_id': user_ids, + 'email': emails, + 'consumption_type': consumption_types, + 'pricing_type': pricing_types, + 'total_spent': total_spents, + 'total_tokens': total_tokens, + 'model': models, + 'purpose_id': purpose_ids, + 'purpose_slug': purpose_slugs, + 'purpose': purposes, + 'created_at': created_ats, + 'github_username': github_usernames, + **data, + } + class MarketingTestSuite(ProvisioningTestCase): """Test /answer""" @@ -33,7 +105,7 @@ def tearDown(self): # When: no auth # Then: should return 401 def test_upload_without_auth(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(content_disposition='attachment; filename="filename.csv"') @@ -49,7 +121,7 @@ def test_upload_without_auth(self): # When: auth and no capability # Then: should return 403 def test_upload_without_capability(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(academy=1, content_disposition='attachment; filename="filename.csv"') @@ -77,7 +149,7 @@ def test_upload_without_capability(self): url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), create=True) def test_no_files(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -113,8 +185,8 @@ def test_no_files(self): create=True) @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_bad_file_type(self): - from breathecode.services.google_cloud import Storage, File from breathecode.marketing.tasks import create_form_entry + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -173,8 +245,8 @@ def test_bad_file_type(self): create=True) @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_bad_format(self): - from breathecode.services.google_cloud import Storage, File from breathecode.marketing.tasks import create_form_entry + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -258,7 +330,7 @@ def test_bad_format(self): @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) @patch('breathecode.provisioning.tasks.upload.delay', MagicMock()) def test_codespaces(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -361,7 +433,7 @@ def test_codespaces(self): @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) @patch('breathecode.provisioning.tasks.upload.delay', MagicMock()) def test_codespaces__update(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -457,7 +529,7 @@ def test_codespaces__update(self): @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) @patch('breathecode.provisioning.tasks.upload.delay', MagicMock()) def test_gitpod(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -558,7 +630,7 @@ def test_gitpod(self): @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) @patch('breathecode.provisioning.tasks.upload.delay', MagicMock()) def test_gitpod__update(self): - from breathecode.services.google_cloud import Storage, File + from breathecode.services.google_cloud import File, Storage self.headers(academy=1) @@ -632,3 +704,163 @@ def test_gitpod__update(self): self.assertEqual(File.upload.call_args_list, []) self.assertEqual(File.url.call_args_list, []) self.bc.check.calls(tasks.upload.delay.call_args_list, [call(hash, total_pages=1)]) + + # When: auth and file with gitpod format + # Then: should return a 201 + @patch('breathecode.marketing.tasks.create_form_entry.delay', MagicMock()) + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=False), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock()) + def test_rigobot(self): + from breathecode.services.google_cloud import File, Storage + + self.headers(academy=1) + + model = self.generate_models(authenticate=True, + group=1, + permission={'codename': 'upload_provisioning_activity'}) + + url = reverse_lazy('provisioning:admin_upload') + + file = tempfile.NamedTemporaryFile(suffix='.csv', delete=False, mode='w+') + + # dictionary of lists + obj = rigobot_csv(lines=3, data={}) + + df = pd.DataFrame.from_dict(obj) + self.file_name = file.name + + df.to_csv(file.name) + + with open(file.name, 'rb') as data: + hash = hashlib.sha256(data.read()).hexdigest() + + with open(file.name, 'rb') as data: + response = self.client.put(url, {'name': file.name, 'file': data}) + j = response.json() + + expected = { + 'failure': [], + 'success': [ + { + 'resources': [ + { + 'display_field': 'index', + 'display_value': 1, + 'pk': hash, + }, + ], + 'status_code': 201, + }, + ], + } + + self.assertEqual(j, expected) + self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) + + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), []) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), []) + + self.assertEqual(Storage.__init__.call_args_list, [call()]) + self.assertEqual(File.__init__.call_args_list, [ + call(Storage().client.bucket('bucket'), hash), + ]) + + args, kwargs = File.upload.call_args_list[0] + + self.assertEqual(len(File.upload.call_args_list), 1) + self.assertEqual(len(args), 1) + + self.assertEqual(args[0].name, os.path.basename(file.name)) + self.assertEqual(kwargs, {'content_type': 'text/csv'}) + + self.assertEqual(File.url.call_args_list, []) + + self.bc.check.calls(tasks.upload.delay.call_args_list, [call(hash, total_pages=1)]) + + # When: auth and file with gitpod format, file exists + # Then: should return a 200 + @patch('breathecode.marketing.tasks.create_form_entry.delay', MagicMock()) + @patch.multiple('breathecode.services.google_cloud.Storage', + __init__=MagicMock(return_value=None), + client=PropertyMock(), + create=True) + @patch.multiple('breathecode.services.google_cloud.File', + __init__=MagicMock(return_value=None), + bucket=PropertyMock(), + file_name=PropertyMock(), + upload=MagicMock(), + exists=MagicMock(return_value=True), + url=MagicMock(return_value='https://storage.cloud.google.com/media-breathecode/hardcoded_url'), + create=True) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + @patch('breathecode.provisioning.tasks.upload.delay', MagicMock()) + def test_rigobot__update(self): + from breathecode.services.google_cloud import File, Storage + + self.headers(academy=1) + + model = self.generate_models(authenticate=True, + group=1, + permission={'codename': 'upload_provisioning_activity'}) + + url = reverse_lazy('provisioning:admin_upload') + + file = tempfile.NamedTemporaryFile(suffix='.csv', delete=False, mode='w+') + + # dictionary of lists + obj = rigobot_csv(lines=3, data={}) + + df = pd.DataFrame.from_dict(obj) + self.file_name = file.name + + df.to_csv(file.name) + + with open(file.name, 'rb') as data: + hash = hashlib.sha256(data.read()).hexdigest() + + with open(file.name, 'rb') as data: + response = self.client.put(url, {'name': file.name, 'file': data}) + j = response.json() + + expected = { + 'failure': [], + 'success': [ + { + 'resources': [ + { + 'display_field': 'index', + 'display_value': 1, + 'pk': hash, + }, + ], + 'status_code': 200, + }, + ], + } + + self.assertEqual(j, expected) + self.assertEqual(response.status_code, status.HTTP_207_MULTI_STATUS) + + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningBill'), []) + self.assertEqual(self.bc.database.list_of('provisioning.ProvisioningUserConsumption'), []) + + self.assertEqual(Storage.__init__.call_args_list, [call()]) + self.assertEqual(File.__init__.call_args_list, [ + call(Storage().client.bucket('bucket'), hash), + ]) + + self.assertEqual(File.upload.call_args_list, []) + self.assertEqual(File.url.call_args_list, []) + self.bc.check.calls(tasks.upload.delay.call_args_list, [call(hash, total_pages=1)]) diff --git a/breathecode/provisioning/views.py b/breathecode/provisioning/views.py index 08b603142..c35ecea58 100644 --- a/breathecode/provisioning/views.py +++ b/breathecode/provisioning/views.py @@ -1,7 +1,7 @@ import hashlib import math import os -from datetime import date +from datetime import date, datetime from urllib.parse import parse_qs, urlencode, urlparse, urlunparse import pandas as pd @@ -308,6 +308,40 @@ def upload(self, lang, file): es='Cada archivo debe tener solo un mes de datos', slug='overflow')) + if format_error: + # rigobot + fields = [ + 'organization', 'consumption_period_id', 'consumption_period_start', 'consumption_period_end', + 'billing_status', 'total_spent_period', 'consumption_item_id', 'user_id', 'email', 'consumption_type', + 'pricing_type', 'total_spent', 'total_tokens', 'model', 'purpose_id', 'purpose_slug', 'purpose', + 'created_at', 'github_username' + ] + + if format_error and len(df.keys().intersection(fields)) == len(fields): + format_error = False + + csv_last_line = cut_csv(file, last=1) + + try: + first = datetime.fromisoformat(df['consumption_period_start'].min()) + last = datetime.fromisoformat(df['consumption_period_end'].max()) + + except Exception: + raise ValidationException( + translation(lang, + en='CSV file from unknown source', + es='Archivo CSV de fuente desconocida', + slug='bad-date-format')) + + delta = relativedelta(last, first) + + if delta.years > 0 or delta.months > 1 or (delta.months > 1 and delta.days > 1): + raise ValidationException( + translation(lang, + en='Each file must have only one month of data', + es='Cada archivo debe tener solo un mes de datos', + slug='overflow')) + # Think about uploading correct files and leaving out incorrect ones if format_error: raise ValidationException( From ff84ff209f21a228e35770c18f7838fad26fe43c Mon Sep 17 00:00:00 2001 From: jefer94 Date: Wed, 29 May 2024 21:15:55 -0500 Subject: [PATCH 28/40] remove a line --- breathecode/provisioning/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/breathecode/provisioning/views.py b/breathecode/provisioning/views.py index c35ecea58..168dbf2c5 100644 --- a/breathecode/provisioning/views.py +++ b/breathecode/provisioning/views.py @@ -320,8 +320,6 @@ def upload(self, lang, file): if format_error and len(df.keys().intersection(fields)) == len(fields): format_error = False - csv_last_line = cut_csv(file, last=1) - try: first = datetime.fromisoformat(df['consumption_period_start'].min()) last = datetime.fromisoformat(df['consumption_period_end'].max()) From a6a725f02a1c8be0419579c71c0c531a121e7363 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Fri, 31 May 2024 11:51:41 -0400 Subject: [PATCH 29/40] Update views.py --- breathecode/marketing/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/breathecode/marketing/views.py b/breathecode/marketing/views.py index 5a3b99f35..e10d9dffa 100644 --- a/breathecode/marketing/views.py +++ b/breathecode/marketing/views.py @@ -153,7 +153,6 @@ def create_lead(request): @api_view(['POST']) @permission_classes([AllowAny]) -@validate_captcha def create_lead_captcha(request): data = request.data.copy() From a591a5ab4f11ac0d6f2d421c118f0ce479c28f01 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 11:53:19 -0400 Subject: [PATCH 30/40] Update validate_captcha_challenge.py --- breathecode/utils/decorators/validate_captcha_challenge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py index ad1161967..a4a105be1 100644 --- a/breathecode/utils/decorators/validate_captcha_challenge.py +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -29,9 +29,9 @@ def wrapper(*args, **kwargs): else: raise IndexError() - apply_captcha = os.getenv('APPLY_CAPTCHA', False) + apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE') - if not apply_captcha: + if not apply_captcha or apply_captcha == 'FALSE': return function(*args, **kwargs) logger.info('VERIFYING THE CAPTCHA') From 6f6f5dd4bc88c663843073146941468c6dd5e517 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 11:53:33 -0400 Subject: [PATCH 31/40] Update validate_captcha_challenge.py --- breathecode/utils/decorators/validate_captcha_challenge.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py index a4a105be1..26c16640d 100644 --- a/breathecode/utils/decorators/validate_captcha_challenge.py +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -34,9 +34,6 @@ def wrapper(*args, **kwargs): if not apply_captcha or apply_captcha == 'FALSE': return function(*args, **kwargs) - logger.info('VERIFYING THE CAPTCHA') - print('VERIFYING THE CAPTCHA') - project_id = os.getenv('GOOGLE_PROJECT_ID', '') site_key = os.getenv('GOOGLE_CAPTCHA_KEY', '') From bca8140414bcd5e95aeca108f080f8e37db4cb98 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Fri, 31 May 2024 11:54:42 -0400 Subject: [PATCH 32/40] Update views.py --- breathecode/marketing/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/marketing/views.py b/breathecode/marketing/views.py index e10d9dffa..5a3b99f35 100644 --- a/breathecode/marketing/views.py +++ b/breathecode/marketing/views.py @@ -153,6 +153,7 @@ def create_lead(request): @api_view(['POST']) @permission_classes([AllowAny]) +@validate_captcha def create_lead_captcha(request): data = request.data.copy() From d61a9afdb51c3235d7c7f908a4342e5831bd3513 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 11:54:56 -0400 Subject: [PATCH 33/40] Update validate_captcha.py --- breathecode/utils/decorators/validate_captcha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/utils/decorators/validate_captcha.py b/breathecode/utils/decorators/validate_captcha.py index 0b767a4f8..e86897152 100644 --- a/breathecode/utils/decorators/validate_captcha.py +++ b/breathecode/utils/decorators/validate_captcha.py @@ -28,14 +28,14 @@ def wrapper(*args, **kwargs): else: raise IndexError() - apply_captcha = os.getenv('APPLY_CAPTCHA', False) + apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE') logger.info('CAPTCHA DECORATOR') print('CAPTCHA DECORATOR') logger.info('apply_captcha') print(apply_captcha) - if not apply_captcha: + if not apply_captcha or apply_captcha == 'FALSE': return function(*args, **kwargs) logger.info('VERIFYING THE CAPTCHA') From 6f4115b7a502969f7a5fae493f52a10f1e0518d3 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 11:57:09 -0400 Subject: [PATCH 34/40] Update validate_captcha.py --- breathecode/utils/decorators/validate_captcha.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/breathecode/utils/decorators/validate_captcha.py b/breathecode/utils/decorators/validate_captcha.py index e86897152..81e3ffc85 100644 --- a/breathecode/utils/decorators/validate_captcha.py +++ b/breathecode/utils/decorators/validate_captcha.py @@ -30,19 +30,10 @@ def wrapper(*args, **kwargs): apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE') - logger.info('CAPTCHA DECORATOR') - print('CAPTCHA DECORATOR') - logger.info('apply_captcha') - print(apply_captcha) - if not apply_captcha or apply_captcha == 'FALSE': return function(*args, **kwargs) - logger.info('VERIFYING THE CAPTCHA') - print('VERIFYING THE CAPTCHA') - project_id = os.getenv('GOOGLE_PROJECT_ID', '') - site_key = os.getenv('GOOGLE_CAPTCHA_KEY', '') token = data['token'] if 'token' in data else None @@ -55,11 +46,6 @@ def wrapper(*args, **kwargs): token=token, recaptcha_action=recaptcha_action) - logger.info('response risk_analysis score') - logger.info(response.risk_analysis.score) - print('response risk_analysis score') - print(response.risk_analysis.score) - if (response.risk_analysis.score < 0.8): raise ValidationException('The action was denied because it was considered suspicious', code=429) From 786132f038e3c5d17cfe214459d16ab8e5d34af1 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 12:00:47 -0400 Subject: [PATCH 35/40] Update validate_captcha.py --- breathecode/utils/decorators/validate_captcha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/utils/decorators/validate_captcha.py b/breathecode/utils/decorators/validate_captcha.py index 81e3ffc85..1ab1f0a84 100644 --- a/breathecode/utils/decorators/validate_captcha.py +++ b/breathecode/utils/decorators/validate_captcha.py @@ -28,9 +28,9 @@ def wrapper(*args, **kwargs): else: raise IndexError() - apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE') + apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE').lower() - if not apply_captcha or apply_captcha == 'FALSE': + if not apply_captcha or apply_captcha == 'false': return function(*args, **kwargs) project_id = os.getenv('GOOGLE_PROJECT_ID', '') From fbb3baa0a9e18ad80e05c9a893ec0c05e4515450 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 12:01:02 -0400 Subject: [PATCH 36/40] Update validate_captcha_challenge.py --- breathecode/utils/decorators/validate_captcha_challenge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py index 26c16640d..8d9b6c691 100644 --- a/breathecode/utils/decorators/validate_captcha_challenge.py +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -29,9 +29,9 @@ def wrapper(*args, **kwargs): else: raise IndexError() - apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE') + apply_captcha = os.getenv('APPLY_CAPTCHA', 'FALSE').lower() - if not apply_captcha or apply_captcha == 'FALSE': + if not apply_captcha or apply_captcha == 'false': return function(*args, **kwargs) project_id = os.getenv('GOOGLE_PROJECT_ID', '') From 049540c1bde245339e2a842fbe656cebef0f2954 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 31 May 2024 12:02:29 -0400 Subject: [PATCH 37/40] Update .env.example --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index f0d089f34..c805d42a1 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ LOG_LEVEL=DEBUG DATABASE_URL=postgres://gitpod@localhost:5432/breathecode CACHE_MIDDLEWARE_MINUTES=15 SECURE_SSL_REDIRECT=FALSE +APPLY_CAPTCHA=FALSE # URLS API_URL=https://breathecode.herokuapp.com From 4ac12c644f710ae0927af7ff196aae5401d76ff6 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 3 Jun 2024 21:31:44 +0000 Subject: [PATCH 38/40] implemented asset superseding principles to version multiple articles for different versions of the same technology --- .env.example | 1 + breathecode/registry/admin.py | 2 +- .../migrations/0043_asset_superseded_by.py | 27 ++++++++++++ breathecode/registry/models.py | 11 +++++ breathecode/registry/serializers.py | 42 +++++++++++++++++++ breathecode/registry/tasks.py | 3 +- breathecode/registry/urls/v1.py | 2 + breathecode/registry/views.py | 32 ++++++++++++++ 8 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 breathecode/registry/migrations/0043_asset_superseded_by.py diff --git a/.env.example b/.env.example index f0d089f34..1b70a09f4 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ ENV=development LOG_LEVEL=DEBUG DATABASE_URL=postgres://gitpod@localhost:5432/breathecode +CACHE=0 CACHE_MIDDLEWARE_MINUTES=15 SECURE_SSL_REDIRECT=FALSE diff --git a/breathecode/registry/admin.py b/breathecode/registry/admin.py index e3da3f293..f97a47d17 100644 --- a/breathecode/registry/admin.py +++ b/breathecode/registry/admin.py @@ -350,7 +350,7 @@ class AssetAdmin(admin.ModelAdmin): 'asset_type', 'status', 'sync_status', 'test_status', 'lang', 'external', AssessmentFilter, WithKeywordFilter, WithDescription, IsMarkdown ] - raw_id_fields = ['author', 'owner'] + raw_id_fields = ['author', 'owner', 'superseded_by'] actions = [ test_asset_integrity, add_gitpod, diff --git a/breathecode/registry/migrations/0043_asset_superseded_by.py b/breathecode/registry/migrations/0043_asset_superseded_by.py new file mode 100644 index 000000000..6cda2d727 --- /dev/null +++ b/breathecode/registry/migrations/0043_asset_superseded_by.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.3 on 2024-06-03 21:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('registry', '0042_asset_enable_table_of_content_alter_asset_gitpod_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='superseded_by', + field=models.OneToOneField( + blank=True, + default=None, + help_text= + 'The newer version of the article (null if it is the latest version). This is used for technology deprecation, for example, a new article to explain the new version of react router', + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='previous_version', + to='registry.asset'), + ), + ] diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index 902c576e9..bed392e09 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -359,6 +359,17 @@ def __init__(self, *args, **kwargs): db_index=True) asset_type = models.CharField(max_length=20, choices=TYPE, db_index=True) + superseded_by = models.OneToOneField( + 'Asset', + related_name='previous_version', + on_delete=models.SET_NULL, + null=True, + default=None, + blank=True, + help_text= + 'The newer version of the article (null if it is the latest version). This is used for technology deprecation, for example, a new article to explain the new version of react router' + ) + status = models.CharField(max_length=20, choices=ASSET_STATUS, default=NOT_STARTED, diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index a641125bb..f720246ab 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -115,6 +115,12 @@ class AssetCategorySmallSerializer(serpy.Serializer): title = serpy.Field() +class AssetTinySerializer(serpy.Serializer): + id = serpy.Field() + slug = serpy.Field() + title = serpy.Field() + + class AssetSmallSerializer(serpy.Serializer): id = serpy.Field() slug = serpy.Field() @@ -295,6 +301,7 @@ class AcademyAssetSerializer(AssetSerializer): published_at = serpy.Field() clusters = serpy.MethodField() + previous_versions = serpy.MethodField() def get_clusters(self, obj): return [k.cluster.slug for k in obj.seo_keywords.all() if k.cluster is not None] @@ -302,6 +309,20 @@ def get_clusters(self, obj): def get_seo_keywords(self, obj): return list(map(lambda t: AssetKeywordSerializer(t).data, obj.seo_keywords.all())) + def get_previous_versions(self, obj): + + prev_versions = [] + _aux = obj + try: + while _aux.previous_version is not None: + prev_versions.append(_aux.previous_version) + _aux = _aux.previous_version + except: + pass + + serializer = AssetTinySerializer(prev_versions, many=True) + return serializer.data + class AssetMidSerializer(AssetSerializer): @@ -335,6 +356,7 @@ class AssetBigSerializer(AssetMidSerializer): cluster = KeywordClusterSmallSerializer(required=False) assets_related = serpy.MethodField() + superseded_by = AssetTinySerializer(required=False) def get_assets_related(self, obj): _assets_related = [AssetSmallSerializer(asset).data for asset in obj.assets_related.all()] @@ -783,6 +805,26 @@ def validate(self, data): if 'category' in data: category = data['category'] + if 'superseded_by' in data and data['superseded_by']: + if data['superseded_by'].id == self.instance.id: + raise ValidationException('One asset cannot supersed itself', code=400) + + try: + _prev = data['superseded_by'].previous_version + if _prev and (not self.instance.superseded_by or _prev.id != self.instance.superseded_by.id): + raise ValidationException( + f'Asset {data["superseded_by"].id} is already superseding {_prev.asset_type}: {_prev.slug}', + code=400) + except: + pass + + try: + previous_version = self.instance.previous_version + if previous_version and data['superseded_by'].id == previous_version.id: + raise ValidationException('One asset cannot have its previous version also superseding', code=400) + except: + pass + if category is None: raise ValidationException('Asset category cannot be null', status.HTTP_400_BAD_REQUEST) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 7c7235335..29cf82aa6 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -446,7 +446,8 @@ def async_synchonize_repository_content(self, webhook): if 'repository' not in payload: raise AbortTask('Missing repository information') elif 'url' not in payload['repository']: - raise AbortTask('Repository payload is invalid, expecting an object with "url" key') + raise AbortTask( + 'Repository payload is invalid, expecting an object with "url" key. Check the webhook content-type') base_repo_url = payload['repository']['url'] default_branch = payload['repository']['default_branch'] diff --git a/breathecode/registry/urls/v1.py b/breathecode/registry/urls/v1.py index 58c280eeb..eee43e5cb 100644 --- a/breathecode/registry/urls/v1.py +++ b/breathecode/registry/urls/v1.py @@ -25,6 +25,7 @@ handle_test_asset, render_preview_html, render_readme, + AssetSupersedesView, ) app_name = 'registry' @@ -34,6 +35,7 @@ path('asset/thumbnail/', AssetThumbnailView.as_view(), name='asset_thumbnail_slug'), path('asset/preview/', render_preview_html), path('asset/gitpod/', forward_asset_url), + path('asset//supersedes', AssetSupersedesView.as_view()), path('asset//github/config', get_config), path('asset/.', render_readme), path('asset/', AssetView.as_view()), diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index cc089947e..9adc2744a 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -83,6 +83,7 @@ SEOReportSerializer, TechnologyPUTSerializer, VariableSmallSerializer, + AssetTinySerializer, ) from .tasks import async_pull_from_github @@ -834,6 +835,37 @@ def get(self, request, asset_slug, academy_id): return handler.response(serializer.data) +class AssetSupersedesView(APIView, GenerateLookupsMixin): + """ + List all snippets, or create a new snippet. + """ + + @capable_of('read_asset') + def get(self, request, asset_slug=None, academy_id=None): + + asset = Asset.get_by_slug(asset_slug, request) + + supersedes = [] + _aux = asset + while _aux.superseded_by is not None: + supersedes.append(_aux.superseded_by) + _aux = _aux.superseded_by + + previous = [] + _aux = asset + try: + while _aux.previous_version is not None: + previous.append(_aux.previous_version) + _aux = _aux.previous_version + except: + pass + + return Response({ + 'supersedes': AssetTinySerializer(supersedes, many=True).data, + 'previous': AssetTinySerializer(previous, many=True).data + }) + + class AcademyAssetView(APIView, GenerateLookupsMixin): """ List all snippets, or create a new snippet. From 586d173bffc7c751a5498c52f4ea0bcf5bdc4444 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 4 Jun 2024 15:25:29 +0000 Subject: [PATCH 39/40] added filter superseded_by on academy/asset/all because it was needed on the admin front end --- breathecode/registry/views.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index 016a276af..ebba5c381 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -940,7 +940,7 @@ def get(self, request, asset_slug=None, academy_id=None): if 'language' in self.request.GET or 'lang' in self.request.GET: param = self.request.GET.get('language') if not param: param = self.request.GET.get('lang') - + if param == 'en': param = 'us' lookup['lang'] = param @@ -992,6 +992,16 @@ def get(self, request, asset_slug=None, academy_id=None): elif param == 'both': lookup.pop('external', None) + if 'superseded_by' in self.request.GET: + param = self.request.GET.get('superseded_by') + if param.lower() in ['none', 'null']: + lookup['superseded_by__isnull'] = True + else: + if param.isnumeric(): + lookup['superseded_by__id'] = param + else: + lookup['superseded_by__slug'] = param + published_before = request.GET.get('published_before', '') if published_before != '': items = items.filter(published_at__lte=published_before) From 5d6044d7270a5d4169f0f69cac9e8abc7ccba13c Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 4 Jun 2024 16:37:08 +0000 Subject: [PATCH 40/40] added filter previous_version on academy/asset/all because it was needed on the admin front end --- breathecode/registry/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index ebba5c381..426d76219 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -1002,6 +1002,16 @@ def get(self, request, asset_slug=None, academy_id=None): else: lookup['superseded_by__slug'] = param + if 'previous_version' in self.request.GET: + param = self.request.GET.get('previous_version') + if param.lower() in ['none', 'null']: + lookup['previous_version__isnull'] = True + else: + if param.isnumeric(): + lookup['previous_version__id'] = param + else: + lookup['previous_version__slug'] = param + published_before = request.GET.get('published_before', '') if published_before != '': items = items.filter(published_at__lte=published_before)