From fbe97036846beacccfc36ddda4ab5c9f75561421 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 19 Apr 2024 17:28:45 +0000 Subject: [PATCH 001/114] fixed en lang on learn.json --- breathecode/registry/actions.py | 182 ++++++++++++++++++-------------- breathecode/registry/admin.py | 9 +- 2 files changed, 110 insertions(+), 81 deletions(-) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 49e573fff..3525cd74c 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -623,6 +623,107 @@ def create(self, delay=600): return self.asset +def process_asset_config(asset, config): + + if not config: + raise Exception('No configuration json found') + # only replace title and description of English language + if 'title' in config: + if isinstance(config['title'], str): + if (lang == '' or asset.title == '' or asset.title is None): + asset.title = config['title'] + elif isinstance(config['title'], dict) and asset.lang in config['title']: + asset.title = config['title'][asset.lang] + + if 'description' in config: + if isinstance(config['description'], str): + # avoid replacing descriptions for other languages + if (lang == '' or asset.description == '' or asset.description is None): + asset.description = config['description'] + # there are multiple translations, and the translation exists for this lang + elif isinstance(config['description'], dict) and asset.lang in config['description']: + asset.description = config['description'][asset.lang] + + if 'preview' in config: + asset.preview = config['preview'] + else: + raise Exception('Missing preview URL') + + if 'video-id' in config: + asset.solution_video_url = get_video_url(str(config['video-id'])) + asset.with_video = True + + if 'video' in config and isinstance(config['video'], dict): + if 'intro' in config['video'] and config['video']['intro'] is not None: + if isinstance(config['video']['intro'], str): + asset.intro_video_url = get_video_url(str(config['video']['intro'])) + else: + if 'en' in config['video']['intro']: + config['video']['intro']['us'] = config['video']['intro']['en'] + elif 'us' in config['video']['intro']: + config['video']['intro']['en'] = config['video']['intro']['us'] + + if asset.lang in config['video']['intro']: + print('get_video_url', get_video_url(str(config['video']['intro'][asset.lang]))) + asset.intro_video_url = get_video_url(str(config['video']['intro'][asset.lang])) + + if 'solution' in config['video'] and config['video']['solution'] is not None: + if isinstance(config['video']['solution'], str): + asset.solution_video_url = get_video_url(str(config['video']['solution'])) + asset.with_video = True + else: + if 'en' in config['video']['solution']: + config['video']['solution']['us'] = config['video']['solution']['en'] + elif 'us' in config['video']['solution']: + config['video']['solution']['en'] = config['video']['solution']['us'] + + if asset.lang in config['video']['solution']: + asset.solution_video_url = get_video_url(str(config['video']['solution'][asset.lang])) + asset.with_video = True + + if 'duration' in config: + asset.duration = config['duration'] + if 'difficulty' in config: + asset.difficulty = config['difficulty'].upper() + if 'solution' in config: + asset.solution_url = config['solution'] + asset.with_solutions = True + + if 'projectType' in config: + asset.gitpod = config['projectType'] == 'tutorial' + + if 'technologies' in config: + asset.technologies.clear() + for tech_slug in config['technologies']: + technology = AssetTechnology.get_or_create(tech_slug) + asset.technologies.add(technology) + + if 'delivery' in config: + if 'instructions' in config['delivery']: + if isinstance(config['delivery']['instructions'], str): + asset.delivery_instructions = config['delivery']['instructions'] + elif isinstance(config['delivery']['instructions'], + dict) and asset.lang in config['delivery']['instructions']: + asset.delivery_instructions = config['delivery']['instructions'][asset.lang] + + if 'formats' in config['delivery']: + if isinstance(config['delivery']['formats'], list): + asset.delivery_formats = ','.join(config['delivery']['formats']) + elif isinstance(config['delivery']['formats'], str): + asset.delivery_formats = config['delivery']['formats'] + + if 'url' in asset.delivery_formats: + if 'regex' in config['delivery'] and isinstance(config['delivery']['regex'], str): + asset.delivery_regex_url = config['delivery']['regex'].replace('\\\\', '\\') + else: + asset.delivery_instructions = '' + asset.delivery_formats = 'url' + asset.delivery_regex_url = '' + + asset.save() + return asset + + def pull_learnpack_asset(github, asset: Asset, override_meta): if asset.readme_url is None: @@ -667,86 +768,7 @@ def pull_learnpack_asset(github, asset: Asset, override_meta): config = json.loads(learn_file.decoded_content.decode('utf-8')) asset.config = config - # only replace title and description of English language - if 'title' in config: - if isinstance(config['title'], str): - if (lang == '' or asset.title == '' or asset.title is None): - asset.title = config['title'] - elif isinstance(config['title'], dict) and asset.lang in config['title']: - asset.title = config['title'][asset.lang] - - if 'description' in config: - if isinstance(config['description'], str): - # avoid replacing descriptions for other languages - if (lang == '' or asset.description == '' or asset.description is None): - asset.description = config['description'] - # there are multiple translations, and the translation exists for this lang - elif isinstance(config['description'], dict) and asset.lang in config['description']: - asset.description = config['description'][asset.lang] - - if 'preview' in config: - asset.preview = config['preview'] - else: - raise Exception('Missing preview URL') - - if 'video-id' in config: - asset.solution_video_url = get_video_url(str(config['video-id'])) - asset.with_video = True - - if 'video' in config and isinstance(config['video'], dict): - if 'intro' in config['video'] and config['video']['intro'] is not None: - if isinstance(config['video']['intro'], str): - asset.intro_video_url = get_video_url(str(config['video']['intro'])) - elif asset.lang in config['video']['intro']: - asset.intro_video_url = get_video_url(str(config['video']['intro'][asset.lang])) - - if 'solution' in config['video'] and config['video']['solution'] is not None: - if isinstance(config['video']['solution'], str): - asset.solution_video_url = get_video_url(str(config['video']['solution'])) - asset.with_video = True - elif asset.lang in config['video']['solution']: - asset.solution_video_url = get_video_url(str(config['video']['solution'][asset.lang])) - asset.with_video = True - - if 'duration' in config: - asset.duration = config['duration'] - if 'difficulty' in config: - asset.difficulty = config['difficulty'].upper() - if 'solution' in config: - asset.solution_url = config['solution'] - asset.with_solutions = True - - if 'projectType' in config: - asset.gitpod = config['projectType'] == 'tutorial' - - if 'technologies' in config: - asset.technologies.clear() - for tech_slug in config['technologies']: - technology = AssetTechnology.get_or_create(tech_slug) - asset.technologies.add(technology) - - if 'delivery' in config: - if 'instructions' in config['delivery']: - if isinstance(config['delivery']['instructions'], str): - asset.delivery_instructions = config['delivery']['instructions'] - elif isinstance(config['delivery']['instructions'], - dict) and asset.lang in config['delivery']['instructions']: - asset.delivery_instructions = config['delivery']['instructions'][asset.lang] - - if 'formats' in config['delivery']: - if isinstance(config['delivery']['formats'], list): - asset.delivery_formats = ','.join(config['delivery']['formats']) - elif isinstance(config['delivery']['formats'], str): - asset.delivery_formats = config['delivery']['formats'] - - if 'url' in asset.delivery_formats: - if 'regex' in config['delivery'] and isinstance(config['delivery']['regex'], str): - asset.delivery_regex_url = config['delivery']['regex'].replace('\\\\', '\\') - else: - asset.delivery_instructions = '' - asset.delivery_formats = 'url' - asset.delivery_regex_url = '' - + asset = process_asset_config(asset, config) return asset diff --git a/breathecode/registry/admin.py b/breathecode/registry/admin.py index 47b6671bd..38447862b 100644 --- a/breathecode/registry/admin.py +++ b/breathecode/registry/admin.py @@ -12,7 +12,7 @@ from .tasks import (async_pull_from_github, async_test_asset, async_download_readme_images, async_remove_img_from_cloud, async_upload_image_to_bucket, async_update_frontend_asset_cache) from .actions import (get_user_from_github_username, AssetThumbnailGenerator, scan_asset_originality, - add_syllabus_translations, clean_asset_readme) + add_syllabus_translations, clean_asset_readme, process_asset_config) logger = logging.getLogger(__name__) lang_flags = { @@ -46,6 +46,12 @@ def make_internal(modeladmin, request, queryset): queryset.update(external=False) +def process_config_object(modeladmin, request, queryset): + assets = queryset.all() + for a in assets: + process_asset_config(a, a.config) + + def pull_content_from_github(modeladmin, request, queryset): queryset.update(sync_status='PENDING', status_text='Starting to sync...') assets = queryset.all() @@ -335,6 +341,7 @@ class AssetAdmin(admin.ModelAdmin): test_asset_integrity, add_gitpod, remove_gitpod, + process_config_object, pull_content_from_github, pull_content_from_github_override_meta, seo_optimization_off, From 5579e48733dfcf89d40d93e61bad2cde912c714e Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:36:41 -0400 Subject: [PATCH 002/114] Update models.py --- breathecode/assignments/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/breathecode/assignments/models.py b/breathecode/assignments/models.py index f6df567c8..4e3b874ce 100644 --- a/breathecode/assignments/models.py +++ b/breathecode/assignments/models.py @@ -116,9 +116,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._current_task_status = self.task_status - def clean(self): - if self.cohort is None: - raise forms.ValidationError('Cohort is required') + # def clean(self): + # if self.cohort is None: + # raise forms.ValidationError('Cohort is required') def save(self, *args, **kwargs): # check the fields before saving From 94383c5c0db4b8497a10abb36434724515e7a3e7 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 22 Apr 2024 10:42:13 -0400 Subject: [PATCH 003/114] Update actions.py --- breathecode/registry/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 24d87e828..e3fc1a1e8 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -949,7 +949,7 @@ def add_syllabus_translations(_json: dict): for ass in day[asset_type]: index += 1 slug = ass['slug'] if 'slug' in ass else ass - _asset = Asset.objects.filter(slug=slug).first() + _asset = Asset.get_by_slug(slug) if _asset is not None: if 'slug' not in ass: _json['days'][day_count][asset_type][index] = { From 12d654d0c669305631c35deabe69140360659aa4 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:42:20 -0400 Subject: [PATCH 004/114] Update actions.py --- breathecode/assessment/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/actions.py b/breathecode/assessment/actions.py index 90378a061..005ce12e0 100644 --- a/breathecode/assessment/actions.py +++ b/breathecode/assessment/actions.py @@ -30,7 +30,7 @@ def create_from_asset(asset): academy=asset.academy, author=asset.author) - if a.question_set.count() > 0: + if a is not None and a.question_set is not None and a.question_set.count() > 0: raise ValidationException( 'Assessment already has questions, only empty assessments can by created from an asset') From 458aaf230a67c584daf9ea1e0206661583589f47 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:54:12 -0400 Subject: [PATCH 005/114] Update actions.py --- breathecode/assessment/actions.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/breathecode/assessment/actions.py b/breathecode/assessment/actions.py index 005ce12e0..0c17bca33 100644 --- a/breathecode/assessment/actions.py +++ b/breathecode/assessment/actions.py @@ -14,11 +14,6 @@ def create_from_asset(asset): a = asset.assessment - if asset.assessment is not None and asset.assessment.asset_set.count() > 1: - associated_assets = ','.join(asset.assessment.asset_set.all()) - raise ValidationException('Assessment has more then one asset associated, please choose only one: ' + - associated_assets) - if asset.assessment is None: a = Assessment.objects.filter(slug=asset.slug).first() if a is not None: @@ -29,6 +24,12 @@ def create_from_asset(asset): slug=asset.slug, academy=asset.academy, author=asset.author) + + if a is not None and a.asset_set.count() > 1: + associated_assets = ','.join(a.asset_set.all()) + raise ValidationException('Assessment has more then one asset associated, please choose only one: ' + + associated_assets) + if a is not None and a.question_set is not None and a.question_set.count() > 0: raise ValidationException( From 71b55401a563f91b8e17ba3e323b7b4c3e1ffc4b Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Wed, 24 Apr 2024 18:02:15 -0400 Subject: [PATCH 006/114] Update actions.py --- breathecode/assessment/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/actions.py b/breathecode/assessment/actions.py index 0c17bca33..b199ae2ce 100644 --- a/breathecode/assessment/actions.py +++ b/breathecode/assessment/actions.py @@ -25,7 +25,7 @@ def create_from_asset(asset): academy=asset.academy, author=asset.author) - if a is not None and a.asset_set.count() > 1: + if a is not None and a.asset_set is not None and a.asset_set.count() > 1: associated_assets = ','.join(a.asset_set.all()) raise ValidationException('Assessment has more then one asset associated, please choose only one: ' + associated_assets) From 0e5b9da3058ad9d37641beb95f90d500e8af35ce Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Thu, 25 Apr 2024 19:59:49 +0000 Subject: [PATCH 007/114] Added new webhook to notify cohort_user_created signal --- breathecode/notify/receivers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index 0e0a4a2cc..58099e9e2 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -156,3 +156,16 @@ def edu_status_updated(sender, instance, **kwargs): 'edu_status_updated', payload_override=serializer.data, academy_override=academy) + + +@receiver(cohort_user_created, sender=CohortUser) +def cohort_user_created(sender, instance, **kwargs): + logger.debug('Sending student to hook with cohort user created') + academy = instance.cohort.academy if instance.cohort is not None else None + model_label = get_model_label(instance) + serializer = CohortUserHookSerializer(instance) + HookManager.process_model_event(instance, + model_label, + 'cohort_user_created', + payload_override=serializer.data, + academy_override=academy) From b93f208d2d5b552a859ab46b0201a3ca102e2b27 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Thu, 25 Apr 2024 20:12:15 +0000 Subject: [PATCH 008/114] Added new webhook to notify cohort_user_created signal --- breathecode/notify/receivers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index 58099e9e2..d4b49b4e0 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -8,7 +8,7 @@ from breathecode.marketing.signals import form_entry_won_or_lost, new_form_entry_deal from breathecode.marketing.models import FormEntry from breathecode.marketing.serializers import FormEntryHookSerializer -from breathecode.admissions.signals import student_edu_status_updated +from breathecode.admissions.signals import student_edu_status_updated, cohort_user_created from breathecode.registry.models import Asset from breathecode.registry.signals import asset_status_updated from breathecode.registry.serializers import AssetHookSerializer From 23280b784beac0f7f1ed6bcf5989a9830a198c93 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 29 Apr 2024 17:01:04 -0400 Subject: [PATCH 009/114] Update actions.py --- breathecode/registry/actions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index e3fc1a1e8..ca52b1352 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -778,6 +778,7 @@ def pull_learnpack_asset(github, asset: Asset, override_meta): base64_readme = str(readme_file.content) asset.readme_raw = base64_readme + config = None if learn_file is not None and (asset.last_synch_at is None or override_meta): config = json.loads(learn_file.decoded_content.decode('utf-8')) asset.config = config From fd54169b06eeef91a82198da620739796627ef61 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 30 Apr 2024 22:19:31 +0000 Subject: [PATCH 010/114] implemented push to github for quiz assets --- breathecode/assessment/actions.py | 139 +++++++++++++++++++++++++----- breathecode/assessment/models.py | 28 ++++++ breathecode/assessment/signals.py | 7 ++ breathecode/registry/actions.py | 47 ++++++++-- breathecode/registry/admin.py | 21 ++++- breathecode/registry/models.py | 12 +++ breathecode/registry/receivers.py | 27 +++++- breathecode/registry/tasks.py | 16 ++++ breathecode/registry/views.py | 2 +- 9 files changed, 265 insertions(+), 34 deletions(-) create mode 100644 breathecode/assessment/signals.py diff --git a/breathecode/assessment/actions.py b/breathecode/assessment/actions.py index 90378a061..fdfd550dc 100644 --- a/breathecode/assessment/actions.py +++ b/breathecode/assessment/actions.py @@ -7,49 +7,142 @@ logger = logging.getLogger(__name__) -def create_from_asset(asset): +def validate_quiz_json(quiz, allow_override=False): + + if 'info' not in quiz: + raise ValidationException(f'Quiz is missing info json property') + + if 'slug' not in quiz['info']: + raise ValidationException(f'Missing info.slug on quiz info') + + _result = {'questions': []} + + # We guarantee that "assessment" property will always be set to something (none or else) + _result['assessment'] = Assessment.objects.filter(slug=quiz['info']['slug']).first() + if not allow_override and _result['assessment']: + raise ValidationException( + f"There is already an assessment with slug {quiz['info']['slug']}, rename your quiz info.slug or delete the previous assessment" + ) + + if 'id' in quiz: _result['id'] = quiz['id'] + elif 'id' in quiz['info']: _result['id'] = quiz['info']['id'] + + if 'questions' not in quiz: + raise Exception(f'Missing "questions" property in quiz') + + title = 'Untitled assessment' + if 'name' in quiz['info']: title = quiz['info']['name'] + if 'title' in quiz['info']: title = quiz['info']['title'] + + _result['info'] = { + 'title': title, + 'slug': quiz['info']['slug'], + } + + _index = 0 + for question in quiz['questions']: + _index += 1 + + _question = {'id': question['id'] if 'id' in question else None} + + title = '' + if 'q' in question: title = question['q'] + elif 'title' in question: title = question['title'] + else: raise Exception(f'Missing "title" property in quiz question #{_index}') + + _question['title'] = title + + options = [] + if 'a' in question: options = question['a'] + elif 'answers' in question: options = question['answers'] + elif 'options' in question: options = question['options'] + else: raise Exception('Missing "options" property in quiz question') + + _question['options'] = [] + + o_index = 0 + for option in options: + o_index += 1 + + _id = None + if 'id' in option: + _id = option['id'] + + title = 'Untitled option' + if 'option' in option: title = option['option'] + elif 'title' in option: title = option['title'] + else: raise Exception(f'Missing "title" property in option {str(o_index)}') + + score = 0 + if 'correct' in option: score = option['correct'] + elif 'score' in option: score = option['score'] + else: raise Exception(f'Missing "score" property in option {str(o_index)}') + + _question['options'].append({'id': _id, 'title': title, 'score': int(score)}) + + _result['questions'].append(_question) + + return _result + + +def create_from_asset(asset, allow_override=False): if asset.academy is None: raise ValidationException(f'Asset {asset.slug} has not academy associated') a = asset.assessment - if asset.assessment is not None and asset.assessment.asset_set.count() > 1: associated_assets = ','.join(asset.assessment.asset_set.all()) raise ValidationException('Assessment has more then one asset associated, please choose only one: ' + associated_assets) + quiz = validate_quiz_json(asset.config, allow_override) if asset.assessment is None: - a = Assessment.objects.filter(slug=asset.slug).first() - if a is not None: - raise ValidationException(f'There is already an assessment with slug {asset.slug}') - a = Assessment.objects.create(title=asset.title, - lang=asset.lang, - slug=asset.slug, - academy=asset.academy, - author=asset.author) + a = quiz['assessment'] + if not a: + a = Assessment.objects.create(title=quiz['info']['title'], + lang=asset.lang, + slug=quiz['info']['slug'], + academy=asset.academy, + author=asset.author) if a.question_set.count() > 0: raise ValidationException( 'Assessment already has questions, only empty assessments can by created from an asset') a.save() - quiz = asset.config + for question in quiz['questions']: - q = Question( - title=question['q'], - lang=asset.lang, - assessment=a, - question_type='SELECT', - ) - q.save() - for option in question['a']: - o = Option( - title=option['option'], - score=int(option['correct']), - question=q, + + q = None + if question['id']: + q = Question.filter(id=question['id']).first() + if not q: + raise ValidationException(f"Question with id {question['id']} not found for quiz {q.id}") + + if not q: + q = Question( + lang=asset.lang, + assessment=a, + question_type='SELECT', ) + + q.title = question['title'] + q.save() + + for option in question['options']: + o = None + if option['id']: + o = Option.filter(id=option['id']).first() + if not o: + raise ValidationException(f"Option with id {option['id']} not found for question {q.id}") + + if not o: + o = Option(question=q) + + o.title = option['title'] + o.score = option['score'] o.save() asset.assessment = a diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 020c8af98..d16f88bf9 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -55,8 +55,36 @@ def __str__(self): return f'{self.slug} ({self.lang})' def save(self, *args, **kwargs): + super().save(*args, **kwargs) + def to_json(self, *args, **kwargs): + + _json = { + 'info': { + 'id': self.id, + 'slug': self.slug, + 'title': self.title, + 'is_instant_feedback': self.is_instant_feedback, + }, + 'questions': [] + } + _questions = self.question_set.all() + for q in _questions: + _q = {'id': q.id, 'title': q.title, 'options': []} + + _options = q.option_set.all() + for o in _options: + _q['options'].append({ + 'id': o.id, + 'title': o.title, + 'score': o.score, + }) + + _json['questions'].append(_q) + + return _json + class AssessmentThreshold(models.Model): diff --git a/breathecode/assessment/signals.py b/breathecode/assessment/signals.py new file mode 100644 index 000000000..ea74e4d8d --- /dev/null +++ b/breathecode/assessment/signals.py @@ -0,0 +1,7 @@ +""" +For each signal you want other apps to be able to receive, you have to +declare a new variable here like this: +""" +from django import dispatch + +assessment_updated = dispatch.Signal() diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 24d87e828..69c365537 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -142,8 +142,9 @@ def pull_from_github(asset_slug, author_id=None, override_meta=False): return asset except Exception as e: + logger.exception(e) message = '' - if hasattr(e, 'data'): + if hasattr(e, 'data') and e.data: message = e.data['message'] else: message = str(e).replace('"', '\'') @@ -204,7 +205,7 @@ def push_to_github(asset_slug, author=None): return asset except Exception as e: - # raise e + logger.exception(e) message = '' if hasattr(e, 'data'): message = e.data['message'] @@ -291,10 +292,19 @@ def push_github_asset(github, asset: Asset): branch, file_path = result.groups() logger.debug(f'Fetching readme: {file_path}') - # we commit the raw readme, we don't want images to be replaced in the original github - decoded_readme = base64.b64decode(asset.readme_raw.encode('utf-8')).decode('utf-8') + decoded_readme = None + if asset.asset_type in ['LESSON', 'ARTICLE']: + # we commit the raw readme, we don't want images to be replaced in the original github + decoded_readme = base64.b64decode(asset.readme_raw.encode('utf-8')).decode('utf-8') + + elif asset.asset_type == 'QUIZ': + decoded_readme = json.dumps(asset.config, indent=4) + + else: + raise Exception(f'Assets with type {asset.asset_type} cannot be commited to Github') + if decoded_readme is None or decoded_readme == 'None' or decoded_readme == '': - raise Exception('The markdown content you are trying to push to Github is empty') + raise Exception('The content you are trying to push to Github is empty') result = set_blob_content(repo, file_path, decoded_readme, file_name, branch=branch) if 'commit' in result: @@ -807,7 +817,32 @@ def pull_quiz_asset(github, asset: Asset): encoded_config = get_blob_content(repo, file_path, branch=branch_name).content decoded_config = Asset.decode(encoded_config) - asset.config = json.loads(decoded_config) + + _config = json.loads(decoded_config) + asset.config = _config + + # "slug": "introduction-networking-es", + # "name": "Introducción a redes", + # "status": "draft", + # "main": "Bienvenido al mundo de las redes. Este primer paso te llevara a grandes cosas en el futuro...", + # "results": "¡Felicidades! Ahora el mundo estará un poco más seguro gracias a tí...", + # "technologies": ["redes"], + # "badges": [ + # { "slug": "cybersecurity_guru", "points": 5 } + # ] + if 'info' in _config: + _config = _config['info'] + if 'name' in _config and _config['name'] != '': asset.title = _config['name'] + + if 'main' in _config and _config['main']: asset.description = _config['main'] + elif 'description' in _config and _config['description']: asset.description = _config['description'] + + if 'technologies' in _config and _config['technologies'] != '': + asset.technologies.clear() + for tech_slug in _config['technologies']: + technology = AssetTechnology.get_or_create(tech_slug) + asset.technologies.add(technology) + asset.save() if asset.assessment is None: diff --git a/breathecode/registry/admin.py b/breathecode/registry/admin.py index 38447862b..e3da3f293 100644 --- a/breathecode/registry/admin.py +++ b/breathecode/registry/admin.py @@ -12,7 +12,7 @@ from .tasks import (async_pull_from_github, async_test_asset, async_download_readme_images, async_remove_img_from_cloud, async_upload_image_to_bucket, async_update_frontend_asset_cache) from .actions import (get_user_from_github_username, AssetThumbnailGenerator, scan_asset_originality, - add_syllabus_translations, clean_asset_readme, process_asset_config) + add_syllabus_translations, clean_asset_readme, process_asset_config, push_to_github) logger = logging.getLogger(__name__) lang_flags = { @@ -56,8 +56,22 @@ def pull_content_from_github(modeladmin, request, queryset): queryset.update(sync_status='PENDING', status_text='Starting to sync...') assets = queryset.all() for a in assets: - async_pull_from_github.delay(a.slug, request.user.id) - # pull_from_github(a.slug) # uncomment for testing purposes + try: + async_pull_from_github.delay(a.slug, request.user.id) + # async_pull_from_github(a.slug, request.user.id) # uncomment for testing purposes + except Exception as e: + messages.error(request, a.slug + ': ' + str(e)) + + +def push_content_to_github(modeladmin, request, queryset): + queryset.update(sync_status='PENDING', status_text='Starting to sync...') + assets = queryset.all() + for a in assets: + # try: + push_to_github(a.slug, request.user) + # # async_push_github_asset(a.slug, request.user.id) # uncomment for testing purposes + # except Exception as e: + # messages.error(request, a.slug + ': ' + str(e)) def pull_content_from_github_override_meta(modeladmin, request, queryset): @@ -344,6 +358,7 @@ class AssetAdmin(admin.ModelAdmin): process_config_object, pull_content_from_github, pull_content_from_github_override_meta, + push_content_to_github, seo_optimization_off, seo_optimization_on, seo_report, diff --git a/breathecode/registry/models.py b/breathecode/registry/models.py index 1b69128ae..edb386986 100644 --- a/breathecode/registry/models.py +++ b/breathecode/registry/models.py @@ -631,6 +631,18 @@ def log_error(self, error_slug, status_text=None): error.save() return error + def generate_quiz_json(self): + + if not self.assessment: + return None + + config = self.assessment.to_json() + config['info']['description'] = self.description + config['lang'] = self.lang + config['technologies'] = [t.slug for t in self.technologies.all()] + + return config + def get_tasks(self): if self.readme is None: diff --git a/breathecode/registry/receivers.py b/breathecode/registry/receivers.py index e443e35c8..b246f43eb 100644 --- a/breathecode/registry/receivers.py +++ b/breathecode/registry/receivers.py @@ -1,9 +1,10 @@ import logging import os -from django.db.models.signals import post_delete +from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from breathecode.assessment.models import Question, Option from breathecode.admissions.models import SyllabusVersion from breathecode.admissions.signals import syllabus_version_json_updated from breathecode.assignments.models import Task @@ -21,6 +22,7 @@ async_remove_img_from_cloud, async_synchonize_repository_content, async_update_frontend_asset_cache, + async_generate_quiz_config, ) logger = logging.getLogger(__name__) @@ -115,3 +117,26 @@ def post_webhook_received(sender, instance, **kwargs): def syllabus_json_updated(sender, instance, **kwargs): logger.debug(f'Syllabus Version json for {instance.syllabus.slug} was updated') async_add_syllabus_translations.delay(instance.syllabus.slug, instance.version) + + +## Keep assessment question and asset.config in synch + + +@receiver(post_save, sender=Question) +def model_a_saved(sender, instance, created, **kwargs): + async_generate_quiz_config(instance.assessment.id) + + +@receiver(post_save, sender=Option) +def model_b_saved(sender, instance, created, **kwargs): + async_generate_quiz_config(instance.question.assessment.id) + + +@receiver(post_delete, sender=Question) +def model_a_deleted(sender, instance, **kwargs): + async_generate_quiz_config(instance.assessment.id) + + +@receiver(post_delete, sender=Option) +def model_b_deleted(sender, instance, **kwargs): + async_generate_quiz_config(instance.question.assessment.id) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 798cf2fb3..189bcf050 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -14,6 +14,7 @@ from task_manager.core.exceptions import AbortTask, RetryTask from task_manager.django.decorators import task +from breathecode.assessment.models import Assessment from breathecode.admissions.models import SyllabusVersion from breathecode.media.models import Media, MediaResolution from breathecode.media.views import media_gallery_bucket @@ -489,3 +490,18 @@ def async_add_syllabus_translations(syllabus_slug, version): syllabus_version.json = add_syllabus_translations(syllabus_version.json) syllabus_version.save() + + +@shared_task(priority=TaskPriority.BACKGROUND.value) +def async_generate_quiz_config(assessment_id): + + assessment = Assessment.objects.filter(id=assessment_id).first() + if assessment is None: + raise Exception(f'Assessment {assessment_id} not found') + + assets = assessment.asset_set.all() + for a in assets: + a.config = a.generate_quiz_json() + a.save() + + return True diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index 68e1fbb6f..87ad5cd89 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -1073,7 +1073,7 @@ def post(self, request, academy_id=None): 'pk', flat=True).order_by('sort_priority') delta = len(data['technologies']) - len(technology_ids) if delta != 0: - raise ValidationException(f'{delta} of the assigned technologies for this lesson are not found') + raise ValidationException(f'{delta} of the assigned technologies for this asset are not found') data['technologies'] = technology_ids From e4414905334c6b15076af0dd62fed86868c96920 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 2 May 2024 19:28:15 +0000 Subject: [PATCH 011/114] assessments are now optionally anonymus, you can track answers --- breathecode/assessment/admin.py | 39 +++++++++- breathecode/assessment/apps.py | 7 ++ .../commands/close_open_user_assessments.py | 20 +++++ ...swer_option_alter_userassessment_status.py | 33 +++++++++ breathecode/assessment/models.py | 45 +++++++++-- breathecode/assessment/receivers.py | 17 +++++ breathecode/assessment/serializers.py | 74 +++++++++++++++---- breathecode/assessment/signals.py | 1 + breathecode/assessment/tasks.py | 38 ++++++++++ breathecode/assessment/views.py | 13 ++-- breathecode/utils/decorators/task.py | 1 + 11 files changed, 259 insertions(+), 29 deletions(-) create mode 100644 breathecode/assessment/management/commands/close_open_user_assessments.py create mode 100644 breathecode/assessment/migrations/0008_alter_answer_option_alter_userassessment_status.py create mode 100644 breathecode/assessment/receivers.py create mode 100644 breathecode/assessment/tasks.py diff --git a/breathecode/assessment/admin.py b/breathecode/assessment/admin.py index 8f96699d0..7c3628e1b 100644 --- a/breathecode/assessment/admin.py +++ b/breathecode/assessment/admin.py @@ -1,6 +1,9 @@ import logging +import re from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin +from breathecode.utils.admin import change_field +from django.utils.html import format_html from .models import (Assessment, UserAssessment, UserProxy, Question, Option, AssessmentThreshold, Answer, AssessmentLayout) from .actions import send_assestment @@ -23,7 +26,6 @@ def send_bulk_assesment(modeladmin, request, queryset): @admin.register(UserProxy) class UserAdmin(UserAdmin): list_display = ('username', 'email', 'first_name', 'last_name') - actions = [send_bulk_assesment] # Register your models here. @@ -52,13 +54,43 @@ class OptionAdmin(admin.ModelAdmin): list_filter = ['lang', 'is_deleted'] -# Register your models here. +def change_status_ANSWERED(modeladmin, request, queryset): + items = queryset.all() + for i in items: + i.status = 'ANSWERED' + i.save() + + @admin.register(UserAssessment) class UserAssessmentAdmin(admin.ModelAdmin): search_fields = ['title', 'question__assessment__title'] readonly_fields = ('token', ) - list_display = ['id', 'title', 'status', 'lang', 'owner', 'total_score', 'assessment'] + list_display = ['id', 'title', 'current_status', 'lang', 'owner', 'total_score', 'assessment', 'academy'] list_filter = ['lang', 'status', 'academy'] + actions = [change_status_ANSWERED] + change_field(['DRAFT', 'SENT', 'ERROR', 'EXPIRED'], name='status') + + def current_status(self, obj): + colors = { + 'DRAFT': 'bg-secondary', + 'SENT': 'bg-warning', + 'ANSWERED': 'bg-success', + 'ERROR': 'bg-error', + 'EXPIRED': 'bg-warning', + None: 'bg-error', + } + + def from_status(s): + if s in colors: + return colors[s] + return '' + + status = 'No status' + if obj.status_text is not None: + status = re.sub(r'[^\w\._\-]', ' ', obj.status_text) + return format_html(f""" + + +
{obj.status}
{status}
""") @admin.register(AssessmentThreshold) @@ -66,6 +98,7 @@ class UserAssessmentThresholdAdmin(admin.ModelAdmin): search_fields = ['assessment__slug', 'assessment__title'] list_display = ['id', 'score_threshold', 'assessment'] list_filter = ['assessment__slug'] + actions = [] @admin.register(Answer) diff --git a/breathecode/assessment/apps.py b/breathecode/assessment/apps.py index b997b8a3b..2f37516c9 100644 --- a/breathecode/assessment/apps.py +++ b/breathecode/assessment/apps.py @@ -1 +1,8 @@ from django.apps import AppConfig # noqa: F401 + + +class AssessmentConfig(AppConfig): + name = 'breathecode.assessment' + + def ready(self): + from . import receivers # noqa: F401 diff --git a/breathecode/assessment/management/commands/close_open_user_assessments.py b/breathecode/assessment/management/commands/close_open_user_assessments.py new file mode 100644 index 000000000..c2855fd55 --- /dev/null +++ b/breathecode/assessment/management/commands/close_open_user_assessments.py @@ -0,0 +1,20 @@ +import os +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User +from django.utils import timezone +from breathecode.admissions.models import CohortUser +from django.db.models import Count + +HOST = os.environ.get('OLD_BREATHECODE_API') +DATETIME_FORMAT = '%Y-%m-%d' + + +class Command(BaseCommand): + help = 'Close user assessments and totalize scores' + + def handle(self, *args, **options): + + unfinished_ua = UserAssessment.objects.filter(finished_at__isnull=True, status='SENT') + total = unfinished_ua.count() + unfinished_ua.update(status='ERROR', status_text='Unfinished user assessment') + self.stdout.write(self.style.SUCCESS(f'{total} user assessments automatically closed with error')) diff --git a/breathecode/assessment/migrations/0008_alter_answer_option_alter_userassessment_status.py b/breathecode/assessment/migrations/0008_alter_answer_option_alter_userassessment_status.py new file mode 100644 index 000000000..f723506cf --- /dev/null +++ b/breathecode/assessment/migrations/0008_alter_answer_option_alter_userassessment_status.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.3 on 2024-05-02 19:26 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assessment', '0007_rename_user_assesment_answer_user_assessment_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='answer', + name='option', + field=models.ForeignKey( + blank=True, + default=None, + help_text='Will be null if open question, no options to pick. Or if option was deleted historically', + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='assessment.option'), + ), + migrations.AlterField( + model_name='userassessment', + name='status', + field=models.CharField(choices=[('DRAFT', 'Draft'), ('SENT', 'Sent'), ('ANSWERED', 'Answered'), + ('ERROR', 'Error'), ('EXPIRED', 'Expired')], + default='DRAFT', + max_length=15), + ), + ] diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 9a86cba10..2cd68dc15 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -5,6 +5,7 @@ from datetime import timedelta from django.contrib.auth.models import User from breathecode.admissions.models import Academy +from . import signals from django.core.validators import RegexValidator __all__ = ['UserProxy', 'Assessment', 'Question', 'Option', 'UserAssessment', 'Answer'] @@ -65,7 +66,6 @@ def __str__(self): return f'{self.slug} ({self.lang})' def save(self, *args, **kwargs): - super().save(*args, **kwargs) def delete(self, *args, **kwargs): @@ -217,12 +217,15 @@ def __str__(self): SURVEY_STATUS = ( (DRAFT, 'Draft'), (SENT, 'Sent'), + (ANSWERED, 'Answered'), # If marked as 'ANSWERED' the total_score will be auto-calculated (ERROR, 'Error'), (EXPIRED, 'Expired'), ) class UserAssessment(models.Model): + _old_status = None + title = models.CharField(max_length=200, blank=True) lang = models.CharField(max_length=3, blank=True, default='en') @@ -267,12 +270,39 @@ class UserAssessment(models.Model): created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._old_status = self.status + def save(self, *args, **kwargs): if not self.pk: self.token = binascii.hexlify(os.urandom(20)).decode() + # Answer is being closed + if self.status != self._old_status: + signals.userassessment_status_updated.send(instance=self, sender=self.__class__) + return super().save(*args, **kwargs) + def get_score(self): + + total_score = 0 + answers = self.answer_set.all().order_by('-created_at') + last_one = None + for a in answers: + last_one = a + + # Ignore open text questions + if a.question.question_type == 'TEXT': + continue + + if a.option: a.value = str(a.option.score) + + if a.value.is_numeric(): + total_score += float(a.value) + + return total_score, last_one + def __str__(self): return self.title @@ -282,12 +312,13 @@ class Answer(models.Model): user_assessment = models.ForeignKey(UserAssessment, on_delete=models.CASCADE, default=None, blank=True, null=True) # Do not implement many-to-many, its better to have many answers, one for each selected option - option = models.ForeignKey(Option, - on_delete=models.CASCADE, - default=None, - blank=True, - null=True, - help_text='Will be null if open question, no options to pick.') + option = models.ForeignKey( + Option, + on_delete=models.SET_NULL, + default=None, + blank=True, + null=True, + help_text='Will be null if open question, no options to pick. Or if option was deleted historically') question = models.ForeignKey(Question, on_delete=models.CASCADE, default=None, blank=True, null=True) value = models.TextField() diff --git a/breathecode/assessment/receivers.py b/breathecode/assessment/receivers.py new file mode 100644 index 000000000..d2b967078 --- /dev/null +++ b/breathecode/assessment/receivers.py @@ -0,0 +1,17 @@ +import logging +from typing import Any, Type +from breathecode.admissions.signals import syllabus_asset_slug_updated +from .signals import userassessment_status_updated +from .models import UserAssessment +from .tasks import async_close_userassignment +from django.dispatch import receiver +from breathecode.assignments import tasks + +logger = logging.getLogger(__name__) + + +@receiver(userassessment_status_updated, sender=UserAssessment) +def userassessment_status_updated(sender: Type[UserAssessment], instance: UserAssessment, **kwargs: Any): + logger.info('Processing userassessment_status_updated: ' + str(instance.id)) + if instance.status == 'ANSWERED': + async_close_userassignment.delay(instance.id) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index 896c00d29..582bdb5e7 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -1,5 +1,6 @@ from breathecode.utils import serpy from breathecode.utils.i18n import translation +from django.contrib.auth.models import AnonymousUser from breathecode.admissions.models import Academy from .models import Assessment, Question, Option, UserAssessment, Answer from breathecode.marketing.models import AcademyAlias @@ -228,24 +229,52 @@ class Meta: class PostUserAssessmentSerializer(serializers.ModelSerializer): + owner_email = serializers.EmailField(required=False) class Meta: model = UserAssessment - exclude = ('total_score', 'created_at', 'updated_at', 'token') + exclude = ('total_score', 'created_at', 'updated_at', 'token', 'owner') read_only_fields = ['id'] def validate(self, data): + lang = self.context['lang'] + request = self.context['request'] + + if 'status' in data and data['status'] not in ['DRAFT', 'SENT']: + raise ValidationException( + translation(lang, + en=f'User assessment cannot be created with status {data["status"]}', + es=f'El user assessment no se puede crear con status {data["status"]}', + slug='invalid-status')) + academy = None - if 'academy' in data: + if 'Academy' in request.headers: + academy_id = request.headers['Academy'] + academy = Academy.objects.filter(id=academy_id).first() + + if not academy and 'academy' in data: academy = data['academy'] - else: - if 'assessment' not in data: - raise ValidationException( - 'No academy or assessment property found to determine academy ownership of this userassessment') + if not academy and 'assessment' in data: academy = data['assessment'].academy + if not academy: + raise ValidationException( + translation(lang, + en=f'Could not determine academy ownership of this user assessment', + es=f'No se ha podido determinar a que academia pertenece este user assessment', + slug='not-academy-detected')) + + if not isinstance(request.user, AnonymousUser): + data['owner'] = request.user + elif 'owner_email' not in data or not data['owner_email']: + raise ValidationException( + translation(lang, + en=f'User assessment cannot be tracked because its missing owner information', + es=f'Este user assessment no puede registrarse porque no tiene informacion del owner', + slug='no-owner-detected')) + return super().validate({**data, 'academy': academy}) def create(self, validated_data): @@ -261,7 +290,7 @@ def create(self, validated_data): if 'lang' in data and data['lang'] == 'us': data['lang'] = 'en' - if 'started_at' not in data: + if 'started_at' not in data or data['started_at'] is None: data['started_at'] = timezone.now() result = super().create({**data, 'total_score': 0, 'academy': validated_data['academy']}) @@ -273,24 +302,41 @@ class PUTUserAssessmentSerializer(serializers.ModelSerializer): class Meta: model = UserAssessment - exclude = ('academy', 'assessment', 'lang', 'total_score', 'token') + exclude = ('academy', 'assessment', 'lang', 'total_score', 'token', 'started_at', 'owner') read_only_fields = [ 'id', 'academy', ] + def validate(self, data): + + lang = self.context['lang'] + + if self.instance.status not in ['DRAFT', 'SENT', 'ERROR']: + raise ValidationException( + translation(lang, + en=f'User assessment cannot be updated because is {self.instance.status}', + es=f'El user assessment status no se puede editar mas porque esta {elf.instance.status}', + slug='invalid-status')) + + return super().validate({**data}) + def update(self, instance, validated_data): + # NOTE: User Assignments that are closed will be automatically scored with assessment.task.async_close_userassignment now = timezone.now() - session_duration = instance.created_at - max_duration = instance.created_at + instance.assessment.max_session_duration - if now > max_duration: - raise ValidationException( - f'Session started {from_now(session_duration)} ago and it expires after {duration_to_str(instance.assessment.max_session_duration)}, no more updates can be made' - ) + data = validated_data.copy() + + # If not being closed + if validated_data['status'] != 'ANSWERED' or instance.status == validated_data['status']: + if now > (instance.created_at + instance.assessment.max_session_duration): + raise ValidationException( + f'Session started {from_now(instance.created_at)} ago and it expires after {duration_to_str(instance.assessment.max_session_duration)}, no more updates can be made' + ) # copy the validated data just to do small last minute corrections data = validated_data.copy() + if 'status_text' in data: del data['status_text'] # "us" language will become "en" language, its the right lang code if 'lang' in data and data['lang'] == 'us': diff --git a/breathecode/assessment/signals.py b/breathecode/assessment/signals.py index ea74e4d8d..31234556e 100644 --- a/breathecode/assessment/signals.py +++ b/breathecode/assessment/signals.py @@ -5,3 +5,4 @@ from django import dispatch assessment_updated = dispatch.Signal() +userassessment_status_updated = dispatch.Signal() diff --git a/breathecode/assessment/tasks.py b/breathecode/assessment/tasks.py new file mode 100644 index 000000000..4728ebe39 --- /dev/null +++ b/breathecode/assessment/tasks.py @@ -0,0 +1,38 @@ +import logging +import os +import re + +from celery import shared_task + +import breathecode.notify.actions as actions +from .models import UserAssessment +from breathecode.utils import TaskPriority + +# Get an instance of a logger +logger = logging.getLogger(__name__) + + +@shared_task(bind=True, priority=TaskPriority.ASSESSMENT.value) +def async_close_userassignment(self, ua_id): + """Notify if the task was change.""" + logger.info('Starting async_close_userassignment') + + ua = UserAssessment.objects.filter(id=ua_id).first() + if not ua: + return False + + score, last_answer = ua.get_score() + + # Not one answer found for the user assessment + if last_answer is None: + ua.status = 'ERROR' + ua.status_text = 'No answers found for this user assessment session' + ua.save() + return True + + ua.total_score = score + ua.status = 'ANSWERED' + ua.status_text = '' + ua.finished_at = last_answer.created_at + ua.save() + return True diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 1872dd549..bf4637fb4 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -37,14 +37,15 @@ class TrackAssessmentView(APIView, GenerateLookupsMixin): """ List all snippets, or create a new snippet. """ + permission_classes = [AllowAny] - def put(self, request, token): - - ass = UserAssessment.objects.filter(token=token).first() + def put(self, request, ua_token): + lang = get_user_language(request) + ass = UserAssessment.objects.filter(token=ua_token).first() if not ass: raise ValidationException('User Assessment not found', 404) - serializer = PUTUserAssessmentSerializer(ass, data=request.data) + serializer = PUTUserAssessmentSerializer(ass, data=request.data, context={'request': request, 'lang': lang}) if serializer.is_valid(): serializer.save() serializer = GetUserAssessmentSerializer(serializer.instance) @@ -53,8 +54,9 @@ def put(self, request, token): def post(self, request): + lang = get_user_language(request) payload = request.data.copy() - serializer = PostUserAssessmentSerializer(data=payload, context={'request': request}) + serializer = PostUserAssessmentSerializer(data=payload, context={'request': request, 'lang': lang}) if serializer.is_valid(): serializer.save() serializer = GetUserAssessmentSerializer(serializer.instance) @@ -543,6 +545,7 @@ class AnswerView(APIView, GenerateLookupsMixin): """ List all snippets, or create a new snippet. """ + permission_classes = [AllowAny] extensions = APIViewExtensions(sort='-created_at', paginate=True) diff --git a/breathecode/utils/decorators/task.py b/breathecode/utils/decorators/task.py index ee52f9804..1c3f4460c 100644 --- a/breathecode/utils/decorators/task.py +++ b/breathecode/utils/decorators/task.py @@ -20,6 +20,7 @@ class TaskPriority(Enum): MONITORING = 2 # monitoring tasks ACTIVITY = 2 # user activity BILL = 2 # postpaid billing + ASSESSMENT = 2 # user assessment CACHE = 3 # cache MARKETING = 4 # marketing purposes OAUTH_CREDENTIALS = 5 # oauth tasks From 4b7fbdf2048627ebebaecd137af56e6b0481b654 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 2 May 2024 22:25:12 -0400 Subject: [PATCH 012/114] Update serializers.py --- breathecode/assessment/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index 582bdb5e7..3a432b0cb 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -293,6 +293,12 @@ def create(self, validated_data): if 'started_at' not in data or data['started_at'] is None: data['started_at'] = timezone.now() + if 'title' not in data or not data['title']: + if 'owner_email' in data and data['owner_email']: + data['title'] = f"{data['assessment'].title} from {data['owner_email']}" + if 'owner' in data and data['owner']: + data['title'] = f"{data['assessment'].title} from {data['owner'].email}" + result = super().create({**data, 'total_score': 0, 'academy': validated_data['academy']}) return result From c4fe99d6cceeea116e57a50e1b75b71f93eac09f Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 03:49:41 +0000 Subject: [PATCH 013/114] added method to get user assginment anonimously --- breathecode/assessment/serializers.py | 38 +++++++++++++++++++++++++-- breathecode/assessment/views.py | 16 +++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index 3a432b0cb..9ac8d778a 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -144,6 +144,40 @@ class GetUserAssessmentSerializer(serpy.Serializer): created_at = serpy.Field() +class PublicUserAssessmentSerializer(serpy.Serializer): + id = serpy.Field() + token = serpy.Field() + title = serpy.Field() + lang = serpy.Field() + + academy = AcademySmallSerializer(required=False) + assessment = AssessmentSmallSerializer() + last_answer = serpy.MethodField() + + owner = UserSerializer(required=False) + owner_email = serpy.Field() + owner_phone = serpy.Field() + + status = serpy.Field() + status_text = serpy.Field() + + conversion_info = serpy.Field() + total_score = serpy.Field() + comment = serpy.Field() + + started_at = serpy.Field() + finished_at = serpy.Field() + + created_at = serpy.Field() + + def get_last_answer(self, obj): + last_answer = obj.answer_set.all().order_by('created_at').first() + if last_answer is None: + return None + + return AnswerSmallSerializer(last_answer).data + + class GetAssessmentBigSerializer(GetAssessmentSerializer): questions = serpy.MethodField() is_instant_feedback = serpy.Field() @@ -295,9 +329,9 @@ def create(self, validated_data): if 'title' not in data or not data['title']: if 'owner_email' in data and data['owner_email']: - data['title'] = f"{data['assessment'].title} from {data['owner_email']}" + data['title'] = f"{data['assessment'].title} from {data['owner_email']}" if 'owner' in data and data['owner']: - data['title'] = f"{data['assessment'].title} from {data['owner'].email}" + data['title'] = f"{data['assessment'].title} from {data['owner'].email}" result = super().create({**data, 'total_score': 0, 'academy': validated_data['academy']}) return result diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index bf4637fb4..1f9bf6c5d 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -30,6 +30,7 @@ PUTUserAssessmentSerializer, AnswerSerializer, AnswerSmallSerializer, + PublicUserAssessmentSerializer, ) @@ -39,6 +40,21 @@ class TrackAssessmentView(APIView, GenerateLookupsMixin): """ permission_classes = [AllowAny] + def get(self, request, token): + lang = get_user_language(request) + now = timezone.now() + + single = UserAssessment.objects.filter(token=token).first() + if single is None or now > single.created_at + single.assessment.max_session_duration: + raise ValidationException(translation(lang, + en=f'User assessment session does not exist or has already expired', + es=f'Esta sessión de evaluación no existe o ya ha expirado', + slug='not-found'), + code=404) + + serializer = PublicUserAssessmentSerializer(single, many=False) + return Response(serializer.data, status=status.HTTP_200_OK) + def put(self, request, ua_token): lang = get_user_language(request) ass = UserAssessment.objects.filter(token=ua_token).first() From 4567b42b007f12a39db8fcbf9f50b61519fc9972 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 00:33:29 -0400 Subject: [PATCH 014/114] Update serializers.py --- breathecode/assessment/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index 9ac8d778a..4cf7c040f 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -171,7 +171,7 @@ class PublicUserAssessmentSerializer(serpy.Serializer): created_at = serpy.Field() def get_last_answer(self, obj): - last_answer = obj.answer_set.all().order_by('created_at').first() + last_answer = obj.answer_set.all().order_by('-created_at').first() if last_answer is None: return None From c16ec0d2adbf4b8f0bc7a782cb9516f4aaa7e82c Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 01:01:35 -0400 Subject: [PATCH 015/114] Update views.py --- breathecode/assessment/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 1f9bf6c5d..83880d22c 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -15,7 +15,7 @@ from breathecode.utils import ValidationException, capable_of from breathecode.utils.i18n import translation -from .models import Assessment, AssessmentThreshold, Option, Question, UserAssessment, Answer +from .models import Assessment, AssessmentThreshold, Option, Question, UserAssessment, Answer, AssessmentLayout from .serializers import ( AssessmentPUTSerializer, GetAssessmentBigSerializer, From a7e508733d7616b12ac7c5fb7dd1bea3ab492eae Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 01:02:29 -0400 Subject: [PATCH 016/114] Update views.py --- breathecode/assessment/views.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 83880d22c..a1e82e925 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -253,14 +253,13 @@ class AssessmentLayoutView(APIView): """ permission_classes = [AllowAny] - def get(self, request, layout_slug=None): + def get(self, request, layout_slug): - if layout_slug: - item = AssessmentLayout.objects.filter(slug=layout_slug).first() - if item is None: - raise ValidationException('Assessment layout not found', 404) - serializer = GetAssessmentLayoutSerializer(items) - return Response(serializer.data, status=status.HTTP_200_OK) + item = AssessmentLayout.objects.filter(slug=layout_slug).first() + if item is None: + raise ValidationException('Assessment layout not found', 404) + serializer = GetAssessmentLayoutSerializer(items) + return Response(serializer.data, status=status.HTTP_200_OK) class AcademyAssessmentLayoutView(APIView): From 537685dd2b0d5ca4703061d64672a00d928c4d36 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 01:09:46 -0400 Subject: [PATCH 017/114] Update views.py --- breathecode/assessment/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index a1e82e925..02deef87f 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -258,7 +258,7 @@ def get(self, request, layout_slug): item = AssessmentLayout.objects.filter(slug=layout_slug).first() if item is None: raise ValidationException('Assessment layout not found', 404) - serializer = GetAssessmentLayoutSerializer(items) + serializer = GetAssessmentLayoutSerializer(item, many=False) return Response(serializer.data, status=status.HTTP_200_OK) From 2011ba8fac31e2ecb61f238a73ce8274c887b248 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 02:19:00 -0400 Subject: [PATCH 018/114] Update views.py --- breathecode/assessment/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/assessment/views.py b/breathecode/assessment/views.py index 02deef87f..aaa901cab 100644 --- a/breathecode/assessment/views.py +++ b/breathecode/assessment/views.py @@ -40,11 +40,11 @@ class TrackAssessmentView(APIView, GenerateLookupsMixin): """ permission_classes = [AllowAny] - def get(self, request, token): + def get(self, request, ua_token): lang = get_user_language(request) now = timezone.now() - single = UserAssessment.objects.filter(token=token).first() + single = UserAssessment.objects.filter(token=ua_token).first() if single is None or now > single.created_at + single.assessment.max_session_duration: raise ValidationException(translation(lang, en=f'User assessment session does not exist or has already expired', From 3a7a0642f45877df2cc4cc40aeee4fe5e3da4481 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 02:19:14 -0400 Subject: [PATCH 019/114] Update urls.py --- breathecode/assessment/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/urls.py b/breathecode/assessment/urls.py index 7167a8ae3..0e49347c3 100644 --- a/breathecode/assessment/urls.py +++ b/breathecode/assessment/urls.py @@ -7,7 +7,7 @@ urlpatterns = [ # user assessments path('user/assessment', TrackAssessmentView.as_view()), - path('user/assessment/', TrackAssessmentView.as_view()), + path('user/assessment/', TrackAssessmentView.as_view()), path('user/assessment//answer', AnswerView.as_view()), path('user/assessment//answer/', AnswerView.as_view()), path('academy/user/assessment//answer/', AcademyAnswerView.as_view()), From 0bccc353dff5c203a0adc8fd13e2e151ca162462 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 02:29:48 -0400 Subject: [PATCH 020/114] Update models.py --- breathecode/assessment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 2cd68dc15..9587e8837 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -298,7 +298,7 @@ def get_score(self): if a.option: a.value = str(a.option.score) - if a.value.is_numeric(): + if a.value.isnumeric(): total_score += float(a.value) return total_score, last_one From b47cc8b1708b34cd1a06f626c638158704040ad6 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 17:18:09 +0000 Subject: [PATCH 021/114] fixed problem calculating assessment score --- breathecode/assessment/models.py | 5 +++-- breathecode/assessment/serializers.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 9587e8837..07d128867 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -295,11 +295,12 @@ def get_score(self): # Ignore open text questions if a.question.question_type == 'TEXT': continue - if a.option: a.value = str(a.option.score) - if a.value.isnumeric(): + try: total_score += float(a.value) + except ValueError: + pass return total_score, last_one diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index 4cf7c040f..b314482fb 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -152,7 +152,6 @@ class PublicUserAssessmentSerializer(serpy.Serializer): academy = AcademySmallSerializer(required=False) assessment = AssessmentSmallSerializer() - last_answer = serpy.MethodField() owner = UserSerializer(required=False) owner_email = serpy.Field() @@ -162,7 +161,6 @@ class PublicUserAssessmentSerializer(serpy.Serializer): status_text = serpy.Field() conversion_info = serpy.Field() - total_score = serpy.Field() comment = serpy.Field() started_at = serpy.Field() @@ -170,12 +168,15 @@ class PublicUserAssessmentSerializer(serpy.Serializer): created_at = serpy.Field() - def get_last_answer(self, obj): - last_answer = obj.answer_set.all().order_by('-created_at').first() - if last_answer is None: - return None + summary = serpy.MethodField() + + def get_summary(self, obj): + total_score, last_one = obj.get_score() + + last_answer = None + if last_one is not None: AnswerSmallSerializer(last_one).data - return AnswerSmallSerializer(last_answer).data + return {'last_answer': last_answer, 'live_score': total_score} class GetAssessmentBigSerializer(GetAssessmentSerializer): From c3c92fc5e8b35416eebdf4c78e83c387870478cc Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 15:04:45 -0400 Subject: [PATCH 022/114] Update serializers.py --- breathecode/assessment/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index b314482fb..1d2c3be6c 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -174,7 +174,8 @@ def get_summary(self, obj): total_score, last_one = obj.get_score() last_answer = None - if last_one is not None: AnswerSmallSerializer(last_one).data + if last_one is not None: + last_answer = AnswerSmallSerializer(last_one).data return {'last_answer': last_answer, 'live_score': total_score} From f625abaeaf6fdb2045c000f5d12ac5bbd4ce479b Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 3 May 2024 15:23:51 -0400 Subject: [PATCH 023/114] Update models.py --- breathecode/assessment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assessment/models.py b/breathecode/assessment/models.py index 07d128867..9df4d5944 100644 --- a/breathecode/assessment/models.py +++ b/breathecode/assessment/models.py @@ -287,7 +287,7 @@ def save(self, *args, **kwargs): def get_score(self): total_score = 0 - answers = self.answer_set.all().order_by('-created_at') + answers = self.answer_set.all().order_by('created_at') last_one = None for a in answers: last_one = a From f7ba4fd365ea8206ce3b7fb43db12c15a5baa99e Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Fri, 3 May 2024 22:41:59 +0000 Subject: [PATCH 024/114] doing recaptcha v2 --- breathecode/marketing/views.py | 5 +- .../services/google_cloud/recaptcha.py | 56 ++++++++++++++ breathecode/utils/decorators/__init__.py | 1 + .../decorators/validate_captcha_challenge.py | 76 +++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 breathecode/utils/decorators/validate_captcha_challenge.py diff --git a/breathecode/marketing/views.py b/breathecode/marketing/views.py index 8e12ff3d6..8cd76f21c 100644 --- a/breathecode/marketing/views.py +++ b/breathecode/marketing/views.py @@ -39,7 +39,7 @@ localize_query, ) from breathecode.utils.api_view_extensions.api_view_extensions import APIViewExtensions -from breathecode.utils.decorators import validate_captcha +from breathecode.utils.decorators import validate_captcha, validate_captcha_challenge from breathecode.utils.find_by_full_name import query_like_by_full_name from breathecode.utils.i18n import translation @@ -133,6 +133,7 @@ def get_alias(request): @api_view(['POST']) @permission_classes([AllowAny]) +@validate_captcha_challenge def create_lead(request): data = request.data.copy() @@ -157,7 +158,7 @@ def create_lead(request): @api_view(['POST']) @permission_classes([AllowAny]) -@validate_captcha +@validate_captcha_challenge def create_lead_captcha(request): data = request.data.copy() diff --git a/breathecode/services/google_cloud/recaptcha.py b/breathecode/services/google_cloud/recaptcha.py index a52e1f206..fd8130b20 100644 --- a/breathecode/services/google_cloud/recaptcha.py +++ b/breathecode/services/google_cloud/recaptcha.py @@ -69,3 +69,59 @@ def create_assessment(self, project_id: str, recaptcha_site_key: str, token: str assessment_name = client.parse_assessment_path(response.name).get('assessment') logger.info(f'Assessment name: {assessment_name}') return response + + def create_assessment_v2(self, project_id: str, recaptcha_site_key: str, token: str, + recaptcha_action: str) -> Assessment: + """Create an assessment to analyze the risk of a UI action. + Args: + project_id: GCloud Project ID + recaptcha_site_key: Site key obtained by registering a domain/app to use recaptcha services. + token: The token obtained from the client on passing the recaptchaSiteKey. + recaptcha_action: Action name corresponding to the token. + """ + client = recaptchaenterprise_v1.RecaptchaEnterpriseServiceClient() + + # Set the properties of the event to be tracked. + event = recaptchaenterprise_v1.Event() + event.site_key = recaptcha_site_key + event.token = token + + assessment = recaptchaenterprise_v1.Assessment() + assessment.event = event + + project_name = f'projects/{project_id}' + + # Build the assessment request. + request = recaptchaenterprise_v1.CreateAssessmentRequest() + request.assessment = assessment + request.parent = project_name + + response = client.create_assessment(request) + + # Check if the token is valid. + if not response.token_properties.valid: + from breathecode.utils.validation_exception import ValidationException + logger.error('The CreateAssessment call failed because the token was ' + + 'invalid for for the following reasons: ' + str(response.token_properties.invalid_reason)) + raise ValidationException( + f'Invalid token for the following reasons: {str(response.token_properties.invalid_reason)}', code=400) + + # Check if the expected action was executed. + if response.token_properties.action != recaptcha_action: + from breathecode.utils.validation_exception import ValidationException + logger.error('The action attribute in your reCAPTCHA tag does' + + 'not match the action you are expecting to score') + raise ValidationException( + 'The action attribute in your reCAPTCHA tag does not match the action you are expecting to score', + code=400) + else: + # Get the risk score and the reason(s) + # For more information on interpreting the assessment, + # see: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment + for reason in response.risk_analysis.reasons: + logger.info(reason) + logger.info('The reCAPTCHA score for this token is: ' + str(response.risk_analysis.score)) + # Get the assessment name (id). Use this to annotate the assessment. + assessment_name = client.parse_assessment_path(response.name).get('assessment') + logger.info(f'Assessment name: {assessment_name}') + return response diff --git a/breathecode/utils/decorators/__init__.py b/breathecode/utils/decorators/__init__.py index ea642b3b2..62fd193fd 100644 --- a/breathecode/utils/decorators/__init__.py +++ b/breathecode/utils/decorators/__init__.py @@ -3,3 +3,4 @@ from .supervisor import * # noqa: F401 from .task import * # noqa: F401 from .validate_captcha import * # noqa: F401 +from .validate_captcha_challenge import * # noqa: F401 diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py new file mode 100644 index 000000000..a234cc534 --- /dev/null +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -0,0 +1,76 @@ +import logging +import os + +from rest_framework.views import APIView + +from breathecode.services.google_cloud import Recaptcha +from breathecode.utils.exceptions import ProgrammingError + +from ..validation_exception import ValidationException + +logger = logging.getLogger(__name__) +__all__ = ['validate_captcha_challenge'] + + +def validate_captcha_challenge(function): + + def wrapper(*args, **kwargs): + try: + if hasattr(args[0], '__class__') and isinstance(args[0], APIView): + data = args[1].data.copy() + + elif hasattr(args[0], 'user') and hasattr(args[0].user, 'has_perm'): + data = args[0].data.copy() + + # websocket support + elif hasattr(args[0], 'ws_request'): + data = args[0].data.copy() + + else: + raise IndexError() + + apply_captcha = os.getenv('APPLY_CAPTCHA', False) + print('apply_captcha') + print(apply_captcha) + + logger.info('CAPTCHA DECORATOR') + print('CAPTCHA DECORATOR') + logger.info('apply_captcha') + print(apply_captcha) + + if not apply_captcha: + 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 + + recaptcha_action = data['action'] if 'action' in data else None + + recaptcha = Recaptcha() + response = recaptcha.create_assessment(project_id=project_id, + recaptcha_site_key=site_key, + token=token, + recaptcha_action=recaptcha_action) + + print('response') + print(response) + 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) + + except IndexError: + raise ProgrammingError('Missing request information, use this decorator with DRF View') + + return function(*args, **kwargs) + + return wrapper From d4f4dae1c947a1393a4f72eb9d205c719cae2047 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Mon, 6 May 2024 12:58:28 -0400 Subject: [PATCH 025/114] Update receivers.py --- breathecode/registry/receivers.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/breathecode/registry/receivers.py b/breathecode/registry/receivers.py index 661070968..3c03640a5 100644 --- a/breathecode/registry/receivers.py +++ b/breathecode/registry/receivers.py @@ -136,11 +136,17 @@ def model_b_saved(sender, instance, created, **kwargs): @receiver(post_delete, sender=Question) def model_a_deleted(sender, instance, **kwargs): - if instance.assessment and not instance.assessment.is_archived: - async_generate_quiz_config(instance.assessment.id) + try: + if instance.assessment and not instance.assessment.is_archived: + async_generate_quiz_config(instance.assessment.id) + except: + pass @receiver(post_delete, sender=Option) def model_b_deleted(sender, instance, **kwargs): - if instance.assessment and not instance.assessment.is_archived: - async_generate_quiz_config(instance.question.assessment.id) + try: + if instance.assessment and not instance.assessment.is_archived: + async_generate_quiz_config(instance.question.assessment.id) + except: + pass From 2eb0606d406a128cc5465647178c0c8ae9655f48 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 7 May 2024 17:59:36 -0400 Subject: [PATCH 026/114] Update actions.py --- breathecode/registry/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 9044d5a30..5f0b4583c 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -132,7 +132,7 @@ def pull_from_github(asset_slug, author_id=None, override_meta=False): elif asset.asset_type in ['QUIZ']: asset = pull_quiz_asset(g, asset) else: - asset = pull_learnpack_asset(g, asset, override_meta=override_meta) + asset = pull_learnpack_asset(g, asset, override_meta=True) asset.status_text = 'Successfully Synched' asset.sync_status = 'OK' From f620fa3f487d39a2f6e192d394d39a9153481e4c Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 7 May 2024 18:42:55 -0400 Subject: [PATCH 027/114] Update models.py --- breathecode/marketing/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/breathecode/marketing/models.py b/breathecode/marketing/models.py index 9b696e126..ebebb3b80 100644 --- a/breathecode/marketing/models.py +++ b/breathecode/marketing/models.py @@ -496,6 +496,9 @@ def save(self, *args, **kwargs): if deal_status_modified: form_entry_won_or_lost.send(instance=self, sender=FormEntry) if is_new_deal: new_form_entry_deal.send(instance=self, sender=FormEntry) + self.__old_deal_status = self.deal_status + self.__old_deal_id = self.ac_deal_id + def is_duplicate(self, incoming_lead): duplicate_leads_delta_avoidance = timedelta(minutes=30) if self.academy is not None and self.academy.activecampaignacademy is not None: From 3fa2cdd5d78a4c23d0c5632216313ab4a1e60005 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Wed, 8 May 2024 17:38:56 +0000 Subject: [PATCH 028/114] Add triggers when planfinancing or subscription is created --- breathecode/notify/receivers.py | 26 ++++++++++++++++++++++++++ breathecode/payments/models.py | 11 +++++++++++ breathecode/payments/signals.py | 4 ++++ 3 files changed, 41 insertions(+) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index 35d4e7c71..d13e5a20a 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -21,6 +21,8 @@ from breathecode.registry.models import Asset from breathecode.registry.serializers import AssetHookSerializer from breathecode.registry.signals import asset_status_updated +from breathecode.payments.signals import subscription_created, planfinancing_created +from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionSerializer from .tasks import send_mentorship_starting_notification from .utils.hook_manager import HookManager @@ -159,3 +161,27 @@ def edu_status_updated(sender, instance, **kwargs): 'edu_status_updated', payload_override=serializer.data, academy_override=academy) + + +@receiver(planfinancing_created, sender=PlanFinancing) +def new_planfinancing_created(sender, instance, **kwargs): + logger.debug('Sending new PlanFinancing to hook') + model_label = get_model_label(instance) + serializer = GetPlanFinancingSerializer(instance) + HookManager.process_model_event(instance, + model_label, + 'planfinancing_created', + payload_override=serializer.data, + academy_override=instance.academy) + + +@receiver(subscription_created, sender=Subscription) +def new_subscription_created(sender, instance, **kwargs): + logger.debug('Sending new Subscription to hook') + model_label = get_model_label(instance) + serializer = GetSubscriptionSerializer(instance) + HookManager.process_model_event(instance, + model_label, + 'subscription_created', + payload_override=serializer.data, + academy_override=instance.academy) diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 9beb70b62..1460fae69 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -1088,6 +1088,11 @@ def clean(self) -> None: def save(self, *args, **kwargs) -> None: self.full_clean() + on_create = self.pk is None + + if on_create: + signals.planfinancing_created.send(instance=self, sender=self.__class__) + return super().save(*args, **kwargs) @@ -1141,6 +1146,12 @@ def clean(self) -> None: def save(self, *args, **kwargs) -> None: self.full_clean() + + on_create = self.pk is None + + if on_create: + signals.subscription_created.send(instance=self, sender=self.__class__) + return super().save(*args, **kwargs) diff --git a/breathecode/payments/signals.py b/breathecode/payments/signals.py index aaa0c2d9d..66e047df5 100644 --- a/breathecode/payments/signals.py +++ b/breathecode/payments/signals.py @@ -17,3 +17,7 @@ # proxy to m2m_changed in Event.service_items update_plan_m2m_service_items = Signal() + +# Plan aquired +planfinancing_created = Signal() +subscription_created = Signal() From ad67e156fe606618675fd891bb92206d94db04c2 Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Thu, 9 May 2024 18:58:28 +0000 Subject: [PATCH 029/114] change python version to 3.12 --- .gitpod.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index b82c0695b..b58caec69 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -18,7 +18,7 @@ RUN sudo apt-get update \ # RUN pyenv update && pyenv install 3.12.3 && pyenv global 3.12.3 -RUN pyenv install 3.12.3 && pyenv global 3.12.3 +RUN pyenv install 3.12 && pyenv global 3.12 RUN pip install pipenv yapf # remove PIP_USER environment From d5fb3445a370c1a7833fcc714eee7daf949d508c Mon Sep 17 00:00:00 2001 From: Jeferson De Freitas Pinto Date: Thu, 9 May 2024 14:43:54 -0500 Subject: [PATCH 030/114] Update .gitpod.Dockerfile --- .gitpod.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index b82c0695b..ded926c5b 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -17,8 +17,8 @@ RUN sudo apt-get update \ # RUN curl https://pyenv.run | bash -# RUN pyenv update && pyenv install 3.12.3 && pyenv global 3.12.3 -RUN pyenv install 3.12.3 && pyenv global 3.12.3 +# RUN pyenv update && pyenv install 3.12.2 && pyenv global 3.12.2 +RUN pyenv install 3.12.2 && pyenv global 3.12.2 RUN pip install pipenv yapf # remove PIP_USER environment From 1cb9f2cedc44d17d12fb602a23b08c44b17298f6 Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Thu, 9 May 2024 20:27:33 +0000 Subject: [PATCH 031/114] change python version to 3.12.2 --- .gitpod.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index b58caec69..54b5facc1 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -18,7 +18,7 @@ RUN sudo apt-get update \ # RUN pyenv update && pyenv install 3.12.3 && pyenv global 3.12.3 -RUN pyenv install 3.12 && pyenv global 3.12 +RUN pyenv install 3.12.2 && pyenv global 3.12.2 RUN pip install pipenv yapf # remove PIP_USER environment From aecfe30c3ad1b6850b9c14bb7c4f7e9f79bd390e Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Thu, 9 May 2024 22:25:51 +0000 Subject: [PATCH 032/114] remove preview img from cloud when deleting asset preview --- breathecode/registry/serializers.py | 8 ++++++++ breathecode/registry/tasks.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/breathecode/registry/serializers.py b/breathecode/registry/serializers.py index 4a47546a5..8bafafa56 100644 --- a/breathecode/registry/serializers.py +++ b/breathecode/registry/serializers.py @@ -807,4 +807,12 @@ def update(self, instance, validated_data): elif validated_data['status'] != 'PUBLISHED': data['published_at'] = None + # Check if preview img is being deleted + if 'preview' in validated_data: + if validated_data['preview'] == None and instance.preview != None: + hash = instance.preview.split('/')[-1] + if hash is not None: + from .tasks import async_remove_asset_preview_from_cloud + async_remove_asset_preview_from_cloud.delay(hash) + return super().update(instance, {**validated_data, **data}) diff --git a/breathecode/registry/tasks.py b/breathecode/registry/tasks.py index 798cf2fb3..40bc66a5a 100644 --- a/breathecode/registry/tasks.py +++ b/breathecode/registry/tasks.py @@ -331,6 +331,27 @@ def async_remove_img_from_cloud(id, **_): return True +@task(priority=TaskPriority.ACADEMY.value) +def async_remove_asset_preview_from_cloud(hash, **_): + + logger.info('async_remove_asset_preview_from_cloud') + + media = Media.objects.filter(hash=hash).first() + if media is None: + raise Exception(f'Media with hash {hash} not found') + + media_name = media.name + + storage = Storage() + extension = media.mime.split('/')[-1] + cloud_file = storage.file(screenshots_bucket(), media.hash + extension) + cloud_file.delete() + media.delete() + + logger.info(f'Media name ({media_name}) was deleted from the cloud') + return True + + @task(priority=TaskPriority.ACADEMY.value) def async_upload_image_to_bucket(id, **_): From 4b10ba4725d437e524aef02bf1f85fd202841c65 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Mon, 13 May 2024 12:33:05 -0400 Subject: [PATCH 033/114] Update serializers.py --- breathecode/assignments/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assignments/serializers.py b/breathecode/assignments/serializers.py index 6611eb23e..cc67dac14 100644 --- a/breathecode/assignments/serializers.py +++ b/breathecode/assignments/serializers.py @@ -226,7 +226,7 @@ def validate(self, data): return data def update(self, instance, validated_data): - if 'opened_at' in validated_data and (instance.opened_at is None + if 'opened_at' in validated_data and validated_data['opened_at'] is not None and (instance.opened_at is None or validated_data['opened_at'] > instance.opened_at): tasks_activity.add_activity.delay(self.context['request'].user.id, 'read_assignment', From e69f3abf44c723856b26f0f65d2e48ddf90e5cec Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 14 May 2024 17:52:29 -0400 Subject: [PATCH 034/114] Update actions.py --- breathecode/registry/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 5f0b4583c..9e66babee 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -361,6 +361,9 @@ def pull_github_lesson(github, asset: Asset, override_meta=False): if 'title' in fm and fm['title'] != '': asset.title = fm['title'] + + if 'video' in fm and fm['video'] != '': + asset.intro_video_url = fm['video'] if 'authors' in fm and fm['authors'] != '': asset.authors_username = ','.join(fm['authors']) From 92bf670472980557fb3b5cf2c331a00434b4563c Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Wed, 15 May 2024 11:56:44 -0400 Subject: [PATCH 035/114] Update receivers.py --- breathecode/notify/receivers.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index 2586cf866..35d4e7c71 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -159,16 +159,3 @@ def edu_status_updated(sender, instance, **kwargs): 'edu_status_updated', payload_override=serializer.data, academy_override=academy) - - -@receiver(cohort_user_created, sender=CohortUser) -def cohort_user_created(sender, instance, **kwargs): - logger.debug('Sending student to hook with cohort user created') - academy = instance.cohort.academy if instance.cohort is not None else None - model_label = get_model_label(instance) - serializer = CohortUserHookSerializer(instance) - HookManager.process_model_event(instance, - model_label, - 'cohort_user_created', - payload_override=serializer.data, - academy_override=academy) From c1ed9de68137d365031c915c4bbe7e00d55405d9 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Wed, 15 May 2024 12:01:21 -0400 Subject: [PATCH 036/114] Update serializers.py --- breathecode/assessment/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/breathecode/assessment/serializers.py b/breathecode/assessment/serializers.py index 1d2c3be6c..ac5a2a2eb 100644 --- a/breathecode/assessment/serializers.py +++ b/breathecode/assessment/serializers.py @@ -7,8 +7,7 @@ from rest_framework import serializers from django.utils import timezone from breathecode.utils.datetime_integer import from_now, duration_to_str -from breathecode.utils.validation_exception import ValidationException - +from capyc.rest_framework.exceptions import ValidationException class UserSerializer(serpy.Serializer): id = serpy.Field() From d99b382fbe274493a7a69ad2e098ffebfa69c464 Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Wed, 15 May 2024 20:08:18 +0000 Subject: [PATCH 037/114] update dependencies --- Pipfile.lock | 932 +++++++++++++++++++++++++-------------------------- 1 file changed, 457 insertions(+), 475 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 96a1a09d1..966ec5ebd 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": [ @@ -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": [ @@ -3396,7 +3378,7 @@ "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], - "markers": "python_version >= '3.8'", + "markers": "python_version >= '3.7'", "version": "==4.11.0" }, "tzdata": { @@ -3645,7 +3627,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:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e", "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf", @@ -3687,14 +3677,6 @@ "markers": "python_version >= '3.7'", "version": "==6.3" }, - "zope.event": { - "hashes": [ - "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", - "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0" - }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", @@ -3909,71 +3891,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": [ @@ -4205,11 +4187,11 @@ }, "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 +4418,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 +4444,12 @@ }, "mkdocstrings-python": { "hashes": [ - "sha256:71678fac657d4d2bb301eed4e4d2d91499c095fd1f8a90fa76422a87a5693828", - "sha256:ba833fbd9d178a4b9d5cb2553a4df06e51dc1f51e41559a4d2398c16a6f69ecc" + "sha256:5fd41a603bc6d80ff21a3c42413fe51f1d22afde09ee419eab1e2b8e9cdaf5c4", + "sha256:7fcfefba80d2f05f198ec072e404d216b969cdff9ebe2d4903b2f7d515f910e1" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.10.0" + "version": "==1.10.1" }, "nodeenv": { "hashes": [ @@ -4518,11 +4500,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 +4516,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 +4732,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 +4881,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": [ From dd1c59566020b1a09bb5db2994071a5e8c0a7e1e Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 16 May 2024 14:29:08 -0500 Subject: [PATCH 038/114] 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 039/114] 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 040/114] 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 1018f36af316caeda103d019f593da484f7a0f4c Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Fri, 17 May 2024 18:25:02 +0000 Subject: [PATCH 041/114] configure challenge recaptcha service --- breathecode/marketing/views.py | 2 +- .../services/google_cloud/recaptcha.py | 29 ++++++---------- .../decorators/validate_captcha_challenge.py | 33 +++++++------------ 3 files changed, 23 insertions(+), 41 deletions(-) diff --git a/breathecode/marketing/views.py b/breathecode/marketing/views.py index 8cd76f21c..029f054aa 100644 --- a/breathecode/marketing/views.py +++ b/breathecode/marketing/views.py @@ -158,7 +158,7 @@ def create_lead(request): @api_view(['POST']) @permission_classes([AllowAny]) -@validate_captcha_challenge +@validate_captcha def create_lead_captcha(request): data = request.data.copy() diff --git a/breathecode/services/google_cloud/recaptcha.py b/breathecode/services/google_cloud/recaptcha.py index fd8130b20..cb69a3e9b 100644 --- a/breathecode/services/google_cloud/recaptcha.py +++ b/breathecode/services/google_cloud/recaptcha.py @@ -106,22 +106,13 @@ def create_assessment_v2(self, project_id: str, recaptcha_site_key: str, token: raise ValidationException( f'Invalid token for the following reasons: {str(response.token_properties.invalid_reason)}', code=400) - # Check if the expected action was executed. - if response.token_properties.action != recaptcha_action: - from breathecode.utils.validation_exception import ValidationException - logger.error('The action attribute in your reCAPTCHA tag does' + - 'not match the action you are expecting to score') - raise ValidationException( - 'The action attribute in your reCAPTCHA tag does not match the action you are expecting to score', - code=400) - else: - # Get the risk score and the reason(s) - # For more information on interpreting the assessment, - # see: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment - for reason in response.risk_analysis.reasons: - logger.info(reason) - logger.info('The reCAPTCHA score for this token is: ' + str(response.risk_analysis.score)) - # Get the assessment name (id). Use this to annotate the assessment. - assessment_name = client.parse_assessment_path(response.name).get('assessment') - logger.info(f'Assessment name: {assessment_name}') - return response + # Get the risk score and the reason(s) + # For more information on interpreting the assessment, + # see: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment + for reason in response.risk_analysis.reasons: + logger.info(reason) + logger.info('The reCAPTCHA score for this token is: ' + str(response.risk_analysis.score)) + # Get the assessment name (id). Use this to annotate the assessment. + assessment_name = client.parse_assessment_path(response.name).get('assessment') + logger.info(f'Assessment name: {assessment_name}') + return response diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py index a234cc534..7b72ffd79 100644 --- a/breathecode/utils/decorators/validate_captcha_challenge.py +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -30,13 +30,6 @@ def wrapper(*args, **kwargs): raise IndexError() apply_captcha = os.getenv('APPLY_CAPTCHA', False) - print('apply_captcha') - print(apply_captcha) - - logger.info('CAPTCHA DECORATOR') - print('CAPTCHA DECORATOR') - logger.info('apply_captcha') - print(apply_captcha) if not apply_captcha: return function(*args, **kwargs) @@ -53,20 +46,18 @@ def wrapper(*args, **kwargs): recaptcha_action = data['action'] if 'action' in data else None recaptcha = Recaptcha() - response = recaptcha.create_assessment(project_id=project_id, - recaptcha_site_key=site_key, - token=token, - recaptcha_action=recaptcha_action) - - print('response') - print(response) - 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) + response = recaptcha.create_assessment_v2(project_id=project_id, + recaptcha_site_key=site_key, + token=token, + recaptcha_action=recaptcha_action) + + # TEMPORALILY DISABLING SCORE ANALYSIS + # Google Recaptcha needs to work some time to learn about the site's traffic + # It may be enabled in the future, though it is not recommended to just block the traffic based on punctuation + # read more: https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment-website?authuser=1&hl=es&_gl=1*1yex6v*_ga*MzE4Mjc4NTMzLjE3MDAxNzgzMDU.*_ga_WH2QY8WWF5*MTcxNTk2NTkzOS41NC4xLjE3MTU5NjYyNDMuMC4wLjA.&_ga=2.84385883.-318278533.1700178305#interpret_scores + + # if (response.risk_analysis.score < 0.6): + # raise ValidationException('The action was denied because it was considered suspicious', code=429) except IndexError: raise ProgrammingError('Missing request information, use this decorator with DRF View') From 2c7ae68f83ca7484928b6edbe6995f06b401d2e9 Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Fri, 17 May 2024 21:36:37 +0000 Subject: [PATCH 042/114] add validation token --- breathecode/services/google_cloud/recaptcha.py | 4 +--- .../utils/decorators/validate_captcha_challenge.py | 9 +++------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/breathecode/services/google_cloud/recaptcha.py b/breathecode/services/google_cloud/recaptcha.py index cb69a3e9b..764158a62 100644 --- a/breathecode/services/google_cloud/recaptcha.py +++ b/breathecode/services/google_cloud/recaptcha.py @@ -70,14 +70,12 @@ def create_assessment(self, project_id: str, recaptcha_site_key: str, token: str logger.info(f'Assessment name: {assessment_name}') return response - def create_assessment_v2(self, project_id: str, recaptcha_site_key: str, token: str, - recaptcha_action: str) -> Assessment: + def create_assessment_v2(self, project_id: str, recaptcha_site_key: str, token: str) -> Assessment: """Create an assessment to analyze the risk of a UI action. Args: project_id: GCloud Project ID recaptcha_site_key: Site key obtained by registering a domain/app to use recaptcha services. token: The token obtained from the client on passing the recaptchaSiteKey. - recaptcha_action: Action name corresponding to the token. """ client = recaptchaenterprise_v1.RecaptchaEnterpriseServiceClient() diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py index 7b72ffd79..3a5df5226 100644 --- a/breathecode/utils/decorators/validate_captcha_challenge.py +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -42,14 +42,11 @@ def wrapper(*args, **kwargs): site_key = os.getenv('GOOGLE_CAPTCHA_KEY', '') token = data['token'] if 'token' in data else None - - recaptcha_action = data['action'] if 'action' in data else None + if token is None: + raise ValidationException('Missing ReCaptcha Token', code=400) recaptcha = Recaptcha() - response = recaptcha.create_assessment_v2(project_id=project_id, - recaptcha_site_key=site_key, - token=token, - recaptcha_action=recaptcha_action) + response = recaptcha.create_assessment_v2(project_id=project_id, recaptcha_site_key=site_key, token=token) # TEMPORALILY DISABLING SCORE ANALYSIS # Google Recaptcha needs to work some time to learn about the site's traffic 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 043/114] 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 044/114] 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 7dd81f1e2ff31192652c597d529ae36846913065 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 21 May 2024 17:22:01 -0400 Subject: [PATCH 045/114] Update validate_captcha_challenge.py --- breathecode/utils/decorators/validate_captcha_challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/utils/decorators/validate_captcha_challenge.py b/breathecode/utils/decorators/validate_captcha_challenge.py index 3a5df5226..ad1161967 100644 --- a/breathecode/utils/decorators/validate_captcha_challenge.py +++ b/breathecode/utils/decorators/validate_captcha_challenge.py @@ -6,7 +6,7 @@ from breathecode.services.google_cloud import Recaptcha from breathecode.utils.exceptions import ProgrammingError -from ..validation_exception import ValidationException +from capyc.rest_framework.exceptions import ValidationException logger = logging.getLogger(__name__) __all__ = ['validate_captcha_challenge'] From 9c95f01d25a913e195a5c651cf829f96e3703178 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 21 May 2024 16:38:10 -0500 Subject: [PATCH 046/114] 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 047/114] 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 048/114] 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 049/114] 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 050/114] 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 051/114] 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 052/114] 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 6318a2917d182c977b48e0e7f098ae5750d66081 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 23 May 2024 17:05:23 -0500 Subject: [PATCH 053/114] update checks.yml --- .github/workflows/checks.yml | 19 +++++++++---------- .gitignore | 2 ++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b4fe0fc38..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: @@ -318,7 +317,7 @@ jobs: run: pipenv run flake8 --select=B tests: - needs: [migrations, dependencies] + needs: [migrations] runs-on: ubuntu-latest steps: @@ -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' && diff --git a/.gitignore b/.gitignore index 6cfeaa413..cb3ff38a0 100644 --- a/.gitignore +++ b/.gitignore @@ -182,3 +182,5 @@ dump.rdb *:Zone.Identifier node_modules/ +.venv* +.env* From 15f49ddde89a48c690944df158520ca1be8a27b3 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 23 May 2024 17:18:00 -0500 Subject: [PATCH 054/114] fix tests --- .../tests/urls/tests_user_me_task.py | 30 ------------------- 1 file changed, 30 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', From e9d1dc9f37a2b8bc27374b0603104b123aa681ea Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Fri, 24 May 2024 01:08:20 +0000 Subject: [PATCH 055/114] add profile picture to Final project serializer --- Pipfile | 1 + Pipfile.lock | 594 +++++++++--------- breathecode/assignments/serializers.py | 17 +- .../tests/urls/tests_final_project_cohort.py | 3 + 4 files changed, 317 insertions(+), 298 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 966ec5ebd..f029bcda3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a60a902b22af50adb3b38a8edc92ac6c17f76389a0f83b2da557e9411e13f709" + "sha256": "66365e8a3a48d8089abc905c1d2d55c1eb29099bc909196dac809734b156084b" }, "pipfile-spec": 6, "requires": {}, @@ -158,11 +158,11 @@ }, "annotated-types": { "hashes": [ - "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", - "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0" + "version": "==0.7.0" }, "anyio": { "hashes": [ @@ -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", @@ -1021,12 +1030,12 @@ }, "google-cloud-bigquery": { "hashes": [ - "sha256:80c8e31a23b68b7d3ae5d138c9a9edff69d100ee812db73a5e63c79a13a5063d", - "sha256:957591e6f948d7cb4aa0f7a8e4e47b4617cd7f0269e28a71c37953c39b6e8a4c" + "sha256:4b4597f9291b42102c9667d3b4528f801d4c8f24ef2b12dd1ecb881273330955", + "sha256:9fb72884fdbec9c4643cea6b7f21e1ecf3eb61d5305f87493d271dc801647a9e" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.22.0" + "version": "==3.23.1" }, "google-cloud-bigquery-storage": { "hashes": [ @@ -1283,54 +1292,54 @@ }, "grpcio": { "hashes": [ - "sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3", - "sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094", - "sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b", - "sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d", - "sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2", - "sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172", - "sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d", - "sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c", - "sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b", - "sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3", - "sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9", - "sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357", - "sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61", - "sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5", - "sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a", - "sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280", - "sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434", - "sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce", - "sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d", - "sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c", - "sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f", - "sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f", - "sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57", - "sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f", - "sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0", - "sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2", - "sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0", - "sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a", - "sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6", - "sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d", - "sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85", - "sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a", - "sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d", - "sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f", - "sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb", - "sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86", - "sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7", - "sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda", - "sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d", - "sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434", - "sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91", - "sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a", - "sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3", - "sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3", - "sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1", - "sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae" - ], - "version": "==1.63.0" + "sha256:01615bbcae6875eee8091e6b9414072f4e4b00d8b7e141f89635bdae7cf784e5", + "sha256:02cc9cc3f816d30f7993d0d408043b4a7d6a02346d251694d8ab1f78cc723e7e", + "sha256:0b2dfe6dcace264807d9123d483d4c43274e3f8c39f90ff51de538245d7a4145", + "sha256:0da1d921f8e4bcee307aeef6c7095eb26e617c471f8cb1c454fd389c5c296d1e", + "sha256:0f30596cdcbed3c98024fb4f1d91745146385b3f9fd10c9f2270cbfe2ed7ed91", + "sha256:1ce4cd5a61d4532651079e7aae0fedf9a80e613eed895d5b9743e66b52d15812", + "sha256:1f279ad72dd7d64412e10f2443f9f34872a938c67387863c4cd2fb837f53e7d2", + "sha256:1f5de082d936e0208ce8db9095821361dfa97af8767a6607ae71425ac8ace15c", + "sha256:1f8ea18b928e539046bb5f9c124d717fbf00cc4b2d960ae0b8468562846f5aa1", + "sha256:2186d76a7e383e1466e0ea2b0febc343ffeae13928c63c6ec6826533c2d69590", + "sha256:23b6887bb21d77649d022fa1859e05853fdc2e60682fd86c3db652a555a282e0", + "sha256:257baf07f53a571c215eebe9679c3058a313fd1d1f7c4eede5a8660108c52d9c", + "sha256:2a18090371d138a57714ee9bffd6c9c9cb2e02ce42c681aac093ae1e7189ed21", + "sha256:2e8fabe2cc57a369638ab1ad8e6043721014fdf9a13baa7c0e35995d3a4a7618", + "sha256:3161a8f8bb38077a6470508c1a7301cd54301c53b8a34bb83e3c9764874ecabd", + "sha256:31890b24d47b62cc27da49a462efe3d02f3c120edb0e6c46dcc0025506acf004", + "sha256:3550493ac1d23198d46dc9c9b24b411cef613798dc31160c7138568ec26bc9b4", + "sha256:3b09c3d9de95461214a11d82cc0e6a46a6f4e1f91834b50782f932895215e5db", + "sha256:3d2004e85cf5213995d09408501f82c8534700d2babeb81dfdba2a3bff0bb396", + "sha256:46b8b43ba6a2a8f3103f103f97996cad507bcfd72359af6516363c48793d5a7b", + "sha256:579dd9fb11bc73f0de061cab5f8b2def21480fd99eb3743ed041ad6a1913ee2f", + "sha256:597191370951b477b7a1441e1aaa5cacebeb46a3b0bd240ec3bb2f28298c7553", + "sha256:59c68df3a934a586c3473d15956d23a618b8f05b5e7a3a904d40300e9c69cbf0", + "sha256:5a56797dea8c02e7d3a85dfea879f286175cf4d14fbd9ab3ef2477277b927baa", + "sha256:650a8150a9b288f40d5b7c1d5400cc11724eae50bd1f501a66e1ea949173649b", + "sha256:6d5541eb460d73a07418524fb64dcfe0adfbcd32e2dac0f8f90ce5b9dd6c046c", + "sha256:6ec5ed15b4ffe56e2c6bc76af45e6b591c9be0224b3fb090adfb205c9012367d", + "sha256:73f84f9e5985a532e47880b3924867de16fa1aa513fff9b26106220c253c70c5", + "sha256:753cb58683ba0c545306f4e17dabf468d29cb6f6b11832e1e432160bb3f8403c", + "sha256:7c1f5b2298244472bcda49b599be04579f26425af0fd80d3f2eb5fd8bc84d106", + "sha256:7e013428ab472892830287dd082b7d129f4d8afef49227a28223a77337555eaa", + "sha256:7f17572dc9acd5e6dfd3014d10c0b533e9f79cd9517fc10b0225746f4c24b58e", + "sha256:85fda90b81da25993aa47fae66cae747b921f8f6777550895fb62375b776a231", + "sha256:874c741c8a66f0834f653a69e7e64b4e67fcd4a8d40296919b93bab2ccc780ba", + "sha256:8d598b5d5e2c9115d7fb7e2cb5508d14286af506a75950762aa1372d60e41851", + "sha256:8de0399b983f8676a7ccfdd45e5b2caec74a7e3cc576c6b1eecf3b3680deda5e", + "sha256:a053584079b793a54bece4a7d1d1b5c0645bdbee729215cd433703dc2532f72b", + "sha256:a54362f03d4dcfae63be455d0a7d4c1403673498b92c6bfe22157d935b57c7a9", + "sha256:aca4f15427d2df592e0c8f3d38847e25135e4092d7f70f02452c0e90d6a02d6d", + "sha256:b2cbdfba18408389a1371f8c2af1659119e1831e5ed24c240cae9e27b4abc38d", + "sha256:b52e1ec7185512103dd47d41cf34ea78e7a7361ba460187ddd2416b480e0938c", + "sha256:c46fb6bfca17bfc49f011eb53416e61472fa96caa0979b4329176bdd38cbbf2a", + "sha256:c56c91bd2923ddb6e7ed28ebb66d15633b03e0df22206f22dfcdde08047e0a48", + "sha256:cf4c8daed18ae2be2f1fc7d613a76ee2a2e28fdf2412d5c128be23144d28283d", + "sha256:d7b7bf346391dffa182fba42506adf3a84f4a718a05e445b37824136047686a1", + "sha256:d9171f025a196f5bcfec7e8e7ffb7c3535f7d60aecd3503f9e250296c7cfc150" + ], + "version": "==1.64.0" }, "grpcio-status": { "hashes": [ @@ -1584,11 +1593,11 @@ }, "jupyter-client": { "hashes": [ - "sha256:3b7bd22f058434e3b9a7ea4b1500ed47de2713872288c0d511d19926f99b459f", - "sha256:e842515e2bab8e19186d89fdfea7abd15e39dd581f94e399f00e2af5a1652d3f" + "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df", + "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f" ], "markers": "python_version >= '3.8'", - "version": "==8.6.1" + "version": "==8.6.2" }, "jupyter-core": { "hashes": [ @@ -1639,11 +1648,11 @@ "requests" ], "hashes": [ - "sha256:0c71f043805e0f32a3f17ed4129f76ad5ba4710f68b092de210939d0d290a8f2", - "sha256:eba1d993a8568ecccc1b9d2f0427ba59df6f8d99575119ba2dc1f609d65fd120" + "sha256:175e677ffc069148ac3674d60482327a3c1ab0f00f049a6002970b64b861e9cc", + "sha256:9e92fb86d0efc11c2d19948ca961f74cf6576b0e1d942e381d219361ca83801a" ], "markers": "python_version >= '3.10'", - "version": "==1.2.1" + "version": "==1.2.3" }, "lxml": { "hashes": [ @@ -1661,6 +1670,7 @@ "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5", "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab", "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316", + "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6", "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df", "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca", "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264", @@ -1716,6 +1726,7 @@ "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa", "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48", "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3", + "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184", "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67", "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7", "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34", @@ -1733,6 +1744,7 @@ "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0", "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b", "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1", + "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f", "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf", "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf", "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0", @@ -1751,6 +1763,7 @@ "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36", "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b", "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07", + "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c", "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573", "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001", "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9", @@ -2098,39 +2111,39 @@ }, "newrelic": { "hashes": [ - "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" + "sha256:02db25b0fd2fc835efe4a7f1c92dbc5bbb95125341aba07152041aa6a5666cda", + "sha256:09912303e04bee6aa1fe1c671e87b4e8e55461081a96210895828798f5ba8c3f", + "sha256:1cc3ddb26c0615ba4e18f87453bca57f0688a43d2fcdd50e2771a77515cfc3ba", + "sha256:1f11d9c17b50982fcc39de71f6592a61920ec5e5c29b9105edc9f8fb7f2480b9", + "sha256:2236f70b8c6aa79635f2175e7315d032f3a80dfd65ad9c9ed12a921f5df4c655", + "sha256:40368dca0d423efe40b210686d7018787d4365a24ee1deca136b3b7c9d850325", + "sha256:4404c649b5e6165dcdd59091092c19b292a43cc96520d5ffd718b628fb866096", + "sha256:4d68fc707d896dc7da8d6939bcc1f995bf9e463c2b911fc63250a10e1502a234", + "sha256:4e573d49c1543a488d6567906a9b2cb0c748cdbf80724c322b06874f8e47c789", + "sha256:524ed5bfa09d330746b45e0087765da994ca34802cce032063041e404e58414c", + "sha256:56f4c309a07a2c66243b12d18056c32aa704735469741495642c31be4a1c77fa", + "sha256:5d18236bf4a80fca4eb1db03448ed72bf8e16b84b3a4ed5fcc29bb91c2d05d54", + "sha256:667722cf1f4ed9f6cd99f4fbe247fc2bdb941935528e14a93659ba2c651dc889", + "sha256:6ed4bc2c9a44dfe59958eeecf1f327f0a0fb6324b5e609515bc511944d12db74", + "sha256:744c815f15ec06e441c11a6c57042d2eca8c41401c11de6f47b3e105d952b9bd", + "sha256:77537a020ce84033f39210e46cc43bb3927cec3fb4b34b5c4df802e96fddaedf", + "sha256:7cd462804a6ede617fb3b4b126e9083b3ee8b4ed1250f7cc12299ebacb785432", + "sha256:8ad9cd5459b8c620ab7a876bd5d920c3ef2943948d1262a42289d4f8d16dadab", + "sha256:a4d4e5670082225ca7ef0ee986ef8e6588f4e530a05d43d66f9368459c0b1f18", + "sha256:acf5cdcafd2971933ad2f9e836284957f4a3eababe88f063cf53b1b1f67f1a16", + "sha256:ae0515f7ab19f1a5dd14e31506420d1b86014c5e1340c2a210833248bc765dae", + "sha256:ae84bacfdc60792bd04e681027cc5c58e6737a04c652e9be2eda84abe21f57f5", + "sha256:b8201a33caf7632b2e55e3f9687584ad6956aaf5751485cdb2bad7c428a9b400", + "sha256:bf6757d422954e61082715dbba4208cae17bf3720006bc337c3f87f19ede2876", + "sha256:ceef4fef2a5cffb69e9e1742bd18a35625ca62c3856c7016c22be68ec876753d", + "sha256:d0c18210648889416da3de61aa282248e012cb507ba9841511407f922fff9a52", + "sha256:d3be6c97d007ceb142f908f5ab2444807b44dc600a0b7f3254dc685b5b03fd10", + "sha256:e2576bbec0b640d9b76454dcfd5b2f03078e0bb062a7ea3952a8db7b9972c352", + "sha256:f4605bc4feb114235e242dfe260b75ec85d0894f5400aa7f30e75fbbc0423b3f" ], "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.1" + "version": "==9.10.0" }, "numpy": { "hashes": [ @@ -2177,12 +2190,12 @@ }, "openai": { "hashes": [ - "sha256:4f85190e577cba0b066e1950b8eb9b11d25bc7ebcc43a86b326ce1bfa564ec74", - "sha256:c9fb3c3545c118bbce8deb824397b9433a66d0d0ede6a96f7009c95b76de4a46" + "sha256:44316818fbff3845278e862a655c4c041e93d907b04eff64629c2835f29bd58e", + "sha256:f86780f40505de60fa389993d9b7f5564f20acfbe5efcabd5c853a12453af2b0" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.30.1" + "version": "==1.30.2" }, "packaging": { "hashes": [ @@ -2238,10 +2251,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:d0e5fdc6c09519e53a331003db7203c9e82bd39ac676d584e297e6523d334448", - "sha256:e9d7b1220a2f376d35b0b7a305fe76fe5ef077e580f7840869dbd29d9c9f74a8" + "sha256:6b28e38ea2bc0d809c1f933f7fccb860d780bc62b9456a2754cb778a620dca76", + "sha256:7cdc76625e0879071ad31f4066867adbc6779ac37d574957c64a72b59b8bc82d" ], - "version": "==8.13.36" + "version": "==8.13.37" }, "pillow": { "hashes": [ @@ -2795,11 +2808,11 @@ }, "pytest": { "hashes": [ - "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", - "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f" + "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", + "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" ], "markers": "python_version >= '3.8'", - "version": "==8.2.0" + "version": "==8.2.1" }, "pytest-django": { "hashes": [ @@ -3025,12 +3038,12 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", + "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.2" }, "rpds-py": { "hashes": [ @@ -3178,11 +3191,11 @@ }, "setuptools": { "hashes": [ - "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", - "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" + "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" ], "markers": "python_version >= '3.8'", - "version": "==69.5.1" + "version": "==70.0.0" }, "six": { "hashes": [ @@ -3282,12 +3295,12 @@ }, "stripe": { "hashes": [ - "sha256:9305d849cea715dc59c5e39d01891475b82e10edb9d95ee1d8189457e5de792f", - "sha256:f519f6810ac7f6e096b4faf562c44b1f8e365138441548e4ab0bc93f86368ad7" + "sha256:63161fca746cb6a81e2158bf61950ccb8d75f1b7231c7f4ac9e05817aab586b4", + "sha256:f630bcf47d49beaad02feba0109784bc016b5e0c974272da6fdf590a0f7ec8a9" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==9.6.0" + "version": "==9.8.0" }, "text-unidecode": { "hashes": [ @@ -3375,11 +3388,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", - "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" + "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", + "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" ], - "markers": "python_version >= '3.7'", - "version": "==4.11.0" + "markers": "python_version >= '3.8'", + "version": "==4.12.0" }, "tzdata": { "hashes": [ @@ -3637,45 +3650,39 @@ }, "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:065f8ff0e034e43b8da05ffed308a9e3311720a2b13b83724f26a8dd6709964d", + "sha256:0940b3b44b6cc0375ea0da5fefee05a9abe8bb53594a3a6e4aafb9f99dc5de8d", + "sha256:15fa208d7a802c0dd3e9d4d5336619a37efd57f2d2ce830d9f9d5843a2b7daba", + "sha256:16f73c42f10f761051157332943ee1f7cf973cc1c78a50d1960c313a211cca4a", + "sha256:18f4061c3456c61557e9d7068e435f5db164b38f15f3d9bd995ff185c6db2c62", + "sha256:1e0678885d07e865047e15be3ebe5c87903cc7f5ca5edfd0045d1c7b43f7fe9d", + "sha256:297ee171f40f8f18665bb75f576df7d1ddce19f3e6696ef6acb930dcbfbf693f", + "sha256:36234d3b8211d053c42684666c2a04eb1a35e0cec6bc3e54586bb60fb0be3b17", + "sha256:3a00983c5c793b17b829020e11032dabab023c4e0ef12f134b90df802ae5adf2", + "sha256:3ca89624d0eabc7ce4f299c6d621531cb8b0ebac3bb4f9ebf2d057477602e1b8", + "sha256:45fb6fe8b5852564e63d6705a7904530a7c886056e6e9aaf938dc5e2bc637097", + "sha256:52901ff6d75a4332457610cbd2883f39b386c5bebe0745ecf78e3fe22cfdd0d9", + "sha256:563a7192a5baf8d9b189dc598c3555e695e00fdce3eafb88b30d6d3df986fcc5", + "sha256:68b13ac49becfaba5b77359559812daac0c5c4b3c0d43cdb293a2dec8db95c24", + "sha256:799743a342249c9b9529abd1115a3f81754800e75dea254b58efdd2984009798", + "sha256:7c18218451823ca9b5131ceaacf655fe9dd4e592ebf848cb0a65fe8428bbf604", + "sha256:8140cf2665c5a07adc285bc859fdda67cfbd7edd62480dfca2211f4798502b54", + "sha256:a430d2cc52aef2af0dc45866852730fbc93463cf8cdeb179e8ee04440e0955c4", + "sha256:a45a7990d143acc37faa905d4a528f5923a5dd30f46536977d8061d10a895b09", + "sha256:aaa1f8967c3f272de80c4bec4b1379f97cd29006323f50558bd2f780a4f637ef", + "sha256:ada5ac54ac7d34bb33423da40b7f3edfc54c6b9623ac9daac7f456dbf25173ba", + "sha256:bf9fa875a9bae5318f24b0d9ab9e2c8a23bccad2979e9e4305eed8119bbe3195", + "sha256:cd3ab863a4e7d888728c949ba052a649664dea156bdd7140eb9269bbe6e33205", + "sha256:d91698257850ca5f523a5513a69775e6fb7c18129311e118996f8e9b463d11b0", + "sha256:dcefd4012593ee410ebf5728ee98f61b3401f0563c5068e760aa2b7720ca68a0", + "sha256:de6c1dad571276768fd6bc92999e8d942151552662a9048e3384cac05b148985", + "sha256:de6eaa0b7df493904d24050dcdc3db6589bd94f7e49caab57971fe47a669b3ea", + "sha256:e7d200aef16e577682dd54a79ff5f4f897a9807722b54bd8a9bca404679c609d", + "sha256:e9961413091e3c9d5c3ed671757049cc6153280f39a154a0b633608efcfdec6b", + "sha256:ec57dec41c0c8b723dd70da1864d50908c689e1c9cf43f32e9b04c0992e5d93d" ], "markers": "python_version >= '3.7'", - "version": "==6.3" + "version": "==6.4.post1" }, "zstandard": { "hashes": [ @@ -4187,62 +4194,62 @@ }, "griffe": { "hashes": [ - "sha256:85cb2868d026ea51c89bdd589ad3ccc94abc5bd8d5d948e3d4450778a2a05b4a", - "sha256:90fe5c90e1b0ca7dd6fee78f9009f4e01b37dbc9ab484a9b2c1578915db1e571" + "sha256:297ec8530d0c68e5b98ff86fb588ebc3aa3559bb5dc21f3caea8d9542a350133", + "sha256:83ce7dcaafd8cb7f43cbf1a455155015a1eb624b1ffd93249e5e1c4a22b2fdb2" ], "markers": "python_version >= '3.8'", - "version": "==0.45.0" + "version": "==0.45.2" }, "grpcio": { "hashes": [ - "sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3", - "sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094", - "sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b", - "sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d", - "sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2", - "sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172", - "sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d", - "sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c", - "sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b", - "sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3", - "sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9", - "sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357", - "sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61", - "sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5", - "sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a", - "sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280", - "sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434", - "sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce", - "sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d", - "sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c", - "sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f", - "sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f", - "sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57", - "sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f", - "sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0", - "sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2", - "sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0", - "sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a", - "sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6", - "sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d", - "sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85", - "sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a", - "sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d", - "sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f", - "sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb", - "sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86", - "sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7", - "sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda", - "sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d", - "sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434", - "sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91", - "sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a", - "sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3", - "sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3", - "sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1", - "sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae" - ], - "version": "==1.63.0" + "sha256:01615bbcae6875eee8091e6b9414072f4e4b00d8b7e141f89635bdae7cf784e5", + "sha256:02cc9cc3f816d30f7993d0d408043b4a7d6a02346d251694d8ab1f78cc723e7e", + "sha256:0b2dfe6dcace264807d9123d483d4c43274e3f8c39f90ff51de538245d7a4145", + "sha256:0da1d921f8e4bcee307aeef6c7095eb26e617c471f8cb1c454fd389c5c296d1e", + "sha256:0f30596cdcbed3c98024fb4f1d91745146385b3f9fd10c9f2270cbfe2ed7ed91", + "sha256:1ce4cd5a61d4532651079e7aae0fedf9a80e613eed895d5b9743e66b52d15812", + "sha256:1f279ad72dd7d64412e10f2443f9f34872a938c67387863c4cd2fb837f53e7d2", + "sha256:1f5de082d936e0208ce8db9095821361dfa97af8767a6607ae71425ac8ace15c", + "sha256:1f8ea18b928e539046bb5f9c124d717fbf00cc4b2d960ae0b8468562846f5aa1", + "sha256:2186d76a7e383e1466e0ea2b0febc343ffeae13928c63c6ec6826533c2d69590", + "sha256:23b6887bb21d77649d022fa1859e05853fdc2e60682fd86c3db652a555a282e0", + "sha256:257baf07f53a571c215eebe9679c3058a313fd1d1f7c4eede5a8660108c52d9c", + "sha256:2a18090371d138a57714ee9bffd6c9c9cb2e02ce42c681aac093ae1e7189ed21", + "sha256:2e8fabe2cc57a369638ab1ad8e6043721014fdf9a13baa7c0e35995d3a4a7618", + "sha256:3161a8f8bb38077a6470508c1a7301cd54301c53b8a34bb83e3c9764874ecabd", + "sha256:31890b24d47b62cc27da49a462efe3d02f3c120edb0e6c46dcc0025506acf004", + "sha256:3550493ac1d23198d46dc9c9b24b411cef613798dc31160c7138568ec26bc9b4", + "sha256:3b09c3d9de95461214a11d82cc0e6a46a6f4e1f91834b50782f932895215e5db", + "sha256:3d2004e85cf5213995d09408501f82c8534700d2babeb81dfdba2a3bff0bb396", + "sha256:46b8b43ba6a2a8f3103f103f97996cad507bcfd72359af6516363c48793d5a7b", + "sha256:579dd9fb11bc73f0de061cab5f8b2def21480fd99eb3743ed041ad6a1913ee2f", + "sha256:597191370951b477b7a1441e1aaa5cacebeb46a3b0bd240ec3bb2f28298c7553", + "sha256:59c68df3a934a586c3473d15956d23a618b8f05b5e7a3a904d40300e9c69cbf0", + "sha256:5a56797dea8c02e7d3a85dfea879f286175cf4d14fbd9ab3ef2477277b927baa", + "sha256:650a8150a9b288f40d5b7c1d5400cc11724eae50bd1f501a66e1ea949173649b", + "sha256:6d5541eb460d73a07418524fb64dcfe0adfbcd32e2dac0f8f90ce5b9dd6c046c", + "sha256:6ec5ed15b4ffe56e2c6bc76af45e6b591c9be0224b3fb090adfb205c9012367d", + "sha256:73f84f9e5985a532e47880b3924867de16fa1aa513fff9b26106220c253c70c5", + "sha256:753cb58683ba0c545306f4e17dabf468d29cb6f6b11832e1e432160bb3f8403c", + "sha256:7c1f5b2298244472bcda49b599be04579f26425af0fd80d3f2eb5fd8bc84d106", + "sha256:7e013428ab472892830287dd082b7d129f4d8afef49227a28223a77337555eaa", + "sha256:7f17572dc9acd5e6dfd3014d10c0b533e9f79cd9517fc10b0225746f4c24b58e", + "sha256:85fda90b81da25993aa47fae66cae747b921f8f6777550895fb62375b776a231", + "sha256:874c741c8a66f0834f653a69e7e64b4e67fcd4a8d40296919b93bab2ccc780ba", + "sha256:8d598b5d5e2c9115d7fb7e2cb5508d14286af506a75950762aa1372d60e41851", + "sha256:8de0399b983f8676a7ccfdd45e5b2caec74a7e3cc576c6b1eecf3b3680deda5e", + "sha256:a053584079b793a54bece4a7d1d1b5c0645bdbee729215cd433703dc2532f72b", + "sha256:a54362f03d4dcfae63be455d0a7d4c1403673498b92c6bfe22157d935b57c7a9", + "sha256:aca4f15427d2df592e0c8f3d38847e25135e4092d7f70f02452c0e90d6a02d6d", + "sha256:b2cbdfba18408389a1371f8c2af1659119e1831e5ed24c240cae9e27b4abc38d", + "sha256:b52e1ec7185512103dd47d41cf34ea78e7a7361ba460187ddd2416b480e0938c", + "sha256:c46fb6bfca17bfc49f011eb53416e61472fa96caa0979b4329176bdd38cbbf2a", + "sha256:c56c91bd2923ddb6e7ed28ebb66d15633b03e0df22206f22dfcdde08047e0a48", + "sha256:cf4c8daed18ae2be2f1fc7d613a76ee2a2e28fdf2412d5c128be23144d28283d", + "sha256:d7b7bf346391dffa182fba42506adf3a84f4a718a05e445b37824136047686a1", + "sha256:d9171f025a196f5bcfec7e8e7ffb7c3535f7d60aecd3503f9e250296c7cfc150" + ], + "version": "==1.64.0" }, "grpcio-status": { "hashes": [ @@ -4418,12 +4425,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:4627fc3f15de2cba2bde9debc2fd59b9888ef494beabfe67eb352e23d14bf288", - "sha256:ffd08a5beaef3cd135aceb58ded8b98bbbbf2b70e5b656f6a14a63c917d9b001" + "sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34", + "sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.23" + "version": "==9.5.24" }, "mkdocs-material-extensions": { "hashes": [ @@ -4444,12 +4451,12 @@ }, "mkdocstrings-python": { "hashes": [ - "sha256:5fd41a603bc6d80ff21a3c42413fe51f1d22afde09ee419eab1e2b8e9cdaf5c4", - "sha256:7fcfefba80d2f05f198ec072e404d216b969cdff9ebe2d4903b2f7d515f910e1" + "sha256:11ff6d21d3818fb03af82c3ea6225b1534837e17f790aa5f09626524171f949b", + "sha256:321cf9c732907ab2b1fedaafa28765eaa089d89320f35f7206d00ea266889d03" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.10.1" + "version": "==1.10.3" }, "nodeenv": { "hashes": [ @@ -4491,12 +4498,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": [ @@ -4614,20 +4621,20 @@ }, "pytest": { "hashes": [ - "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", - "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f" + "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", + "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" ], "markers": "python_version >= '3.8'", - "version": "==8.2.0" + "version": "==8.2.1" }, "pytest-asyncio": { "hashes": [ - "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", - "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f" + "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b", + "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.6" + "version": "==0.23.7" }, "pytest-cov": { "hashes": [ @@ -4817,12 +4824,12 @@ }, "requests": { "hashes": [ - "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", - "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" + "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", + "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "markers": "python_version >= '3.8'", + "version": "==2.32.2" }, "requests-oauthlib": { "hashes": [ @@ -4842,11 +4849,11 @@ }, "setuptools": { "hashes": [ - "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", - "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" + "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", + "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0" ], "markers": "python_version >= '3.8'", - "version": "==69.5.1" + "version": "==70.0.0" }, "six": { "hashes": [ @@ -4889,38 +4896,41 @@ }, "watchdog": { "hashes": [ - "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257", - "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca", - "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b", - "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85", - "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b", - "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19", - "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50", - "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92", - "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269", - "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f", - "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c", - "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b", - "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87", - "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b", - "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b", - "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8", - "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c", - "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3", - "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7", - "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605", - "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935", - "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b", - "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927", - "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101", - "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07", - "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec", - "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4", - "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245", - "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d" + "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7", + "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", + "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175", + "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", + "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", + "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", + "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", + "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", + "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7", + "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28", + "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235", + "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", + "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", + "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5", + "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709", + "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee", + "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", + "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd", + "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba", + "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db", + "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682", + "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35", + "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", + "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645", + "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", + "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193", + "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b", + "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", + "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b", + "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625", + "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", + "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5" ], "markers": "python_version >= '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "yapf": { "hashes": [ @@ -4933,11 +4943,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": [ @@ -4949,45 +4959,39 @@ }, "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:065f8ff0e034e43b8da05ffed308a9e3311720a2b13b83724f26a8dd6709964d", + "sha256:0940b3b44b6cc0375ea0da5fefee05a9abe8bb53594a3a6e4aafb9f99dc5de8d", + "sha256:15fa208d7a802c0dd3e9d4d5336619a37efd57f2d2ce830d9f9d5843a2b7daba", + "sha256:16f73c42f10f761051157332943ee1f7cf973cc1c78a50d1960c313a211cca4a", + "sha256:18f4061c3456c61557e9d7068e435f5db164b38f15f3d9bd995ff185c6db2c62", + "sha256:1e0678885d07e865047e15be3ebe5c87903cc7f5ca5edfd0045d1c7b43f7fe9d", + "sha256:297ee171f40f8f18665bb75f576df7d1ddce19f3e6696ef6acb930dcbfbf693f", + "sha256:36234d3b8211d053c42684666c2a04eb1a35e0cec6bc3e54586bb60fb0be3b17", + "sha256:3a00983c5c793b17b829020e11032dabab023c4e0ef12f134b90df802ae5adf2", + "sha256:3ca89624d0eabc7ce4f299c6d621531cb8b0ebac3bb4f9ebf2d057477602e1b8", + "sha256:45fb6fe8b5852564e63d6705a7904530a7c886056e6e9aaf938dc5e2bc637097", + "sha256:52901ff6d75a4332457610cbd2883f39b386c5bebe0745ecf78e3fe22cfdd0d9", + "sha256:563a7192a5baf8d9b189dc598c3555e695e00fdce3eafb88b30d6d3df986fcc5", + "sha256:68b13ac49becfaba5b77359559812daac0c5c4b3c0d43cdb293a2dec8db95c24", + "sha256:799743a342249c9b9529abd1115a3f81754800e75dea254b58efdd2984009798", + "sha256:7c18218451823ca9b5131ceaacf655fe9dd4e592ebf848cb0a65fe8428bbf604", + "sha256:8140cf2665c5a07adc285bc859fdda67cfbd7edd62480dfca2211f4798502b54", + "sha256:a430d2cc52aef2af0dc45866852730fbc93463cf8cdeb179e8ee04440e0955c4", + "sha256:a45a7990d143acc37faa905d4a528f5923a5dd30f46536977d8061d10a895b09", + "sha256:aaa1f8967c3f272de80c4bec4b1379f97cd29006323f50558bd2f780a4f637ef", + "sha256:ada5ac54ac7d34bb33423da40b7f3edfc54c6b9623ac9daac7f456dbf25173ba", + "sha256:bf9fa875a9bae5318f24b0d9ab9e2c8a23bccad2979e9e4305eed8119bbe3195", + "sha256:cd3ab863a4e7d888728c949ba052a649664dea156bdd7140eb9269bbe6e33205", + "sha256:d91698257850ca5f523a5513a69775e6fb7c18129311e118996f8e9b463d11b0", + "sha256:dcefd4012593ee410ebf5728ee98f61b3401f0563c5068e760aa2b7720ca68a0", + "sha256:de6c1dad571276768fd6bc92999e8d942151552662a9048e3384cac05b148985", + "sha256:de6eaa0b7df493904d24050dcdc3db6589bd94f7e49caab57971fe47a669b3ea", + "sha256:e7d200aef16e577682dd54a79ff5f4f897a9807722b54bd8a9bca404679c609d", + "sha256:e9961413091e3c9d5c3ed671757049cc6153280f39a154a0b633608efcfdec6b", + "sha256:ec57dec41c0c8b723dd70da1864d50908c689e1c9cf43f32e9b04c0992e5d93d" ], "markers": "python_version >= '3.7'", - "version": "==6.3" + "version": "==6.4.post1" } } } diff --git a/breathecode/assignments/serializers.py b/breathecode/assignments/serializers.py index 5403a0984..43fb40e30 100644 --- a/breathecode/assignments/serializers.py +++ b/breathecode/assignments/serializers.py @@ -15,6 +15,17 @@ logger = logging.getLogger(__name__) +class ProfileSmallSerializer(serpy.Serializer): + avatar_url = serpy.Field() + + +class UserMediumSerializer(serpy.Serializer): + id = serpy.Field() + first_name = serpy.Field() + last_name = serpy.Field() + profile = ProfileSmallSerializer() + + class UserSmallSerializer(serpy.Serializer): id = serpy.Field() first_name = serpy.Field() @@ -227,8 +238,8 @@ def validate(self, data): return data def update(self, instance, validated_data): - if 'opened_at' in validated_data and validated_data['opened_at'] is not None and (instance.opened_at is None - or validated_data['opened_at'] > instance.opened_at): + if 'opened_at' in validated_data and validated_data['opened_at'] is not None and ( + instance.opened_at is None or validated_data['opened_at'] > instance.opened_at): tasks_activity.add_activity.delay(self.context['request'].user.id, 'read_assignment', related_type='assignments.Task', @@ -277,7 +288,7 @@ class FinalProjectGETSerializer(serpy.Serializer): members = serpy.MethodField() def get_members(self, obj): - return [UserSmallSerializer(m).data for m in obj.members.all()] + return [UserMediumSerializer(m).data for m in obj.members.all()] class PostFinalProjectSerializer(serializers.ModelSerializer): diff --git a/breathecode/assignments/tests/urls/tests_final_project_cohort.py b/breathecode/assignments/tests/urls/tests_final_project_cohort.py index 49342b741..08d264fc0 100644 --- a/breathecode/assignments/tests/urls/tests_final_project_cohort.py +++ b/breathecode/assignments/tests/urls/tests_final_project_cohort.py @@ -52,6 +52,9 @@ def members_serializer(member, data={}): 'id': member.id, 'first_name': member.first_name, 'last_name': member.last_name, + 'profile': { + 'avatar_url': member.profile.avatar_url + } if member.profile is not None else None } From 3e12589a5cdc71426fb5273e31e2a2ebd4e90982 Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Fri, 24 May 2024 15:13:26 +0000 Subject: [PATCH 056/114] fix bug with profile serializer --- breathecode/assignments/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/assignments/serializers.py b/breathecode/assignments/serializers.py index 43fb40e30..0d1fba69b 100644 --- a/breathecode/assignments/serializers.py +++ b/breathecode/assignments/serializers.py @@ -23,7 +23,7 @@ class UserMediumSerializer(serpy.Serializer): id = serpy.Field() first_name = serpy.Field() last_name = serpy.Field() - profile = ProfileSmallSerializer() + profile = ProfileSmallSerializer(required=False) class UserSmallSerializer(serpy.Serializer): From b54222f802dd261cf88f7c74000bbf37ec12a23c Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez Date: Fri, 24 May 2024 17:20:40 +0000 Subject: [PATCH 057/114] Webhook for subscriptions added --- breathecode/notify/receivers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index d13e5a20a..d0534eaf3 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -22,6 +22,7 @@ from breathecode.registry.serializers import AssetHookSerializer from breathecode.registry.signals import asset_status_updated from breathecode.payments.signals import subscription_created, planfinancing_created +from breathecode.payments.models import PlanFinancing, Subscription from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionSerializer from .tasks import send_mentorship_starting_notification From dceabf815effd898b252d01e5dc554fef7cc77b3 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 28 May 2024 00:03:18 -0500 Subject: [PATCH 058/114] update make charges command --- .../management/commands/make_charges.py | 15 +- breathecode/payments/tasks.py | 6 + .../management/commands/tests_make_charges.py | 360 +++++++++--------- .../tasks/tests_charge_plan_financing.py | 53 +++ .../tests/tasks/tests_charge_subscription.py | 57 ++- 5 files changed, 313 insertions(+), 178 deletions(-) diff --git a/breathecode/payments/management/commands/make_charges.py b/breathecode/payments/management/commands/make_charges.py index a8374a589..cbfb89d9a 100644 --- a/breathecode/payments/management/commands/make_charges.py +++ b/breathecode/payments/management/commands/make_charges.py @@ -1,10 +1,12 @@ from datetime import timedelta + from django.core.management.base import BaseCommand -from breathecode.payments import tasks from django.db.models.query_utils import Q +from django.utils import timezone + +from breathecode.payments import tasks from ...models import PlanFinancing, Subscription -from django.utils import timezone # renew the subscriptions every 1 hours @@ -18,8 +20,13 @@ def handle(self, *args, **options): avoid_expire_these_statuses = Q(status='EXPIRED') | Q(status='ERROR') | Q(status='PAYMENT_ISSUE') | Q( status='FULLY_PAID') | Q(status='FREE_TRIAL') | Q(status='CANCELLED') | Q(status='DEPRECATED') - subscriptions = Subscription.objects.filter(valid_until__lte=utc_now + timedelta(days=1)) - plan_financings = PlanFinancing.objects.filter(valid_until__lte=utc_now + timedelta(days=1)) + args = (Q(valid_until__isnull=True) | Q(valid_until__gt=utc_now), ) + params = { + 'next_payment_at__lte': utc_now + timedelta(days=1), + } + + subscriptions = Subscription.objects.filter(*args, **params) + plan_financings = PlanFinancing.objects.filter(*args, **params) Subscription.objects.filter(valid_until__lte=utc_now).exclude(avoid_expire_these_statuses).update( status='EXPIRED') diff --git a/breathecode/payments/tasks.py b/breathecode/payments/tasks.py index c54200d12..a8fdead90 100644 --- a/breathecode/payments/tasks.py +++ b/breathecode/payments/tasks.py @@ -250,6 +250,9 @@ def charge_subscription(self, subscription_id: int, **_: Any): if subscription.valid_until and subscription.valid_until < utc_now: raise AbortTask(f'The subscription {subscription.id} is over') + if subscription.next_payment_at > utc_now: + raise AbortTask(f'The subscription with id {subscription_id} was paid this month') + settings = get_user_settings(subscription.user.id) try: @@ -363,6 +366,9 @@ def charge_plan_financing(self, plan_financing_id: int, **_: Any): if plan_financing.valid_until < utc_now: raise AbortTask(f'PlanFinancing with id {plan_financing_id} is over') + if plan_financing.next_payment_at > utc_now: + raise AbortTask(f'PlanFinancing with id {plan_financing_id} was paid this month') + settings = get_user_settings(plan_financing.user.id) try: diff --git a/breathecode/payments/tests/management/commands/tests_make_charges.py b/breathecode/payments/tests/management/commands/tests_make_charges.py index d06439675..6362471bd 100644 --- a/breathecode/payments/tests/management/commands/tests_make_charges.py +++ b/breathecode/payments/tests/management/commands/tests_make_charges.py @@ -1,178 +1,196 @@ import random -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch + +import pytest +from dateutil.relativedelta import relativedelta +from django.utils import timezone + from breathecode.payments import tasks from breathecode.payments.management.commands.make_charges import Command -from breathecode.payments.tests.mixins import PaymentsTestCase -from django.utils import timezone -from dateutil.relativedelta import relativedelta +from breathecode.tests.mixins.breathecode_mixin.breathecode import Breathecode UTC_NOW = timezone.now() -class SlackTestSuite(PaymentsTestCase): - """ - 🔽🔽🔽 Subscription cases - """ - - @patch('breathecode.payments.tasks.charge_subscription.delay', MagicMock()) - def test_with_zero_subscriptions(self): - """Testing when context is None or not provided.""" - - command = Command() - result = command.handle() - - self.assertEqual(result, None) - self.assertEqual(self.bc.database.list_of('payments.Subscription'), []) - self.assertEqual(tasks.charge_subscription.delay.call_args_list, []) - - @patch('breathecode.payments.tasks.charge_subscription.delay', MagicMock()) - def test_with_two_subscriptions__wrong_cases(self): - """Testing when context is None or not provided.""" - utc_now = timezone.now() - cases = [ - (utc_now + relativedelta(days=1, minutes=1), 'ACTIVE'), - (utc_now + relativedelta(days=1, minutes=1), 'ERROR'), - (utc_now + relativedelta(days=1, minutes=1), 'PAYMENT_ISSUE'), - (utc_now - relativedelta(days=1, seconds=1), 'CANCELLED'), - (utc_now - relativedelta(days=1, seconds=1), 'FREE_TRIAL'), - (utc_now - relativedelta(days=1, seconds=1), 'DEPRECATED'), - ] - for valid_until, status in cases: - subscription = {'valid_until': valid_until, 'status': status} - - model = self.bc.database.create(subscription=(2, subscription)) - - command = Command() - result = command.handle() - - self.assertEqual(result, None) - self.assertEqual( - self.bc.database.list_of('payments.Subscription'), - self.bc.format.to_dict(model.subscription), - ) - self.assertEqual(tasks.charge_subscription.delay.call_args_list, []) - - # teardown - self.bc.database.delete('payments.Subscription') - - @patch('breathecode.payments.tasks.charge_subscription.delay', MagicMock()) - def test_with_two_subscriptions__valid_cases(self): - """Testing when context is None or not provided.""" - utc_now = timezone.now() - cases = [ - (utc_now - relativedelta(days=1, seconds=1), 'ACTIVE', True), - (utc_now - relativedelta(days=1, seconds=1), 'ERROR', False), - (utc_now - relativedelta(days=1, seconds=1), 'PAYMENT_ISSUE', False), - ] - for valid_until, status, status_changed in cases: - subscription = {'valid_until': valid_until, 'status': status} - - model = self.bc.database.create(subscription=(2, subscription)) - - command = Command() - result = command.handle() - - self.assertEqual(result, None) - db = self.bc.format.to_dict(model.subscription) - for i in range(len(db)): - if status_changed: - db[i]['status'] = 'EXPIRED' - - self.assertEqual(self.bc.database.list_of('payments.Subscription'), db) - self.assertEqual(tasks.charge_subscription.delay.call_args_list, [ - call(model.subscription[0].id), - call(model.subscription[1].id), - ]) - - # teardown - self.bc.database.delete('payments.Subscription') - tasks.charge_subscription.delay.call_args_list = [] - - """ - 🔽🔽🔽 PlanFinancing cases - """ - - @patch('breathecode.payments.tasks.charge_plan_financing.delay', MagicMock()) - def test_with_zero_plan_financings(self): - """Testing when context is None or not provided.""" - - command = Command() - result = command.handle() - - self.assertEqual(result, None) - self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), []) - self.assertEqual(tasks.charge_plan_financing.delay.call_args_list, []) - - @patch('breathecode.payments.tasks.charge_plan_financing.delay', MagicMock()) - def test_with_two_plan_financings__wrong_cases(self): - """Testing when context is None or not provided.""" - utc_now = timezone.now() - cases = [ - (utc_now + relativedelta(days=1, minutes=1), 'ACTIVE'), - (utc_now + relativedelta(days=1, minutes=1), 'ERROR'), - (utc_now + relativedelta(days=1, minutes=1), 'PAYMENT_ISSUE'), - (utc_now - relativedelta(days=1, seconds=1), 'CANCELLED'), - (utc_now - relativedelta(days=1, seconds=1), 'FREE_TRIAL'), - (utc_now - relativedelta(days=1, seconds=1), 'DEPRECATED'), - ] - for valid_until, status in cases: - plan_financing = { - 'valid_until': valid_until, - 'status': status, - 'monthly_price': (random.random() * 99) + 1, - 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), - } - - model = self.bc.database.create(plan_financing=(2, plan_financing)) - - command = Command() - result = command.handle() - - self.assertEqual(result, None) - self.assertEqual( - self.bc.database.list_of('payments.PlanFinancing'), - self.bc.format.to_dict(model.plan_financing), - ) - self.assertEqual(tasks.charge_plan_financing.delay.call_args_list, []) - - # teardown - self.bc.database.delete('payments.PlanFinancing') - - @patch('breathecode.payments.tasks.charge_plan_financing.delay', MagicMock()) - def test_with_two_plan_financings__valid_cases(self): - """Testing when context is None or not provided.""" - utc_now = timezone.now() - cases = [ - (utc_now - relativedelta(days=1, seconds=1), 'ACTIVE', True), - (utc_now - relativedelta(days=1, seconds=1), 'ERROR', False), - (utc_now - relativedelta(days=1, seconds=1), 'PAYMENT_ISSUE', False), - ] - for valid_until, status, status_changed in cases: - plan_financing = { - 'valid_until': valid_until, - 'status': status, - 'monthly_price': (random.random() * 99) + 1, - 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), - } - - model = self.bc.database.create(plan_financing=(2, plan_financing)) - - command = Command() - result = command.handle() - - self.assertEqual(result, None) - - db = self.bc.format.to_dict(model.plan_financing) - for i in range(len(db)): - if status_changed: - db[i]['status'] = 'EXPIRED' - - self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), db) - self.assertEqual(tasks.charge_plan_financing.delay.call_args_list, [ - call(model.plan_financing[0].id), - call(model.plan_financing[1].id), - ]) - - # teardown - self.bc.database.delete('payments.PlanFinancing') - tasks.charge_plan_financing.delay.call_args_list = [] +@pytest.fixture(autouse=True) +def setup(db: None, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(tasks.charge_subscription, 'delay', MagicMock()) + monkeypatch.setattr(tasks.charge_plan_financing, 'delay', MagicMock()) + + +def test_with_zero_subscriptions(bc: Breathecode): + command = Command() + result = command.handle() + + assert result == None + assert bc.database.list_of('payments.Subscription') == [] + assert tasks.charge_subscription.delay.call_args_list == [] + + +@pytest.mark.parametrize('delta, status', [ + (relativedelta(days=1, minutes=1), 'ACTIVE'), + (relativedelta(days=1, minutes=1), 'ERROR'), + (relativedelta(days=1, minutes=1), 'PAYMENT_ISSUE'), + (-relativedelta(days=1, seconds=1), 'CANCELLED'), + (-relativedelta(days=1, seconds=1), 'FREE_TRIAL'), + (-relativedelta(days=1, seconds=1), 'DEPRECATED'), +]) +def test_with_two_subscriptions__wrong_cases(bc: Breathecode, delta, status, utc_now): + valid_until = utc_now + delta + subscription = {'valid_until': valid_until, 'next_payment_at': valid_until, 'status': status} + + model = bc.database.create(subscription=(2, subscription)) + + command = Command() + result = command.handle() + + assert result == None + assert bc.database.list_of('payments.Subscription') == bc.format.to_dict(model.subscription) + assert tasks.charge_subscription.delay.call_args_list == [] + + +@pytest.mark.parametrize('delta, status, status_changed', [ + (-relativedelta(days=1, seconds=1), 'ACTIVE', True), + (-relativedelta(days=1, seconds=1), 'ERROR', False), + (-relativedelta(days=1, seconds=1), 'PAYMENT_ISSUE', False), +]) +def test_with_two_subscriptions__expired(bc: Breathecode, delta, status, status_changed, utc_now): + valid_until = utc_now + delta + subscription = {'valid_until': valid_until, 'next_payment_at': valid_until, 'status': status} + + model = bc.database.create(subscription=(2, subscription)) + + command = Command() + result = command.handle() + + assert result == None + db = bc.format.to_dict(model.subscription) + for i in range(len(db)): + if status_changed: + db[i]['status'] = 'EXPIRED' + + assert bc.database.list_of('payments.Subscription') == db + assert tasks.charge_subscription.delay.call_args_list == [] + + +@pytest.mark.parametrize('delta, status', [ + (relativedelta(days=2, seconds=1), 'ACTIVE'), + (relativedelta(days=2, seconds=1), 'ERROR'), + (relativedelta(days=2, seconds=1), 'PAYMENT_ISSUE'), +]) +def test_with_two_subscriptions__valid_cases(bc: Breathecode, delta, status, utc_now): + valid_until = utc_now + delta + next_payment_at = utc_now - delta + subscription = {'valid_until': valid_until, 'next_payment_at': next_payment_at, 'status': status} + + model = bc.database.create(subscription=(2, subscription)) + + command = Command() + result = command.handle() + + assert result == None + assert bc.database.list_of('payments.Subscription') == bc.format.to_dict(model.subscription) + assert tasks.charge_subscription.delay.call_args_list == [ + call(model.subscription[0].id), + call(model.subscription[1].id), + ] + + +# 🔽🔽🔽 PlanFinancing cases + + +def test_with_zero_plan_financings(bc: Breathecode): + command = Command() + result = command.handle() + + assert result == None + assert bc.database.list_of('payments.PlanFinancing') == [] + assert tasks.charge_plan_financing.delay.call_args_list == [] + + +@pytest.mark.parametrize('delta, status', [ + (relativedelta(days=1, minutes=1), 'ACTIVE'), + (relativedelta(days=1, minutes=1), 'ERROR'), + (relativedelta(days=1, minutes=1), 'PAYMENT_ISSUE'), + (-relativedelta(days=1, seconds=1), 'CANCELLED'), + (-relativedelta(days=1, seconds=1), 'FREE_TRIAL'), + (-relativedelta(days=1, seconds=1), 'DEPRECATED'), +]) +def test_with_two_plan_financings__wrong_cases(bc: Breathecode, delta, status, utc_now): + valid_until = utc_now + delta + plan_financing = { + 'next_payment_at': valid_until, + 'valid_until': UTC_NOW + relativedelta(months=random.randint(12, 24)), + 'status': status, + 'monthly_price': (random.random() * 99) + 1, + 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + } + + model = bc.database.create(plan_financing=(2, plan_financing)) + + command = Command() + result = command.handle() + + assert result == None + assert bc.database.list_of('payments.PlanFinancing') == bc.format.to_dict(model.plan_financing) + assert tasks.charge_plan_financing.delay.call_args_list == [] + + +@pytest.mark.parametrize('delta, status, status_changed', [ + (-relativedelta(days=1, seconds=1), 'ACTIVE', True), + (-relativedelta(days=1, seconds=1), 'ERROR', False), + (-relativedelta(days=1, seconds=1), 'PAYMENT_ISSUE', False), +]) +def test_with_two_plan_financings__expired(bc: Breathecode, delta, status, status_changed, utc_now): + valid_until = utc_now + delta + plan_financing = { + 'next_payment_at': valid_until, + 'valid_until': valid_until, + 'status': status, + 'monthly_price': (random.random() * 99) + 1, + 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + } + + model = bc.database.create(plan_financing=(2, plan_financing)) + + command = Command() + result = command.handle() + + assert result == None + + db = bc.format.to_dict(model.plan_financing) + for i in range(len(db)): + if status_changed: + db[i]['status'] = 'EXPIRED' + + assert bc.database.list_of('payments.PlanFinancing') == db + assert tasks.charge_plan_financing.delay.call_args_list == [] + + +@pytest.mark.parametrize('delta, status', [ + (relativedelta(days=2, seconds=1), 'ACTIVE'), + (relativedelta(days=2, seconds=1), 'ERROR'), + (relativedelta(days=2, seconds=1), 'PAYMENT_ISSUE'), +]) +def test_with_two_plan_financings__valid_cases(bc: Breathecode, delta, status, utc_now): + valid_until = utc_now + delta + next_payment_at = utc_now - delta + plan_financing = { + 'next_payment_at': next_payment_at, + 'valid_until': valid_until, + 'status': status, + 'monthly_price': (random.random() * 99) + 1, + 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + } + + model = bc.database.create(plan_financing=(2, plan_financing)) + + command = Command() + result = command.handle() + + assert result == None + assert bc.database.list_of('payments.PlanFinancing') == bc.format.to_dict(model.plan_financing) + assert tasks.charge_plan_financing.delay.call_args_list == [ + call(model.plan_financing[0].id), + call(model.plan_financing[1].id), + ] diff --git a/breathecode/payments/tests/tasks/tests_charge_plan_financing.py b/breathecode/payments/tests/tasks/tests_charge_plan_financing.py index 5743caada..9279d5ca0 100644 --- a/breathecode/payments/tests/tasks/tests_charge_plan_financing.py +++ b/breathecode/payments/tests/tasks/tests_charge_plan_financing.py @@ -371,6 +371,59 @@ def test_plan_financing_is_over(self): call(1, 'bag_created', related_type='payments.Bag', related_id=1), ]) + """ + 🔽🔽🔽 PlanFinancing was paid + """ + + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.notify.actions.send_email_message', MagicMock()) + @patch('breathecode.payments.tasks.renew_plan_financing_consumables.delay', MagicMock()) + @patch('mixer.main.LOGGER.info', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + def test_plan_financing_was_paid(self): + plan_financing = { + 'valid_until': UTC_NOW + relativedelta(minutes=1), + 'next_payment_at': UTC_NOW + relativedelta(minutes=1), + 'monthly_price': (random.random() * 99) + 1, + 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + } + plan = {'is_renewable': False} + model = self.bc.database.create(plan_financing=plan_financing, invoice=1, plan=plan) + + with patch('breathecode.payments.services.stripe.Stripe.pay', MagicMock(side_effect=Exception('fake error'))): + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + charge_plan_financing.delay(1) + + self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) + + self.assertEqual(logging.Logger.info.call_args_list, [ + call('Starting charge_plan_financing for id 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('PlanFinancing with id 1 was paid this month', exc_info=True), + ]) + + self.assertEqual(self.bc.database.list_of('payments.Bag'), [ + self.bc.format.to_dict(model.bag), + ]) + self.assertEqual(self.bc.database.list_of('payments.Invoice'), [ + self.bc.format.to_dict(model.invoice), + ]) + + self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), [ + { + **self.bc.format.to_dict(model.plan_financing), + }, + ]) + self.assertEqual(notify_actions.send_email_message.call_args_list, []) + self.bc.check.calls(activity_tasks.add_activity.delay.call_args_list, [ + call(1, 'bag_created', related_type='payments.Bag', related_id=1), + ]) + """ 🔽🔽🔽 PlanFinancing try to charge, but a undexpected exception is raised, the database is rollbacked and the error is register in PlanFinancing diff --git a/breathecode/payments/tests/tasks/tests_charge_subscription.py b/breathecode/payments/tests/tasks/tests_charge_subscription.py index a55243957..230adb19e 100644 --- a/breathecode/payments/tests/tasks/tests_charge_subscription.py +++ b/breathecode/payments/tests/tasks/tests_charge_subscription.py @@ -10,13 +10,10 @@ from dateutil.relativedelta import relativedelta from django.utils import timezone from mixer.backend.django import mixer -from rest_framework.test import APIClient import breathecode.activity.tasks as activity_tasks from breathecode.notify import actions as notify_actions from breathecode.payments.services import Stripe -from breathecode.tests.mixins.breathecode_mixin import Breathecode -from breathecode.tests.mixins.legacy import LegacyAPITestCase from ...tasks import charge_subscription from ..mixins import PaymentsTestCase @@ -368,6 +365,60 @@ def test_subscription_is_over(self): call(1, 'bag_created', related_type='payments.Bag', related_id=1), ]) + """ + 🔽🔽🔽 Subscription was paid + """ + + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.notify.actions.send_email_message', MagicMock()) + @patch('breathecode.payments.tasks.renew_subscription_consumables.delay', MagicMock()) + @patch('mixer.main.LOGGER.info', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + def test_subscription_was_paid(self): + unit = random.choice([1, 3, 6, 12]) + unit_type = 'MONTH' + subscription = { + 'pay_every': unit, + 'pay_every_unit': unit_type, + 'valid_until': UTC_NOW + relativedelta(seconds=1), + 'next_payment_at': UTC_NOW + relativedelta(seconds=1), + } + model = self.bc.database.create(subscription=subscription, invoice=1) + + with patch('breathecode.payments.services.stripe.Stripe.pay', MagicMock(side_effect=Exception('fake error'))): + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + charge_subscription.delay(1) + + self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) + + self.assertEqual(logging.Logger.info.call_args_list, [ + call('Starting charge_subscription for subscription 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('The subscription with id 1 was paid this month', exc_info=True), + ]) + + self.assertEqual(self.bc.database.list_of('payments.Bag'), [ + self.bc.format.to_dict(model.bag), + ]) + self.assertEqual(self.bc.database.list_of('payments.Invoice'), [ + self.bc.format.to_dict(model.invoice), + ]) + + self.assertEqual(self.bc.database.list_of('payments.Subscription'), [ + { + **self.bc.format.to_dict(model.subscription), + }, + ]) + self.assertEqual(notify_actions.send_email_message.call_args_list, []) + self.bc.check.calls(activity_tasks.add_activity.delay.call_args_list, [ + call(1, 'bag_created', related_type='payments.Bag', related_id=1), + ]) + """ 🔽🔽🔽 Subscription try to charge, but a undexpected exception is raised, the database is rollbacked and the error is register in Subscription From 9098034a9fe1a502d4c33e2d569e82ca7b5dfec0 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 28 May 2024 12:28:28 -0400 Subject: [PATCH 059/114] Update settings.py --- breathecode/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/breathecode/settings.py b/breathecode/settings.py index 0eef4ca0f..e68e07186 100644 --- a/breathecode/settings.py +++ b/breathecode/settings.py @@ -517,6 +517,8 @@ def get(self, key, *args, **kwargs): 'form_entry.won_or_lost': 'marketing.FormEntry.won_or_lost', 'form_entry.new_deal': 'marketing.FormEntry.new_deal', 'session.mentorship_session_status': 'mentorship.MentorshipSession.mentorship_session_status', + 'planfinancing.planfinancing_created': 'payments.PlanFinancing.planfinancing_created', + 'subscription.subscription_created': 'payments.Subscription.subscription_created', } # Websocket From ff6df62d140eab1d65e84553b86286559c1dc919 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 15:08:46 -0400 Subject: [PATCH 060/114] 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 1804393e77d71c6205ba99d831f1540a1ee3fe1c Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 28 May 2024 14:14:14 -0500 Subject: [PATCH 061/114] fix tests --- .../management/commands/tests_supervisor.py | 20 ++++----- breathecode/notify/receivers.py | 6 +-- breathecode/payments/models.py | 9 ++-- capyc/core/pytest/fixtures/__init__.py | 1 + capyc/core/pytest/fixtures/fake.py | 11 ++--- capyc/core/pytest/fixtures/random.py | 11 ++--- capyc/core/pytest/fixtures/seed.py | 43 +++++++++++++++++++ capyc/django/pytest/fixtures/database.py | 2 +- conftest.py | 2 +- scripts/parallel_test.py | 26 +++-------- 10 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 capyc/core/pytest/fixtures/seed.py diff --git a/breathecode/monitoring/tests/management/commands/tests_supervisor.py b/breathecode/monitoring/tests/management/commands/tests_supervisor.py index e05640b67..445e90df3 100644 --- a/breathecode/monitoring/tests/management/commands/tests_supervisor.py +++ b/breathecode/monitoring/tests/management/commands/tests_supervisor.py @@ -88,7 +88,6 @@ def setup(): def db(data={}): return { 'delta': timedelta(seconds=3600), - 'id': 0, 'ran_at': None, 'task_module': '', 'task_name': '', @@ -96,6 +95,10 @@ def db(data={}): } +def remove_ids(dbs): + return [x for x in dbs if x.pop('id')] + + class TestIssue: @pytest.mark.parametrize('with_supervisor, with_issues', [ @@ -131,10 +134,9 @@ def tests_older_issues_are_removed(self, database: dfx.Database, supervisor: Sup } in supervisor.list() assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == [] assert db({ - 'id': 1, 'task_module': 'breathecode.payments.supervisors', 'task_name': 'supervise_all_consumption_sessions', - }) in database.list_of('monitoring.Supervisor') + }) in remove_ids(database.list_of('monitoring.Supervisor')) assert database.list_of('monitoring.SupervisorIssue') == [] assert call(supervisor.id('breathecode.payments.supervisors', 'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list @@ -166,10 +168,9 @@ def tests_recent_issues_keeps__available_attempts(self, bc: Breathecode, databas assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == [x.error for x in model.supervisor_issue] assert db({ - 'id': 1, 'task_module': 'breathecode.payments.supervisors', 'task_name': 'supervise_all_consumption_sessions', - }) in database.list_of('monitoring.Supervisor') + }) in remove_ids(database.list_of('monitoring.Supervisor')) assert database.list_of('monitoring.SupervisorIssue') == bc.format.to_dict(model.supervisor_issue) assert call(supervisor.id('breathecode.payments.supervisors', 'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list @@ -201,10 +202,9 @@ def tests_recent_issues_keeps__no_available_attempts(self, bc: Breathecode, data assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == [x.error for x in model.supervisor_issue] assert db({ - 'id': 1, 'task_module': 'breathecode.payments.supervisors', 'task_name': 'supervise_all_consumption_sessions', - }) in database.list_of('monitoring.Supervisor') + }) in remove_ids(database.list_of('monitoring.Supervisor')) assert database.list_of('monitoring.SupervisorIssue') == bc.format.to_dict(model.supervisor_issue) assert call(supervisor.id('breathecode.payments.supervisors', 'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list @@ -245,12 +245,11 @@ def tests_pending_to_be_scheduled(self, database: dfx.Database, supervisor: Supe } in supervisor.list() assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == [] assert db({ - 'id': 1, 'delta': delta, 'ran_at': ran_at, 'task_module': 'breathecode.payments.supervisors', 'task_name': 'supervise_all_consumption_sessions', - }) in database.list_of('monitoring.Supervisor') + }) in remove_ids(database.list_of('monitoring.Supervisor')) assert database.list_of('monitoring.SupervisorIssue') == [] assert call(supervisor.id('breathecode.payments.supervisors', 'supervise_all_consumption_sessions')) in run_supervisor_mock.call_args_list @@ -283,12 +282,11 @@ def tests_in_cooldown(self, database: dfx.Database, supervisor: Supervisor, patc } in supervisor.list() assert supervisor.log('breathecode.payments.supervisors', 'supervise_all_consumption_sessions') == [] assert db({ - 'id': 1, 'delta': delta, 'ran_at': ran_at, 'task_module': 'breathecode.payments.supervisors', 'task_name': 'supervise_all_consumption_sessions', - }) in database.list_of('monitoring.Supervisor') + }) in remove_ids(database.list_of('monitoring.Supervisor')) assert database.list_of('monitoring.SupervisorIssue') == [] assert call(supervisor.id('breathecode.payments.supervisors', 'supervise_all_consumption_sessions')) not in run_supervisor_mock.call_args_list diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index d0534eaf3..f2bfb8e4e 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -18,12 +18,12 @@ from breathecode.mentorship.models import MentorshipSession from breathecode.mentorship.serializers import SessionHookSerializer from breathecode.mentorship.signals import mentorship_session_status +from breathecode.payments.models import PlanFinancing, Subscription +from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionSerializer +from breathecode.payments.signals import planfinancing_created, subscription_created from breathecode.registry.models import Asset from breathecode.registry.serializers import AssetHookSerializer from breathecode.registry.signals import asset_status_updated -from breathecode.payments.signals import subscription_created, planfinancing_created -from breathecode.payments.models import PlanFinancing, Subscription -from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionSerializer from .tasks import send_mentorship_starting_notification from .utils.hook_manager import HookManager diff --git a/breathecode/payments/models.py b/breathecode/payments/models.py index 1460fae69..cb754ff3d 100644 --- a/breathecode/payments/models.py +++ b/breathecode/payments/models.py @@ -1090,11 +1090,11 @@ def save(self, *args, **kwargs) -> None: self.full_clean() on_create = self.pk is None + super().save(*args, **kwargs) + if on_create: signals.planfinancing_created.send(instance=self, sender=self.__class__) - return super().save(*args, **kwargs) - class Subscription(AbstractIOweYou): """Allows to create a subscription to a plan and services.""" @@ -1146,14 +1146,13 @@ def clean(self) -> None: def save(self, *args, **kwargs) -> None: self.full_clean() - on_create = self.pk is None + super().save(*args, **kwargs) + if on_create: signals.subscription_created.send(instance=self, sender=self.__class__) - return super().save(*args, **kwargs) - class SubscriptionServiceItem(models.Model): subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE, help_text='Subscription') diff --git a/capyc/core/pytest/fixtures/__init__.py b/capyc/core/pytest/fixtures/__init__.py index b888d480c..bc25698cd 100644 --- a/capyc/core/pytest/fixtures/__init__.py +++ b/capyc/core/pytest/fixtures/__init__.py @@ -6,3 +6,4 @@ from .image import * # noqa: F401 from .no_http_requests import * # noqa: F401 from .random import * # noqa: F401 +from .seed import * # noqa: F401 diff --git a/capyc/core/pytest/fixtures/fake.py b/capyc/core/pytest/fixtures/fake.py index 87a42a040..827662b65 100644 --- a/capyc/core/pytest/fixtures/fake.py +++ b/capyc/core/pytest/fixtures/fake.py @@ -1,13 +1,14 @@ -from typing import Generator +from typing import Generator, Optional import pytest from faker import Faker as Fake __all__ = ['fake', 'Fake'] -FAKE = Fake() +@pytest.fixture(autouse=True) +def fake(seed: Optional[int]) -> Generator[Fake, None, None]: + f = Fake() + f.seed_instance(seed) -@pytest.fixture(scope='module') -def fake() -> Generator[Fake, None, None]: - return FAKE + yield f diff --git a/capyc/core/pytest/fixtures/random.py b/capyc/core/pytest/fixtures/random.py index b3939f586..cf6272a49 100644 --- a/capyc/core/pytest/fixtures/random.py +++ b/capyc/core/pytest/fixtures/random.py @@ -1,6 +1,6 @@ import random as r import string -from typing import Any, Dict, Generator, Tuple, final +from typing import Any, Dict, Generator, Optional, Tuple, final import pytest from faker import Faker @@ -14,8 +14,9 @@ class Random: Random utils. """ - def __init__(self, fake: Faker) -> None: + def __init__(self, fake: Faker, seed: Optional[int]) -> None: self._fake = fake + r.seed(seed) def seed(self, a=None, version=2) -> None: """Initialize internal state from a seed. @@ -104,10 +105,10 @@ def string(self, size=0, lower=False, upper=False, symbol=False, number=False) - return ''.join(r.choices(chars, k=size)) -@pytest.fixture() -def random(fake) -> Generator[Random, None, None]: +@pytest.fixture(autouse=True) +def random(fake: Faker, seed: Optional[int]) -> Generator[Random, None, None]: """Image fixtures.""" - x = Random(fake) + x = Random(fake, seed) yield x diff --git a/capyc/core/pytest/fixtures/seed.py b/capyc/core/pytest/fixtures/seed.py new file mode 100644 index 000000000..0ad2ed40b --- /dev/null +++ b/capyc/core/pytest/fixtures/seed.py @@ -0,0 +1,43 @@ +import random +from typing import Generator, Optional + +import pytest + +__all__ = ['seed', 'Seed', 'pytest_addoption', 'pytest_terminal_summary'] + +type Seed = Optional[int] +SEED = random.randint(0, 2 ** 32 - 1) +seed_used = None +SEED_KEY = '_+* SEED *+_' +END_KEY = '_+* END *+_' + +def pytest_addoption(parser: pytest.Parser): + try: + parser.addoption('--seed', action='store', default=None, type=int, help='Set the random seed for tests') + except Exception: + ... + + +def pytest_terminal_summary(terminalreporter, config: pytest.Config) -> None: + ... + # if hasattr(config, END_KEY) is False: + # setattr(config, END_KEY, True) + # seed = getattr(config, SEED_KEY, None) + # terminalreporter.write_sep("=", "Capy Core Summary") + # terminalreporter.write_line(f"Seed: {seed}") + + + +@pytest.fixture(autouse=True) +def seed(request: pytest.FixtureRequest) -> Generator[Optional[int], None, None]: + global SEED + + seed = request.config.getoption('--seed') + if seed is None: + seed = SEED + + # if hasattr(request.config, SEED_KEY) is False: + # setattr(request.config, SEED_KEY, seed) + + yield seed + print(f'Seed used: {seed}') diff --git a/capyc/django/pytest/fixtures/database.py b/capyc/django/pytest/fixtures/database.py index 8da56a78e..7766346b0 100644 --- a/capyc/django/pytest/fixtures/database.py +++ b/capyc/django/pytest/fixtures/database.py @@ -524,5 +524,5 @@ def create(cls, **models): @pytest.fixture -def database(db) -> Generator[Database, None, None]: +def database(db, seed) -> Generator[Database, None, None]: yield Database() diff --git a/conftest.py b/conftest.py index c1de2fea7..d58434b9f 100644 --- a/conftest.py +++ b/conftest.py @@ -47,7 +47,7 @@ def get_kwargs(random: Random) -> Generator[callable, None, None]: @pytest.fixture -def bc(): +def bc(seed): return Breathecode(None) diff --git a/scripts/parallel_test.py b/scripts/parallel_test.py index 49bec3d1b..d2b434d19 100644 --- a/scripts/parallel_test.py +++ b/scripts/parallel_test.py @@ -2,41 +2,25 @@ from __future__ import absolute_import -import argparse import os -import random import subprocess import sys - -def parse_arguments(): - parser = argparse.ArgumentParser(description='Run pytest with optional seed.') - parser.add_argument('--seed', type=int, help='Seed for randomness') - parser.add_argument('pytest_args', nargs='*', help='Arguments to pass to pytest') - return parser.parse_args() - - if __name__ == '__main__': - args = parse_arguments() + args = '' - if args.seed is None: - seed = random.randint(0, 4294967295) - else: - seed = args.seed + if len(sys.argv) > 1: + sys.argv.pop(0) + args = ' '.join(sys.argv) - pytest_args = ' '.join(args.pytest_args) - command = f'pytest {pytest_args} --disable-pytest-warnings -n auto --nomigrations --durations=1' + command = f'pytest {args} --disable-pytest-warnings -n auto --nomigrations --durations=1' # command = f'pytest {pytest_args} -n auto --nomigrations --durations=1' env = os.environ.copy() env['ENV'] = 'test' - env['RANDOM_SEED'] = str(seed) exit_code = subprocess.run(command, env=env, shell=True).returncode - print() - print(f'Seed {seed} used, you can provide it locally to reproduce random errors') - # python doesn't return 256 if exit_code: sys.exit(1) From a53a3b62ca660e9af27dd19abacea307e4a31ff3 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 28 May 2024 19:59:12 +0000 Subject: [PATCH 062/114] 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 063/114] 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 064/114] 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 065/114] 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 066/114] 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 067/114] 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 068/114] 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 069/114] 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 070/114] 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 071/114] 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 072/114] 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 073/114] 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 074/114] 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 075/114] 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 076/114] 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 eab6d8270f2ff9a933be7be88a6822d085f7a399 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 31 May 2024 00:13:52 -0500 Subject: [PATCH 077/114] fix tests, prevent do a charge twice --- .../management/commands/make_charges.py | 11 +- breathecode/payments/tasks.py | 313 ++++++++++-------- .../management/commands/tests_make_charges.py | 12 +- .../tests/tasks/tests_build_plan_financing.py | 8 +- .../tasks/tests_charge_plan_financing.py | 102 +++++- .../tests/tasks/tests_charge_subscription.py | 8 +- .../tests/tasks/tests_renew_consumables.py | 57 +--- .../tests_renew_plan_financing_consumables.py | 43 +-- 8 files changed, 305 insertions(+), 249 deletions(-) diff --git a/breathecode/payments/management/commands/make_charges.py b/breathecode/payments/management/commands/make_charges.py index cbfb89d9a..86e769796 100644 --- a/breathecode/payments/management/commands/make_charges.py +++ b/breathecode/payments/management/commands/make_charges.py @@ -20,20 +20,21 @@ def handle(self, *args, **options): avoid_expire_these_statuses = Q(status='EXPIRED') | Q(status='ERROR') | Q(status='PAYMENT_ISSUE') | Q( status='FULLY_PAID') | Q(status='FREE_TRIAL') | Q(status='CANCELLED') | Q(status='DEPRECATED') - args = (Q(valid_until__isnull=True) | Q(valid_until__gt=utc_now), ) + subscription_args = (Q(valid_until__isnull=True) | Q(valid_until__gt=utc_now), ) + financing_args = (Q(plan_expires_at__isnull=True) | Q(plan_expires_at__gt=utc_now), ) params = { 'next_payment_at__lte': utc_now + timedelta(days=1), } - subscriptions = Subscription.objects.filter(*args, **params) - plan_financings = PlanFinancing.objects.filter(*args, **params) - Subscription.objects.filter(valid_until__lte=utc_now).exclude(avoid_expire_these_statuses).update( status='EXPIRED') - PlanFinancing.objects.filter(valid_until__lte=utc_now).exclude(avoid_expire_these_statuses).update( + PlanFinancing.objects.filter(plan_expires_at__lte=utc_now).exclude(avoid_expire_these_statuses).update( status='EXPIRED') + subscriptions = Subscription.objects.filter(*subscription_args, **params) + plan_financings = PlanFinancing.objects.filter(*financing_args, **params) + for status in statuses: subscriptions = subscriptions.exclude(status=status) diff --git a/breathecode/payments/tasks.py b/breathecode/payments/tasks.py index a8fdead90..a17222a17 100644 --- a/breathecode/payments/tasks.py +++ b/breathecode/payments/tasks.py @@ -3,7 +3,10 @@ from typing import Any, Optional from dateutil.relativedelta import relativedelta +from django.core.cache import cache from django.utils import timezone +from django_redis import get_redis_connection +from redis.exceptions import LockError from task_manager.core.exceptions import AbortTask, RetryTask from task_manager.django.decorators import task @@ -14,6 +17,7 @@ from breathecode.payments.signals import consume_service, reimburse_service_units from breathecode.utils.decorators import TaskPriority from breathecode.utils.i18n import translation +from breathecode.utils.redis import Lock from .models import ( AbstractIOweYou, @@ -33,6 +37,7 @@ ) logger = logging.getLogger(__name__) +IS_DJANGO_REDIS = hasattr(cache, 'delete_pattern') @task(bind=True, priority=TaskPriority.WEB_SERVICE_PAYMENT.value) @@ -70,7 +75,7 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): # is over if (scheduler.plan_handler and scheduler.plan_handler.plan_financing - and scheduler.plan_handler.plan_financing.valid_until < utc_now): + and scheduler.plan_handler.plan_financing.plan_expires_at < utc_now): raise AbortTask(f'The plan financing {scheduler.plan_handler.plan_financing.id} is over') # it needs to be paid @@ -79,15 +84,6 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): raise AbortTask(f'The plan financing {scheduler.plan_handler.plan_financing.id} needs to be paid to renew ' 'the consumables') - if (scheduler.plan_handler and scheduler.plan_handler.plan_financing - and scheduler.plan_handler.handler.plan.time_of_life - and scheduler.plan_handler.handler.plan.time_of_life_unit - and scheduler.plan_handler.plan_financing.created_at + actions.calculate_relative_delta( - scheduler.plan_handler.handler.plan.time_of_life, scheduler.plan_handler.handler.plan.time_of_life_unit) - < utc_now): - logger.info(f'The services related to PlanFinancing {scheduler.plan_handler.plan_financing.id} is over') - return - # is over if (scheduler.subscription_handler and scheduler.subscription_handler.subscription and scheduler.subscription_handler.subscription.valid_until < utc_now): @@ -117,7 +113,7 @@ def get_resource_lookup(i_owe_you: AbstractIOweYou, service: Service): elif scheduler.plan_handler and scheduler.plan_handler.plan_financing: user = scheduler.plan_handler.plan_financing.user service_item = scheduler.plan_handler.handler.service_item - resource_valid_until = scheduler.plan_handler.plan_financing.valid_until + resource_valid_until = scheduler.plan_handler.plan_financing.plan_expires_at selected_lookup = get_resource_lookup(scheduler.plan_handler.plan_financing, service_item.service) @@ -197,9 +193,6 @@ def renew_plan_financing_consumables(self, plan_financing_id: int, **_: Any): raise RetryTask(f'PlanFinancing with id {plan_financing_id} not found') utc_now = timezone.now() - if plan_financing.valid_until and plan_financing.valid_until < utc_now: - raise AbortTask(f'The plan financing {plan_financing.id} is over') - if plan_financing.next_payment_at < utc_now: raise AbortTask(f'The PlanFinancing {plan_financing.id} needs to be paid to renew the consumables') @@ -242,89 +235,103 @@ def charge_subscription(self, subscription_id: int, **_: Any): logger.info(f'Starting charge_subscription for subscription {subscription_id}') - if not (subscription := Subscription.objects.filter(id=subscription_id).first()): - raise AbortTask(f'Subscription with id {subscription_id} not found') + client = None + if IS_DJANGO_REDIS: + client = get_redis_connection('default') - utc_now = timezone.now() + try: + with Lock(client, f'lock:subscription:{subscription_id}', timeout=30, blocking_timeout=30): + if not (subscription := Subscription.objects.filter(id=subscription_id).first()): + raise AbortTask(f'Subscription with id {subscription_id} not found') - if subscription.valid_until and subscription.valid_until < utc_now: - raise AbortTask(f'The subscription {subscription.id} is over') + utc_now = timezone.now() - if subscription.next_payment_at > utc_now: - raise AbortTask(f'The subscription with id {subscription_id} was paid this month') + if subscription.valid_until and subscription.valid_until < utc_now: + raise AbortTask(f'The subscription {subscription.id} is over') - settings = get_user_settings(subscription.user.id) + if subscription.next_payment_at > utc_now: + raise AbortTask(f'The subscription with id {subscription_id} was paid this month') - try: - bag = actions.get_bag_from_subscription(subscription, settings) - except Exception as e: - subscription.status = 'ERROR' - subscription.status_message = str(e) - subscription.save() - raise AbortTask(f'Error getting bag from subscription {subscription_id}: {e}') + settings = get_user_settings(subscription.user.id) - amount = actions.get_amount_by_chosen_period(bag, bag.chosen_period, settings.lang) + try: + bag = actions.get_bag_from_subscription(subscription, settings) + except Exception as e: + subscription.status = 'ERROR' + subscription.status_message = str(e) + subscription.save() + raise AbortTask(f'Error getting bag from subscription {subscription_id}: {e}') - try: - s = Stripe() - s.set_language_from_settings(settings) - invoice = s.pay(subscription.user, bag, amount, currency=bag.currency) + amount = actions.get_amount_by_chosen_period(bag, bag.chosen_period, settings.lang) - except Exception: - subject = translation(settings.lang, - en='Your 4Geeks subscription could not be renewed', - es='Tu suscripción 4Geeks no pudo ser renovada') + try: + s = Stripe() + s.set_language_from_settings(settings) + invoice = s.pay(subscription.user, bag, amount, currency=bag.currency) - message = translation(settings.lang, - en='Please update your payment methods', - es='Por favor actualiza tus métodos de pago') + except Exception: + subject = translation(settings.lang, + en='Your 4Geeks subscription could not be renewed', + es='Tu suscripción 4Geeks no pudo ser renovada') - button = translation(settings.lang, - en='Please update your payment methods', - es='Por favor actualiza tus métodos de pago') + message = translation(settings.lang, + en='Please update your payment methods', + es='Por favor actualiza tus métodos de pago') - notify_actions.send_email_message('message', - subscription.user.email, { - 'SUBJECT': subject, - 'MESSAGE': message, - 'BUTTON': button, - 'LINK': f'{get_app_url()}/subscription/{subscription.id}', - }, - academy=subscription.academy) + button = translation(settings.lang, + en='Please update your payment methods', + es='Por favor actualiza tus métodos de pago') - bag.delete() + notify_actions.send_email_message('message', + subscription.user.email, { + 'SUBJECT': subject, + 'MESSAGE': message, + 'BUTTON': button, + 'LINK': f'{get_app_url()}/subscription/{subscription.id}', + }, + academy=subscription.academy) - subscription.status = 'PAYMENT_ISSUE' - subscription.save() - return + bag.delete() - subscription.paid_at = utc_now - subscription.next_payment_at = utc_now + actions.calculate_relative_delta(subscription.pay_every, - subscription.pay_every_unit) + subscription.status = 'PAYMENT_ISSUE' + subscription.save() + return - subscription.invoices.add(invoice) + subscription.paid_at = utc_now + delta = actions.calculate_relative_delta(subscription.pay_every, subscription.pay_every_unit) - subscription.save() - value = invoice.currency.format_price(invoice.amount) + subscription.next_payment_at += delta + while utc_now > subscription.next_payment_at: + subscription.next_payment_at += delta + if subscription.valid_until: + subscription.valid_until += delta + + subscription.invoices.add(invoice) + + subscription.save() + value = invoice.currency.format_price(invoice.amount) + + subject = translation(settings.lang, + en='Your 4Geeks subscription was successfully renewed', + es='Tu suscripción 4Geeks fue renovada exitosamente') - subject = translation(settings.lang, - en='Your 4Geeks subscription was successfully renewed', - es='Tu suscripción 4Geeks fue renovada exitosamente') + message = translation(settings.lang, en=f'The amount was {value}', es=f'El monto fue {value}') - message = translation(settings.lang, en=f'The amount was {value}', es=f'El monto fue {value}') + button = translation(settings.lang, en='See the invoice', es='Ver la factura') - button = translation(settings.lang, en='See the invoice', es='Ver la factura') + notify_actions.send_email_message('message', + invoice.user.email, { + 'SUBJECT': subject, + 'MESSAGE': message, + 'BUTTON': button, + 'LINK': f'{get_app_url()}/subscription/{subscription.id}', + }, + academy=subscription.academy) - notify_actions.send_email_message('message', - invoice.user.email, { - 'SUBJECT': subject, - 'MESSAGE': message, - 'BUTTON': button, - 'LINK': f'{get_app_url()}/subscription/{subscription.id}', - }, - academy=subscription.academy) + renew_subscription_consumables.delay(subscription.id) - renew_subscription_consumables.delay(subscription.id) + except LockError: + raise RetryTask('Could not acquire lock for activity, operation timed out.') def fallback_charge_plan_financing(self, plan_financing_id: int, exception: Exception, **_: Any): @@ -358,88 +365,120 @@ def charge_plan_financing(self, plan_financing_id: int, **_: Any): logger.info(f'Starting charge_plan_financing for id {plan_financing_id}') - if not (plan_financing := PlanFinancing.objects.filter(id=plan_financing_id).first()): - raise AbortTask(f'PlanFinancing with id {plan_financing_id} not found') + client = None + if IS_DJANGO_REDIS: + client = get_redis_connection('default') - utc_now = timezone.now() + try: + with Lock(client, f'lock:plan_financing:{plan_financing_id}', timeout=30, blocking_timeout=30): - if plan_financing.valid_until < utc_now: - raise AbortTask(f'PlanFinancing with id {plan_financing_id} is over') + if not (plan_financing := PlanFinancing.objects.filter(id=plan_financing_id).first()): + raise AbortTask(f'PlanFinancing with id {plan_financing_id} not found') - if plan_financing.next_payment_at > utc_now: - raise AbortTask(f'PlanFinancing with id {plan_financing_id} was paid this month') + utc_now = timezone.now() - settings = get_user_settings(plan_financing.user.id) + if plan_financing.plan_expires_at < utc_now: + raise AbortTask(f'PlanFinancing with id {plan_financing_id} is over') - try: - bag = actions.get_bag_from_plan_financing(plan_financing, settings) - except Exception as e: - plan_financing.status = 'ERROR' - plan_financing.status_message = str(e) - plan_financing.save() + if plan_financing.next_payment_at > utc_now: + raise AbortTask(f'PlanFinancing with id {plan_financing_id} was paid this month') - raise AbortTask(f'Error getting bag from plan financing {plan_financing_id}: {e}') + settings = get_user_settings(plan_financing.user.id) - amount = plan_financing.monthly_price + try: + bag = actions.get_bag_from_plan_financing(plan_financing, settings) + except Exception as e: + plan_financing.status = 'ERROR' + plan_financing.status_message = str(e) + plan_financing.save() - try: - s = Stripe() - s.set_language_from_settings(settings) + raise AbortTask(f'Error getting bag from plan financing {plan_financing_id}: {e}') - invoice = s.pay(plan_financing.user, bag, amount, currency=bag.currency) + amount = plan_financing.monthly_price - except Exception: - subject = translation(settings.lang, - en='Your 4Geeks subscription could not be renewed', - es='Tu suscripción 4Geeks no pudo ser renovada') + invoices = plan_financing.invoices.order_by('created_at') + first_invoice = invoices.first() + last_invoice = invoices.last() - message = translation(settings.lang, - en='Please update your payment methods', - es='Por favor actualiza tus métodos de pago') + installments = first_invoice.bag.how_many_installments - button = translation(settings.lang, - en='Please update your payment methods', - es='Por favor actualiza tus métodos de pago') + if utc_now - last_invoice.created_at < timedelta(days=5): + raise AbortTask(f'PlanFinancing with id {plan_financing_id} was paid earlier') - notify_actions.send_email_message('message', - plan_financing.user.email, { - 'SUBJECT': subject, - 'MESSAGE': message, - 'BUTTON': button, - 'LINK': f'{get_app_url()}/plan-financing/{plan_financing.id}', - }, - academy=plan_financing.academy) + remaining_installments = installments - invoices.count() - bag.delete() + if remaining_installments > 0: + try: + s = Stripe() + s.set_language_from_settings(settings) - plan_financing.status = 'PAYMENT_ISSUE' - plan_financing.save() - return + invoice = s.pay(plan_financing.user, bag, amount, currency=bag.currency) - plan_financing.next_payment_at = utc_now + relativedelta(months=1) - plan_financing.invoices.add(invoice) - plan_financing.save() + except Exception: + subject = translation(settings.lang, + en='Your 4Geeks subscription could not be renewed', + es='Tu suscripción 4Geeks no pudo ser renovada') - value = invoice.currency.format_price(invoice.amount) + message = translation(settings.lang, + en='Please update your payment methods', + es='Por favor actualiza tus métodos de pago') - subject = translation(settings.lang, - en='Your installment at 4Geeks was successfully charged', - es='Tu cuota en 4Geeks fue cobrada exitosamente') + button = translation(settings.lang, + en='Please update your payment methods', + es='Por favor actualiza tus métodos de pago') - message = translation(settings.lang, en=f'The amount was {value}', es=f'El monto fue {value}') + notify_actions.send_email_message('message', + plan_financing.user.email, { + 'SUBJECT': subject, + 'MESSAGE': message, + 'BUTTON': button, + 'LINK': f'{get_app_url()}/plan-financing/{plan_financing.id}', + }, + academy=plan_financing.academy) - button = translation(settings.lang, en='See the invoice', es='Ver la factura') + bag.delete() - notify_actions.send_email_message('message', - invoice.user.email, { - 'SUBJECT': subject, - 'MESSAGE': message, - 'BUTTON': button, - 'LINK': f'{get_app_url()}/plan-financing/{plan_financing.id}', - }, - academy=plan_financing.academy) + plan_financing.status = 'PAYMENT_ISSUE' + plan_financing.save() + return - renew_plan_financing_consumables.delay(plan_financing.id) + if utc_now > plan_financing.valid_until: + remaining_installments -= 1 + plan_financing.valid_until = utc_now + relativedelta(months=remaining_installments) + + plan_financing.invoices.add(invoice) + + value = invoice.currency.format_price(invoice.amount) + + subject = translation(settings.lang, + en='Your installment at 4Geeks was successfully charged', + es='Tu cuota en 4Geeks fue cobrada exitosamente') + + message = translation(settings.lang, en=f'The amount was {value}', es=f'El monto fue {value}') + + button = translation(settings.lang, en='See the invoice', es='Ver la factura') + + notify_actions.send_email_message('message', + invoice.user.email, { + 'SUBJECT': subject, + 'MESSAGE': message, + 'BUTTON': button, + 'LINK': f'{get_app_url()}/plan-financing/{plan_financing.id}', + }, + academy=plan_financing.academy) + + delta = relativedelta(months=1) + + while utc_now >= plan_financing.next_payment_at + delta: + delta += relativedelta(months=1) + + plan_financing.next_payment_at += delta + plan_financing.save() + + renew_plan_financing_consumables.delay(plan_financing.id) + + except LockError: + raise RetryTask('Could not acquire lock for activity, operation timed out.') @task(bind=True, priority=TaskPriority.WEB_SERVICE_PAYMENT.value) @@ -684,7 +723,7 @@ def build_plan_financing(self, bag_id: int, invoice_id: int, is_free: bool = Fal selected_event_type_set=event_type_set, selected_mentorship_service_set=mentorship_service_set, selected_service_set=service_set, - valid_until=invoice.paid_at + relativedelta(months=months), + valid_until=invoice.paid_at + relativedelta(months=months - 1), plan_expires_at=invoice.paid_at + delta, monthly_price=invoice.amount, status='ACTIVE') diff --git a/breathecode/payments/tests/management/commands/tests_make_charges.py b/breathecode/payments/tests/management/commands/tests_make_charges.py index 6362471bd..4650e2efa 100644 --- a/breathecode/payments/tests/management/commands/tests_make_charges.py +++ b/breathecode/payments/tests/management/commands/tests_make_charges.py @@ -120,10 +120,10 @@ def test_with_two_plan_financings__wrong_cases(bc: Breathecode, delta, status, u valid_until = utc_now + delta plan_financing = { 'next_payment_at': valid_until, - 'valid_until': UTC_NOW + relativedelta(months=random.randint(12, 24)), + 'valid_until': UTC_NOW + relativedelta(months=random.randint(1, 12)), 'status': status, 'monthly_price': (random.random() * 99) + 1, - 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(12, 24)), } model = bc.database.create(plan_financing=(2, plan_financing)) @@ -145,10 +145,10 @@ def test_with_two_plan_financings__expired(bc: Breathecode, delta, status, statu valid_until = utc_now + delta plan_financing = { 'next_payment_at': valid_until, - 'valid_until': valid_until, + 'valid_until': UTC_NOW + relativedelta(months=random.randint(1, 12)), 'status': status, 'monthly_price': (random.random() * 99) + 1, - 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + 'plan_expires_at': valid_until, } model = bc.database.create(plan_financing=(2, plan_financing)) @@ -177,10 +177,10 @@ def test_with_two_plan_financings__valid_cases(bc: Breathecode, delta, status, u next_payment_at = utc_now - delta plan_financing = { 'next_payment_at': next_payment_at, - 'valid_until': valid_until, + 'valid_until': UTC_NOW + relativedelta(months=random.randint(1, 12)), 'status': status, 'monthly_price': (random.random() * 99) + 1, - 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + 'plan_expires_at': valid_until, } model = bc.database.create(plan_financing=(2, plan_financing)) diff --git a/breathecode/payments/tests/tasks/tests_build_plan_financing.py b/breathecode/payments/tests/tasks/tests_build_plan_financing.py index 6febf0cc6..c3fe7204d 100644 --- a/breathecode/payments/tests/tasks/tests_build_plan_financing.py +++ b/breathecode/payments/tests/tasks/tests_build_plan_financing.py @@ -219,7 +219,7 @@ def test_subscription_was_created(self): 'monthly_price': model.invoice.amount, 'valid_until': - model.invoice.paid_at + relativedelta(months=months), + model.invoice.paid_at + relativedelta(months=months - 1), 'next_payment_at': model.invoice.paid_at + relativedelta(months=1), 'plan_expires_at': @@ -292,7 +292,7 @@ def test_subscription_was_created__bag_with_cohort(self): 'selected_cohort_set_id': 1, 'valid_until': - model.invoice.paid_at + relativedelta(months=months), + model.invoice.paid_at + relativedelta(months=months - 1), 'next_payment_at': model.invoice.paid_at + relativedelta(months=1), 'plan_expires_at': @@ -364,7 +364,7 @@ def test_subscription_was_created__bag_with_event_type_set(self): 'selected_event_type_set_id': 1, 'valid_until': - model.invoice.paid_at + relativedelta(months=months), + model.invoice.paid_at + relativedelta(months=months - 1), 'next_payment_at': model.invoice.paid_at + relativedelta(months=1), 'plan_expires_at': @@ -436,7 +436,7 @@ def test_subscription_was_created__bag_with_mentorship_service_set(self): 'selected_mentorship_service_set_id': 1, 'valid_until': - model.invoice.paid_at + relativedelta(months=months), + model.invoice.paid_at + relativedelta(months=months - 1), 'next_payment_at': model.invoice.paid_at + relativedelta(months=1), 'plan_expires_at': diff --git a/breathecode/payments/tests/tasks/tests_charge_plan_financing.py b/breathecode/payments/tests/tasks/tests_charge_plan_financing.py index 9279d5ca0..362b61938 100644 --- a/breathecode/payments/tests/tasks/tests_charge_plan_financing.py +++ b/breathecode/payments/tests/tasks/tests_charge_plan_financing.py @@ -184,6 +184,69 @@ def test_plan_financing_without_invoices(self): 🔽🔽🔽 PlanFinancing process to charge """ + @patch('logging.Logger.info', MagicMock()) + @patch('logging.Logger.error', MagicMock()) + @patch('breathecode.notify.actions.send_email_message', MagicMock()) + @patch('breathecode.payments.tasks.renew_plan_financing_consumables.delay', MagicMock()) + @patch('mixer.main.LOGGER.info', MagicMock()) + @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) + def test_plan_financing_was_paid_this_month(self): + delta = relativedelta(months=random.randint(1, 12)) + plan_financing = { + 'valid_until': UTC_NOW + delta, + 'next_payment_at': UTC_NOW + delta, + 'monthly_price': (random.random() * 99) + 1, + 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + } + plan = {'is_renewable': False} + invoice = {'paid_at': UTC_NOW - relativedelta(hours=24, seconds=1)} + bag = {'how_many_installments': 3} + with patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW - relativedelta(months=2))): + model = self.bc.database.create(academy=1, + plan_financing=plan_financing, + invoice=invoice, + plan=plan, + bag=bag) + + with patch('breathecode.payments.services.stripe.Stripe.pay', + MagicMock(side_effect=fake_stripe_pay(paid_at=UTC_NOW, academy=model.academy))): + # remove prints from mixer + logging.Logger.info.call_args_list = [] + logging.Logger.error.call_args_list = [] + + charge_plan_financing.delay(1) + + self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) + + self.assertEqual(logging.Logger.info.call_args_list, [ + call('Starting charge_plan_financing for id 1'), + ]) + self.assertEqual(logging.Logger.error.call_args_list, [ + call('PlanFinancing with id 1 was paid this month', exc_info=True), + ]) + + self.assertEqual(self.bc.database.list_of('payments.Bag'), [ + self.bc.format.to_dict(model.bag), + ]) + self.assertEqual(self.bc.database.list_of('payments.Invoice'), [ + self.bc.format.to_dict(model.invoice), + ]) + + self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), [ + { + **self.bc.format.to_dict(model.plan_financing), + 'status': 'ACTIVE', + }, + ]) + self.assertEqual(notify_actions.send_email_message.call_args_list, []) + self.bc.check.calls(activity_tasks.add_activity.delay.call_args_list, [ + call(1, 'bag_created', related_type='payments.Bag', related_id=1), + ]) + + """ + 🔽🔽🔽 PlanFinancing process to charge + """ + @patch('logging.Logger.info', MagicMock()) @patch('logging.Logger.error', MagicMock()) @patch('breathecode.notify.actions.send_email_message', MagicMock()) @@ -191,13 +254,22 @@ def test_plan_financing_without_invoices(self): @patch('mixer.main.LOGGER.info', MagicMock()) @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_plan_financing_process_to_charge(self): + delta = relativedelta(months=random.randint(1, 12)) plan_financing = { - 'valid_until': UTC_NOW + relativedelta(minutes=1), + 'valid_until': UTC_NOW + delta, + 'next_payment_at': UTC_NOW - delta, 'monthly_price': (random.random() * 99) + 1, 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), } plan = {'is_renewable': False} - model = self.bc.database.create(academy=1, plan_financing=plan_financing, invoice=1, plan=plan) + invoice = {'paid_at': UTC_NOW - relativedelta(hours=24, seconds=1)} + bag = {'how_many_installments': 3} + with patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW - relativedelta(months=2))): + model = self.bc.database.create(academy=1, + plan_financing=plan_financing, + invoice=invoice, + plan=plan, + bag=bag) with patch('breathecode.payments.services.stripe.Stripe.pay', MagicMock(side_effect=fake_stripe_pay(paid_at=UTC_NOW, academy=model.academy))): @@ -239,7 +311,7 @@ def test_plan_financing_process_to_charge(self): { **self.bc.format.to_dict(model.plan_financing), 'status': 'ACTIVE', - 'next_payment_at': UTC_NOW + relativedelta(months=1), + 'next_payment_at': model.plan_financing.next_payment_at + (delta + relativedelta(months=1)), }, ]) self.assertEqual(notify_actions.send_email_message.call_args_list, [ @@ -274,7 +346,9 @@ def test_plan_financing_error_when_try_to_charge(self): 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), } plan = {'is_renewable': False} - model = self.bc.database.create(plan_financing=plan_financing, invoice=1, plan=plan) + bag = {'how_many_installments': 3} + with patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW - relativedelta(months=2))): + model = self.bc.database.create(plan_financing=plan_financing, invoice=1, plan=plan, bag=bag) with patch('breathecode.payments.services.stripe.Stripe.pay', MagicMock(side_effect=Exception('fake error'))): # remove prints from mixer @@ -331,9 +405,9 @@ def test_plan_financing_error_when_try_to_charge(self): @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) def test_plan_financing_is_over(self): plan_financing = { - 'valid_until': UTC_NOW - relativedelta(minutes=1), + 'valid_until': UTC_NOW + relativedelta(months=random.randint(1, 12)), 'monthly_price': (random.random() * 99) + 1, - 'plan_expires_at': UTC_NOW + relativedelta(months=random.randint(1, 12)), + 'plan_expires_at': UTC_NOW - relativedelta(minutes=1), } plan = {'is_renewable': False} model = self.bc.database.create(plan_financing=plan_financing, invoice=1, plan=plan) @@ -443,7 +517,13 @@ def test_plan_financing_process_to_charge__but_a_undexpected_exception_is_raised } invoice = {'paid_at': UTC_NOW - relativedelta(hours=24, seconds=1)} plan = {'is_renewable': False} - model = self.bc.database.create(academy=1, plan_financing=plan_financing, invoice=invoice, plan=plan) + bag = {'how_many_installments': 3} + with patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW - relativedelta(months=2))): + model = self.bc.database.create(academy=1, + plan_financing=plan_financing, + invoice=invoice, + plan=plan, + bag=bag) error = self.bc.fake.text() @@ -501,7 +581,13 @@ def test_plan_financing_process_to_charge__but_a_undexpected_exception_is_raised } invoice = {'paid_at': UTC_NOW - relativedelta(hours=random.randint(1, 23))} plan = {'is_renewable': False} - model = self.bc.database.create(academy=1, plan_financing=plan_financing, invoice=invoice, plan=plan) + bag = {'how_many_installments': 3} + with patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW - relativedelta(months=2))): + model = self.bc.database.create(academy=1, + plan_financing=plan_financing, + invoice=invoice, + plan=plan, + bag=bag) error = self.bc.fake.text() diff --git a/breathecode/payments/tests/tasks/tests_charge_subscription.py b/breathecode/payments/tests/tasks/tests_charge_subscription.py index 230adb19e..9bea0449f 100644 --- a/breathecode/payments/tests/tasks/tests_charge_subscription.py +++ b/breathecode/payments/tests/tasks/tests_charge_subscription.py @@ -4,6 +4,7 @@ import logging import os import random +from datetime import timedelta from unittest.mock import MagicMock, call, patch import pytest @@ -188,6 +189,7 @@ def test_subscription_process_to_charge(self): subscription = { 'pay_every': unit, 'pay_every_unit': unit_type, + 'next_payment_at': UTC_NOW - relativedelta(days=25, months=unit * 2), } model = self.bc.database.create(subscription=subscription, invoice=1) @@ -226,13 +228,17 @@ def test_subscription_process_to_charge(self): 'paid_at': UTC_NOW, }), ]) + next_payment_at = model.subscription.next_payment_at + delta = calculate_relative_delta(unit, unit_type) + for _ in range(3): + next_payment_at += delta self.assertEqual(self.bc.database.list_of('payments.Subscription'), [ { **self.bc.format.to_dict(model.subscription), 'status': 'ACTIVE', 'paid_at': UTC_NOW, - 'next_payment_at': UTC_NOW + calculate_relative_delta(unit, unit_type), + 'next_payment_at': next_payment_at, }, ]) diff --git a/breathecode/payments/tests/tasks/tests_renew_consumables.py b/breathecode/payments/tests/tasks/tests_renew_consumables.py index 27e57e5df..a2aa18572 100644 --- a/breathecode/payments/tests/tasks/tests_renew_consumables.py +++ b/breathecode/payments/tests/tasks/tests_renew_consumables.py @@ -103,8 +103,8 @@ def test_plan_financing_is_over(self): def test_plan_financing_without_be_paid(self): plan_financing = { 'monthly_price': random.random() * 99.99 + 0.01, - 'plan_expires_at': UTC_NOW - relativedelta(seconds=1), - 'valid_until': UTC_NOW + relativedelta(minutes=3), + 'plan_expires_at': UTC_NOW + relativedelta(minutes=3), + 'valid_until': UTC_NOW - relativedelta(seconds=1), } plan = {'is_renewable': False} @@ -137,8 +137,8 @@ def test_plan_financing_without_be_paid(self): def test_plan_financing_with_plan_service_item_without_a_resource_linked(self): plan_financing = { 'monthly_price': random.random() * 99.99 + 0.01, - 'plan_expires_at': UTC_NOW - relativedelta(seconds=1), - 'valid_until': UTC_NOW + relativedelta(minutes=3), + 'plan_expires_at': UTC_NOW + relativedelta(minutes=3), + 'valid_until': UTC_NOW - relativedelta(seconds=1), 'next_payment_at': UTC_NOW + relativedelta(minutes=3), } plan = {'is_renewable': False} @@ -173,8 +173,8 @@ def test_plan_financing_with_plan_service_item_without_a_resource_linked(self): def test_plan_financing_with_plan_service_item_with_two_cohorts_linked(self): plan_financing = { 'monthly_price': random.random() * 99.99 + 0.01, - 'plan_expires_at': UTC_NOW - relativedelta(seconds=4), - 'valid_until': UTC_NOW + relativedelta(minutes=5), + 'plan_expires_at': UTC_NOW + relativedelta(minutes=5), + 'valid_until': UTC_NOW - relativedelta(seconds=4), 'next_payment_at': UTC_NOW + relativedelta(minutes=3), } plan = {'is_renewable': False} @@ -215,43 +215,6 @@ def test_plan_financing_with_plan_service_item_with_two_cohorts_linked(self): }), ]) - """ - 🔽🔽🔽 ServiceStockScheduler with PlanFinancing with Plan which is over - """ - - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_plan_financing_with_plan_than_is_over(self): - plan_financing = { - 'monthly_price': random.random() * 99.99 + 0.01, - 'plan_expires_at': UTC_NOW - relativedelta(days=3), - 'valid_until': UTC_NOW + relativedelta(days=4), - 'next_payment_at': UTC_NOW + relativedelta(days=2), - } - - plan = {'time_of_life': 1, 'time_of_life_unit': 'DAY', 'is_renewable': False} - - model = self.bc.database.create(service_stock_scheduler=1, - plan_financing=plan_financing, - plan_service_item_handler=1, - cohort=2, - plan=plan) - - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - with patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW + relativedelta(days=1, minutes=3))): - renew_consumables.delay(1) - - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting renew_consumables for service stock scheduler 1'), - call('The services related to PlanFinancing 1 is over'), - ]) - self.assertEqual(logging.Logger.error.call_args_list, []) - - self.assertEqual(self.bc.database.list_of('payments.Consumable'), []) - """ 🔽🔽🔽 ServiceStockScheduler with PlanFinancing with a PlanServiceItem linked to a resource """ @@ -262,8 +225,8 @@ def test_plan_financing_with_plan_than_is_over(self): def test_plan_financing_with_plan_service_item_with_two_mentorship_services_linked(self): plan_financing = { 'monthly_price': random.random() * 99.99 + 0.01, - 'plan_expires_at': UTC_NOW - relativedelta(seconds=4), - 'valid_until': UTC_NOW + relativedelta(minutes=5), + 'plan_expires_at': UTC_NOW + relativedelta(minutes=5), + 'valid_until': UTC_NOW - relativedelta(seconds=4), 'next_payment_at': UTC_NOW + relativedelta(minutes=3), } @@ -315,8 +278,8 @@ def test_plan_financing_with_plan_service_item_with_two_mentorship_services_link def test_plan_financing_with_plan_service_item__do_not_needs_renew(self): plan_financing = { 'monthly_price': random.random() * 99.99 + 0.01, - 'plan_expires_at': UTC_NOW - relativedelta(seconds=4), - 'valid_until': UTC_NOW + relativedelta(minutes=5), + 'plan_expires_at': UTC_NOW + relativedelta(minutes=5), + 'valid_until': UTC_NOW - relativedelta(seconds=4), 'next_payment_at': UTC_NOW + relativedelta(minutes=3), } diff --git a/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py b/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py index 018533cb4..2b9559118 100644 --- a/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py +++ b/breathecode/payments/tests/tasks/tests_renew_plan_financing_consumables.py @@ -5,14 +5,14 @@ import random from unittest.mock import MagicMock, call, patch +from dateutil.relativedelta import relativedelta from django.utils import timezone + from breathecode.payments import tasks from breathecode.payments.actions import calculate_relative_delta from ...tasks import renew_plan_financing_consumables - from ..mixins import PaymentsTestCase -from dateutil.relativedelta import relativedelta UTC_NOW = timezone.now() @@ -130,45 +130,6 @@ def test_subscription_was_paid__without_stock_scheduler(self): ]) self.assertEqual(self.bc.database.list_of('payments.ServiceStockScheduler'), []) - """ - 🔽🔽🔽 Subscription is over - """ - - @patch('logging.Logger.info', MagicMock()) - @patch('logging.Logger.error', MagicMock()) - @patch('breathecode.payments.tasks.renew_consumables.delay', MagicMock()) - @patch('django.utils.timezone.now', MagicMock(return_value=UTC_NOW)) - def test_subscription_is_over(self): - subscription = { - 'next_payment_at': UTC_NOW + relativedelta(minutes=3), - 'valid_until': UTC_NOW - relativedelta(seconds=1), - 'plan_expires_at': UTC_NOW + relativedelta(minutes=3), - 'monthly_price': random.random() * 99.99 + 0.01, - } - plan = {'is_renewable': False} - model = self.bc.database.create(plan_financing=subscription, plan=plan) - - logging.Logger.info.call_args_list = [] - logging.Logger.error.call_args_list = [] - - renew_plan_financing_consumables.delay(1) - - self.assertEqual(self.bc.database.list_of('admissions.Cohort'), []) - - self.assertEqual(logging.Logger.info.call_args_list, [ - call('Starting renew_plan_financing_consumables for id 1'), - ]) - self.assertEqual(logging.Logger.error.call_args_list, [ - call('The plan financing 1 is over', exc_info=True), - ]) - - self.assertEqual(tasks.renew_consumables.delay.call_args_list, []) - - self.assertEqual(self.bc.database.list_of('payments.PlanFinancing'), [ - self.bc.format.to_dict(model.plan_financing), - ]) - self.assertEqual(self.bc.database.list_of('payments.ServiceStockScheduler'), []) - """ 🔽🔽🔽 Subscription was not paid """ From 03806324d707d28a97aa36ccd4cee40e0a448504 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 31 May 2024 00:15:38 -0500 Subject: [PATCH 078/114] update deps --- Pipfile.lock | 480 ++++++++++++++++++++++++++------------------------- 1 file changed, 246 insertions(+), 234 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index f029bcda3..64b0c5125 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -166,11 +166,11 @@ }, "anyio": { "hashes": [ - "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", - "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6" + "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", + "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7" ], "markers": "python_version >= '3.8'", - "version": "==4.3.0" + "version": "==4.4.0" }, "asgiref": { "hashes": [ @@ -714,11 +714,11 @@ }, "dj-database-url": { "hashes": [ - "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0", - "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f" + "sha256:3e792567b0aa9a4884860af05fe2aa4968071ad351e033b6db632f97ac6db9de", + "sha256:9f9b05058ddf888f1e6f840048b8d705ff9395e3b52a07165daa3d8b9360551b" ], "index": "pypi", - "version": "==2.1.0" + "version": "==2.2.0" }, "django": { "hashes": [ @@ -2190,12 +2190,12 @@ }, "openai": { "hashes": [ - "sha256:44316818fbff3845278e862a655c4c041e93d907b04eff64629c2835f29bd58e", - "sha256:f86780f40505de60fa389993d9b7f5564f20acfbe5efcabd5c853a12453af2b0" + "sha256:2ad95e926de0d2e09cde632a9204b0a6dca4a03c2cdcc84329b01f355784355a", + "sha256:5366562eb2c5917e6116ae0391b7ae6e3acd62b0ae3f565ada32b35d8fcfa106" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.30.2" + "version": "==1.30.5" }, "packaging": { "hashes": [ @@ -2380,11 +2380,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d", - "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6" + "sha256:07c60ee4ab7b7e90824b61afa840c8f5aad2d46b3e2e10acc33d8ecc94a49089", + "sha256:a29b89160e494e3ea8622b09fa5897610b437884dcdcd054fdc1308883326c2a" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.43" + "version": "==3.0.45" }, "proto-plus": { "hashes": [ @@ -2641,96 +2641,96 @@ }, "pydantic": { "hashes": [ - "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5", - "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc" + "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7", + "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7" ], "markers": "python_version >= '3.8'", - "version": "==2.7.1" + "version": "==2.7.2" }, "pydantic-core": { "hashes": [ - "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b", - "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a", - "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90", - "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d", - "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e", - "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d", - "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027", - "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804", - "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347", - "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400", - "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3", - "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399", - "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349", - "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd", - "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c", - "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e", - "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413", - "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3", - "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e", - "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3", - "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91", - "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce", - "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c", - "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb", - "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664", - "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6", - "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd", - "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3", - "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af", - "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043", - "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350", - "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7", - "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0", - "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563", - "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761", - "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72", - "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3", - "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb", - "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788", - "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b", - "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c", - "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038", - "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250", - "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec", - "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c", - "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74", - "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81", - "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439", - "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75", - "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0", - "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8", - "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150", - "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438", - "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae", - "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857", - "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038", - "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374", - "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f", - "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241", - "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592", - "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4", - "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d", - "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b", - "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b", - "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182", - "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e", - "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641", - "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70", - "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9", - "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a", - "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543", - "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b", - "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f", - "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38", - "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845", - "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2", - "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0", - "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4", - "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242" + "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9", + "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b", + "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426", + "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc", + "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9", + "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc", + "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4", + "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702", + "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6", + "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231", + "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083", + "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60", + "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7", + "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a", + "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4", + "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6", + "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39", + "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0", + "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026", + "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c", + "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442", + "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7", + "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044", + "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd", + "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94", + "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361", + "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af", + "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4", + "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022", + "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7", + "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd", + "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8", + "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2", + "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5", + "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be", + "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c", + "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9", + "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417", + "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1", + "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326", + "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4", + "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb", + "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34", + "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d", + "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9", + "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812", + "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558", + "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779", + "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be", + "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c", + "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe", + "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0", + "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c", + "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2", + "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a", + "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab", + "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048", + "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3", + "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106", + "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d", + "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb", + "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6", + "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b", + "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef", + "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5", + "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb", + "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c", + "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06", + "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059", + "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943", + "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1", + "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2", + "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c", + "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522", + "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f", + "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78", + "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7", + "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a", + "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5" ], "markers": "python_version >= '3.8'", - "version": "==2.18.2" + "version": "==2.18.3" }, "pyfcm": { "hashes": [ @@ -3038,12 +3038,12 @@ }, "requests": { "hashes": [ - "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", - "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.32.2" + "version": "==2.32.3" }, "rpds-py": { "hashes": [ @@ -3295,12 +3295,12 @@ }, "stripe": { "hashes": [ - "sha256:63161fca746cb6a81e2158bf61950ccb8d75f1b7231c7f4ac9e05817aab586b4", - "sha256:f630bcf47d49beaad02feba0109784bc016b5e0c974272da6fdf590a0f7ec8a9" + "sha256:3cf4bab592afecaaff69c12ecb99c8376a00bc22e26ea7130a0596e209bf3e88", + "sha256:c42d8f6b4463a54f3a025581810f4e632e2d5a71de6100fc595d75581f69a492" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==9.8.0" + "version": "==9.9.0" }, "text-unidecode": { "hashes": [ @@ -3359,12 +3359,12 @@ }, "twilio": { "hashes": [ - "sha256:5e09e910b9368f50f23cb3c3dd5ba77164d80a81e9d97db955cbac322deb2a4e", - "sha256:e9b5727943584d25d618fe502f0100fc5283215f31c863f80b5c64581b4702b0" + "sha256:ab2eb19c779855bf02cdca8a7e02ebaa64feee47da7b591ac9088ec07a6962e2", + "sha256:eb4687a9f81dc3118e8981c5a46d9f8184baee135c79afed47c714c759c31bbc" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==9.0.5" + "version": "==9.1.0" }, "twisted": { "extras": [ @@ -3421,12 +3421,12 @@ }, "uvicorn": { "hashes": [ - "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", - "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0" + "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab", + "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.29.0" + "version": "==0.30.0" }, "vine": { "hashes": [ @@ -3650,39 +3650,45 @@ }, "zope.interface": { "hashes": [ - "sha256:065f8ff0e034e43b8da05ffed308a9e3311720a2b13b83724f26a8dd6709964d", - "sha256:0940b3b44b6cc0375ea0da5fefee05a9abe8bb53594a3a6e4aafb9f99dc5de8d", - "sha256:15fa208d7a802c0dd3e9d4d5336619a37efd57f2d2ce830d9f9d5843a2b7daba", - "sha256:16f73c42f10f761051157332943ee1f7cf973cc1c78a50d1960c313a211cca4a", - "sha256:18f4061c3456c61557e9d7068e435f5db164b38f15f3d9bd995ff185c6db2c62", - "sha256:1e0678885d07e865047e15be3ebe5c87903cc7f5ca5edfd0045d1c7b43f7fe9d", - "sha256:297ee171f40f8f18665bb75f576df7d1ddce19f3e6696ef6acb930dcbfbf693f", - "sha256:36234d3b8211d053c42684666c2a04eb1a35e0cec6bc3e54586bb60fb0be3b17", - "sha256:3a00983c5c793b17b829020e11032dabab023c4e0ef12f134b90df802ae5adf2", - "sha256:3ca89624d0eabc7ce4f299c6d621531cb8b0ebac3bb4f9ebf2d057477602e1b8", - "sha256:45fb6fe8b5852564e63d6705a7904530a7c886056e6e9aaf938dc5e2bc637097", - "sha256:52901ff6d75a4332457610cbd2883f39b386c5bebe0745ecf78e3fe22cfdd0d9", - "sha256:563a7192a5baf8d9b189dc598c3555e695e00fdce3eafb88b30d6d3df986fcc5", - "sha256:68b13ac49becfaba5b77359559812daac0c5c4b3c0d43cdb293a2dec8db95c24", - "sha256:799743a342249c9b9529abd1115a3f81754800e75dea254b58efdd2984009798", - "sha256:7c18218451823ca9b5131ceaacf655fe9dd4e592ebf848cb0a65fe8428bbf604", - "sha256:8140cf2665c5a07adc285bc859fdda67cfbd7edd62480dfca2211f4798502b54", - "sha256:a430d2cc52aef2af0dc45866852730fbc93463cf8cdeb179e8ee04440e0955c4", - "sha256:a45a7990d143acc37faa905d4a528f5923a5dd30f46536977d8061d10a895b09", - "sha256:aaa1f8967c3f272de80c4bec4b1379f97cd29006323f50558bd2f780a4f637ef", - "sha256:ada5ac54ac7d34bb33423da40b7f3edfc54c6b9623ac9daac7f456dbf25173ba", - "sha256:bf9fa875a9bae5318f24b0d9ab9e2c8a23bccad2979e9e4305eed8119bbe3195", - "sha256:cd3ab863a4e7d888728c949ba052a649664dea156bdd7140eb9269bbe6e33205", - "sha256:d91698257850ca5f523a5513a69775e6fb7c18129311e118996f8e9b463d11b0", - "sha256:dcefd4012593ee410ebf5728ee98f61b3401f0563c5068e760aa2b7720ca68a0", - "sha256:de6c1dad571276768fd6bc92999e8d942151552662a9048e3384cac05b148985", - "sha256:de6eaa0b7df493904d24050dcdc3db6589bd94f7e49caab57971fe47a669b3ea", - "sha256:e7d200aef16e577682dd54a79ff5f4f897a9807722b54bd8a9bca404679c609d", - "sha256:e9961413091e3c9d5c3ed671757049cc6153280f39a154a0b633608efcfdec6b", - "sha256:ec57dec41c0c8b723dd70da1864d50908c689e1c9cf43f32e9b04c0992e5d93d" + "sha256:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854", + "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b", + "sha256:136cacdde1a2c5e5bc3d0b2a1beed733f97e2dad8c2ad3c2e17116f6590a3827", + "sha256:1730c93a38b5a18d24549bc81613223962a19d457cfda9bdc66e542f475a36f4", + "sha256:1a62fd6cd518693568e23e02f41816adedfca637f26716837681c90b36af3671", + "sha256:1c207e6f6dfd5749a26f5a5fd966602d6b824ec00d2df84a7e9a924e8933654e", + "sha256:2eccd5bef45883802848f821d940367c1d0ad588de71e5cabe3813175444202c", + "sha256:33ee982237cffaf946db365c3a6ebaa37855d8e3ca5800f6f48890209c1cfefc", + "sha256:3d136e5b8821073e1a09dde3eb076ea9988e7010c54ffe4d39701adf0c303438", + "sha256:47654177e675bafdf4e4738ce58cdc5c6d6ee2157ac0a78a3fa460942b9d64a8", + "sha256:47937cf2e7ed4e0e37f7851c76edeb8543ec9b0eae149b36ecd26176ff1ca874", + "sha256:4ac46298e0143d91e4644a27a769d1388d5d89e82ee0cf37bf2b0b001b9712a4", + "sha256:4c0b208a5d6c81434bdfa0f06d9b667e5de15af84d8cae5723c3a33ba6611b82", + "sha256:551db2fe892fcbefb38f6f81ffa62de11090c8119fd4e66a60f3adff70751ec7", + "sha256:599f3b07bde2627e163ce484d5497a54a0a8437779362395c6b25e68c6590ede", + "sha256:5ef8356f16b1a83609f7a992a6e33d792bb5eff2370712c9eaae0d02e1924341", + "sha256:5fe919027f29b12f7a2562ba0daf3e045cb388f844e022552a5674fcdf5d21f1", + "sha256:6f0a6be264afb094975b5ef55c911379d6989caa87c4e558814ec4f5125cfa2e", + "sha256:706efc19f9679a1b425d6fa2b4bc770d976d0984335eaea0869bd32f627591d2", + "sha256:73f9752cf3596771c7726f7eea5b9e634ad47c6d863043589a1c3bb31325c7eb", + "sha256:762e616199f6319bb98e7f4f27d254c84c5fb1c25c908c2a9d0f92b92fb27530", + "sha256:866a0f583be79f0def667a5d2c60b7b4cc68f0c0a470f227e1122691b443c934", + "sha256:86a94af4a88110ed4bb8961f5ac72edf782958e665d5bfceaab6bf388420a78b", + "sha256:8e0343a6e06d94f6b6ac52fbc75269b41dd3c57066541a6c76517f69fe67cb43", + "sha256:97e615eab34bd8477c3f34197a17ce08c648d38467489359cb9eb7394f1083f7", + "sha256:a96e6d4074db29b152222c34d7eec2e2db2f92638d2b2b2c704f9e8db3ae0edc", + "sha256:b912750b13d76af8aac45ddf4679535def304b2a48a07989ec736508d0bbfbde", + "sha256:bc2676312cc3468a25aac001ec727168994ea3b69b48914944a44c6a0b251e79", + "sha256:cebff2fe5dc82cb22122e4e1225e00a4a506b1a16fafa911142ee124febf2c9e", + "sha256:d22fce0b0f5715cdac082e35a9e735a1752dc8585f005d045abb1a7c20e197f9", + "sha256:d3f7e001328bd6466b3414215f66dde3c7c13d8025a9c160a75d7b2687090d15", + "sha256:d3fe667935e9562407c2511570dca14604a654988a13d8725667e95161d92e9b", + "sha256:dabb70a6e3d9c22df50e08dc55b14ca2a99da95a2d941954255ac76fd6982bc5", + "sha256:e2fb8e8158306567a3a9a41670c1ff99d0567d7fc96fa93b7abf8b519a46b250", + "sha256:e96ac6b3169940a8cd57b4f2b8edcad8f5213b60efcd197d59fbe52f0accd66e", + "sha256:fbf649bc77510ef2521cf797700b96167bb77838c40780da7ea3edd8b78044d1" ], "markers": "python_version >= '3.7'", - "version": "==6.4.post1" + "version": "==6.4.post2" }, "zstandard": { "hashes": [ @@ -3898,62 +3904,62 @@ "toml" ], "hashes": [ - "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" + "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523", + "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f", + "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d", + "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb", + "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0", + "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c", + "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98", + "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83", + "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8", + "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7", + "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac", + "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84", + "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb", + "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3", + "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884", + "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614", + "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd", + "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807", + "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd", + "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8", + "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc", + "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db", + "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0", + "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08", + "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232", + "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d", + "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a", + "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1", + "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286", + "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303", + "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341", + "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84", + "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45", + "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc", + "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec", + "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd", + "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155", + "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52", + "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d", + "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485", + "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31", + "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d", + "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d", + "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d", + "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85", + "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce", + "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb", + "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974", + "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24", + "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56", + "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9", + "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==7.5.1" + "version": "==7.5.3" }, "coveralls": { "hashes": [ @@ -4425,12 +4431,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34", - "sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6" + "sha256:68fdab047a0b9bfbefe79ce267e8a7daaf5128bcf7867065fcd201ee335fece1", + "sha256:d0662561efb725b712207e0ee01f035ca15633f29a64628e24f01ec99d7078f4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.24" + "version": "==9.5.25" }, "mkdocs-material-extensions": { "hashes": [ @@ -4460,11 +4466,11 @@ }, "nodeenv": { "hashes": [ - "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", - "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1", + "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a" ], "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": "==1.8.0" + "version": "==1.9.0" }, "oauthlib": { "hashes": [ @@ -4824,12 +4830,12 @@ }, "requests": { "hashes": [ - "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", - "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.32.2" + "version": "==2.32.3" }, "requests-oauthlib": { "hashes": [ @@ -4943,11 +4949,11 @@ }, "zipp": { "hashes": [ - "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059", - "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e" + "sha256:952df858fb3164426c976d9338d3961e8e8b3758e2e059e0f754b8c4262625ee", + "sha256:96dc6ad62f1441bcaccef23b274ec471518daf4fbbc580341204936a5a3dddec" ], "markers": "python_version >= '3.8'", - "version": "==3.18.2" + "version": "==3.19.0" }, "zope.event": { "hashes": [ @@ -4959,39 +4965,45 @@ }, "zope.interface": { "hashes": [ - "sha256:065f8ff0e034e43b8da05ffed308a9e3311720a2b13b83724f26a8dd6709964d", - "sha256:0940b3b44b6cc0375ea0da5fefee05a9abe8bb53594a3a6e4aafb9f99dc5de8d", - "sha256:15fa208d7a802c0dd3e9d4d5336619a37efd57f2d2ce830d9f9d5843a2b7daba", - "sha256:16f73c42f10f761051157332943ee1f7cf973cc1c78a50d1960c313a211cca4a", - "sha256:18f4061c3456c61557e9d7068e435f5db164b38f15f3d9bd995ff185c6db2c62", - "sha256:1e0678885d07e865047e15be3ebe5c87903cc7f5ca5edfd0045d1c7b43f7fe9d", - "sha256:297ee171f40f8f18665bb75f576df7d1ddce19f3e6696ef6acb930dcbfbf693f", - "sha256:36234d3b8211d053c42684666c2a04eb1a35e0cec6bc3e54586bb60fb0be3b17", - "sha256:3a00983c5c793b17b829020e11032dabab023c4e0ef12f134b90df802ae5adf2", - "sha256:3ca89624d0eabc7ce4f299c6d621531cb8b0ebac3bb4f9ebf2d057477602e1b8", - "sha256:45fb6fe8b5852564e63d6705a7904530a7c886056e6e9aaf938dc5e2bc637097", - "sha256:52901ff6d75a4332457610cbd2883f39b386c5bebe0745ecf78e3fe22cfdd0d9", - "sha256:563a7192a5baf8d9b189dc598c3555e695e00fdce3eafb88b30d6d3df986fcc5", - "sha256:68b13ac49becfaba5b77359559812daac0c5c4b3c0d43cdb293a2dec8db95c24", - "sha256:799743a342249c9b9529abd1115a3f81754800e75dea254b58efdd2984009798", - "sha256:7c18218451823ca9b5131ceaacf655fe9dd4e592ebf848cb0a65fe8428bbf604", - "sha256:8140cf2665c5a07adc285bc859fdda67cfbd7edd62480dfca2211f4798502b54", - "sha256:a430d2cc52aef2af0dc45866852730fbc93463cf8cdeb179e8ee04440e0955c4", - "sha256:a45a7990d143acc37faa905d4a528f5923a5dd30f46536977d8061d10a895b09", - "sha256:aaa1f8967c3f272de80c4bec4b1379f97cd29006323f50558bd2f780a4f637ef", - "sha256:ada5ac54ac7d34bb33423da40b7f3edfc54c6b9623ac9daac7f456dbf25173ba", - "sha256:bf9fa875a9bae5318f24b0d9ab9e2c8a23bccad2979e9e4305eed8119bbe3195", - "sha256:cd3ab863a4e7d888728c949ba052a649664dea156bdd7140eb9269bbe6e33205", - "sha256:d91698257850ca5f523a5513a69775e6fb7c18129311e118996f8e9b463d11b0", - "sha256:dcefd4012593ee410ebf5728ee98f61b3401f0563c5068e760aa2b7720ca68a0", - "sha256:de6c1dad571276768fd6bc92999e8d942151552662a9048e3384cac05b148985", - "sha256:de6eaa0b7df493904d24050dcdc3db6589bd94f7e49caab57971fe47a669b3ea", - "sha256:e7d200aef16e577682dd54a79ff5f4f897a9807722b54bd8a9bca404679c609d", - "sha256:e9961413091e3c9d5c3ed671757049cc6153280f39a154a0b633608efcfdec6b", - "sha256:ec57dec41c0c8b723dd70da1864d50908c689e1c9cf43f32e9b04c0992e5d93d" + "sha256:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854", + "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b", + "sha256:136cacdde1a2c5e5bc3d0b2a1beed733f97e2dad8c2ad3c2e17116f6590a3827", + "sha256:1730c93a38b5a18d24549bc81613223962a19d457cfda9bdc66e542f475a36f4", + "sha256:1a62fd6cd518693568e23e02f41816adedfca637f26716837681c90b36af3671", + "sha256:1c207e6f6dfd5749a26f5a5fd966602d6b824ec00d2df84a7e9a924e8933654e", + "sha256:2eccd5bef45883802848f821d940367c1d0ad588de71e5cabe3813175444202c", + "sha256:33ee982237cffaf946db365c3a6ebaa37855d8e3ca5800f6f48890209c1cfefc", + "sha256:3d136e5b8821073e1a09dde3eb076ea9988e7010c54ffe4d39701adf0c303438", + "sha256:47654177e675bafdf4e4738ce58cdc5c6d6ee2157ac0a78a3fa460942b9d64a8", + "sha256:47937cf2e7ed4e0e37f7851c76edeb8543ec9b0eae149b36ecd26176ff1ca874", + "sha256:4ac46298e0143d91e4644a27a769d1388d5d89e82ee0cf37bf2b0b001b9712a4", + "sha256:4c0b208a5d6c81434bdfa0f06d9b667e5de15af84d8cae5723c3a33ba6611b82", + "sha256:551db2fe892fcbefb38f6f81ffa62de11090c8119fd4e66a60f3adff70751ec7", + "sha256:599f3b07bde2627e163ce484d5497a54a0a8437779362395c6b25e68c6590ede", + "sha256:5ef8356f16b1a83609f7a992a6e33d792bb5eff2370712c9eaae0d02e1924341", + "sha256:5fe919027f29b12f7a2562ba0daf3e045cb388f844e022552a5674fcdf5d21f1", + "sha256:6f0a6be264afb094975b5ef55c911379d6989caa87c4e558814ec4f5125cfa2e", + "sha256:706efc19f9679a1b425d6fa2b4bc770d976d0984335eaea0869bd32f627591d2", + "sha256:73f9752cf3596771c7726f7eea5b9e634ad47c6d863043589a1c3bb31325c7eb", + "sha256:762e616199f6319bb98e7f4f27d254c84c5fb1c25c908c2a9d0f92b92fb27530", + "sha256:866a0f583be79f0def667a5d2c60b7b4cc68f0c0a470f227e1122691b443c934", + "sha256:86a94af4a88110ed4bb8961f5ac72edf782958e665d5bfceaab6bf388420a78b", + "sha256:8e0343a6e06d94f6b6ac52fbc75269b41dd3c57066541a6c76517f69fe67cb43", + "sha256:97e615eab34bd8477c3f34197a17ce08c648d38467489359cb9eb7394f1083f7", + "sha256:a96e6d4074db29b152222c34d7eec2e2db2f92638d2b2b2c704f9e8db3ae0edc", + "sha256:b912750b13d76af8aac45ddf4679535def304b2a48a07989ec736508d0bbfbde", + "sha256:bc2676312cc3468a25aac001ec727168994ea3b69b48914944a44c6a0b251e79", + "sha256:cebff2fe5dc82cb22122e4e1225e00a4a506b1a16fafa911142ee124febf2c9e", + "sha256:d22fce0b0f5715cdac082e35a9e735a1752dc8585f005d045abb1a7c20e197f9", + "sha256:d3f7e001328bd6466b3414215f66dde3c7c13d8025a9c160a75d7b2687090d15", + "sha256:d3fe667935e9562407c2511570dca14604a654988a13d8725667e95161d92e9b", + "sha256:dabb70a6e3d9c22df50e08dc55b14ca2a99da95a2d941954255ac76fd6982bc5", + "sha256:e2fb8e8158306567a3a9a41670c1ff99d0567d7fc96fa93b7abf8b519a46b250", + "sha256:e96ac6b3169940a8cd57b4f2b8edcad8f5213b60efcd197d59fbe52f0accd66e", + "sha256:fbf649bc77510ef2521cf797700b96167bb77838c40780da7ea3edd8b78044d1" ], "markers": "python_version >= '3.7'", - "version": "==6.4.post1" + "version": "==6.4.post2" } } } 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 079/114] 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 080/114] 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 081/114] 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 082/114] 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 083/114] 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 084/114] 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 085/114] 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 086/114] 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 087/114] 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 088/114] 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 089/114] 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 090/114] 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) From 2512ebf597d13ce9e6516056d993b24e93cdf90a Mon Sep 17 00:00:00 2001 From: jefer94 Date: Tue, 4 Jun 2024 16:23:50 -0500 Subject: [PATCH 091/114] update deps --- Pipfile.lock | 546 ++++++++++++++++++++++++++------------------------- 1 file changed, 277 insertions(+), 269 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 70e5b2154..1a95d27ff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -371,12 +371,12 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", + "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.6.2" }, "cffi": { "hashes": [ @@ -617,42 +617,42 @@ }, "cryptography": { "hashes": [ - "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", - "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", - "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", - "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", - "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", - "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", - "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", - "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", - "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", - "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", - "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", - "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", - "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", - "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", - "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", - "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", - "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", - "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", - "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", - "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", - "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", - "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", - "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", - "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", - "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", - "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", - "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", - "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", - "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", - "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", - "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", - "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" + "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", + "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", + "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", + "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", + "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", + "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", + "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", + "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", + "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", + "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", + "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", + "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", + "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", + "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", + "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", + "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", + "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", + "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", + "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", + "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", + "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", + "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", + "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", + "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", + "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", + "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", + "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", + "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", + "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", + "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", + "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", + "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==42.0.7" + "version": "==42.0.8" }, "cssselect": { "hashes": [ @@ -664,11 +664,11 @@ }, "cssutils": { "hashes": [ - "sha256:220816dc6d413e81281bbd568c473a8ae28f73b1af008b1bacf3a7ebd21e0334", - "sha256:cd24a30b9a848ca92d80f0d1b362139c0b69de31394d585dbf1b17a5dc4aa627" + "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2", + "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1" ], "markers": "python_version >= '3.8'", - "version": "==2.11.0" + "version": "==2.11.1" }, "currencies": { "hashes": [ @@ -1030,12 +1030,12 @@ }, "google-cloud-bigquery": { "hashes": [ - "sha256:4b4597f9291b42102c9667d3b4528f801d4c8f24ef2b12dd1ecb881273330955", - "sha256:9fb72884fdbec9c4643cea6b7f21e1ecf3eb61d5305f87493d271dc801647a9e" + "sha256:bc08323ce99dee4e811b7c3d0cde8929f5bf0b1aeaed6bcd75fc89796dd87652", + "sha256:e95e6f6e0aa32e6c453d44e2b3298931fdd7947c309ea329a31b6ff1f939e17e" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.23.1" + "version": "==3.24.0" }, "google-cloud-bigquery-storage": { "hashes": [ @@ -1181,11 +1181,11 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e", - "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632" + "sha256:0e1c2cdfcbc354b76e4a211a35ea35d6926a835cba1377073c4861db904a1877", + "sha256:c6442f7a0a6b2a80369457d79e6672bb7dcbaab88e0848302497e3ec80780a6a" ], "markers": "python_version >= '3.7'", - "version": "==1.63.0" + "version": "==1.63.1" }, "graphene": { "hashes": [ @@ -1287,59 +1287,59 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==3.0.3" }, "grpcio": { "hashes": [ - "sha256:01615bbcae6875eee8091e6b9414072f4e4b00d8b7e141f89635bdae7cf784e5", - "sha256:02cc9cc3f816d30f7993d0d408043b4a7d6a02346d251694d8ab1f78cc723e7e", - "sha256:0b2dfe6dcace264807d9123d483d4c43274e3f8c39f90ff51de538245d7a4145", - "sha256:0da1d921f8e4bcee307aeef6c7095eb26e617c471f8cb1c454fd389c5c296d1e", - "sha256:0f30596cdcbed3c98024fb4f1d91745146385b3f9fd10c9f2270cbfe2ed7ed91", - "sha256:1ce4cd5a61d4532651079e7aae0fedf9a80e613eed895d5b9743e66b52d15812", - "sha256:1f279ad72dd7d64412e10f2443f9f34872a938c67387863c4cd2fb837f53e7d2", - "sha256:1f5de082d936e0208ce8db9095821361dfa97af8767a6607ae71425ac8ace15c", - "sha256:1f8ea18b928e539046bb5f9c124d717fbf00cc4b2d960ae0b8468562846f5aa1", - "sha256:2186d76a7e383e1466e0ea2b0febc343ffeae13928c63c6ec6826533c2d69590", - "sha256:23b6887bb21d77649d022fa1859e05853fdc2e60682fd86c3db652a555a282e0", - "sha256:257baf07f53a571c215eebe9679c3058a313fd1d1f7c4eede5a8660108c52d9c", - "sha256:2a18090371d138a57714ee9bffd6c9c9cb2e02ce42c681aac093ae1e7189ed21", - "sha256:2e8fabe2cc57a369638ab1ad8e6043721014fdf9a13baa7c0e35995d3a4a7618", - "sha256:3161a8f8bb38077a6470508c1a7301cd54301c53b8a34bb83e3c9764874ecabd", - "sha256:31890b24d47b62cc27da49a462efe3d02f3c120edb0e6c46dcc0025506acf004", - "sha256:3550493ac1d23198d46dc9c9b24b411cef613798dc31160c7138568ec26bc9b4", - "sha256:3b09c3d9de95461214a11d82cc0e6a46a6f4e1f91834b50782f932895215e5db", - "sha256:3d2004e85cf5213995d09408501f82c8534700d2babeb81dfdba2a3bff0bb396", - "sha256:46b8b43ba6a2a8f3103f103f97996cad507bcfd72359af6516363c48793d5a7b", - "sha256:579dd9fb11bc73f0de061cab5f8b2def21480fd99eb3743ed041ad6a1913ee2f", - "sha256:597191370951b477b7a1441e1aaa5cacebeb46a3b0bd240ec3bb2f28298c7553", - "sha256:59c68df3a934a586c3473d15956d23a618b8f05b5e7a3a904d40300e9c69cbf0", - "sha256:5a56797dea8c02e7d3a85dfea879f286175cf4d14fbd9ab3ef2477277b927baa", - "sha256:650a8150a9b288f40d5b7c1d5400cc11724eae50bd1f501a66e1ea949173649b", - "sha256:6d5541eb460d73a07418524fb64dcfe0adfbcd32e2dac0f8f90ce5b9dd6c046c", - "sha256:6ec5ed15b4ffe56e2c6bc76af45e6b591c9be0224b3fb090adfb205c9012367d", - "sha256:73f84f9e5985a532e47880b3924867de16fa1aa513fff9b26106220c253c70c5", - "sha256:753cb58683ba0c545306f4e17dabf468d29cb6f6b11832e1e432160bb3f8403c", - "sha256:7c1f5b2298244472bcda49b599be04579f26425af0fd80d3f2eb5fd8bc84d106", - "sha256:7e013428ab472892830287dd082b7d129f4d8afef49227a28223a77337555eaa", - "sha256:7f17572dc9acd5e6dfd3014d10c0b533e9f79cd9517fc10b0225746f4c24b58e", - "sha256:85fda90b81da25993aa47fae66cae747b921f8f6777550895fb62375b776a231", - "sha256:874c741c8a66f0834f653a69e7e64b4e67fcd4a8d40296919b93bab2ccc780ba", - "sha256:8d598b5d5e2c9115d7fb7e2cb5508d14286af506a75950762aa1372d60e41851", - "sha256:8de0399b983f8676a7ccfdd45e5b2caec74a7e3cc576c6b1eecf3b3680deda5e", - "sha256:a053584079b793a54bece4a7d1d1b5c0645bdbee729215cd433703dc2532f72b", - "sha256:a54362f03d4dcfae63be455d0a7d4c1403673498b92c6bfe22157d935b57c7a9", - "sha256:aca4f15427d2df592e0c8f3d38847e25135e4092d7f70f02452c0e90d6a02d6d", - "sha256:b2cbdfba18408389a1371f8c2af1659119e1831e5ed24c240cae9e27b4abc38d", - "sha256:b52e1ec7185512103dd47d41cf34ea78e7a7361ba460187ddd2416b480e0938c", - "sha256:c46fb6bfca17bfc49f011eb53416e61472fa96caa0979b4329176bdd38cbbf2a", - "sha256:c56c91bd2923ddb6e7ed28ebb66d15633b03e0df22206f22dfcdde08047e0a48", - "sha256:cf4c8daed18ae2be2f1fc7d613a76ee2a2e28fdf2412d5c128be23144d28283d", - "sha256:d7b7bf346391dffa182fba42506adf3a84f4a718a05e445b37824136047686a1", - "sha256:d9171f025a196f5bcfec7e8e7ffb7c3535f7d60aecd3503f9e250296c7cfc150" - ], - "version": "==1.64.0" + "sha256:03b43d0ccf99c557ec671c7dede64f023c7da9bb632ac65dbc57f166e4970040", + "sha256:0a12ddb1678ebc6a84ec6b0487feac020ee2b1659cbe69b80f06dbffdb249122", + "sha256:0a2813093ddb27418a4c99f9b1c223fab0b053157176a64cc9db0f4557b69bd9", + "sha256:0cc79c982ccb2feec8aad0e8fb0d168bcbca85bc77b080d0d3c5f2f15c24ea8f", + "sha256:1257b76748612aca0f89beec7fa0615727fd6f2a1ad580a9638816a4b2eb18fd", + "sha256:1262402af5a511c245c3ae918167eca57342c72320dffae5d9b51840c4b2f86d", + "sha256:19264fc964576ddb065368cae953f8d0514ecc6cb3da8903766d9fb9d4554c33", + "sha256:198908f9b22e2672a998870355e226a725aeab327ac4e6ff3a1399792ece4762", + "sha256:1de403fc1305fd96cfa75e83be3dee8538f2413a6b1685b8452301c7ba33c294", + "sha256:20405cb8b13fd779135df23fabadc53b86522d0f1cba8cca0e87968587f50650", + "sha256:2981c7365a9353f9b5c864595c510c983251b1ab403e05b1ccc70a3d9541a73b", + "sha256:2c3c1b90ab93fed424e454e93c0ed0b9d552bdf1b0929712b094f5ecfe7a23ad", + "sha256:39b9d0acaa8d835a6566c640f48b50054f422d03e77e49716d4c4e8e279665a1", + "sha256:3b64ae304c175671efdaa7ec9ae2cc36996b681eb63ca39c464958396697daff", + "sha256:4657d24c8063e6095f850b68f2d1ba3b39f2b287a38242dcabc166453e950c59", + "sha256:4d6dab6124225496010bd22690f2d9bd35c7cbb267b3f14e7a3eb05c911325d4", + "sha256:55260032b95c49bee69a423c2f5365baa9369d2f7d233e933564d8a47b893027", + "sha256:55697ecec192bc3f2f3cc13a295ab670f51de29884ca9ae6cd6247df55df2502", + "sha256:5841dd1f284bd1b3d8a6eca3a7f062b06f1eec09b184397e1d1d43447e89a7ae", + "sha256:58b1041e7c870bb30ee41d3090cbd6f0851f30ae4eb68228955d973d3efa2e61", + "sha256:5e42634a989c3aa6049f132266faf6b949ec2a6f7d302dbb5c15395b77d757eb", + "sha256:5e56462b05a6f860b72f0fa50dca06d5b26543a4e88d0396259a07dc30f4e5aa", + "sha256:5f8b75f64d5d324c565b263c67dbe4f0af595635bbdd93bb1a88189fc62ed2e5", + "sha256:62b4e6eb7bf901719fce0ca83e3ed474ae5022bb3827b0a501e056458c51c0a1", + "sha256:6503b64c8b2dfad299749cad1b595c650c91e5b2c8a1b775380fcf8d2cbba1e9", + "sha256:6c024ffc22d6dc59000faf8ad781696d81e8e38f4078cb0f2630b4a3cf231a90", + "sha256:73819689c169417a4f978e562d24f2def2be75739c4bed1992435d007819da1b", + "sha256:75dbbf415026d2862192fe1b28d71f209e2fd87079d98470db90bebe57b33179", + "sha256:8caee47e970b92b3dd948371230fcceb80d3f2277b3bf7fbd7c0564e7d39068e", + "sha256:8d51dd1c59d5fa0f34266b80a3805ec29a1f26425c2a54736133f6d87fc4968a", + "sha256:940e3ec884520155f68a3b712d045e077d61c520a195d1a5932c531f11883489", + "sha256:a011ac6c03cfe162ff2b727bcb530567826cec85eb8d4ad2bfb4bd023287a52d", + "sha256:a3a035c37ce7565b8f4f35ff683a4db34d24e53dc487e47438e434eb3f701b2a", + "sha256:a5e771d0252e871ce194d0fdcafd13971f1aae0ddacc5f25615030d5df55c3a2", + "sha256:ac15b6c2c80a4d1338b04d42a02d376a53395ddf0ec9ab157cbaf44191f3ffdd", + "sha256:b1a82e0b9b3022799c336e1fc0f6210adc019ae84efb7321d668129d28ee1efb", + "sha256:bac71b4b28bc9af61efcdc7630b166440bbfbaa80940c9a697271b5e1dabbc61", + "sha256:bbc5b1d78a7822b0a84c6f8917faa986c1a744e65d762ef6d8be9d75677af2ca", + "sha256:c1a786ac592b47573a5bb7e35665c08064a5d77ab88a076eec11f8ae86b3e3f6", + "sha256:c84ad903d0d94311a2b7eea608da163dace97c5fe9412ea311e72c3684925602", + "sha256:d4d29cc612e1332237877dfa7fe687157973aab1d63bd0f84cf06692f04c0367", + "sha256:e3d9f8d1221baa0ced7ec7322a981e28deb23749c76eeeb3d33e18b72935ab62", + "sha256:e7cd5c1325f6808b8ae31657d281aadb2a51ac11ab081ae335f4f7fc44c1721d", + "sha256:ed6091fa0adcc7e4ff944090cf203a52da35c37a130efa564ded02b7aff63bcd", + "sha256:ee73a2f5ca4ba44fa33b4d7d2c71e2c8a9e9f78d53f6507ad68e7d2ad5f64a22", + "sha256:f10193c69fc9d3d726e83bbf0f3d316f1847c3071c8c93d8090cf5f326b14309" + ], + "version": "==1.64.1" }, "grpcio-status": { "hashes": [ @@ -1926,6 +1926,14 @@ "markers": "python_version >= '3.7'", "version": "==7.2.2" }, + "more-itertools": { + "hashes": [ + "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", + "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1" + ], + "markers": "python_version >= '3.8'", + "version": "==10.2.0" + }, "msgpack": { "hashes": [ "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", @@ -2190,12 +2198,12 @@ }, "openai": { "hashes": [ - "sha256:2ad95e926de0d2e09cde632a9204b0a6dca4a03c2cdcc84329b01f355784355a", - "sha256:5366562eb2c5917e6116ae0391b7ae6e3acd62b0ae3f565ada32b35d8fcfa106" + "sha256:54ae0625b005d6a3b895db2b8438dae1059cffff0cd262a26e9015c13a29ab06", + "sha256:82044ee3122113f2a468a1f308a8882324d09556ba5348687c535d3655ee331c" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.30.5" + "version": "==1.31.0" }, "packaging": { "hashes": [ @@ -2251,10 +2259,10 @@ }, "phonenumberslite": { "hashes": [ - "sha256:6b28e38ea2bc0d809c1f933f7fccb860d780bc62b9456a2754cb778a620dca76", - "sha256:7cdc76625e0879071ad31f4066867adbc6779ac37d574957c64a72b59b8bc82d" + "sha256:20e821bb9b9e2fcaa10cd3618e7606264ed0cf4b04877c938ec5d6211ba9efa6", + "sha256:ebad5c1df879de316899e93b8ea9730a822e1b2ec6d83230f0aa1946a5900153" ], - "version": "==8.13.37" + "version": "==8.13.38" }, "pillow": { "hashes": [ @@ -2380,11 +2388,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:07c60ee4ab7b7e90824b61afa840c8f5aad2d46b3e2e10acc33d8ecc94a49089", - "sha256:a29b89160e494e3ea8622b09fa5897610b437884dcdcd054fdc1308883326c2a" + "sha256:45abe60a8300f3c618b23c16c4bb98c6fc80af8ce8b17c7ae92db48db3ee63c1", + "sha256:869c50d682152336e23c4db7f74667639b5047494202ffe7670817053fd57795" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.45" + "version": "==3.0.46" }, "proto-plus": { "hashes": [ @@ -2641,96 +2649,96 @@ }, "pydantic": { "hashes": [ - "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7", - "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7" + "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e", + "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4" ], "markers": "python_version >= '3.8'", - "version": "==2.7.2" + "version": "==2.7.3" }, "pydantic-core": { "hashes": [ - "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9", - "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b", - "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426", - "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc", - "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9", - "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc", - "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4", - "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702", - "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6", - "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231", - "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083", - "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60", - "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7", - "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a", - "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4", - "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6", - "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39", - "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0", - "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026", - "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c", - "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442", - "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7", - "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044", - "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd", - "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94", - "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361", - "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af", - "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4", - "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022", - "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7", - "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd", - "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8", - "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2", - "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5", - "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be", - "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c", - "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9", - "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417", - "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1", - "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326", - "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4", - "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb", - "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34", - "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d", - "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9", - "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812", - "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558", - "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779", - "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be", - "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c", - "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe", - "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0", - "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c", - "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2", - "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a", - "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab", - "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048", - "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3", - "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106", - "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d", - "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb", - "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6", - "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b", - "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef", - "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5", - "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb", - "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c", - "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06", - "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059", - "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943", - "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1", - "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2", - "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c", - "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522", - "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f", - "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78", - "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7", - "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a", - "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5" + "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3", + "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8", + "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8", + "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30", + "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a", + "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8", + "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d", + "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc", + "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2", + "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab", + "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077", + "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e", + "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9", + "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9", + "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef", + "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1", + "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507", + "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528", + "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558", + "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b", + "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154", + "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724", + "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695", + "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9", + "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851", + "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805", + "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a", + "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5", + "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94", + "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c", + "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d", + "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef", + "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26", + "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2", + "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c", + "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0", + "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2", + "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4", + "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d", + "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2", + "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce", + "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34", + "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f", + "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d", + "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b", + "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07", + "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312", + "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057", + "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d", + "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af", + "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb", + "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd", + "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78", + "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b", + "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223", + "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a", + "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4", + "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5", + "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23", + "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a", + "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4", + "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8", + "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d", + "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443", + "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e", + "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f", + "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e", + "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d", + "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc", + "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443", + "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be", + "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2", + "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee", + "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f", + "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae", + "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864", + "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4", + "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951", + "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc" ], "markers": "python_version >= '3.8'", - "version": "==2.18.3" + "version": "==2.18.4" }, "pyfcm": { "hashes": [ @@ -2808,11 +2816,11 @@ }, "pytest": { "hashes": [ - "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", - "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "markers": "python_version >= '3.8'", - "version": "==8.2.1" + "version": "==8.2.2" }, "pytest-django": { "hashes": [ @@ -3388,11 +3396,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8", - "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594" + "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a", + "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1" ], - "markers": "python_version >= '3.7'", - "version": "==4.12.0" + "markers": "python_version >= '3.8'", + "version": "==4.12.1" }, "tzdata": { "hashes": [ @@ -3421,12 +3429,12 @@ }, "uvicorn": { "hashes": [ - "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab", - "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37" + "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81", + "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.30.0" + "version": "==0.30.1" }, "vine": { "hashes": [ @@ -3640,15 +3648,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:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854", "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b", @@ -3690,6 +3690,14 @@ "markers": "python_version >= '3.7'", "version": "==6.4.post2" }, + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", @@ -3772,12 +3780,12 @@ }, "certifi": { "hashes": [ - "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", - "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" + "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", + "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2024.2.2" + "version": "==2024.6.2" }, "cfgv": { "hashes": [ @@ -4128,11 +4136,11 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e", - "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632" + "sha256:0e1c2cdfcbc354b76e4a211a35ea35d6926a835cba1377073c4861db904a1877", + "sha256:c6442f7a0a6b2a80369457d79e6672bb7dcbaab88e0848302497e3ec80780a6a" ], "markers": "python_version >= '3.7'", - "version": "==1.63.0" + "version": "==1.63.1" }, "greenlet": { "hashes": [ @@ -4195,7 +4203,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3.7'", + "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==3.0.3" }, "griffe": { @@ -4208,54 +4216,54 @@ }, "grpcio": { "hashes": [ - "sha256:01615bbcae6875eee8091e6b9414072f4e4b00d8b7e141f89635bdae7cf784e5", - "sha256:02cc9cc3f816d30f7993d0d408043b4a7d6a02346d251694d8ab1f78cc723e7e", - "sha256:0b2dfe6dcace264807d9123d483d4c43274e3f8c39f90ff51de538245d7a4145", - "sha256:0da1d921f8e4bcee307aeef6c7095eb26e617c471f8cb1c454fd389c5c296d1e", - "sha256:0f30596cdcbed3c98024fb4f1d91745146385b3f9fd10c9f2270cbfe2ed7ed91", - "sha256:1ce4cd5a61d4532651079e7aae0fedf9a80e613eed895d5b9743e66b52d15812", - "sha256:1f279ad72dd7d64412e10f2443f9f34872a938c67387863c4cd2fb837f53e7d2", - "sha256:1f5de082d936e0208ce8db9095821361dfa97af8767a6607ae71425ac8ace15c", - "sha256:1f8ea18b928e539046bb5f9c124d717fbf00cc4b2d960ae0b8468562846f5aa1", - "sha256:2186d76a7e383e1466e0ea2b0febc343ffeae13928c63c6ec6826533c2d69590", - "sha256:23b6887bb21d77649d022fa1859e05853fdc2e60682fd86c3db652a555a282e0", - "sha256:257baf07f53a571c215eebe9679c3058a313fd1d1f7c4eede5a8660108c52d9c", - "sha256:2a18090371d138a57714ee9bffd6c9c9cb2e02ce42c681aac093ae1e7189ed21", - "sha256:2e8fabe2cc57a369638ab1ad8e6043721014fdf9a13baa7c0e35995d3a4a7618", - "sha256:3161a8f8bb38077a6470508c1a7301cd54301c53b8a34bb83e3c9764874ecabd", - "sha256:31890b24d47b62cc27da49a462efe3d02f3c120edb0e6c46dcc0025506acf004", - "sha256:3550493ac1d23198d46dc9c9b24b411cef613798dc31160c7138568ec26bc9b4", - "sha256:3b09c3d9de95461214a11d82cc0e6a46a6f4e1f91834b50782f932895215e5db", - "sha256:3d2004e85cf5213995d09408501f82c8534700d2babeb81dfdba2a3bff0bb396", - "sha256:46b8b43ba6a2a8f3103f103f97996cad507bcfd72359af6516363c48793d5a7b", - "sha256:579dd9fb11bc73f0de061cab5f8b2def21480fd99eb3743ed041ad6a1913ee2f", - "sha256:597191370951b477b7a1441e1aaa5cacebeb46a3b0bd240ec3bb2f28298c7553", - "sha256:59c68df3a934a586c3473d15956d23a618b8f05b5e7a3a904d40300e9c69cbf0", - "sha256:5a56797dea8c02e7d3a85dfea879f286175cf4d14fbd9ab3ef2477277b927baa", - "sha256:650a8150a9b288f40d5b7c1d5400cc11724eae50bd1f501a66e1ea949173649b", - "sha256:6d5541eb460d73a07418524fb64dcfe0adfbcd32e2dac0f8f90ce5b9dd6c046c", - "sha256:6ec5ed15b4ffe56e2c6bc76af45e6b591c9be0224b3fb090adfb205c9012367d", - "sha256:73f84f9e5985a532e47880b3924867de16fa1aa513fff9b26106220c253c70c5", - "sha256:753cb58683ba0c545306f4e17dabf468d29cb6f6b11832e1e432160bb3f8403c", - "sha256:7c1f5b2298244472bcda49b599be04579f26425af0fd80d3f2eb5fd8bc84d106", - "sha256:7e013428ab472892830287dd082b7d129f4d8afef49227a28223a77337555eaa", - "sha256:7f17572dc9acd5e6dfd3014d10c0b533e9f79cd9517fc10b0225746f4c24b58e", - "sha256:85fda90b81da25993aa47fae66cae747b921f8f6777550895fb62375b776a231", - "sha256:874c741c8a66f0834f653a69e7e64b4e67fcd4a8d40296919b93bab2ccc780ba", - "sha256:8d598b5d5e2c9115d7fb7e2cb5508d14286af506a75950762aa1372d60e41851", - "sha256:8de0399b983f8676a7ccfdd45e5b2caec74a7e3cc576c6b1eecf3b3680deda5e", - "sha256:a053584079b793a54bece4a7d1d1b5c0645bdbee729215cd433703dc2532f72b", - "sha256:a54362f03d4dcfae63be455d0a7d4c1403673498b92c6bfe22157d935b57c7a9", - "sha256:aca4f15427d2df592e0c8f3d38847e25135e4092d7f70f02452c0e90d6a02d6d", - "sha256:b2cbdfba18408389a1371f8c2af1659119e1831e5ed24c240cae9e27b4abc38d", - "sha256:b52e1ec7185512103dd47d41cf34ea78e7a7361ba460187ddd2416b480e0938c", - "sha256:c46fb6bfca17bfc49f011eb53416e61472fa96caa0979b4329176bdd38cbbf2a", - "sha256:c56c91bd2923ddb6e7ed28ebb66d15633b03e0df22206f22dfcdde08047e0a48", - "sha256:cf4c8daed18ae2be2f1fc7d613a76ee2a2e28fdf2412d5c128be23144d28283d", - "sha256:d7b7bf346391dffa182fba42506adf3a84f4a718a05e445b37824136047686a1", - "sha256:d9171f025a196f5bcfec7e8e7ffb7c3535f7d60aecd3503f9e250296c7cfc150" - ], - "version": "==1.64.0" + "sha256:03b43d0ccf99c557ec671c7dede64f023c7da9bb632ac65dbc57f166e4970040", + "sha256:0a12ddb1678ebc6a84ec6b0487feac020ee2b1659cbe69b80f06dbffdb249122", + "sha256:0a2813093ddb27418a4c99f9b1c223fab0b053157176a64cc9db0f4557b69bd9", + "sha256:0cc79c982ccb2feec8aad0e8fb0d168bcbca85bc77b080d0d3c5f2f15c24ea8f", + "sha256:1257b76748612aca0f89beec7fa0615727fd6f2a1ad580a9638816a4b2eb18fd", + "sha256:1262402af5a511c245c3ae918167eca57342c72320dffae5d9b51840c4b2f86d", + "sha256:19264fc964576ddb065368cae953f8d0514ecc6cb3da8903766d9fb9d4554c33", + "sha256:198908f9b22e2672a998870355e226a725aeab327ac4e6ff3a1399792ece4762", + "sha256:1de403fc1305fd96cfa75e83be3dee8538f2413a6b1685b8452301c7ba33c294", + "sha256:20405cb8b13fd779135df23fabadc53b86522d0f1cba8cca0e87968587f50650", + "sha256:2981c7365a9353f9b5c864595c510c983251b1ab403e05b1ccc70a3d9541a73b", + "sha256:2c3c1b90ab93fed424e454e93c0ed0b9d552bdf1b0929712b094f5ecfe7a23ad", + "sha256:39b9d0acaa8d835a6566c640f48b50054f422d03e77e49716d4c4e8e279665a1", + "sha256:3b64ae304c175671efdaa7ec9ae2cc36996b681eb63ca39c464958396697daff", + "sha256:4657d24c8063e6095f850b68f2d1ba3b39f2b287a38242dcabc166453e950c59", + "sha256:4d6dab6124225496010bd22690f2d9bd35c7cbb267b3f14e7a3eb05c911325d4", + "sha256:55260032b95c49bee69a423c2f5365baa9369d2f7d233e933564d8a47b893027", + "sha256:55697ecec192bc3f2f3cc13a295ab670f51de29884ca9ae6cd6247df55df2502", + "sha256:5841dd1f284bd1b3d8a6eca3a7f062b06f1eec09b184397e1d1d43447e89a7ae", + "sha256:58b1041e7c870bb30ee41d3090cbd6f0851f30ae4eb68228955d973d3efa2e61", + "sha256:5e42634a989c3aa6049f132266faf6b949ec2a6f7d302dbb5c15395b77d757eb", + "sha256:5e56462b05a6f860b72f0fa50dca06d5b26543a4e88d0396259a07dc30f4e5aa", + "sha256:5f8b75f64d5d324c565b263c67dbe4f0af595635bbdd93bb1a88189fc62ed2e5", + "sha256:62b4e6eb7bf901719fce0ca83e3ed474ae5022bb3827b0a501e056458c51c0a1", + "sha256:6503b64c8b2dfad299749cad1b595c650c91e5b2c8a1b775380fcf8d2cbba1e9", + "sha256:6c024ffc22d6dc59000faf8ad781696d81e8e38f4078cb0f2630b4a3cf231a90", + "sha256:73819689c169417a4f978e562d24f2def2be75739c4bed1992435d007819da1b", + "sha256:75dbbf415026d2862192fe1b28d71f209e2fd87079d98470db90bebe57b33179", + "sha256:8caee47e970b92b3dd948371230fcceb80d3f2277b3bf7fbd7c0564e7d39068e", + "sha256:8d51dd1c59d5fa0f34266b80a3805ec29a1f26425c2a54736133f6d87fc4968a", + "sha256:940e3ec884520155f68a3b712d045e077d61c520a195d1a5932c531f11883489", + "sha256:a011ac6c03cfe162ff2b727bcb530567826cec85eb8d4ad2bfb4bd023287a52d", + "sha256:a3a035c37ce7565b8f4f35ff683a4db34d24e53dc487e47438e434eb3f701b2a", + "sha256:a5e771d0252e871ce194d0fdcafd13971f1aae0ddacc5f25615030d5df55c3a2", + "sha256:ac15b6c2c80a4d1338b04d42a02d376a53395ddf0ec9ab157cbaf44191f3ffdd", + "sha256:b1a82e0b9b3022799c336e1fc0f6210adc019ae84efb7321d668129d28ee1efb", + "sha256:bac71b4b28bc9af61efcdc7630b166440bbfbaa80940c9a697271b5e1dabbc61", + "sha256:bbc5b1d78a7822b0a84c6f8917faa986c1a744e65d762ef6d8be9d75677af2ca", + "sha256:c1a786ac592b47573a5bb7e35665c08064a5d77ab88a076eec11f8ae86b3e3f6", + "sha256:c84ad903d0d94311a2b7eea608da163dace97c5fe9412ea311e72c3684925602", + "sha256:d4d29cc612e1332237877dfa7fe687157973aab1d63bd0f84cf06692f04c0367", + "sha256:e3d9f8d1221baa0ced7ec7322a981e28deb23749c76eeeb3d33e18b72935ab62", + "sha256:e7cd5c1325f6808b8ae31657d281aadb2a51ac11ab081ae335f4f7fc44c1721d", + "sha256:ed6091fa0adcc7e4ff944090cf203a52da35c37a130efa564ded02b7aff63bcd", + "sha256:ee73a2f5ca4ba44fa33b4d7d2c71e2c8a9e9f78d53f6507ad68e7d2ad5f64a22", + "sha256:f10193c69fc9d3d726e83bbf0f3d316f1847c3071c8c93d8090cf5f326b14309" + ], + "version": "==1.64.1" }, "grpcio-status": { "hashes": [ @@ -4466,11 +4474,11 @@ }, "nodeenv": { "hashes": [ - "sha256:07f144e90dae547bf0d4ee8da0ee42664a42a04e02ed68e06324348dafe4bdb1", - "sha256:508ecec98f9f3330b636d4448c0f1a56fc68017c68f1e7857ebc52acf0eb879a" + "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", + "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9" ], "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": "==1.9.0" + "version": "==1.9.1" }, "oauthlib": { "hashes": [ @@ -4627,11 +4635,11 @@ }, "pytest": { "hashes": [ - "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd", - "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1" + "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", + "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977" ], "markers": "python_version >= '3.8'", - "version": "==8.2.1" + "version": "==8.2.2" }, "pytest-asyncio": { "hashes": [ @@ -4949,11 +4957,11 @@ }, "zipp": { "hashes": [ - "sha256:952df858fb3164426c976d9338d3961e8e8b3758e2e059e0f754b8c4262625ee", - "sha256:96dc6ad62f1441bcaccef23b274ec471518daf4fbbc580341204936a5a3dddec" + "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19", + "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c" ], "markers": "python_version >= '3.8'", - "version": "==3.19.0" + "version": "==3.19.2" }, "zope.event": { "hashes": [ From 13ffbd9fef7ec666ad722d02294df9739ff99e4a Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 4 Jun 2024 18:29:22 -0400 Subject: [PATCH 092/114] Update actions.py --- breathecode/registry/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index f88a26239..68f4112e2 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -661,7 +661,7 @@ def process_asset_config(asset, config): # only replace title and description of English language if 'title' in config: if isinstance(config['title'], str): - if (lang == '' or asset.title == '' or asset.title is None): + if (asset.lang in ['us', 'en'] or asset.title == '' or asset.title is None): asset.title = config['title'] elif isinstance(config['title'], dict) and asset.lang in config['title']: asset.title = config['title'][asset.lang] From 652bd568abbbabfec8b45db5ad0527396d6034f6 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 4 Jun 2024 18:31:37 -0400 Subject: [PATCH 093/114] Update actions.py --- breathecode/registry/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/registry/actions.py b/breathecode/registry/actions.py index 68f4112e2..81f6c1f44 100644 --- a/breathecode/registry/actions.py +++ b/breathecode/registry/actions.py @@ -661,7 +661,7 @@ def process_asset_config(asset, config): # only replace title and description of English language if 'title' in config: if isinstance(config['title'], str): - if (asset.lang in ['us', 'en'] or asset.title == '' or asset.title is None): + if (asset.lang in ['','us', 'en'] or asset.title == '' or asset.title is None): asset.title = config['title'] elif isinstance(config['title'], dict) and asset.lang in config['title']: asset.title = config['title'][asset.lang] @@ -669,7 +669,7 @@ def process_asset_config(asset, config): if 'description' in config: if isinstance(config['description'], str): # avoid replacing descriptions for other languages - if (lang == '' or asset.description == '' or asset.description is None): + if (asset.lang in ['','us', 'en'] or asset.description == '' or asset.description is None): asset.description = config['description'] # there are multiple translations, and the translation exists for this lang elif isinstance(config['description'], dict) and asset.lang in config['description']: From b3d3986cfe8b4855a879445ecd0e4152498b7742 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:01:14 -0400 Subject: [PATCH 094/114] Update serializers.py --- breathecode/payments/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/breathecode/payments/serializers.py b/breathecode/payments/serializers.py index 8e40a7718..72c17402a 100644 --- a/breathecode/payments/serializers.py +++ b/breathecode/payments/serializers.py @@ -383,6 +383,13 @@ class GetPlanFinancingSerializer(GetAbstractIOweYouSerializer): monthly_price = serpy.Field() +class GetSubscriptionHookSerializer(GetAbstractIOweYouSerializer): + paid_at = serpy.Field() + is_refundable = serpy.Field() + + pay_every = serpy.Field() + pay_every_unit = serpy.Field() + class GetSubscriptionSerializer(GetAbstractIOweYouSerializer): paid_at = serpy.Field() is_refundable = serpy.Field() @@ -395,7 +402,6 @@ class GetSubscriptionSerializer(GetAbstractIOweYouSerializer): def get_service_items(self, obj): return GetServiceItemSerializer(obj.service_items.filter(), many=True).data - class GetBagSerializer(serpy.Serializer): id = serpy.Field() service_items = serpy.MethodField() From 32206e1d2e0273652690881cb7bd0ddd835f7d92 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:01:44 -0400 Subject: [PATCH 095/114] Update receivers.py --- breathecode/notify/receivers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/notify/receivers.py b/breathecode/notify/receivers.py index f2bfb8e4e..4b997c2bc 100644 --- a/breathecode/notify/receivers.py +++ b/breathecode/notify/receivers.py @@ -19,7 +19,7 @@ from breathecode.mentorship.serializers import SessionHookSerializer from breathecode.mentorship.signals import mentorship_session_status from breathecode.payments.models import PlanFinancing, Subscription -from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionSerializer +from breathecode.payments.serializers import GetPlanFinancingSerializer, GetSubscriptionHookSerializer from breathecode.payments.signals import planfinancing_created, subscription_created from breathecode.registry.models import Asset from breathecode.registry.serializers import AssetHookSerializer @@ -180,7 +180,7 @@ def new_planfinancing_created(sender, instance, **kwargs): def new_subscription_created(sender, instance, **kwargs): logger.debug('Sending new Subscription to hook') model_label = get_model_label(instance) - serializer = GetSubscriptionSerializer(instance) + serializer = GetSubscriptionHookSerializer(instance) HookManager.process_model_event(instance, model_label, 'subscription_created', From ad4dc697f8238a6688d9f3f05083bb106d29c1e8 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Tue, 4 Jun 2024 19:14:34 -0400 Subject: [PATCH 096/114] Update hook_manager.py --- breathecode/notify/utils/hook_manager.py | 29 +++++++++++++----------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/breathecode/notify/utils/hook_manager.py b/breathecode/notify/utils/hook_manager.py index ed713cf44..958f8b8d4 100644 --- a/breathecode/notify/utils/hook_manager.py +++ b/breathecode/notify/utils/hook_manager.py @@ -201,18 +201,21 @@ def deliver_hook(self, hook, instance, payload_override=None, academy_override=N return such object. If callable is used it should accept 2 arguments: `hook` and `instance`. """ - if payload_override is None: - payload = hook.serialize_hook(instance) - else: - payload = payload_override - - if callable(payload): - payload = payload(hook, instance) - - logger.debug(f'Calling delayed task deliver_hook for hook {hook.id}') - async_deliver_hook.delay(hook.target, payload, hook_id=hook.id) - - return None - + try: + if payload_override is None: + payload = hook.serialize_hook(instance) + else: + payload = payload_override + + if callable(payload): + payload = payload(hook, instance) + + logger.debug(f'Calling delayed task deliver_hook for hook {hook.id}') + async_deliver_hook.delay(hook.target, payload, hook_id=hook.id) + + return None + except Exception: + # TODO: FIXME + pass HookManager = HookManagerClass() From e663f5af57b6d30e7bdb64b091ee14c43def1e7d Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Tue, 4 Jun 2024 21:48:13 -0400 Subject: [PATCH 097/114] Update serializers.py --- breathecode/monitoring/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/breathecode/monitoring/serializers.py b/breathecode/monitoring/serializers.py index dae950f0e..18d681519 100644 --- a/breathecode/monitoring/serializers.py +++ b/breathecode/monitoring/serializers.py @@ -42,6 +42,7 @@ class RepoSubscriptionSmallSerializer(serpy.Serializer): id = serpy.Field() hook_id = serpy.Field() repository = serpy.Field() + token = serpy.Field() updated_at = serpy.Field() status = serpy.Field() status_message = serpy.Field() From d1c2a9a20dd5049ae4af6e198389956c45010e9a Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Wed, 5 Jun 2024 11:42:02 -0400 Subject: [PATCH 098/114] Update set_permissions.py --- breathecode/authenticate/management/commands/set_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/authenticate/management/commands/set_permissions.py b/breathecode/authenticate/management/commands/set_permissions.py index 3c27b84eb..2e86d7b4b 100644 --- a/breathecode/authenticate/management/commands/set_permissions.py +++ b/breathecode/authenticate/management/commands/set_permissions.py @@ -121,7 +121,7 @@ }, { 'name': 'Legacy', - 'permissions': [], + 'permissions': ['get_my_certificate'], 'inherit': ['Classes', 'Events', 'Mentorships'] }, ] From 8f9b56099beaf693aa2857edac50e69adf25fb92 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 5 Jun 2024 12:35:49 -0400 Subject: [PATCH 099/114] Update views.py --- breathecode/registry/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/breathecode/registry/views.py b/breathecode/registry/views.py index 426d76219..9f8f4737e 100644 --- a/breathecode/registry/views.py +++ b/breathecode/registry/views.py @@ -708,9 +708,9 @@ def put(self, request, asset_slug, action_slug, academy_id=None): override_meta = request.data['override_meta'] pull_from_github(asset.slug, override_meta=override_meta) elif action_slug == 'push': - if asset.asset_type not in ['ARTICLE', 'LESSON']: + if asset.asset_type not in ['ARTICLE', 'LESSON', 'QUIZ']: raise ValidationException( - 'Only lessons and articles and be pushed to github, please update the Github repository yourself and come back to pull the changes from here' + f'Asset type {asset.asset_type} cannot be pushed to GitHub, please update the Github repository manually' ) push_to_github(asset.slug, author=request.user) From 5d7b970484cebe154a35e848784bd616fd56f13f Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 5 Jun 2024 15:32:08 -0400 Subject: [PATCH 100/114] Update admin.py --- breathecode/monitoring/admin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/breathecode/monitoring/admin.py b/breathecode/monitoring/admin.py index 905283c36..1d1695cc2 100644 --- a/breathecode/monitoring/admin.py +++ b/breathecode/monitoring/admin.py @@ -232,6 +232,9 @@ def has_delete_permission(self, request, obj=None): # Return False to remove the "Delete" button from the update form. # You can add additional logic here if you want to conditionally # enable the delete button for certain cases. + if obj.status == 'DISABLED': + return True + return False def repo(self, obj): From 9136d7cf1daf277825a265be9a0d02446ace09fd Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Wed, 5 Jun 2024 17:20:45 -0400 Subject: [PATCH 101/114] Update admin.py --- breathecode/monitoring/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/monitoring/admin.py b/breathecode/monitoring/admin.py index 1d1695cc2..d17aba29f 100644 --- a/breathecode/monitoring/admin.py +++ b/breathecode/monitoring/admin.py @@ -232,7 +232,7 @@ def has_delete_permission(self, request, obj=None): # Return False to remove the "Delete" button from the update form. # You can add additional logic here if you want to conditionally # enable the delete button for certain cases. - if obj.status == 'DISABLED': + if obj and obj.status == 'DISABLED': return True return False From 851b906ba1b5ed840ee0c6d7486dc1d57fd0c39b Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 6 Jun 2024 00:41:12 -0500 Subject: [PATCH 102/114] update pipfile --- Pipfile.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 1a95d27ff..aa38c58df 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -363,11 +363,11 @@ "django" ], "hashes": [ - "sha256:3c6a472b1d6aad3dd67c422ed82be7369177479953ae98d618216624951ddaac", - "sha256:bc3cffe2df0c1a19c742ad434a9cac848cb9ea62931036675d0fa0112d729f12" + "sha256:1054b3284a272b3d60edcbd8dbee9aba61721101047023a328bc41bac3609645", + "sha256:33ca925de4a0dd96f3e13477cf3a54f9ee2304416345b0311547fbb3c9c04fab" ], "markers": "python_version >= '3.11'", - "version": "==1.4.0" + "version": "==1.5.0" }, "certifi": { "hashes": [ @@ -1287,7 +1287,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "markers": "python_version >= '3.7'", "version": "==3.0.3" }, "grpcio": { @@ -2198,12 +2198,12 @@ }, "openai": { "hashes": [ - "sha256:54ae0625b005d6a3b895db2b8438dae1059cffff0cd262a26e9015c13a29ab06", - "sha256:82044ee3122113f2a468a1f308a8882324d09556ba5348687c535d3655ee331c" + "sha256:a15266827de20f407d4bf9837030b168074b5b29acd54f10bb38d5f53e95f083", + "sha256:a746cf070798a4048cfea00b0fc7cb9760ee7ead5a08c48115b914d1afbd1b53" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.31.0" + "version": "==1.31.1" }, "packaging": { "hashes": [ @@ -4203,7 +4203,7 @@ "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" ], - "markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "markers": "python_version >= '3.7'", "version": "==3.0.3" }, "griffe": { From de9d5b85db367943f0d1421769160864fc847282 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 6 Jun 2024 11:21:31 -0400 Subject: [PATCH 103/114] Update serializers.py --- breathecode/monitoring/serializers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/breathecode/monitoring/serializers.py b/breathecode/monitoring/serializers.py index 18d681519..a11021b2f 100644 --- a/breathecode/monitoring/serializers.py +++ b/breathecode/monitoring/serializers.py @@ -5,6 +5,7 @@ from breathecode.authenticate.models import AcademyAuthSettings from breathecode.monitoring.actions import subscribe_repository from breathecode.monitoring.models import RepositorySubscription +from django.core.validators import URLValidator from breathecode.monitoring.tasks import async_subscribe_repo, async_unsubscribe_repo from breathecode.utils import serpy from breathecode.utils.i18n import translation @@ -72,6 +73,18 @@ def validate(self, data): en='You must specify a repository url', es='Debes especificar el URL del repositorio a subscribir', slug='missing-repo')) + + url_validator = URLValidator() + try: + url_validator(data['repository']) + if "github.com" not in data['repository']: + raise ValidationError("Only GitHub repositories can be subscribed to") + except ValidationError as e: + raise ValidationException( + translation(lang, + en=str(e), + es='La URL del repositorio debe ser valida y apuntar a github.com", + slug='invalid-repo-url')) subs = RepositorySubscription.objects.filter(owner__id=academy_id, repository=data['repository']).first() # Sabe repo and academy subscription cannot be CREATED twice From 4aad8dfbe3dc4da802d7893ad819ca103a6aa369 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 6 Jun 2024 11:40:37 -0400 Subject: [PATCH 104/114] Update actions.py --- breathecode/monitoring/actions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/breathecode/monitoring/actions.py b/breathecode/monitoring/actions.py index 39e33f23e..efcaf30f3 100644 --- a/breathecode/monitoring/actions.py +++ b/breathecode/monitoring/actions.py @@ -96,13 +96,11 @@ def subscribe_repository(subs_id, settings=None): subscription.status_message = 'OK' subscription.hook_id = result['id'] subscription.save() - - return subscription except Exception as e: subscription.status = 'CRITICAL' subscription.status_message = 'Error subscribing to repo: ' + str(e) subscription.save() - raise e + return subscription def get_website_text(endp): From 2acc13cae194df75782ae4fd2dd133ad89b4659e Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 6 Jun 2024 15:53:35 +0000 Subject: [PATCH 105/114] updated all places were subscribe_repository was being used to validate status=OPERATIONAL --- breathecode/monitoring/admin.py | 6 ++++-- breathecode/monitoring/serializers.py | 21 +++++++++++-------- breathecode/monitoring/tasks.py | 3 ++- .../commands/subscribe_asset_repos.py | 4 +++- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/breathecode/monitoring/admin.py b/breathecode/monitoring/admin.py index d17aba29f..1bd1150ad 100644 --- a/breathecode/monitoring/admin.py +++ b/breathecode/monitoring/admin.py @@ -208,7 +208,9 @@ def activate_subscription(modeladmin, request, queryset): # stay this here for use the poor mocking system for subs in queryset.all(): try: - subscribe_repository(subs.id) + subscription = subscribe_repository(subs.id) + if subscription.status != 'OPERATIONAL': + raise Exception(subscription.status_message) except Exception as e: messages.error(request, str(e)) return False @@ -234,7 +236,7 @@ def has_delete_permission(self, request, obj=None): # enable the delete button for certain cases. if obj and obj.status == 'DISABLED': return True - + return False def repo(self, obj): diff --git a/breathecode/monitoring/serializers.py b/breathecode/monitoring/serializers.py index a11021b2f..69941fc5b 100644 --- a/breathecode/monitoring/serializers.py +++ b/breathecode/monitoring/serializers.py @@ -73,17 +73,17 @@ def validate(self, data): en='You must specify a repository url', es='Debes especificar el URL del repositorio a subscribir', slug='missing-repo')) - + url_validator = URLValidator() try: url_validator(data['repository']) - if "github.com" not in data['repository']: - raise ValidationError("Only GitHub repositories can be subscribed to") + if 'github.com' not in data['repository']: + raise ValidationError('Only GitHub repositories can be subscribed to') except ValidationError as e: raise ValidationException( translation(lang, en=str(e), - es='La URL del repositorio debe ser valida y apuntar a github.com", + es='La URL del repositorio debe ser valida y apuntar a github.com', slug='invalid-repo-url')) subs = RepositorySubscription.objects.filter(owner__id=academy_id, repository=data['repository']).first() @@ -138,14 +138,17 @@ def create(self, validated_data): }) try: - subscribe_repository(instance.id, settings) + subscription = subscribe_repository(instance.id, settings) + if subscription.status != 'OPERATIONAL': + raise Exception(subscription.status_message) except Exception as e: logger.error(str(e)) raise ValidationException( - translation(lang, - en='Error when connecting with Github to register repo subscription', - es='Error al intentar subscribirse al repositorio durante la conexión con Github', - slug='github-error')) + translation( + lang, + en=str(e), + es='Error al intentar subscribirse al repositorio, revisa la subscripción para mas detalles', + slug='github-error')) def update(self, instance, validated_data): if instance.status == 'DISABLED' and validated_data['status'] == 'OPERATIONAL': diff --git a/breathecode/monitoring/tasks.py b/breathecode/monitoring/tasks.py index f9bc13e61..7efdab948 100644 --- a/breathecode/monitoring/tasks.py +++ b/breathecode/monitoring/tasks.py @@ -135,7 +135,8 @@ def async_unsubscribe_repo(self, subs_id, force_delete): @shared_task(bind=True, priority=TaskPriority.MARKETING.value) def async_subscribe_repo(self, subs_id): logger.debug('Async subscribe to repo') - return subscribe_repository(subs_id) != False + subscription = subscribe_repository(subs_id) + return subscription != False and subscription.status != 'OPERATIONAL' @task(priority=TaskPriority.MARKETING.value) diff --git a/breathecode/registry/management/commands/subscribe_asset_repos.py b/breathecode/registry/management/commands/subscribe_asset_repos.py index 3eff43bb9..e1c9d0c0f 100644 --- a/breathecode/registry/management/commands/subscribe_asset_repos.py +++ b/breathecode/registry/management/commands/subscribe_asset_repos.py @@ -37,7 +37,9 @@ def handle(self, *args, **options): try: if settings[academy_id] is not None: subs = subscribe_repository(subs.id, settings[academy_id]) - logger.debug(f'Successfully subscribed asset {a.slug}, repo {repo_url}') + logger.debug( + f'Successfully created asset subscription with status {subs.status} for {a.slug}, repo {repo_url}' + ) else: raise Exception(f'No subscription found for academy {academy_id}') except Exception as e: From 4cd5c64e9e982276497504ef03d1e01debcf0007 Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Thu, 6 Jun 2024 16:10:08 +0000 Subject: [PATCH 106/114] updated all places were subscribe_repository was being used to validate status=OPERATIONAL --- breathecode/monitoring/models.py | 4 ++++ breathecode/monitoring/serializers.py | 2 ++ breathecode/monitoring/views.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/breathecode/monitoring/models.py b/breathecode/monitoring/models.py index 59130feb6..2ac802daa 100644 --- a/breathecode/monitoring/models.py +++ b/breathecode/monitoring/models.py @@ -189,6 +189,10 @@ class RepositorySubscription(models.Model): hook_id = models.IntegerField(default=None, null=True, blank=True, help_text='Assigned from github') + last_call = models.DateTimeField(default=None, + null=True, + blank=True, + help_text='Last time github notified updates on this repo subscription') # disabled means it will be ignored from now on status = models.CharField(max_length=20, choices=SUBSCRIPTION_STATUS, default=CRITICAL) status_message = models.TextField(null=True, blank=True, default='Waiting for ping') diff --git a/breathecode/monitoring/serializers.py b/breathecode/monitoring/serializers.py index 69941fc5b..2ba95a58f 100644 --- a/breathecode/monitoring/serializers.py +++ b/breathecode/monitoring/serializers.py @@ -47,6 +47,7 @@ class RepoSubscriptionSmallSerializer(serpy.Serializer): updated_at = serpy.Field() status = serpy.Field() status_message = serpy.Field() + last_call = serpy.Field() owner = AcademySmallSerializer() @@ -55,6 +56,7 @@ class RepositorySubscriptionSerializer(serializers.ModelSerializer): repository = serializers.CharField(required=False) owner = serializers.IntegerField(read_only=True) hook_id = serializers.IntegerField(read_only=True) + last_call = serializers.DateTimeField(read_only=True) status_message = serializers.CharField(read_only=True) class Meta: diff --git a/breathecode/monitoring/views.py b/breathecode/monitoring/views.py index 1a6042c6a..0af0a4910 100644 --- a/breathecode/monitoring/views.py +++ b/breathecode/monitoring/views.py @@ -2,6 +2,7 @@ import os import stripe +from django.utils import timezone from circuitbreaker import CircuitBreakerError from django.db.models import Q from django.http import StreamingHttpResponse @@ -147,6 +148,8 @@ def process_github_webhook(request, subscription_token): subscription.save() return Response('Ready', status=status.HTTP_200_OK) + subscription.last_call = timezone.now() + subscription.save() for academy_slug in academy_slugs: webhook = add_github_webhook(payload, academy_slug) if webhook: From 7f9e445d589d7b70f19552854f73d2be49c1ddc3 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:47:02 -0400 Subject: [PATCH 107/114] Update consumers.py --- breathecode/events/permissions/consumers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/breathecode/events/permissions/consumers.py b/breathecode/events/permissions/consumers.py index 7a07b967c..4e06dfa5d 100644 --- a/breathecode/events/permissions/consumers.py +++ b/breathecode/events/permissions/consumers.py @@ -62,8 +62,8 @@ def event_by_url_param(context: PermissionContextType, args: tuple, kwargs: dict Q(cohort__available_as_saas=False) | Q(cohort__available_as_saas=None, cohort__academy__available_as_saas=False), user=request.user).exists() - if not is_host and not is_free_for_all and (not is_free_for_bootcamps or not user_with_available_as_saas_false): + print('is_free_for_all', is_free_for_all) context['will_consume'] = True if context['will_consume'] is False and is_no_saas_student_up_to_date_in_any_cohort(context['request'].user, @@ -85,6 +85,7 @@ def event_by_url_param(context: PermissionContextType, args: tuple, kwargs: dict code=400) if context['will_consume']: + print('is_free_for_all', is_free_for_all) delta = event.ending_at - utc_now context['time_of_life'] = delta From 264c048195f47baac5f87680e85340d437335f53 Mon Sep 17 00:00:00 2001 From: Tomas Gonzalez <56565994+tommygonzaleza@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:11:26 -0400 Subject: [PATCH 108/114] Update views.py --- breathecode/marketing/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/breathecode/marketing/views.py b/breathecode/marketing/views.py index 5a3b99f35..f41d99bfd 100644 --- a/breathecode/marketing/views.py +++ b/breathecode/marketing/views.py @@ -1262,7 +1262,7 @@ def get(self, request, course_slug=None): items = items.filter(query) items = items.annotate(lang=Value(lang, output_field=CharField())) - + items = items.order_by('created_at') items = handler.queryset(items) serializer = GetCourseSerializer(items, context={'lang': lang}, many=True) return handler.response(serializer.data) From 2d22aeca4d23f6f671416ef3e0c4fb1cdc4bf5ad Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 6 Jun 2024 13:15:59 -0500 Subject: [PATCH 109/114] fix to join a cohort for plan financing --- breathecode/admissions/views.py | 1 + breathecode/registry/tests/urls/v1/tests_academy_asset.py | 5 +++++ .../registry/tests/urls/v2/tests_academy_asset_slug.py | 1 + 3 files changed, 7 insertions(+) diff --git a/breathecode/admissions/views.py b/breathecode/admissions/views.py index 7e553abd9..2f6194e09 100644 --- a/breathecode/admissions/views.py +++ b/breathecode/admissions/views.py @@ -1894,6 +1894,7 @@ def post(self, request, cohort_id=None): resource_belongs_to_user).exclude(excludes).first() if not resource: + resource_available_now = Q(plan_expires_at__gte=timezone.now()) resource = PlanFinancing.objects.filter(resource_available_now, resource_belongs_to_user).exclude(excludes).first() diff --git a/breathecode/registry/tests/urls/v1/tests_academy_asset.py b/breathecode/registry/tests/urls/v1/tests_academy_asset.py index dc8095e0e..5bef0b4e8 100644 --- a/breathecode/registry/tests/urls/v1/tests_academy_asset.py +++ b/breathecode/registry/tests/urls/v1/tests_academy_asset.py @@ -70,6 +70,8 @@ def database_item(academy, category, data={}): 'with_solutions': False, 'with_video': False, 'is_auto_subscribed': True, + 'superseded_by_id': None, + 'enable_table_of_content': True, **data, } @@ -124,6 +126,8 @@ def post_serializer(academy, category, data={}): 'with_solutions': False, 'assets_related': [], 'with_video': False, + 'superseded_by': None, + 'enable_table_of_content': True, **data, } @@ -144,6 +148,7 @@ def put_serializer(academy, category, asset, data={}): 'cleaning_status_details': None, 'clusters': [], 'assets_related': [], + 'previous_versions': [], 'description': None, 'difficulty': None, 'readme_updated_at': None, diff --git a/breathecode/registry/tests/urls/v2/tests_academy_asset_slug.py b/breathecode/registry/tests/urls/v2/tests_academy_asset_slug.py index 6b469812e..f6d93c5fd 100644 --- a/breathecode/registry/tests/urls/v2/tests_academy_asset_slug.py +++ b/breathecode/registry/tests/urls/v2/tests_academy_asset_slug.py @@ -30,6 +30,7 @@ def get_serializer(bc: Breathecode, asset, asset_category=None, data={}): 'cleaning_status_details': None, 'clusters': [], 'assets_related': [], + 'previous_versions': [], 'description': None, 'difficulty': None, 'readme_updated_at': None, From 8ed5f86be212ec1babee4d0bec0f4fd07ed6b756 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 7 Jun 2024 01:12:49 -0500 Subject: [PATCH 110/114] add migration --- .../0023_repositorysubscription_last_call.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 breathecode/monitoring/migrations/0023_repositorysubscription_last_call.py diff --git a/breathecode/monitoring/migrations/0023_repositorysubscription_last_call.py b/breathecode/monitoring/migrations/0023_repositorysubscription_last_call.py new file mode 100644 index 000000000..1b1b0e505 --- /dev/null +++ b/breathecode/monitoring/migrations/0023_repositorysubscription_last_call.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.6 on 2024-06-07 06:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('monitoring', '0022_supervisorissue_attempts_supervisorissue_code_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='repositorysubscription', + name='last_call', + field=models.DateTimeField(blank=True, + default=None, + help_text='Last time github notified updates on this repo subscription', + null=True), + ), + ] From da3712e581dd2c4e9bc06af1d4955aa93a05b273 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 7 Jun 2024 18:44:52 -0500 Subject: [PATCH 111/114] update deps --- Pipfile | 2 +- Pipfile.lock | 108 +++++++++--------- .../commands/tests_set_permissions.py | 4 +- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/Pipfile b/Pipfile index 178413e22..6e7955362 100644 --- a/Pipfile +++ b/Pipfile @@ -139,5 +139,5 @@ aiodns = "*" eventlet = "*" linked-services = {extras = ["django", "aiohttp", "requests"], version = "*"} celery-task-manager = {extras = ["django"], version = "*"} -django-sql-explorer = {extras = ["xls"], version = "==4.0.2"} +django-sql-explorer = {extras = ["xls"], version = "*"} contextlib2 = "*" diff --git a/Pipfile.lock b/Pipfile.lock index aa38c58df..f95696bfc 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "66365e8a3a48d8089abc905c1d2d55c1eb29099bc909196dac809734b156084b" + "sha256": "80d4c86258b6b7f34519eb6f1dff4f5401a51b8afe734f7c06390d91f59967e8" }, "pipfile-spec": 6, "requires": {}, @@ -363,11 +363,11 @@ "django" ], "hashes": [ - "sha256:1054b3284a272b3d60edcbd8dbee9aba61721101047023a328bc41bac3609645", - "sha256:33ca925de4a0dd96f3e13477cf3a54f9ee2304416345b0311547fbb3c9c04fab" + "sha256:110d56d074a71721eb3818f52079b97e384f9f9acd1fe9ce8f96e42d27020a72", + "sha256:d3a221929fa2b9cdca596e9e355e58c0a154b134ac5562a10cfce3a83e35bc7c" ], "markers": "python_version >= '3.11'", - "version": "==1.5.0" + "version": "==1.6.0" }, "certifi": { "hashes": [ @@ -780,11 +780,11 @@ "xls" ], "hashes": [ - "sha256:1c0fd62879f7f600523eda0b1ab3c15c199988dd2532b4b7ef15e0d8d04ac751", - "sha256:4b6c392adb97205ecf8e9f3505437c07da8aab1b84491387acb38ee63a5b36bb" + "sha256:9c0735b08270ac60276ea2697a499cb9457acf00f0d0016a230c092ab3b4d310", + "sha256:c853bc0a270b290646d4ca731769f1e223a151e5724aee69e3b3967f0cf7b96f" ], "markers": "python_version >= '3.8'", - "version": "==4.0.2" + "version": "==4.3" }, "django-storages": { "extras": [ @@ -1022,11 +1022,11 @@ }, "google-auth": { "hashes": [ - "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", - "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415" + "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5", + "sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688" ], "markers": "python_version >= '3.7'", - "version": "==2.29.0" + "version": "==2.30.0" }, "google-cloud-bigquery": { "hashes": [ @@ -1287,7 +1287,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": { @@ -2198,12 +2198,12 @@ }, "openai": { "hashes": [ - "sha256:a15266827de20f407d4bf9837030b168074b5b29acd54f10bb38d5f53e95f083", - "sha256:a746cf070798a4048cfea00b0fc7cb9760ee7ead5a08c48115b914d1afbd1b53" + "sha256:1169211a7b326ecbc821cafb427c29bfd0871f9a3e0947dd9e51acb3b0f1df78", + "sha256:621163b56570897ab8389d187f686a53d4771fd6ce95d481c0a9611fe8bc4229" ], "index": "pypi", "markers": "python_full_version >= '3.7.1'", - "version": "==1.31.1" + "version": "==1.33.0" }, "packaging": { "hashes": [ @@ -3030,11 +3030,11 @@ "hiredis" ], "hashes": [ - "sha256:7adc2835c7a9b5033b7ad8f8918d09b7344188228809c98df07af226d39dec91", - "sha256:ec31f2ed9675cc54c21ba854cfe0462e6faf1d83c8ce5944709db8a4700b9c61" + "sha256:30b47d4ebb6b7a0b9b40c1275a19b87bb6f46b3bed82a89012cf56dea4024ada", + "sha256:3417688621acf6ee368dec4a04dd95881be24efd34c79f00d31f62bb528800ae" ], "markers": "python_version >= '3.7'", - "version": "==5.0.4" + "version": "==5.0.5" }, "referencing": { "hashes": [ @@ -3303,12 +3303,12 @@ }, "stripe": { "hashes": [ - "sha256:3cf4bab592afecaaff69c12ecb99c8376a00bc22e26ea7130a0596e209bf3e88", - "sha256:c42d8f6b4463a54f3a025581810f4e632e2d5a71de6100fc595d75581f69a492" + "sha256:2384749fdc4d252f3c42bb915ef175b7f93253c115311d77db37fbcd24683849", + "sha256:40206520758cfab6b3fbae05f54d3659b018fcee54c955c08e606c91817aa05f" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==9.9.0" + "version": "==9.10.0" }, "text-unidecode": { "hashes": [ @@ -3334,20 +3334,20 @@ }, "tornado": { "hashes": [ - "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0", - "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63", - "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263", - "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052", - "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f", - "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee", - "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78", - "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579", - "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212", - "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e", - "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2" + "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8", + "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f", + "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4", + "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3", + "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14", + "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842", + "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9", + "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698", + "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7", + "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d", + "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4" ], "markers": "python_version >= '3.8'", - "version": "==6.4" + "version": "==6.4.1" }, "tqdm": { "hashes": [ @@ -3367,12 +3367,12 @@ }, "twilio": { "hashes": [ - "sha256:ab2eb19c779855bf02cdca8a7e02ebaa64feee47da7b591ac9088ec07a6962e2", - "sha256:eb4687a9f81dc3118e8981c5a46d9f8184baee135c79afed47c714c759c31bbc" + "sha256:cc3e090c3884db7d70e7c647358b9cf1f4d30fd3fbe0412adcae0df8459d29b0", + "sha256:cfe72b12cabac2f0997f1060d53cea14bd1196e2cbda14789e53c7dd762c4349" ], "index": "pypi", "markers": "python_full_version >= '3.7.0'", - "version": "==9.1.0" + "version": "==9.1.1" }, "twisted": { "extras": [ @@ -3396,11 +3396,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a", - "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1" + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], "markers": "python_version >= '3.8'", - "version": "==4.12.1" + "version": "==4.12.2" }, "tzdata": { "hashes": [ @@ -3648,7 +3648,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:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854", "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b", @@ -3690,14 +3698,6 @@ "markers": "python_version >= '3.7'", "version": "==6.4.post2" }, - "zope.event": { - "hashes": [ - "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", - "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0" - }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", @@ -4111,11 +4111,11 @@ }, "google-auth": { "hashes": [ - "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360", - "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415" + "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5", + "sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688" ], "markers": "python_version >= '3.7'", - "version": "==2.29.0" + "version": "==2.30.0" }, "google-auth-httplib2": { "hashes": [ @@ -4203,7 +4203,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": { @@ -4439,12 +4439,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:68fdab047a0b9bfbefe79ce267e8a7daaf5128bcf7867065fcd201ee335fece1", - "sha256:d0662561efb725b712207e0ee01f035ca15633f29a64628e24f01ec99d7078f4" + "sha256:56aeb91d94cffa43b6296fa4fbf0eb7c840136e563eecfd12c2d9e92e50ba326", + "sha256:5d01fb0aa1c7946a1e3ae8689aa2b11a030621ecb54894e35aabb74c21016312" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.25" + "version": "==9.5.26" }, "mkdocs-material-extensions": { "hashes": [ diff --git a/breathecode/authenticate/tests/management/commands/tests_set_permissions.py b/breathecode/authenticate/tests/management/commands/tests_set_permissions.py index 3fd412299..c2ffe8ed6 100644 --- a/breathecode/authenticate/tests/management/commands/tests_set_permissions.py +++ b/breathecode/authenticate/tests/management/commands/tests_set_permissions.py @@ -71,8 +71,8 @@ def setUp(self): # the behavior of permissions is not exact, this changes every time you add a model self.latest_content_type_id = content_type.id self.latest_permission_id = permission.id - self.job_content_type_id = self.latest_content_type_id - 59 - self.can_delete_job_permission_id = self.latest_permission_id - 237 + self.job_content_type_id = self.latest_content_type_id - 61 + self.can_delete_job_permission_id = self.latest_permission_id - 245 """ 🔽🔽🔽 format of PERMISSIONS From d089d5e9194d4f6ebdf2c636c1587e085938f67f Mon Sep 17 00:00:00 2001 From: jefer94 Date: Fri, 7 Jun 2024 22:25:43 -0500 Subject: [PATCH 112/114] update deps --- Pipfile.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index f95696bfc..250f7a912 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -363,11 +363,11 @@ "django" ], "hashes": [ - "sha256:110d56d074a71721eb3818f52079b97e384f9f9acd1fe9ce8f96e42d27020a72", - "sha256:d3a221929fa2b9cdca596e9e355e58c0a154b134ac5562a10cfce3a83e35bc7c" + "sha256:11d1db362ff6bfa54f7fb188942fddee6233ef2892c843241fb49bfeaf01b578", + "sha256:870da932189a2377dc119f33ab06f48b8690e32800575e1b1f23e841e9dd71d8" ], "markers": "python_version >= '3.11'", - "version": "==1.6.0" + "version": "==1.7.0" }, "certifi": { "hashes": [ @@ -1287,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": { @@ -3648,15 +3648,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:00b5c3e9744dcdc9e84c24ed6646d5cf0cf66551347b310b3ffd70f056535854", "sha256:0e4fa5d34d7973e6b0efa46fe4405090f3b406f64b6290facbb19dcbf642ad6b", @@ -3698,6 +3690,14 @@ "markers": "python_version >= '3.7'", "version": "==6.4.post2" }, + "zope.event": { + "hashes": [ + "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", + "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd" + ], + "markers": "python_version >= '3.7'", + "version": "==5.0" + }, "zstandard": { "hashes": [ "sha256:11f0d1aab9516a497137b41e3d3ed4bbf7b2ee2abc79e5c8b010ad286d7464bd", @@ -4203,7 +4203,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": { From 26b31b35c21201648be11174c9d9a8bc04bdcf6a Mon Sep 17 00:00:00 2001 From: gustavomm19 Date: Sat, 8 Jun 2024 05:30:12 +0000 Subject: [PATCH 113/114] add date_joined to user_me serializer --- breathecode/authenticate/serializers.py | 1 + .../authenticate/tests/urls/tests_app_user.py | 22 +++++++++++++++---- .../tests/urls/tests_app_user_id.py | 19 ++++++++++++---- .../authenticate/tests/urls/tests_user_me.py | 2 ++ 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/breathecode/authenticate/serializers.py b/breathecode/authenticate/serializers.py index 4a4b1dc35..8ac1982c3 100644 --- a/breathecode/authenticate/serializers.py +++ b/breathecode/authenticate/serializers.py @@ -368,6 +368,7 @@ class AppUserSerializer(serpy.Serializer): username = serpy.Field() first_name = serpy.Field() last_name = serpy.Field() + date_joined = serpy.Field() github = serpy.MethodField() profile = serpy.MethodField() diff --git a/breathecode/authenticate/tests/urls/tests_app_user.py b/breathecode/authenticate/tests/urls/tests_app_user.py index 3be243291..c64a95bfa 100644 --- a/breathecode/authenticate/tests/urls/tests_app_user.py +++ b/breathecode/authenticate/tests/urls/tests_app_user.py @@ -24,7 +24,7 @@ def profile_serializer(credentials_github): } -def get_serializer(user, credentials_github=None, profile=None): +def get_serializer(user, credentials_github=None, profile=None, **data): return { 'email': user.email, 'username': user.username, @@ -33,6 +33,7 @@ def get_serializer(user, credentials_github=None, profile=None): 'id': user.id, 'last_name': user.last_name, 'profile': profile_serializer(profile) if profile else None, + **data, } @@ -82,7 +83,12 @@ def test_sign_with_user__get_own_info(self): response = self.client.get(url) json = response.json() - expected = [get_serializer(model.user[0], model.credentials_github[0], model.profile[0])] + expected = [ + get_serializer(model.user[0], + model.credentials_github[0], + model.profile[0], + date_joined=self.bc.datetime.to_iso_string(model.user[0].date_joined)) + ] self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -134,7 +140,10 @@ def test_sign_without_user(self): response = self.client.get(url) json = response.json() - expected = get_serializer(user, model.credentials_github[user.id - 1], model.profile[user.id - 1]) + expected = get_serializer(user, + model.credentials_github[user.id - 1], + model.profile[user.id - 1], + date_joined=self.bc.datetime.to_iso_string(user.date_joined)) self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -176,7 +185,12 @@ def test_user_with_agreement(self): response = self.client.get(url) json = response.json() - expected = [get_serializer(model.user, model.credentials_github, model.profile)] + expected = [ + get_serializer(model.user, + model.credentials_github, + model.profile, + date_joined=self.bc.datetime.to_iso_string(model.user.date_joined)) + ] self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/breathecode/authenticate/tests/urls/tests_app_user_id.py b/breathecode/authenticate/tests/urls/tests_app_user_id.py index d8e8870df..f6102407d 100644 --- a/breathecode/authenticate/tests/urls/tests_app_user_id.py +++ b/breathecode/authenticate/tests/urls/tests_app_user_id.py @@ -24,7 +24,7 @@ def profile_serializer(credentials_github): } -def get_serializer(user, credentials_github=None, profile=None): +def get_serializer(user, credentials_github=None, profile=None, **data): return { 'email': user.email, 'username': user.username, @@ -32,7 +32,9 @@ def get_serializer(user, credentials_github=None, profile=None): 'github': credentials_github_serializer(credentials_github) if credentials_github else None, 'id': user.id, 'last_name': user.last_name, + 'date_joined': user.date_joined, 'profile': profile_serializer(profile) if profile else None, + **data } @@ -82,7 +84,10 @@ def test_sign_with_user__get_own_info(self): response = self.client.get(url) json = response.json() - expected = get_serializer(model.user[0], model.credentials_github[0], model.profile[0]) + expected = get_serializer(model.user[0], + model.credentials_github[0], + model.profile[0], + date_joined=self.bc.datetime.to_iso_string(model.user[0].date_joined)) self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -134,7 +139,10 @@ def test_sign_without_user(self): response = self.client.get(url) json = response.json() - expected = get_serializer(user, model.credentials_github[user.id - 1], model.profile[user.id - 1]) + expected = get_serializer(user, + model.credentials_github[user.id - 1], + model.profile[user.id - 1], + date_joined=self.bc.datetime.to_iso_string(user.date_joined)) self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -181,7 +189,10 @@ def test_user_with_agreement(self): response = self.client.get(url) json = response.json() - expected = get_serializer(model.user, model.credentials_github, model.profile) + expected = get_serializer(model.user, + model.credentials_github, + model.profile, + date_joined=self.bc.datetime.to_iso_string(model.user.date_joined)) self.assertEqual(json, expected) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/breathecode/authenticate/tests/urls/tests_user_me.py b/breathecode/authenticate/tests/urls/tests_user_me.py index 3ee7688d4..07a5e09ea 100644 --- a/breathecode/authenticate/tests/urls/tests_user_me.py +++ b/breathecode/authenticate/tests/urls/tests_user_me.py @@ -43,6 +43,8 @@ def get_serializer(self, user.first_name, 'last_name': user.last_name, + 'date_joined': + self.bc.datetime.to_iso_string(user.date_joined), 'username': user.username, 'settings': From 4634c90d6f5c2ef998a8237d587bd4e672297bc2 Mon Sep 17 00:00:00 2001 From: jefer94 Date: Thu, 13 Jun 2024 21:03:45 -0500 Subject: [PATCH 114/114] change in asgi.py --- breathecode/asgi.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/breathecode/asgi.py b/breathecode/asgi.py index 29c8ec47c..ea2837143 100644 --- a/breathecode/asgi.py +++ b/breathecode/asgi.py @@ -7,11 +7,6 @@ https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ """ -# keeps this above -import newrelic.agent - -newrelic.agent.initialize() - # the rest of your ASGI file contents go here import os @@ -20,5 +15,3 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'breathecode.settings') application = get_asgi_application() -if os.getenv('NOWRAP_APP') != '1': - application = newrelic.agent.ASGIApplicationWrapper(application)