From 7a8fe772cb0ea282ef5f866241d7e955f2b829c6 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Tue, 3 Sep 2024 11:46:36 -0500 Subject: [PATCH 01/41] feat: Add/Update endpoint to retrieve activities assigned to a participant (M2-6223) (#1565) This PR adds a new endpoint `/activities/applet/{applet_id}/subject/{subject_id}`, which allows the caller to fetch an object containing the list of activities and activity flows that have been assigned to the subject specified by `subject_id` in some way. Each activity or flow will have a non-nullable array property called `assignments` containing the list of assignments of that activity/flow where the given subject is the **respondent**. It is yet undecided at a product level whether auto assigned activities/flows can be manually assigned, but the API currently allows this. Thus, it is possible for the data returned from this endpoint to contain an activity/flow with both `autoAssign` set to `true`, and a non-empty `assignments` property. The caller of the endpoint must be authenticated as a user with one of the following user roles: - Owner - Manager - Coordinator - Reviewer (with access to the subject) --- src/apps/activities/api/activities.py | 54 ++ src/apps/activities/domain/activity.py | 5 + src/apps/activities/router.py | 19 +- src/apps/activities/tests/test_activities.py | 506 +++++++++++++++++- .../domain/assignments.py | 1 + src/apps/activity_assignments/service.py | 1 + src/apps/activity_flows/domain/flow.py | 5 + src/apps/activity_flows/service/flow.py | 7 +- src/apps/applets/domain/applet.py | 16 +- src/cli.py | 6 +- 10 files changed, 612 insertions(+), 8 deletions(-) diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index c2b7238bcb4..8f242d6359e 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -7,13 +7,17 @@ from apps.activities.domain.activity import ( ActivityLanguageWithItemsMobileDetailPublic, ActivitySingleLanguageWithItemsDetailPublic, + ActivityWithAssignmentDetailsPublic, ) from apps.activities.filters import AppletActivityFilter from apps.activities.services.activity import ActivityItemService, ActivityService +from apps.activity_assignments.service import ActivityAssignmentService +from apps.activity_flows.domain.flow import FlowWithAssignmentDetailsPublic from apps.activity_flows.service.flow import FlowService from apps.answers.deps.preprocess_arbitrary import get_answer_session from apps.answers.service import AnswerService from apps.applets.domain.applet import ( + ActivitiesAndFlowsWithAssignmentDetailsPublic, AppletActivitiesAndFlowsDetailsPublic, AppletActivitiesDetailsPublic, AppletSingleLanguageDetailMobilePublic, @@ -99,6 +103,56 @@ async def applet_activities( return Response(result=result) +async def applet_activities_for_subject( + applet_id: uuid.UUID, + subject_id: uuid.UUID, + user: User = Depends(get_current_user), + language: str = Depends(get_language), + session=Depends(get_session), +) -> Response[ActivitiesAndFlowsWithAssignmentDetailsPublic]: + async with atomic(session): + applet_service = AppletService(session, user.id) + await applet_service.exist_by_id(applet_id) + await CheckAccessService(session, user.id).check_applet_respondent_list_access(applet_id) + + # Ensure reviewers can access the subject + await CheckAccessService(session, user.id).check_subject_subject_access(applet_id, subject_id) + + activities_future = ActivityService(session, user.id).get_single_language_by_applet_id(applet_id, language) + flows_future = FlowService(session).get_single_language_by_applet_id(applet_id, language) + assignments_future = ActivityAssignmentService(session).get_all_by_respondent(applet_id, subject_id) + + activities, flows, assignments = await asyncio.gather(activities_future, flows_future, assignments_future) + result = ActivitiesAndFlowsWithAssignmentDetailsPublic( + activities=[], + activity_flows=[], + ) + + for activity in activities: + activity_with_assignment = ActivityWithAssignmentDetailsPublic( + **activity.dict(exclude={"report_included_activity_name", "report_included_item_name"}) + ) + activity_with_assignment.assignments = [ + assignment for assignment in assignments if assignment.activity_id == activity.id + ] + + if activity_with_assignment.auto_assign is True or len(activity_with_assignment.assignments) > 0: + result.activities.append(activity_with_assignment) + + for flow in flows: + flow_with_assignment = FlowWithAssignmentDetailsPublic( + **flow.dict(exclude={"created_at", "report_included_activity_name", "report_included_item_name"}) + ) + flow_with_assignment.assignments = [ + assignment for assignment in assignments if assignment.activity_flow_id == flow.id + ] + + if flow_with_assignment.auto_assign is True or len(flow_with_assignment.assignments) > 0: + result.activity_flows.append(flow_with_assignment) + + return Response(result=result) + + async def applet_activities_and_flows( applet_id: uuid.UUID, user: User = Depends(get_current_user), diff --git a/src/apps/activities/domain/activity.py b/src/apps/activities/domain/activity.py index e5c6b1960dc..36f60b45cbf 100644 --- a/src/apps/activities/domain/activity.py +++ b/src/apps/activities/domain/activity.py @@ -11,6 +11,7 @@ ) from apps.activities.domain.response_type_config import PerformanceTaskType, ResponseType from apps.activities.domain.scores_reports import ScoresAndReports +from apps.activity_assignments.domain.assignments import ActivityAssignmentWithSubject from apps.shared.domain import InternalModel, PublicModel @@ -96,6 +97,10 @@ class ActivityLanguageWithItemsMobileDetailPublic(PublicModel): is_performance_task: bool = False +class ActivityWithAssignmentDetailsPublic(ActivitySingleLanguageDetailPublic): + assignments: list[ActivityAssignmentWithSubject] = Field(default_factory=list) + + class ActivityBaseInfo(ActivityMinimumInfo, InternalModel): contains_response_types: list[ResponseType] item_count: int diff --git a/src/apps/activities/router.py b/src/apps/activities/router.py index 40f8e4bdc7f..87c8c9a35af 100644 --- a/src/apps/activities/router.py +++ b/src/apps/activities/router.py @@ -5,12 +5,17 @@ activity_retrieve, applet_activities, applet_activities_and_flows, + applet_activities_for_subject, public_activity_retrieve, ) from apps.activities.api.reusable_item_choices import item_choice_create, item_choice_delete, item_choice_retrieve from apps.activities.domain.activity import ActivitySingleLanguageWithItemsDetailPublic from apps.activities.domain.reusable_item_choices import PublicReusableItemChoice -from apps.applets.domain.applet import AppletActivitiesAndFlowsDetailsPublic, AppletActivitiesDetailsPublic +from apps.applets.domain.applet import ( + ActivitiesAndFlowsWithAssignmentDetailsPublic, + AppletActivitiesAndFlowsDetailsPublic, + AppletActivitiesDetailsPublic, +) from apps.shared.domain import Response, ResponseMulti from apps.shared.domain.response import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE @@ -87,3 +92,15 @@ **DEFAULT_OPENAPI_RESPONSE, }, )(applet_activities_and_flows) + +router.get( + "/applet/{applet_id}/subject/{subject_id}", + description="""Get all assigned activities and activity flows for a specific subject. + """, + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": Response[ActivitiesAndFlowsWithAssignmentDetailsPublic]}, + **AUTHENTICATION_ERROR_RESPONSES, + **DEFAULT_OPENAPI_RESPONSE, + }, +)(applet_activities_for_subject) diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index afd95e4c8fe..9a566a88fb3 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -1,19 +1,37 @@ import http import json import uuid -from typing import cast +from typing import AsyncGenerator, cast import pytest +from _pytest.config import Config +from pytest_mock import MockerFixture +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from apps.activities.domain.activity_create import ActivityCreate +from apps.activities.domain.activity_update import ActivityUpdate from apps.activities.domain.response_type_config import SingleSelectionConfig from apps.activities.domain.response_values import SingleSelectionValues +from apps.activities.services.activity import ActivityService +from apps.activity_assignments.domain.assignments import ActivityAssignmentCreate +from apps.activity_assignments.service import ActivityAssignmentService +from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate +from apps.activity_flows.service.flow import FlowService +from apps.applets.domain.applet_create_update import AppletCreate from apps.applets.domain.applet_full import AppletFull from apps.applets.domain.applet_link import CreateAccessLink +from apps.applets.domain.base import AppletBase from apps.applets.service.applet import AppletService +from apps.applets.tests.fixtures.applets import _get_or_create_applet +from apps.applets.tests.utils import teardown_applet from apps.shared.enums import Language +from apps.subjects.db.schemas import SubjectSchema +from apps.subjects.domain import Subject from apps.themes.domain import Theme from apps.users.domain import User +from apps.workspaces.domain.constants import Role +from apps.workspaces.service.user_applet_access import UserAppletAccessService @pytest.fixture @@ -25,6 +43,134 @@ async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletF return applet +@pytest.fixture +async def lucy_applet_one_subject(session: AsyncSession, lucy: User, applet_one_lucy_respondent: AppletFull) -> Subject: + applet_id = applet_one_lucy_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def applet_one_user_respondent(session: AsyncSession, applet_one: AppletFull, tom, user) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_one.id).add_role(user.id, Role.RESPONDENT) + return applet_one + + +@pytest.fixture +async def user_applet_one_subject(session: AsyncSession, user: User, applet_one_user_respondent: AppletFull) -> Subject: + applet_id = applet_one_user_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == user.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def applet_one_lucy_reviewer( + session: AsyncSession, + applet_one: AppletFull, + mocker: MockerFixture, + tom: User, + lucy: User, + user_applet_one_subject: Subject, +) -> AppletFull: + mocker.patch( + "apps.workspaces.service.user_applet_access.UserAppletAccessService._get_default_role_meta", + return_value={"subjects": [str(user_applet_one_subject.id)]}, + ) + await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.REVIEWER) + return applet_one + + +@pytest.fixture +async def empty_applet( + session: AsyncSession, + tom: User, + applet_base_data: AppletBase, + pytestconfig: Config, +) -> AsyncGenerator[AppletFull, None]: + applet_id = uuid.UUID("92917a56-d586-4613-b7aa-991f2c4b15b0") + applet_name = "Empty Applet" + applet_minimal_data = AppletCreate(**applet_base_data.dict(), activities=[], activity_flows=[]) + applet = await _get_or_create_applet(session, applet_name, applet_id, applet_minimal_data, tom.id) + yield applet + if not pytestconfig.getoption("--keepdb"): + await teardown_applet(session, applet.id) + + +@pytest.fixture +async def empty_applet_lucy_manager( + session: AsyncSession, empty_applet: AppletFull, tom: User, lucy: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, empty_applet.id).add_role(lucy.id, Role.MANAGER) + return empty_applet + + +@pytest.fixture +async def applet_activity_flow_lucy_manager( + session: AsyncSession, applet_activity_flow: AppletFull, tom: User, lucy: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_activity_flow.id).add_role(lucy.id, Role.MANAGER) + return applet_activity_flow + + +@pytest.fixture +async def empty_applet_lucy_respondent( + session: AsyncSession, empty_applet: AppletFull, tom: User, lucy: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, empty_applet.id).add_role(lucy.id, Role.RESPONDENT) + return empty_applet + + +@pytest.fixture +async def lucy_empty_applet_subject( + session: AsyncSession, lucy: User, empty_applet_lucy_respondent: AppletFull +) -> Subject: + applet_id = empty_applet_lucy_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def empty_applet_user_respondent(session: AsyncSession, empty_applet: AppletFull, tom, user) -> AppletFull: + await UserAppletAccessService(session, tom.id, empty_applet.id).add_role(user.id, Role.RESPONDENT) + return empty_applet + + +@pytest.fixture +async def user_empty_applet_subject( + session: AsyncSession, user: User, empty_applet_user_respondent: AppletFull +) -> Subject: + applet_id = empty_applet_user_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == user.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def applet_activity_flow_lucy_respondent( + session: AsyncSession, applet_activity_flow: AppletFull, tom: User, lucy: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_activity_flow.id).add_role(lucy.id, Role.RESPONDENT) + return applet_activity_flow + + +@pytest.fixture +async def lucy_applet_activity_flow_subject( + session: AsyncSession, lucy: User, applet_activity_flow_lucy_respondent: AppletFull +) -> Subject: + applet_id = applet_activity_flow_lucy_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + class TestActivities: login_url = "/auth/login" activity_detail = "/activities/{pk}" @@ -33,6 +179,7 @@ class TestActivities: public_activity_detail = "public/activities/{pk}" answer_url = "/answers" applet_update_url = "applets/{applet_id}" + subject_assigned_activities_url = "/activities/applet/{applet_id}/subject/{subject_id}" async def test_activity_detail(self, client, applet_one: AppletFull, tom: User): activity = applet_one.activities[0] @@ -252,7 +399,7 @@ async def test_public_activity_detail(self, client, applet_one_with_public_link: assert len(result["items"]) == len(activity.items) assert result["items"][0]["question"] == activity.items[0].question[Language.ENGLISH] - # Get only applet activies with submitted answers + # Get only applet activities with submitted answers async def test_activities_applet_has_submitted( self, client, applet_one: AppletFull, default_theme: Theme, tom: User ): @@ -299,7 +446,7 @@ async def test_activities_applet_has_submitted( assert result["activitiesDetails"][0]["id"] == str(activity.id) assert result["activitiesDetails"][0]["name"] == activity.name - # Get only applet activies with score + # Get only applet activities with score async def test_activities_applet_has_score(self, client, applet_one: AppletFull, default_theme: Theme, tom: User): client.login(tom) @@ -396,3 +543,356 @@ async def test_activities_applet_has_score(self, client, applet_one: AppletFull, assert len(item["responseValues"]["options"]) == 2 option = item["responseValues"]["options"][0] assert option["score"] > 0 + + async def test_subject_assigned_activities_editor( + self, client, applet_one_lucy_editor, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=applet_one_lucy_editor.id, subject_id=lucy_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied to applet." + + async def test_subject_assigned_activities_incorrect_reviewer( + self, client, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=applet_one_lucy_reviewer.id, subject_id=lucy_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied." + + async def test_subject_assigned_activities_participant( + self, client, applet_one_lucy_respondent, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=applet_one_lucy_respondent.id, subject_id=lucy_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied to applet." + + async def test_subject_assigned_activities_participant_other( + self, client, applet_one_lucy_respondent, lucy, applet_one_user_respondent, user_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=applet_one_lucy_respondent.id, subject_id=user_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied to applet." + + async def test_subject_assigned_activities_invalid_applet( + self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + applet_id = uuid.uuid4() + + response = await client.get( + self.subject_assigned_activities_url.format(applet_id=applet_id, subject_id=lucy_applet_one_subject.id) + ) + + assert response.status_code == http.HTTPStatus.NOT_FOUND + result = response.json()["result"] + + assert result[0]["type"] == "NOT_FOUND" + assert result[0]["message"] == f"No such applets with id={applet_id}." + + async def test_subject_assigned_activities_invalid_subject( + self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + subject_id = uuid.uuid4() + + response = await client.get( + self.subject_assigned_activities_url.format(applet_id=applet_one_lucy_manager.id, subject_id=subject_id) + ) + + assert response.status_code == http.HTTPStatus.NOT_FOUND + result = response.json()["result"] + + assert result[0]["type"] == "NOT_FOUND" + assert result[0]["message"] == f"Respondent subject id {subject_id} not found" + + async def test_subject_assigned_activities_empty_applet( + self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject + ): + client.login(lucy) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=lucy_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result["activities"] == [] + assert result["activityFlows"] == [] + + async def test_subject_assigned_activities_auto_assigned( + self, client, applet_activity_flow_lucy_manager, lucy, lucy_applet_activity_flow_subject + ): + client.login(lucy) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=applet_activity_flow_lucy_manager.id, subject_id=lucy_applet_activity_flow_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result["activityFlows"]) == 1 + assert len(result["activities"]) == 1 + + activity = applet_activity_flow_lucy_manager.activities[0] + activity_result = result["activities"][0] + + assert activity_result["id"] == str(activity.id) + assert activity_result["name"] == activity.name + assert activity_result["description"] == activity.description[Language.ENGLISH] + assert activity_result["autoAssign"] == activity.auto_assign + assert len(activity_result["assignments"]) == 0 + + flow = applet_activity_flow_lucy_manager.activity_flows[0] + flow_result = result["activityFlows"][0] + + assert flow_result["id"] == str(flow.id) + assert flow_result["name"] == flow.name + assert flow_result["description"] == flow.description[Language.ENGLISH] + assert flow_result["autoAssign"] == flow.auto_assign + assert len(flow_result["assignments"]) == 0 + assert flow_result["activityIds"][0] == str(flow.items[0].activity_id) + + async def test_subject_assigned_activities_manually_assigned( + self, + session, + client, + empty_applet_lucy_manager, + lucy, + user_empty_applet_subject, + activity_create_session: ActivityCreate, + ): + client.login(lucy) + + activities = await ActivityService(session, lucy.id).update_create( + empty_applet_lucy_manager.id, + [ + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity", + auto_assign=False, + ) + ], + ) + manual_activity = next((activity for activity in activities if activity.name == "Manual Activity")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=manual_activity.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ) + ], + ) + + flows = await FlowService(session).update_create( + empty_applet_lucy_manager.id, + [ + FlowUpdate( + name="Manual Flow", + description={Language.ENGLISH: "Manual Flow"}, + auto_assign=False, + items=[ActivityFlowItemUpdate(activity_key=manual_activity.key)], + ) + ], + {manual_activity.key: manual_activity.id}, + ) + + manual_flow = next((flow for flow in flows if flow.name == "Manual Flow")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_flow_id=manual_flow.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ) + ], + ) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result["activityFlows"]) == 1 + assert len(result["activities"]) == 1 + + activity_result = result["activities"][0] + + assert activity_result["id"] == str(manual_activity.id) + assert activity_result["name"] == manual_activity.name + assert activity_result["description"] == manual_activity.description[Language.ENGLISH] + assert activity_result["autoAssign"] is False + assert len(activity_result["assignments"]) == 1 + + activity_assignment = activity_result["assignments"][0] + assert activity_assignment["activityId"] == str(manual_activity.id) + assert activity_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) + assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + flow_result = result["activityFlows"][0] + + assert flow_result["id"] == str(manual_flow.id) + assert flow_result["name"] == manual_flow.name + assert flow_result["description"] == manual_flow.description[Language.ENGLISH] + assert flow_result["autoAssign"] is False + assert len(flow_result["assignments"]) == 1 + assert flow_result["activityIds"][0] == str(manual_flow.items[0].activity_id) + + flow_assignment = flow_result["assignments"][0] + assert flow_assignment["activityFlowId"] == str(manual_flow.id) + assert flow_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) + assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + async def test_subject_assigned_activities_auto_and_manually_assigned( + self, + session, + client, + empty_applet_lucy_manager, + lucy, + user_empty_applet_subject, + activity_create_session: ActivityCreate, + ): + client.login(lucy) + + activities = await ActivityService(session, lucy.id).update_create( + empty_applet_lucy_manager.id, + [ + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Hybrid Activity", + auto_assign=True, + ) + ], + ) + manual_activity = next((activity for activity in activities if activity.name == "Hybrid Activity")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=manual_activity.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ) + ], + ) + + flows = await FlowService(session).update_create( + empty_applet_lucy_manager.id, + [ + FlowUpdate( + name="Hybrid Flow", + description={Language.ENGLISH: "Hybrid Flow"}, + auto_assign=True, + items=[ActivityFlowItemUpdate(activity_key=manual_activity.key)], + ) + ], + {manual_activity.key: manual_activity.id}, + ) + + manual_flow = next((flow for flow in flows if flow.name == "Hybrid Flow")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_flow_id=manual_flow.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ) + ], + ) + + response = await client.get( + self.subject_assigned_activities_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result["activityFlows"]) == 1 + assert len(result["activities"]) == 1 + + activity_result = result["activities"][0] + + assert activity_result["id"] == str(manual_activity.id) + assert activity_result["name"] == manual_activity.name + assert activity_result["description"] == manual_activity.description[Language.ENGLISH] + assert activity_result["autoAssign"] is True + assert len(activity_result["assignments"]) == 1 + + activity_assignment = activity_result["assignments"][0] + assert activity_assignment["activityId"] == str(manual_activity.id) + assert activity_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) + assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + flow_result = result["activityFlows"][0] + + assert flow_result["id"] == str(manual_flow.id) + assert flow_result["name"] == manual_flow.name + assert flow_result["description"] == manual_flow.description[Language.ENGLISH] + assert flow_result["autoAssign"] is True + assert len(flow_result["assignments"]) == 1 + assert flow_result["activityIds"][0] == str(manual_flow.items[0].activity_id) + + flow_assignment = flow_result["assignments"][0] + assert flow_assignment["activityFlowId"] == str(manual_flow.id) + assert flow_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) + assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) diff --git a/src/apps/activity_assignments/domain/assignments.py b/src/apps/activity_assignments/domain/assignments.py index ea7fbd81264..ed1cc906d13 100644 --- a/src/apps/activity_assignments/domain/assignments.py +++ b/src/apps/activity_assignments/domain/assignments.py @@ -47,6 +47,7 @@ class ActivityAssignmentsListQueryParams(InternalModel): class ActivityAssignmentWithSubject(PublicModel): + id: UUID activity_flow_id: UUID | None activity_id: UUID | None respondent_subject: SubjectReadResponse diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index 4f8362731a7..a4a03285bce 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -269,6 +269,7 @@ async def get_all_by_respondent( return [ ActivityAssignmentWithSubject( + id=assignment.id, activity_id=assignment.activity_id, activity_flow_id=assignment.activity_flow_id, respondent_subject=SubjectReadResponse( diff --git a/src/apps/activity_flows/domain/flow.py b/src/apps/activity_flows/domain/flow.py index b8751360990..2cd8fb3ddfe 100644 --- a/src/apps/activity_flows/domain/flow.py +++ b/src/apps/activity_flows/domain/flow.py @@ -3,6 +3,7 @@ from pydantic import Field +from apps.activity_assignments.domain.assignments import ActivityAssignmentWithSubject from apps.activity_flows.domain.base import FlowBase from apps.shared.domain import InternalModel, PublicModel @@ -49,6 +50,10 @@ class FlowSingleLanguageMobileDetailPublic(FlowBaseInfo, InternalModel): is_single_report: bool = False +class FlowWithAssignmentDetailsPublic(FlowSingleLanguageMobileDetailPublic): + assignments: list[ActivityAssignmentWithSubject] = Field(default_factory=list) + + class FlowDuplicate(FlowBase, InternalModel): id: uuid.UUID order: int diff --git a/src/apps/activity_flows/service/flow.py b/src/apps/activity_flows/service/flow.py index 400dc5c3791..ad09ba947b1 100644 --- a/src/apps/activity_flows/service/flow.py +++ b/src/apps/activity_flows/service/flow.py @@ -2,7 +2,12 @@ from apps.activity_flows.crud import FlowItemsCRUD, FlowsCRUD, FlowsHistoryCRUD from apps.activity_flows.db.schemas import ActivityFlowSchema -from apps.activity_flows.domain.flow import Flow, FlowBaseInfo, FlowDuplicate, FlowSingleLanguageDetail +from apps.activity_flows.domain.flow import ( + Flow, + FlowBaseInfo, + FlowDuplicate, + FlowSingleLanguageDetail, +) from apps.activity_flows.domain.flow_create import FlowCreate, PreparedFlowItemCreate from apps.activity_flows.domain.flow_full import FlowFull from apps.activity_flows.domain.flow_update import ActivityFlowReportConfiguration, FlowUpdate, PreparedFlowItemUpdate diff --git a/src/apps/applets/domain/applet.py b/src/apps/applets/domain/applet.py index c55ecd0344c..76e6d4e9dd7 100644 --- a/src/apps/applets/domain/applet.py +++ b/src/apps/applets/domain/applet.py @@ -11,6 +11,7 @@ ActivitySingleLanguageDetail, ActivitySingleLanguageDetailPublic, ActivitySingleLanguageMobileDetailPublic, + ActivityWithAssignmentDetailsPublic, ) from apps.activities.errors import PeriodIsRequiredError from apps.activity_flows.domain.flow import ( @@ -18,6 +19,7 @@ FlowSingleLanguageDetail, FlowSingleLanguageDetailPublic, FlowSingleLanguageMobileDetailPublic, + FlowWithAssignmentDetailsPublic, ) from apps.activity_flows.domain.flow_full import PublicFlowFull from apps.applets.domain.base import AppletBaseInfo, AppletFetchBase, Encryption @@ -154,11 +156,23 @@ class PublicFlowFullType(PublicFlowFull): type = "activityFlow" -# Returns a combination of activity and activity flows class AppletActivitiesAndFlowsDetailsPublic(PublicModel): + """ + Returns a combination of activity and activity flows + """ + details: list[ActivityLanguageWithItemsMobileDetailPublicType | PublicFlowFullType] = Field(default_factory=list) +class ActivitiesAndFlowsWithAssignmentDetailsPublic(PublicModel): + """ + Returns a combination of activity and activity flows + """ + + activities: list[ActivityWithAssignmentDetailsPublic] = Field(default_factory=list) + activity_flows: list[FlowWithAssignmentDetailsPublic] = Field(default_factory=list) + + class AppletActivitiesBaseInfo(AppletMinimumInfo, PublicModel): id: uuid.UUID created_at: datetime.datetime | None diff --git a/src/cli.py b/src/cli.py index 16b9753b983..74d471d971a 100755 --- a/src/cli.py +++ b/src/cli.py @@ -9,8 +9,10 @@ from apps.activities.commands import activities # noqa: E402 from apps.answers.commands import convert_assessments # noqa: E402 -from apps.applets.commands import applet_cli # noqa: E402 -from apps.applets.commands import applet_ema_cli # noqa: E402 +from apps.applets.commands import ( # noqa: E402 + applet_cli, # noqa: E402 + applet_ema_cli, # noqa: E402 +) # noqa: E402 from apps.shared.commands import encryption_cli, patch # noqa: E402 from apps.users.commands import token_cli # noqa: E402 from apps.workspaces.commands import arbitrary_server_cli # noqa: E402 From 9176d6fd5751fcfe77694fc4d16f572b69cb133f Mon Sep 17 00:00:00 2001 From: AlejandroCoronadoN Date: Tue, 3 Sep 2024 12:26:39 -0600 Subject: [PATCH 02/41] feat: unassign activity or flow endpoint (M2-7358) (#1556) This pull request introduces the unassign endpoint for activity assignments. The unassign functionality allows authorized users to mark assignments as deleted. It ensures that previously assigned activities or flows can be unassigned, effectively updating the database to reflect the unassignment. * feature: Completed unassign endpoint using the same json structure of the assign assignments endpoint request. Implemented get_assignments_by_activity_or_flow_id_and_subject_id to retrieve assignment_id using activity_id and subject_id. Crated unassign_many to update multiple schemas at a time using the _update CRUD base function * Calling assignment endpoint and then unassign to test functionality * Created tests for unassign feature * Recover applet_assignments function and completed unassign_many function * Typing consistency for delete requests and Optional types on unassign feature * Update src/apps/activity_assignments/router.py Co-authored-by: Farmer Paul --------- Co-authored-by: Farmer Paul --- src/apps/activity_assignments/api.py | 59 ++ .../activity_assignments/crud/assignments.py | 107 +++- .../domain/assignments.py | 40 +- src/apps/activity_assignments/errors.py | 12 + src/apps/activity_assignments/router.py | 23 +- src/apps/activity_assignments/service.py | 117 +++- .../tests/test_unassignments.py | 588 ++++++++++++++++++ 7 files changed, 936 insertions(+), 10 deletions(-) create mode 100644 src/apps/activity_assignments/tests/test_unassignments.py diff --git a/src/apps/activity_assignments/api.py b/src/apps/activity_assignments/api.py index d8772f355a2..7b897288a17 100644 --- a/src/apps/activity_assignments/api.py +++ b/src/apps/activity_assignments/api.py @@ -5,6 +5,7 @@ from apps.activity_assignments.domain.assignments import ( ActivitiesAssignments, ActivitiesAssignmentsCreate, + ActivitiesAssignmentsDelete, ActivitiesAssignmentsWithSubjects, ActivityAssignmentsListQueryParams, ) @@ -27,6 +28,31 @@ async def assignments_create( schema: ActivitiesAssignmentsCreate = Body(...), session=Depends(get_session), ) -> Response[ActivitiesAssignments]: + """ + Creates multiple activity assignments for a specified applet. + + This endpoint allows authorized users to assign activities or flows to participants + by creating new assignment records in the database. + + Parameters: + ----------- + applet_id : uuid.UUID + The ID of the applet for which assignments are being created. + + user : User, optional + The current user making the request (automatically injected). + + schema : ActivitiesAssignmentsCreate + The schema containing the list of assignments to be created. + + session : Depends, optional + The database session (automatically injected). + + Returns: + -------- + Response[ActivitiesAssignments] + A response object containing the newly created assignments for the applet. + """ async with atomic(session): service = AppletService(session, user.id) await service.exist_by_id(applet_id) @@ -41,6 +67,39 @@ async def assignments_create( ) +async def assignment_delete( + applet_id: uuid.UUID, + user: User = Depends(get_current_user), + schema: ActivitiesAssignmentsDelete = Body(...), + session=Depends(get_session), +) -> None: + """ + Unassigns multiple activity assignments for a specified applet. + + This endpoint allows authorized users to unassign activities or flows from + participants by marking the corresponding assignments as deleted. + + Parameters: + ----------- + applet_id : uuid.UUID + The ID of the applet from which assignments are being unassigned. + + user : User, optional + The current user making the request (automatically injected). + + schema : ActivitiesAssignmentsDelete + The schema containing the list of assignments to be unassigned. + + session : Depends, optional + The database session (automatically injected). + """ + async with atomic(session): + service = AppletService(session, user.id) + await service.exist_by_id(applet_id) + await CheckAccessService(session, user.id).check_applet_activity_assignment_access(applet_id) + await ActivityAssignmentService(session).unassign_many(schema.assignments) + + async def applet_assignments( applet_id: uuid.UUID, user: User = Depends(get_current_user), diff --git a/src/apps/activity_assignments/crud/assignments.py b/src/apps/activity_assignments/crud/assignments.py index 78e6e666104..91c453bdc78 100644 --- a/src/apps/activity_assignments/crud/assignments.py +++ b/src/apps/activity_assignments/crud/assignments.py @@ -1,6 +1,6 @@ import uuid -from sqlalchemy import or_, select +from sqlalchemy import or_, select, tuple_, update from sqlalchemy.orm import Query, aliased from apps.activities.db.schemas import ActivitySchema @@ -44,9 +44,54 @@ class ActivityAssigmentCRUD(BaseCRUD[ActivityAssigmentSchema]): schema_class = ActivityAssigmentSchema async def create_many(self, schemas: list[ActivityAssigmentSchema]) -> list[ActivityAssigmentSchema]: + """ + Creates multiple activity assignment records in the database. + + This method utilizes the `_create_many` method from the `BaseCRUD` class to insert + multiple `ActivityAssigmentSchema` records into the database in a single operation. + + Parameters: + ----------- + schemas : list[ActivityAssigmentSchema] + A list of activity assignment schemas to be created. + + Returns: + -------- + list[ActivityAssigmentSchema] + A list of the created `ActivityAssigmentSchema` objects. + + Notes: + ------ + - Inherits functionality from `BaseCRUD`, which provides the `_create_many` method for + bulk insertion operations. + - Ensures that all new assignments are created in a single database transaction. + """ return await self._create_many(schemas) async def already_exists(self, schema: ActivityAssigmentSchema) -> bool: + """ + Checks if an activity assignment already exists in the database. + + This method builds a query to check for the existence of an assignment with the same + `activity_id`, `activity_flow_id`, `respondent_subject_id`, and `target_subject_id`, + while ensuring the record has not been soft-deleted. + + Parameters: + ----------- + schema : ActivityAssigmentSchema + The activity assignment schema to check for existence. + + Returns: + -------- + bool + `True` if the assignment already exists, otherwise `False`. + + Notes: + ------ + - This method uses the `_execute` method from the `BaseCRUD` class to run the query and + check for the existence of the assignment. + - The existence check is based on the combination of IDs and considers soft-deleted records. + """ query: Query = select(ActivityAssigmentSchema) query = query.where(ActivityAssigmentSchema.activity_id == schema.activity_id) query = query.where(ActivityAssigmentSchema.respondent_subject_id == schema.respondent_subject_id) @@ -57,6 +102,61 @@ async def already_exists(self, schema: ActivityAssigmentSchema) -> bool: db_result = await self._execute(select(query)) return db_result.scalars().first() or False + async def unassign_many( + self, + activity_or_flow_ids: list[uuid.UUID], + respondent_subject_ids: list[uuid.UUID], + target_subject_ids: list[uuid.UUID], + ) -> None: + """ + Marks the `is_deleted` field as True for all matching assignments based on the provided + activity or flow IDs, respondent subject IDs, and target subject IDs. The method ensures + that each set of IDs corresponds to a unique record by treating the IDs in a tuple + (activity/flow ID, respondent subject ID, target subject ID) as a unique combination. + + Parameters: + ---------- + activity_or_flow_ids : list[uuid.UUID] + List of activity or flow IDs to search for. These IDs may correspond to either + `activity_id` or `activity_flow_id` fields. + respondent_subject_ids : list[uuid.UUID] + List of respondent subject IDs to match against. + target_subject_ids : list[uuid.UUID] + List of target subject IDs to match against. + + Returns: + ------- + None + + Raises: + ------ + AssertionError + If the lengths of the provided ID lists do not match. + """ + + # Ensure all lists are of equal length + assert len(activity_or_flow_ids) == len(respondent_subject_ids) == len(target_subject_ids) + + query: Query = ( + update(ActivityAssigmentSchema) + .where( + or_( + tuple_( + ActivityAssigmentSchema.activity_id, + ActivityAssigmentSchema.respondent_subject_id, + ActivityAssigmentSchema.target_subject_id, + ).in_(zip(activity_or_flow_ids, respondent_subject_ids, target_subject_ids)), + tuple_( + ActivityAssigmentSchema.activity_flow_id, + ActivityAssigmentSchema.respondent_subject_id, + ActivityAssigmentSchema.target_subject_id, + ).in_(zip(activity_or_flow_ids, respondent_subject_ids, target_subject_ids)), + ) + ) + .values(is_deleted=True) + ) + await self._execute(query) + async def get_by_respondent_subject_id(self, respondent_subject_id) -> list[ActivityAssigmentSchema]: query: Query = select(ActivityAssigmentSchema) query = query.where(ActivityAssigmentSchema.respondent_subject_id == respondent_subject_id) @@ -75,10 +175,7 @@ async def get_by_applet(self, applet_id: uuid.UUID, query_params: QueryParams) - .join(respondent_schema, respondent_schema.id == ActivityAssigmentSchema.respondent_subject_id) .join(target_schema, target_schema.id == ActivityAssigmentSchema.target_subject_id) .where( - or_( - ActivityFlowSchema.applet_id == applet_id, - ActivitySchema.applet_id == applet_id, - ), + or_(ActivityFlowSchema.applet_id == applet_id, ActivitySchema.applet_id == applet_id), ActivityAssigmentSchema.soft_exists(), respondent_schema.soft_exists(), target_schema.soft_exists(), diff --git a/src/apps/activity_assignments/domain/assignments.py b/src/apps/activity_assignments/domain/assignments.py index ed1cc906d13..8b4ba2ab172 100644 --- a/src/apps/activity_assignments/domain/assignments.py +++ b/src/apps/activity_assignments/domain/assignments.py @@ -2,7 +2,12 @@ from pydantic import BaseModel, root_validator -from apps.activity_assignments.errors import ActivityAssignmentActivityOrFlowError +from apps.activity_assignments.errors import ( + ActivityAssignmentActivityOrFlowError, + ActivityAssignmentMissingRespondentError, + ActivityAssignmentMissingTargetError, + ActivityAssignmentNotActivityAndFlowError, +) from apps.shared.domain import InternalModel, PublicModel from apps.subjects.domain import SubjectReadResponse @@ -16,7 +21,7 @@ class ActivityAssignmentCreate(BaseModel): @root_validator def validate_assignments(cls, values): if not values.get("activity_id") and not values.get("activity_flow_id"): - raise ActivityAssignmentActivityOrFlowError() + raise ActivityAssignmentNotActivityAndFlowError() if values.get("activity_id") and values.get("activity_flow_id"): raise ActivityAssignmentActivityOrFlowError("Only one of activity_id or activity_flow_id must be provided") @@ -57,3 +62,34 @@ class ActivityAssignmentWithSubject(PublicModel): class ActivitiesAssignmentsWithSubjects(PublicModel): applet_id: UUID assignments: list[ActivityAssignmentWithSubject] + + +class ActivityAssignmentDelete(BaseModel): + activity_id: UUID | None + activity_flow_id: UUID | None + respondent_subject_id: UUID + target_subject_id: UUID + + @root_validator + def validate_assignments(cls, values): + # Validate that exactly one of activity_id or activity_flow_id is provided + if not values.get("activity_id") and not values.get("activity_flow_id"): + raise ActivityAssignmentNotActivityAndFlowError() + + # Validate that respondent_subject_id is provided + if not values.get("respondent_subject_id"): + raise ActivityAssignmentMissingRespondentError() + + # Validate that target_subject_id is provided + if not values.get("target_subject_id"): + raise ActivityAssignmentMissingTargetError() + + # Ensure that only one of activity_id or activity_flow_id is provided + if values.get("activity_id") and values.get("activity_flow_id"): + raise ActivityAssignmentActivityOrFlowError() + + return values + + +class ActivitiesAssignmentsDelete(InternalModel): + assignments: list[ActivityAssignmentDelete] diff --git a/src/apps/activity_assignments/errors.py b/src/apps/activity_assignments/errors.py index baa454f3b0e..c1dc3d2c0b1 100644 --- a/src/apps/activity_assignments/errors.py +++ b/src/apps/activity_assignments/errors.py @@ -4,4 +4,16 @@ class ActivityAssignmentActivityOrFlowError(ValidationError): + message = _("Either activity_id or activity_flow_id must be provided, but not both") + + +class ActivityAssignmentNotActivityAndFlowError(ValidationError): message = _("Either activity_id or activity_flow_id must be provided") + + +class ActivityAssignmentMissingRespondentError(ValidationError): + message = _("Respondent subject ID must be provided") + + +class ActivityAssignmentMissingTargetError(ValidationError): + message = _("Target subject ID must be provided") diff --git a/src/apps/activity_assignments/router.py b/src/apps/activity_assignments/router.py index 08062d2a43a..7d141bce909 100644 --- a/src/apps/activity_assignments/router.py +++ b/src/apps/activity_assignments/router.py @@ -1,7 +1,12 @@ from fastapi.routing import APIRouter from starlette import status -from apps.activity_assignments.api import applet_assignments, applet_respondent_assignments, assignments_create +from apps.activity_assignments.api import ( + applet_assignments, + applet_respondent_assignments, + assignment_delete, + assignments_create, +) from apps.activity_assignments.domain.assignments import ActivitiesAssignments, ActivitiesAssignmentsWithSubjects from apps.shared.domain import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE, Response @@ -10,7 +15,7 @@ router.post( "/applet/{applet_id}", description="""Create a set of activity assignments. For each - assignment, provide subject respondent ID (full account + assignment, provide respondent subject ID (full account or pending full account), target subject ID and activity or activity flow ID """, @@ -22,6 +27,20 @@ }, )(assignments_create) +router.delete( + "/applet/{applet_id}", + description="""Unassign a set of activity assignments. For each + assignment, provide respondent subject ID (full account + or pending account), target subject ID, and activity or activity flow ID. + """, + status_code=status.HTTP_204_NO_CONTENT, + responses={ + status.HTTP_204_NO_CONTENT: {"description": "Successfully unassigned the activities or flows."}, + **DEFAULT_OPENAPI_RESPONSE, + **AUTHENTICATION_ERROR_RESPONSES, + }, +)(assignment_delete) + router.get( "/applet/{applet_id}", description="""Get all activity assignments for an applet. diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index a4a03285bce..e03fa91217f 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -9,6 +9,7 @@ from apps.activity_assignments.domain.assignments import ( ActivityAssignment, ActivityAssignmentCreate, + ActivityAssignmentDelete, ActivityAssignmentWithSubject, ) from apps.activity_flows.crud import FlowsCRUD @@ -38,6 +39,44 @@ def __init__(self, session): async def create_many( self, applet_id: uuid.UUID, assignments_create: list[ActivityAssignmentCreate] ) -> list[ActivityAssignment]: + """ + Creates multiple activity assignments for the given applet. + + This method takes a list of assignment creation requests, validates them, checks for + existing assignments, and then creates new assignments in the database. If an assignment + already exists, it is skipped. The method returns the successfully created assignments. + + Parameters: + ----------- + applet_id : uuid.UUID + The ID of the applet for which the assignments are being created. + + assignments_create : list[ActivityAssignmentCreate] + A list of assignment creation objects that specify the details of each assignment. + Each object contains information such as `activity_id`, `activity_flow_id`, + `respondent_subject_id`, and `target_subject_id`. + + Returns: + -------- + list[ActivityAssignment] + A list of `ActivityAssignment` objects that have been successfully created. + + Process: + -------- + 1. Retrieves all relevant entities (activities, flows, subjects) using + `_get_assignments_entities`. + 2. Validates each assignment and retrieves the corresponding activity or flow name + using `_validate_assignment_and_get_activity_or_flow_name`. + 3. Checks if the assignment already exists using `ActivityAssigmentCRUD.already_exists`. + If it does, the assignment is skipped. + 4. Creates a new `ActivityAssigmentSchema` for each valid assignment and collects them + into a list. + 5. Inserts the new assignments into the database using `ActivityAssigmentCRUD.create_many`. + 6. Optionally, stores respondent activities for future processing (e.g., sending emails). + (This step is marked as a TODO.) + 7. Returns the newly created `ActivityAssignment` objects. + + """ entities = await self._get_assignments_entities(applet_id, assignments_create) respondent_activities: dict[uuid.UUID, set[str]] = defaultdict(set) @@ -113,7 +152,9 @@ async def check_for_assignment_and_notify(self, applet_id: uuid.UUID, respondent if len(respondent_activities[respondent_subject_id]) > 0: await self.send_email_notification( - applet_id, {respondent_subject_id: respondent_subject}, respondent_activities + applet_id, + {respondent_subject_id: respondent_subject}, + respondent_activities, ) async def send_email_notification( @@ -159,6 +200,38 @@ async def _check_for_already_existing_assignment(self, schema: ActivityAssigment def _validate_assignment_and_get_activity_or_flow_name( assignment: ActivityAssignmentCreate, entities: _AssignmentEntities ) -> str: + """ + Validates the assignment request and retrieves the name of the activity or flow. + + This method checks the validity of the provided `activity_id`, `activity_flow_id`, + `respondent_subject_id`, and `target_subject_id` by ensuring they correspond to existing + records. If validation passes, it returns the name of the activity or flow. If any validation + fails, it raises a `ValidationError`. + + Parameters: + ---------- + assignment : ActivityAssignmentCreate + The assignment request object containing the details of the assignment, + including `activity_id`, `activity_flow_id`, `respondent_subject_id`, and `target_subject_id`. + + entities : _AssignmentEntities + A collection of entities that includes activities, flows, respondent subjects, + and target subjects, used for validation. + + Returns: + ------- + str + The name of the activity or flow corresponding to the assignment. + + Raises: + ------ + ValidationError + If any of the following conditions are met: + - The `activity_id` provided does not correspond to an existing activity. + - The `activity_flow_id` provided does not correspond to an existing activity flow. + - The `respondent_subject_id` provided does not correspond to an existing respondent subject. + - The `target_subject_id` provided does not correspond to an existing target subject. + """ name: str = "" activity_flow_message = "" if assignment.activity_id: @@ -225,6 +298,48 @@ async def _get_assignments_entities( return entities + async def unassign_many(self, assignments_unassign: list[ActivityAssignmentDelete]) -> None: + """ + Unassigns multiple activity assignments by marking them as deleted. + + This method takes a list of assignment requests, retrieves the corresponding + assignment entities from the database, marks them as deleted, and then returns + the updated assignments. + + Parameters: + ----------- + assignments_unassign : list[ActivityAssignmentDelete] + A list of assignment creation objects that specify which assignments to unassign. + Each object contains details such as `activity_id`, `activity_flow_id`, + `respondent_subject_id`, and `target_subject_id`. + + Returns: + -------- + list[ActivityAssignment] + A list of `ActivityAssignment` objects that have been marked as deleted. + """ + + activity_or_flow_ids = [] + respondent_subject_ids = [] + target_subject_ids = [] + + for assignment in assignments_unassign: + # Append only non-None values to the activity_or_flow_ids list + if assignment.activity_id is not None: + activity_or_flow_ids.append(assignment.activity_id) + elif assignment.activity_flow_id is not None: + activity_or_flow_ids.append(assignment.activity_flow_id) + + # Append other necessary IDs + target_subject_ids.append(assignment.target_subject_id) + respondent_subject_ids.append(assignment.respondent_subject_id) + + await ActivityAssigmentCRUD(self.session).unassign_many( + activity_or_flow_ids=activity_or_flow_ids, + respondent_subject_ids=respondent_subject_ids, + target_subject_ids=target_subject_ids, + ) + async def get_all(self, applet_id: uuid.UUID, query_params: QueryParams) -> list[ActivityAssignment]: assignments = await ActivityAssigmentCRUD(self.session).get_by_applet(applet_id, query_params) diff --git a/src/apps/activity_assignments/tests/test_unassignments.py b/src/apps/activity_assignments/tests/test_unassignments.py new file mode 100644 index 00000000000..ecabcc8c531 --- /dev/null +++ b/src/apps/activity_assignments/tests/test_unassignments.py @@ -0,0 +1,588 @@ +import http +import uuid + +import pytest +from pydantic import EmailStr +from sqlalchemy import or_, select +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.activity_assignments.db.schemas import ActivityAssigmentSchema +from apps.activity_assignments.domain.assignments import ( + ActivitiesAssignmentsCreate, + ActivitiesAssignmentsDelete, + ActivityAssignmentCreate, + ActivityAssignmentDelete, +) +from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate +from apps.applets.domain.applet_create_update import AppletUpdate +from apps.applets.domain.applet_full import AppletFull +from apps.applets.service import AppletService +from apps.invitations.domain import InvitationRespondentRequest +from apps.shared.enums import Language +from apps.shared.test import BaseTest +from apps.shared.test.client import TestClient +from apps.subjects.db.schemas import SubjectSchema +from apps.subjects.domain import Subject, SubjectCreate, SubjectFull +from apps.subjects.services import SubjectsService +from apps.users import User + + +@pytest.fixture +def invitation_respondent_data() -> InvitationRespondentRequest: + return InvitationRespondentRequest( + email=EmailStr("pending@example.com"), + first_name="User", + last_name="pending", + language="en", + secret_user_id=str(uuid.uuid4()), + nickname=str(uuid.uuid4()), + tag="respondentTag", + ) + + +@pytest.fixture +async def lucy_applet_one_subject(session: AsyncSession, lucy: User, applet_one_lucy_respondent: AppletFull) -> Subject: + applet_id = applet_one_lucy_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def lucy_applet_two_subject(session: AsyncSession, lucy: User, applet_two_lucy_respondent: AppletFull) -> Subject: + applet_id = applet_two_lucy_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def applet_one_pending_subject( + client, + tom: User, + invitation_respondent_data, + applet_one: AppletFull, + session: AsyncSession, +) -> Subject: + # invite a new respondent + client.login(tom) + response = await client.post( + "/invitations/{applet_id}/respondent".format(applet_id=str(applet_one.id)), + invitation_respondent_data, + ) + assert response.status_code == http.HTTPStatus.OK + + query = select(SubjectSchema).where( + SubjectSchema.applet_id == applet_one.id, + SubjectSchema.email == invitation_respondent_data.email, + ) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + +@pytest.fixture +async def applet_one_with_flow( + session: AsyncSession, + applet_one: AppletFull, + applet_minimal_data: AppletFull, + tom: User, +): + data = AppletUpdate(**applet_minimal_data.dict()) + flow = FlowUpdate( + name="flow", + items=[ActivityFlowItemUpdate(id=None, activity_key=data.activities[0].key)], + description={Language.ENGLISH: "description"}, + id=None, + ) + data.activity_flows = [flow] + srv = AppletService(session, tom.id) + await srv.update(applet_one.id, data) + applet = await srv.get_full_applet(applet_one.id) + return applet + + +@pytest.fixture +async def applet_two_with_flow( + session: AsyncSession, + applet_two: AppletFull, + applet_minimal_data: AppletFull, + tom: User, +): + data = AppletUpdate(**applet_minimal_data.dict()) + flow = FlowUpdate( + name="flow_two", + items=[ActivityFlowItemUpdate(id=None, activity_key=data.activities[0].key)], + description={Language.ENGLISH: "description for flow two"}, + id=None, + ) + data.activity_flows = [flow] + srv = AppletService(session, tom.id) + await srv.update(applet_two.id, data) + applet = await srv.get_full_applet(applet_two.id) + return applet + + +@pytest.fixture +async def applet_one_shell_account(session: AsyncSession, applet_one: AppletFull, tom: User) -> Subject: + return await SubjectsService(session, tom.id).create( + SubjectCreate( + applet_id=applet_one.id, + creator_id=tom.id, + first_name="Shell", + last_name="Account", + nickname="shell-account-0", + tag="shell-account-0-tag", + secret_user_id=f"{uuid.uuid4()}", + ) + ) + + +class TestActivityUnassignments(BaseTest): + activities_assign_unassign_applet = "/assignments/applet/{applet_id}" + + async def test_create_one_unassignment( + self, + client: TestClient, + applet_one: AppletFull, + tom: User, + lucy_applet_one_subject: SubjectFull, + tom_applet_one_subject, + session: AsyncSession, + ): + client.login(tom) + activity_assignment_id = applet_one.activities[0].id + subject_id = lucy_applet_one_subject.id + target_id = tom_applet_one_subject.id + # Use the same details as the existing assignment + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=activity_assignment_id, + respondent_subject_id=subject_id, + target_subject_id=target_id, + ) + ] + ) + + assignment_response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert assignment_response.status_code == http.HTTPStatus.CREATED, assignment_response.json() + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create.dict(), + ) + + assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT + + # Query based on activity_id, respondent_subject_id, and target_subject_id + query = select(ActivityAssigmentSchema).where( + ActivityAssigmentSchema.activity_id == activity_assignment_id, + ActivityAssigmentSchema.respondent_subject_id == subject_id, + ActivityAssigmentSchema.target_subject_id == target_id, + ) + + res = await session.execute(query) + model = res.scalars().one() + + assert model.activity_id == activity_assignment_id + assert model.respondent_subject_id == subject_id + assert model.target_subject_id == target_id + assert model.is_deleted is True + + async def test_unassign_no_effect_with_wrong_activity( + self, + client: TestClient, + applet_one: AppletFull, + applet_two: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + session: AsyncSession, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign it using the wrong applet_id (applet_one) + assignment_delete = ActivitiesAssignmentsDelete( + assignments=[ + ActivityAssignmentDelete( + activity_id=applet_two.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete.dict(), + ) + + # Expect a 204 No Content because no assignments match the given applet_id + assert response.status_code == http.HTTPStatus.NO_CONTENT + + # Verify that the assignment in applet_two still exists and is not marked as deleted + query = select(ActivityAssigmentSchema).where( + ActivityAssigmentSchema.activity_id == applet_one.activities[0].id, + ActivityAssigmentSchema.respondent_subject_id == tom_applet_one_subject.id, + ActivityAssigmentSchema.target_subject_id == tom_applet_one_subject.id, + ) + res = await session.execute(query) + assignment = res.scalars().first() + + assert assignment is not None # The assignment should still exist + assert assignment.is_deleted is False + + async def test_unassign_fail_missing_activity_and_flow( + self, + client: TestClient, + applet_one: AppletFull, + applet_two: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign without providing activity_id or activity_flow_id + assignment_delete = dict( + assignments=[ + dict( + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, + ) + + # Expect a 400 Bad Request because neither activity_id nor activity_flow_id was provided + assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST + result = unassign_response.json()["result"][0] + assert result["message"] == "Either activity_id or activity_flow_id must be provided" + + async def test_unassign_fail_both_activity_and_flow( + self, + client: TestClient, + applet_one: AppletFull, + applet_two: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign with both activity_id and activity_flow_id provided + assignment_delete = dict( + assignments=[ + dict( + activity_id=applet_one.activities[0].id, + activity_flow_id=applet_two.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, + ) + + assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST, unassign_response.json() + result = unassign_response.json()["result"][0] + assert result["message"] == "Either activity_id or activity_flow_id must be provided, but not both" + + async def test_unassign_multiple_assignments_with_flow( + self, + client: TestClient, + applet_one_with_flow: AppletFull, + tom: User, + lucy_applet_one_subject: SubjectFull, + tom_applet_one_subject: SubjectFull, + session: AsyncSession, + ): + client.login(tom) + + # Step 1: Assign multiple activities/flows + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ActivityAssignmentCreate( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ) + + # Create the assignments + assignment_response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignments_create, + ) + + assert assignment_response.status_code == http.HTTPStatus.CREATED, assignment_response.json() + + # Step 2: Unassign the previously assigned activities/flows + assignment_delete = ActivitiesAssignmentsDelete( + assignments=[ + ActivityAssignmentDelete( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ActivityAssignmentDelete( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ) + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignment_delete.dict(), + ) + + assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT, unassign_response.json() + + # Validate that the assignments have been unassigned + for assignment in assignment_delete.assignments: + query = select(ActivityAssigmentSchema).where( + or_( + ActivityAssigmentSchema.activity_id == assignment.activity_id, + ActivityAssigmentSchema.activity_flow_id == assignment.activity_flow_id, + ), + ActivityAssigmentSchema.respondent_subject_id == assignment.respondent_subject_id, + ActivityAssigmentSchema.target_subject_id == assignment.target_subject_id, + ) + res = await session.execute(query) + model = res.scalars().first() + + assert model is not None + assert model.is_deleted is True + + async def test_unassign_no_effect_with_wrong_flow( + self, + client: TestClient, + applet_one_with_flow: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + session: AsyncSession, + ): + client.login(tom) + + # Step 1: Assign an activity flow first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign it using a wrong flow_id from a different applet + wrong_applet_id = "7db2b7fe-3eba-4c70-8d02-dcf55b74d1c3" + assignment_delete = ActivitiesAssignmentsDelete( + assignments=[ + ActivityAssignmentDelete( + activity_flow_id=wrong_applet_id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignment_delete.dict(), + ) + + # Expect a 204 No Content because no assignments match the given flow_id + assert response.status_code == http.HTTPStatus.NO_CONTENT + + # Verify that the assignment in applet_one_with_flow still exists and is not marked as deleted + query = select(ActivityAssigmentSchema).where( + ActivityAssigmentSchema.activity_flow_id == applet_one_with_flow.activity_flows[0].id, + ActivityAssigmentSchema.respondent_subject_id == tom_applet_one_subject.id, + ActivityAssigmentSchema.target_subject_id == tom_applet_one_subject.id, + ) + res = await session.execute(query) + assignment = res.scalars().first() + + assert assignment is not None # The assignment should still exist + assert assignment.is_deleted is False + + async def test_unassign_fail_missing_respondent( + self, + client: TestClient, + applet_one: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create.dict(), + ) + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign without providing target_subject_id using a dictionary + assignment_delete = { + "assignments": [ + { + "activity_id": str(applet_one.activities[0].id), + "respondent_subject_id": str(tom_applet_one_subject.id), + "target_subject_id": None, # Missing target_subject_id + } + ] + } + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, + ) + + # Expect a 400 Bad Request due to missing target_subject_id + assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST + result = unassign_response.json()["result"][0] + assert result["message"] == "Target subject ID must be provided" + + async def test_unassign_fail_missing_target( + self, + client: TestClient, + applet_one: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create.dict(), + ) + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign without providing target_subject_id using a dictionary + assignment_delete = { + "assignments": [ + { + "activity_id": str(applet_one.activities[0].id), + "respondent_subject_id": str(tom_applet_one_subject.id), + "target_subject_id": None, # Missing target_subject_id + } + ] + } + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, # Use json= to pass the dictionary as JSON + ) + + # Expect a 400 Bad Request due to missing target_subject_id + assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST + result = unassign_response.json()["result"][0] + assert result["message"] == "Target subject ID must be provided" From 0b19e869dedd9965c1075c84ce4ae0d709a3365e Mon Sep 17 00:00:00 2001 From: Farmer Paul Date: Thu, 5 Sep 2024 15:45:16 -0400 Subject: [PATCH 03/41] fix: add `items` to assigned activities endpoint (#1591) --- src/apps/activities/api/activities.py | 4 +++- src/apps/activities/crud/activity.py | 1 + src/apps/activities/domain/activity.py | 3 ++- src/apps/activities/services/activity.py | 1 + src/apps/activities/tests/test_activities.py | 3 +++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index 8f242d6359e..67494445856 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -118,7 +118,9 @@ async def applet_activities_for_subject( # Ensure reviewers can access the subject await CheckAccessService(session, user.id).check_subject_subject_access(applet_id, subject_id) - activities_future = ActivityService(session, user.id).get_single_language_by_applet_id(applet_id, language) + activities_future = ActivityService(session, user.id).get_single_language_with_items_by_applet_id( + applet_id, language + ) flows_future = FlowService(session).get_single_language_by_applet_id(applet_id, language) assignments_future = ActivityAssignmentService(session).get_all_by_respondent(applet_id, subject_id) diff --git a/src/apps/activities/crud/activity.py b/src/apps/activities/crud/activity.py index f3e970b2fa3..c2f7635cfbf 100644 --- a/src/apps/activities/crud/activity.py +++ b/src/apps/activities/crud/activity.py @@ -60,6 +60,7 @@ async def get_mobile_with_items_by_applet_id( ActivitySchema.scores_and_reports, ActivitySchema.performance_task_type, ActivitySchema.is_performance_task, + ActivitySchema.auto_assign, ) query = query.where(ActivitySchema.applet_id == applet_id) diff --git a/src/apps/activities/domain/activity.py b/src/apps/activities/domain/activity.py index 36f60b45cbf..74e5f76402e 100644 --- a/src/apps/activities/domain/activity.py +++ b/src/apps/activities/domain/activity.py @@ -95,9 +95,10 @@ class ActivityLanguageWithItemsMobileDetailPublic(PublicModel): scores_and_reports: ScoresAndReports | None = None performance_task_type: PerformanceTaskType | None = None is_performance_task: bool = False + auto_assign: bool | None = True -class ActivityWithAssignmentDetailsPublic(ActivitySingleLanguageDetailPublic): +class ActivityWithAssignmentDetailsPublic(ActivityLanguageWithItemsMobileDetailPublic): assignments: list[ActivityAssignmentWithSubject] = Field(default_factory=list) diff --git a/src/apps/activities/services/activity.py b/src/apps/activities/services/activity.py index 9c0b24a08ac..6f973f0a003 100644 --- a/src/apps/activities/services/activity.py +++ b/src/apps/activities/services/activity.py @@ -279,6 +279,7 @@ async def get_single_language_with_items_by_applet_id( scores_and_reports=schema.scores_and_reports, performance_task_type=schema.performance_task_type, is_performance_task=schema.is_performance_task, + auto_assign=schema.auto_assign, ) activities.append(activity) diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 9a566a88fb3..46c751d83ad 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -687,6 +687,7 @@ async def test_subject_assigned_activities_auto_assigned( assert activity_result["name"] == activity.name assert activity_result["description"] == activity.description[Language.ENGLISH] assert activity_result["autoAssign"] == activity.auto_assign + assert len(activity_result["items"]) == 1 assert len(activity_result["assignments"]) == 0 flow = applet_activity_flow_lucy_manager.activity_flows[0] @@ -777,6 +778,7 @@ async def test_subject_assigned_activities_manually_assigned( assert activity_result["name"] == manual_activity.name assert activity_result["description"] == manual_activity.description[Language.ENGLISH] assert activity_result["autoAssign"] is False + assert len(activity_result["items"]) == 1 assert len(activity_result["assignments"]) == 1 activity_assignment = activity_result["assignments"][0] @@ -876,6 +878,7 @@ async def test_subject_assigned_activities_auto_and_manually_assigned( assert activity_result["name"] == manual_activity.name assert activity_result["description"] == manual_activity.description[Language.ENGLISH] assert activity_result["autoAssign"] is True + assert len(activity_result["items"]) == 1 assert len(activity_result["assignments"]) == 1 activity_assignment = activity_result["assignments"][0] From 7e52955988c3eeff9d1940166f4c313037eba24b Mon Sep 17 00:00:00 2001 From: Farmer Paul Date: Fri, 6 Sep 2024 12:41:53 -0400 Subject: [PATCH 04/41] fix: Return assignments matching either respondent or target subject ID (M2-6223) (#1592) * fix: match by both respondent & target subject Assignments being returned by this endpoint were only matching by respondent subject, but the AC for M2-6223 states to return assignments matching both by respondent & target subject. So adjusted the function to match by one of those, or both, based on the `match_by` parameter. Fixed tests to test this condition as well. * fix: remove redundant CRUD function The `check_for_assignment_and_notify` function was using a CRUD function `get_by_respondent_subject_id` to return assignments matching a given respondent subject, which did not perform soft-delete integrity checks. The same functionality exists in `get_by_applet_and_subject` but with soft-delete integrity checks, so removed the old function in favour of this one. * refactor: use `Filters` class, simplify code On @rcmerlo's recommendation, switched to using `QueryParams` and `Filters` class for filtering assignments queries by respondent or target subject. In doing so, discovered that `get_by_applet_and_subject` was not in fact needed; all the functionality is now provided by `get_by_applet`. --- src/apps/activities/api/activities.py | 4 +- src/apps/activities/tests/test_activities.py | 49 ++++++++++++++++-- src/apps/activity_assignments/api.py | 5 +- .../activity_assignments/crud/assignments.py | 44 +++------------- src/apps/activity_assignments/service.py | 50 ++++++++++--------- src/apps/shared/query_params.py | 2 +- 6 files changed, 85 insertions(+), 69 deletions(-) diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index 67494445856..8c033428a12 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -122,7 +122,9 @@ async def applet_activities_for_subject( applet_id, language ) flows_future = FlowService(session).get_single_language_by_applet_id(applet_id, language) - assignments_future = ActivityAssignmentService(session).get_all_by_respondent(applet_id, subject_id) + + query_params = QueryParams(filters={"respondent_subject_id": subject_id, "target_subject_id": subject_id}) + assignments_future = ActivityAssignmentService(session).get_all_with_subject_entities(applet_id, query_params) activities, flows, assignments = await asyncio.gather(activities_future, flows_future, assignments_future) result = ActivitiesAndFlowsWithAssignmentDetailsPublic( diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 46c751d83ad..a124194c80e 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -644,7 +644,7 @@ async def test_subject_assigned_activities_invalid_subject( result = response.json()["result"] assert result[0]["type"] == "NOT_FOUND" - assert result[0]["message"] == f"Respondent subject id {subject_id} not found" + assert result[0]["message"] == f"Subject with id {subject_id} not found" async def test_subject_assigned_activities_empty_applet( self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject @@ -706,6 +706,7 @@ async def test_subject_assigned_activities_manually_assigned( client, empty_applet_lucy_manager, lucy, + lucy_empty_applet_subject, user_empty_applet_subject, activity_create_session: ActivityCreate, ): @@ -728,9 +729,28 @@ async def test_subject_assigned_activities_manually_assigned( [ ActivityAssignmentCreate( activity_id=manual_activity.id, + activity_flow_id=None, respondent_subject_id=user_empty_applet_subject.id, target_subject_id=user_empty_applet_subject.id, - ) + ), + ActivityAssignmentCreate( + activity_id=manual_activity.id, + activity_flow_id=None, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=manual_activity.id, + activity_flow_id=None, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=manual_activity.id, + activity_flow_id=None, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), ], ) @@ -753,10 +773,29 @@ async def test_subject_assigned_activities_manually_assigned( empty_applet_lucy_manager.id, [ ActivityAssignmentCreate( + activity_id=None, activity_flow_id=manual_flow.id, respondent_subject_id=user_empty_applet_subject.id, target_subject_id=user_empty_applet_subject.id, - ) + ), + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow.id, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow.id, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), ], ) @@ -779,7 +818,7 @@ async def test_subject_assigned_activities_manually_assigned( assert activity_result["description"] == manual_activity.description[Language.ENGLISH] assert activity_result["autoAssign"] is False assert len(activity_result["items"]) == 1 - assert len(activity_result["assignments"]) == 1 + assert len(activity_result["assignments"]) == 3 activity_assignment = activity_result["assignments"][0] assert activity_assignment["activityId"] == str(manual_activity.id) @@ -792,7 +831,7 @@ async def test_subject_assigned_activities_manually_assigned( assert flow_result["name"] == manual_flow.name assert flow_result["description"] == manual_flow.description[Language.ENGLISH] assert flow_result["autoAssign"] is False - assert len(flow_result["assignments"]) == 1 + assert len(flow_result["assignments"]) == 3 assert flow_result["activityIds"][0] == str(manual_flow.items[0].activity_id) flow_assignment = flow_result["assignments"][0] diff --git a/src/apps/activity_assignments/api.py b/src/apps/activity_assignments/api.py index 7b897288a17..41e2cd7654c 100644 --- a/src/apps/activity_assignments/api.py +++ b/src/apps/activity_assignments/api.py @@ -127,9 +127,10 @@ async def applet_respondent_assignments( respondent_subject = await SubjectsService(session, user.id).get_by_user_and_applet(user.id, applet_id) if not respondent_subject: - raise NotFoundError(f"User don't have subject role in applet {applet_id}") + raise NotFoundError(f"User doesn't have subject role in applet {applet_id}") - assignments = await ActivityAssignmentService(session).get_all_by_respondent(applet_id, respondent_subject.id) + query_params = QueryParams(filters={"respondent_subject_id": respondent_subject.id}) + assignments = await ActivityAssignmentService(session).get_all_with_subject_entities(applet_id, query_params) return Response( result=ActivitiesAssignmentsWithSubjects( diff --git a/src/apps/activity_assignments/crud/assignments.py b/src/apps/activity_assignments/crud/assignments.py index 91c453bdc78..9a0946d8699 100644 --- a/src/apps/activity_assignments/crud/assignments.py +++ b/src/apps/activity_assignments/crud/assignments.py @@ -1,6 +1,6 @@ import uuid -from sqlalchemy import or_, select, tuple_, update +from sqlalchemy import and_, or_, select, tuple_, update from sqlalchemy.orm import Query, aliased from apps.activities.db.schemas import ActivitySchema @@ -40,6 +40,11 @@ def filter_by_activities_or_flows(self, field, values: list | str): return field.in_(values) +class _ActivityAssignmentSubjectFilter(Filtering): + respondent_subject_id = FilterField(ActivityAssigmentSchema.respondent_subject_id) + target_subject_id = FilterField(ActivityAssigmentSchema.target_subject_id) + + class ActivityAssigmentCRUD(BaseCRUD[ActivityAssigmentSchema]): schema_class = ActivityAssigmentSchema @@ -157,13 +162,6 @@ async def unassign_many( ) await self._execute(query) - async def get_by_respondent_subject_id(self, respondent_subject_id) -> list[ActivityAssigmentSchema]: - query: Query = select(ActivityAssigmentSchema) - query = query.where(ActivityAssigmentSchema.respondent_subject_id == respondent_subject_id) - db_result = await self._execute(query) - - return db_result.scalars().all() - async def get_by_applet(self, applet_id: uuid.UUID, query_params: QueryParams) -> list[ActivityAssigmentSchema]: respondent_schema = aliased(SubjectSchema) target_schema = aliased(SubjectSchema) @@ -184,35 +182,9 @@ async def get_by_applet(self, applet_id: uuid.UUID, query_params: QueryParams) - if query_params.filters: activities_clause = _ActivityAssignmentActivitiesFilter().get_clauses(**query_params.filters) flows_clause = _ActivityAssignmentFlowsFilter().get_clauses(**query_params.filters) + subject_clauses = _ActivityAssignmentSubjectFilter().get_clauses(**query_params.filters) - query = query.where(or_(*activities_clause, *flows_clause)) - - db_result = await self._execute(query) - - return db_result.scalars().all() - - async def get_by_applet_and_respondent( - self, applet_id: uuid.UUID, respondent_subject_id: uuid.UUID - ) -> list[ActivityAssigmentSchema]: - respondent_schema = aliased(SubjectSchema) - target_schema = aliased(SubjectSchema) - query = ( - select(ActivityAssigmentSchema) - .outerjoin(ActivitySchema, ActivitySchema.id == ActivityAssigmentSchema.activity_id) - .outerjoin(ActivityFlowSchema, ActivityFlowSchema.id == ActivityAssigmentSchema.activity_flow_id) - .join(respondent_schema, respondent_schema.id == ActivityAssigmentSchema.respondent_subject_id) - .join(target_schema, target_schema.id == ActivityAssigmentSchema.target_subject_id) - .where( - or_( - ActivityFlowSchema.applet_id == applet_id, - ActivitySchema.applet_id == applet_id, - ), - ActivityAssigmentSchema.soft_exists(), - respondent_schema.soft_exists(), - target_schema.soft_exists(), - respondent_schema.id == respondent_subject_id, - ) - ) + query = query.where(and_(or_(*activities_clause, *flows_clause), or_(*subject_clauses))) db_result = await self._execute(query) diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index e03fa91217f..0d7b5baedf2 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -119,7 +119,8 @@ async def create_many( ] async def check_for_assignment_and_notify(self, applet_id: uuid.UUID, respondent_subject_id: uuid.UUID) -> None: - assignments = await ActivityAssigmentCRUD(self.session).get_by_respondent_subject_id(respondent_subject_id) + query_params = QueryParams(filters={"respondent_subject_id": respondent_subject_id}) + assignments = await ActivityAssigmentCRUD(self.session).get_by_applet(applet_id, query_params) if not assignments: return @@ -341,6 +342,9 @@ async def unassign_many(self, assignments_unassign: list[ActivityAssignmentDelet ) async def get_all(self, applet_id: uuid.UUID, query_params: QueryParams) -> list[ActivityAssignment]: + """ + Returns assignments for given applet ID and matching any filters provided in query_params. + """ assignments = await ActivityAssigmentCRUD(self.session).get_by_applet(applet_id, query_params) return [ @@ -354,20 +358,28 @@ async def get_all(self, applet_id: uuid.UUID, query_params: QueryParams) -> list for assignment in assignments ] - async def get_all_by_respondent( - self, applet_id: uuid.UUID, respondent_subject_id: uuid.UUID + async def get_all_with_subject_entities( + self, + applet_id: uuid.UUID, + query_params: QueryParams, ) -> list[ActivityAssignmentWithSubject]: - respondent_subject = await SubjectsCrud(self.session).get_by_id(respondent_subject_id) - if not respondent_subject: - raise NotFoundError(f"Respondent subject id {respondent_subject_id} not found") + """ + Returns assignments for given applet ID and matching any filters provided in query_params, with + respondent_subject and target_subject properties containing hydrated subject entities. + """ + for subject_id in set(query_params.filters.values()): + subject_exists = await SubjectsCrud(self.session).exist(subject_id, applet_id) + if not subject_exists: + raise NotFoundError(f"Subject with id {subject_id} not found") - assignments = await ActivityAssigmentCRUD(self.session).get_by_applet_and_respondent( - applet_id, respondent_subject_id - ) + assignments = await ActivityAssigmentCRUD(self.session).get_by_applet(applet_id, query_params) - target_subject_ids = [assignment.target_subject_id for assignment in assignments] + subject_ids: set[uuid.UUID] = set() + for assignment in assignments: + subject_ids.add(assignment.respondent_subject_id) + subject_ids.add(assignment.target_subject_id) - target_subjects = { + subjects = { subject.id: SubjectReadResponse( id=subject.id, first_name=subject.first_name, @@ -379,7 +391,7 @@ async def get_all_by_respondent( tag=subject.tag, applet_id=subject.applet_id, ) - for subject in await SubjectsCrud(self.session).get_by_ids(target_subject_ids) + for subject in await SubjectsCrud(self.session).get_by_ids(list(subject_ids)) } return [ @@ -387,18 +399,8 @@ async def get_all_by_respondent( id=assignment.id, activity_id=assignment.activity_id, activity_flow_id=assignment.activity_flow_id, - respondent_subject=SubjectReadResponse( - id=respondent_subject.id, - first_name=respondent_subject.first_name, - last_name=respondent_subject.last_name, - email=respondent_subject.email, - language=respondent_subject.language, - nickname=respondent_subject.nickname, - secret_user_id=respondent_subject.secret_user_id, - tag=respondent_subject.tag, - applet_id=respondent_subject.applet_id, - ), - target_subject=target_subjects[assignment.target_subject_id], + respondent_subject=subjects[assignment.respondent_subject_id], + target_subject=subjects[assignment.target_subject_id], ) for assignment in assignments ] diff --git a/src/apps/shared/query_params.py b/src/apps/shared/query_params.py index 01da9b2da26..e58fb66c6ed 100644 --- a/src/apps/shared/query_params.py +++ b/src/apps/shared/query_params.py @@ -25,7 +25,7 @@ class QueryParams(InternalModel): """ filters: dict[str, Any] = Field(default_factory=dict) - search: str | None + search: str | None = None page: int = Field(gt=0, default=1) limit: int = Field(gt=0, default=10, le=settings.service.result_limit) ordering: list[str] = Field(default_factory=list) From 3b8e7c68f7f9bef0e815b7acddc03c47b5357bdc Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Fri, 6 Sep 2024 13:09:21 -0400 Subject: [PATCH 05/41] Deploy to dev --- .github/workflows/run_build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_build.yaml b/.github/workflows/run_build.yaml index ab2be73bf32..57c63dc0e07 100644 --- a/.github/workflows/run_build.yaml +++ b/.github/workflows/run_build.yaml @@ -1,7 +1,7 @@ name: Build and Deploy on: -# push: {} + push: {} workflow_dispatch: {} # pull_request: # types: From f9c5f1aeb6cbb803124ddff1ff3181d514fc0327 Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Fri, 6 Sep 2024 13:10:00 -0400 Subject: [PATCH 06/41] Deploy to dev 2 --- .github/workflows/run_build.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run_build.yaml b/.github/workflows/run_build.yaml index 57c63dc0e07..c24761b7342 100644 --- a/.github/workflows/run_build.yaml +++ b/.github/workflows/run_build.yaml @@ -1,7 +1,9 @@ name: Build and Deploy on: - push: {} + push: + branches: + - develop workflow_dispatch: {} # pull_request: # types: From 7073b6d71592cc284c911e061c7588b01e2699b1 Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Fri, 6 Sep 2024 13:11:27 -0400 Subject: [PATCH 07/41] Fix --- .github/workflows/run_build.yaml | 14 +------------- .github/workflows/run_deploy_dev.yaml | 1 + 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/run_build.yaml b/.github/workflows/run_build.yaml index c24761b7342..c42fbf912f8 100644 --- a/.github/workflows/run_build.yaml +++ b/.github/workflows/run_build.yaml @@ -97,18 +97,6 @@ jobs: tags: ${{ steps.sha.outputs.IMAGE_NAME }},${{ env.ECR_REPO }}:latest platforms: linux/amd64,linux/arm64 -# - name: Build image for ECR -# id: build_step -# run: | -# echo "IMAGE_NAME=${ECR_REPO}:${GITHUB_BRANCH_OR_TAG/\//-}-${GITHUB_SHA:0:5}" >> "$GITHUB_OUTPUT" -# docker build -t ${ECR_REPO}:${GITHUB_BRANCH_OR_TAG/\//-}-${GITHUB_SHA:0:5} -f ./compose/fastapi/Dockerfile . -# docker tag ${ECR_REPO}:${GITHUB_BRANCH_OR_TAG/\//-}-${GITHUB_SHA:0:5} ${ECR_ADDRESS}/${ECR_REPO}:${GITHUB_BRANCH_OR_TAG/\//-}-${GITHUB_SHA:0:5} -# -# - name: Push image to ECR -# run: | -# aws ecr get-login-password | docker login --username AWS --password-stdin ${ECR_ADDRESS} -# docker push ${ECR_REPO}:${GITHUB_BRANCH_OR_TAG/\//-}-${GITHUB_SHA:0:5} - - name: Upload artifacts uses: actions/upload-artifact@v4 with: @@ -120,7 +108,7 @@ jobs: deploy_to_dev: name: Deploy to Dev needs: build_job - environment: dev +# environment: dev #if: ${{ github.event.pull_request.merged }} uses: ./.github/workflows/run_deploy_dev.yaml with: diff --git a/.github/workflows/run_deploy_dev.yaml b/.github/workflows/run_deploy_dev.yaml index 3010440fc18..b531626334e 100644 --- a/.github/workflows/run_deploy_dev.yaml +++ b/.github/workflows/run_deploy_dev.yaml @@ -18,6 +18,7 @@ env: jobs: run_migration: + environment: dev name: Run Database Migrations runs-on: ubuntu-latest env: From c63ca1a8977ffe774233c7b8316cdb9d705585a7 Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Fri, 6 Sep 2024 13:28:33 -0400 Subject: [PATCH 08/41] Update ECS task runner --- .github/workflows/run_deploy_dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_deploy_dev.yaml b/.github/workflows/run_deploy_dev.yaml index b531626334e..473a2c21b3f 100644 --- a/.github/workflows/run_deploy_dev.yaml +++ b/.github/workflows/run_deploy_dev.yaml @@ -56,7 +56,7 @@ jobs: - name: Run migration container id: run-task - uses: geekcell/github-action-aws-ecs-run-task@v3.0.0 + uses: geekcell/github-action-aws-ecs-run-task@v4 with: cluster: ${{ env.ECS_CLUSTER_NAME }} task-definition: ${{ env.TASK_DEFINITION }} From bf40f4b7e97db57686aae6c1033db36d27dfc271 Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Mon, 9 Sep 2024 13:26:21 -0400 Subject: [PATCH 09/41] Run migrations on startup (#1593) * Created new startup script * testing env * feature env version * feature start manifest --- compose/fastapi/Dockerfile | 6 ++++++ compose/fastapi/ecs-start | 7 +++++++ compose/fastapi/ecs-start-feature | 7 +++++++ copilot/mindlogger-backend/manifest.yml | 2 +- 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 compose/fastapi/ecs-start create mode 100644 compose/fastapi/ecs-start-feature diff --git a/compose/fastapi/Dockerfile b/compose/fastapi/Dockerfile index b4117eebdcc..477d602936a 100644 --- a/compose/fastapi/Dockerfile +++ b/compose/fastapi/Dockerfile @@ -40,6 +40,12 @@ RUN sed -i 's/\r$//g' /fastapi-start && chmod +x /fastapi-start COPY --chown=code:code ./compose/fastapi/migrate /fastapi-migrate RUN sed -i 's/\r$//g' /fastapi-migrate && chmod +x /fastapi-migrate +COPY --chown=code:code ./compose/fastapi/ecs-start /ecs-start +RUN sed -i 's/\r$//g' /ecs-start && chmod +x /ecs-start + +COPY --chown=code:code ./compose/fastapi/ecs-start-feature /ecs-start-feature +RUN sed -i 's/\r$//g' /ecs-start-feature && chmod +x /ecs-start-feature + # Select internal user USER code diff --git a/compose/fastapi/ecs-start b/compose/fastapi/ecs-start new file mode 100644 index 00000000000..2e15eb7950e --- /dev/null +++ b/compose/fastapi/ecs-start @@ -0,0 +1,7 @@ +#!/bin/bash + +set -eo pipefail +set -o nounset + +/fastapi-migrate +/fastapi-start \ No newline at end of file diff --git a/compose/fastapi/ecs-start-feature b/compose/fastapi/ecs-start-feature new file mode 100644 index 00000000000..55127c6f89f --- /dev/null +++ b/compose/fastapi/ecs-start-feature @@ -0,0 +1,7 @@ +#!/bin/bash + +set -eo pipefail +set -o nounset + +ENV=testing /fastapi-migrate +/fastapi-start \ No newline at end of file diff --git a/copilot/mindlogger-backend/manifest.yml b/copilot/mindlogger-backend/manifest.yml index 7de146fe3a0..a1cd1e0ef4f 100644 --- a/copilot/mindlogger-backend/manifest.yml +++ b/copilot/mindlogger-backend/manifest.yml @@ -30,7 +30,7 @@ image: port: 80 entrypoint: /fastapi-entrypoint -command: /fastapi-start +command: /ecs-start-feature #command: tail -f /dev/null cpu: 512 # Number of CPU units for the task. From 9f16bfc81a7b955acb1dae50a7853a4e13de2dc4 Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Mon, 9 Sep 2024 13:33:59 -0400 Subject: [PATCH 10/41] M2-7550 Disabling OASDiff --- .github/workflows/run_deploy_dev.yaml | 98 +++++++++++++-------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/workflows/run_deploy_dev.yaml b/.github/workflows/run_deploy_dev.yaml index 473a2c21b3f..8d4f36eb745 100644 --- a/.github/workflows/run_deploy_dev.yaml +++ b/.github/workflows/run_deploy_dev.yaml @@ -17,55 +17,55 @@ env: IMAGE_NAME: ${{ inputs.IMAGE_NAME }} jobs: - run_migration: - environment: dev - name: Run Database Migrations - runs-on: ubuntu-latest - env: - TASK_DEFINITION: migration - ECS_SERVICE_NAME: migration - - steps: - - name: echo IMAGE_NAME - run: | - echo $IMAGE_NAME - - - name: configure aws credentials - uses: aws-actions/configure-aws-credentials@v3 - with: - role-to-assume: arn:aws:iam::017925157769:role/cmiml-dev-oidc-github-role - role-session-name: OIDC-GHA-session - aws-region: ${{ env.AWS_REGION }} - - - name: Download task definition - run: | - aws ecs describe-task-definition --task-definition ${{ env.TASK_DEFINITION }} --query taskDefinition > task-definition.json - - - name: Render Amazon ECS task definition - id: task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: task-definition.json - container-name: mind_logger - image: ${{ inputs.IMAGE_NAME }} - - - name: Update Task Definition - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def.outputs.task-definition }} - - - name: Run migration container - id: run-task - uses: geekcell/github-action-aws-ecs-run-task@v4 - with: - cluster: ${{ env.ECS_CLUSTER_NAME }} - task-definition: ${{ env.TASK_DEFINITION }} - subnet-ids: subnet-02b7cfd48947b31ef - security-group-ids: sg-0976f7b2b2b5bf411 - override-container-command: | - /bin/sh - -c - /fastapi-migrate all +# run_migration: +# environment: dev +# name: Run Database Migrations +# runs-on: ubuntu-latest +# env: +# TASK_DEFINITION: migration +# ECS_SERVICE_NAME: migration +# +# steps: +# - name: echo IMAGE_NAME +# run: | +# echo $IMAGE_NAME +# +# - name: configure aws credentials +# uses: aws-actions/configure-aws-credentials@v3 +# with: +# role-to-assume: arn:aws:iam::017925157769:role/cmiml-dev-oidc-github-role +# role-session-name: OIDC-GHA-session +# aws-region: ${{ env.AWS_REGION }} +# +# - name: Download task definition +# run: | +# aws ecs describe-task-definition --task-definition ${{ env.TASK_DEFINITION }} --query taskDefinition > task-definition.json +# +# - name: Render Amazon ECS task definition +# id: task-def +# uses: aws-actions/amazon-ecs-render-task-definition@v1 +# with: +# task-definition: task-definition.json +# container-name: mind_logger +# image: ${{ inputs.IMAGE_NAME }} +# +# - name: Update Task Definition +# uses: aws-actions/amazon-ecs-deploy-task-definition@v1 +# with: +# task-definition: ${{ steps.task-def.outputs.task-definition }} +# +# - name: Run migration container +# id: run-task +# uses: geekcell/github-action-aws-ecs-run-task@v4 +# with: +# cluster: ${{ env.ECS_CLUSTER_NAME }} +# task-definition: ${{ env.TASK_DEFINITION }} +# subnet-ids: subnet-02b7cfd48947b31ef +# security-group-ids: sg-0976f7b2b2b5bf411 +# override-container-command: | +# /bin/sh +# -c +# /fastapi-migrate all deploy_to_ecs: runs-on: ubuntu-latest From 9d4dc23c6b40426d4ed079d840e53d3e9bbf6401 Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Mon, 9 Sep 2024 13:39:17 -0400 Subject: [PATCH 11/41] Dev build/deploy --- .github/workflows/{run_build.yaml => run_build_deploy.yaml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{run_build.yaml => run_build_deploy.yaml} (99%) diff --git a/.github/workflows/run_build.yaml b/.github/workflows/run_build_deploy.yaml similarity index 99% rename from .github/workflows/run_build.yaml rename to .github/workflows/run_build_deploy.yaml index c42fbf912f8..8824a87132f 100644 --- a/.github/workflows/run_build.yaml +++ b/.github/workflows/run_build_deploy.yaml @@ -2,8 +2,8 @@ name: Build and Deploy on: push: - branches: - - develop +# branches: +# - develop workflow_dispatch: {} # pull_request: # types: From ea47ba18c40491d5c5fabdd07f1a7fb234a8e83d Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Mon, 9 Sep 2024 13:43:21 -0400 Subject: [PATCH 12/41] Fix dev build --- .github/workflows/run_deploy_dev.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run_deploy_dev.yaml b/.github/workflows/run_deploy_dev.yaml index 8d4f36eb745..47d4ac81c98 100644 --- a/.github/workflows/run_deploy_dev.yaml +++ b/.github/workflows/run_deploy_dev.yaml @@ -1,5 +1,12 @@ name: Deploy to Dev on: + workflow_dispatch: + inputs: + IMAGE_NAME: + required: true + type: string + description: Image tag + workflow_call: inputs: IMAGE_NAME: @@ -69,7 +76,7 @@ jobs: deploy_to_ecs: runs-on: ubuntu-latest - needs: run_migration +# needs: run_migration name: Deploy ${{ matrix.apps.name }} strategy: @@ -128,7 +135,7 @@ jobs: on-failure: runs-on: ubuntu-latest - if: ${{ always() && (needs.deploy_to_ecs.result == 'failure' || needs.deploy_to_ecs.result == 'timed_out') }} + if: ${{ !cancelled() && (needs.deploy_to_ecs.result == 'failure' || needs.deploy_to_ecs.result == 'timed_out') }} needs: - deploy_to_ecs steps: @@ -145,7 +152,7 @@ jobs: on-success: runs-on: ubuntu-latest - if: ${{ always() && (needs.deploy_to_ecs.result == 'success') }} + if: ${{ !cancelled() && (needs.deploy_to_ecs.result == 'success') }} needs: - deploy_to_ecs steps: From 68aaa86f399ac5ee1b422ee39a46b272ab21a20e Mon Sep 17 00:00:00 2001 From: Andrew Weiland Date: Mon, 9 Sep 2024 14:57:14 -0400 Subject: [PATCH 13/41] Disabling OASDIFF --- .github/workflows/oasdif.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/oasdif.yaml b/.github/workflows/oasdif.yaml index d9761202280..8e6e57d9fea 100644 --- a/.github/workflows/oasdif.yaml +++ b/.github/workflows/oasdif.yaml @@ -1,9 +1,10 @@ name: OASDIFF breaking changes on: - pull_request: - branches: - - develop - - main + workflow_call: {} +# pull_request: +# branches: +# - develop +# - main jobs: compare: runs-on: ubuntu-latest @@ -25,7 +26,7 @@ jobs: on-failure: runs-on: ubuntu-latest - if: ${{ always() && (needs.compare.result == 'failure' || needs.compare.result == 'timed_out') }} + if: ${{ !cancelled() && (needs.compare.result == 'failure' || needs.compare.result == 'timed_out') }} needs: - compare steps: From 3b4117c062206cf34444bc90d1d66a80824a5027 Mon Sep 17 00:00:00 2001 From: Ramir Mesquita <790844+ramirlm@users.noreply.github.com> Date: Tue, 10 Sep 2024 08:52:15 -0300 Subject: [PATCH 14/41] Add support to Unity activity type (M2-7441) (#1563) * feat: adding unity type * change to fix odd validation behavior on item responses * refactor unityFile to unity * ruff formatting * ruff linting import order change * Adding unity tests * ruff format * test name fix * re-adding wrongly removed test * moving unity file file to config --------- Co-authored-by: Ramir Mesquita --- .../activities/domain/custom_validation.py | 1 + .../activities/domain/response_type_config.py | 10 +++++++++ src/apps/activities/domain/response_values.py | 8 +++++++ .../activities/tests/fixtures/activities.py | 22 +++++++++++++++++++ src/apps/activities/tests/fixtures/configs.py | 6 +++++ src/apps/applets/tests/fixtures/applets.py | 2 ++ .../tests/test_applet_activity_items.py | 1 + 7 files changed, 50 insertions(+) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 12dd2f30774..0fb6e6c3c5d 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -203,6 +203,7 @@ def validate_performance_task_type(values: dict): elif item.response_type in ( ResponseType.FLANKER, ResponseType.ABTRAILS, + ResponseType.UNITY, ): values["performance_task_type"] = item.response_type return values diff --git a/src/apps/activities/domain/response_type_config.py b/src/apps/activities/domain/response_type_config.py index a33ebf5776b..5504df1138c 100644 --- a/src/apps/activities/domain/response_type_config.py +++ b/src/apps/activities/domain/response_type_config.py @@ -54,6 +54,7 @@ class ResponseType(str, Enum): FLANKER = "flanker" STABILITYTRACKER = "stabilityTracker" ABTRAILS = "ABTrails" + UNITY = "unity" PHRASAL_TEMPLATE = "phrasalTemplate" @classmethod @@ -71,6 +72,7 @@ def get_non_response_types(cls): cls.FLANKER, cls.STABILITYTRACKER, cls.ABTRAILS, + cls.UNITY, ) @@ -227,6 +229,12 @@ class PhrasalTemplateConfig(PublicModel): remove_back_button: bool +class UnityConfig(PublicModel): + type: Literal[ResponseType.UNITY] | None + device_type: str | None + file: str | None + + class InputType(str, Enum): GYROSCOPE = "gyroscope" TOUCH = "touch" @@ -398,6 +406,7 @@ class PerformanceTaskType(str, Enum): GYROSCOPE = "gyroscope" TOUCH = "touch" ABTRAILS = "ABTrails" + UNITY = "unity" @classmethod def get_values(cls) -> list[str]: @@ -428,4 +437,5 @@ def get_values(cls) -> list[str]: | StabilityTrackerConfig | ABTrailsConfig | PhrasalTemplateConfig + | UnityConfig ) diff --git a/src/apps/activities/domain/response_values.py b/src/apps/activities/domain/response_values.py index f8dc45e8fba..c4d59824cb0 100644 --- a/src/apps/activities/domain/response_values.py +++ b/src/apps/activities/domain/response_values.py @@ -28,6 +28,7 @@ TextConfig, TimeConfig, TimeRangeConfig, + UnityConfig, VideoConfig, ) from apps.activities.errors import ( @@ -103,6 +104,10 @@ class ABTrailsValues(PublicModel): type: Literal[ResponseType.ABTRAILS] | None +class UnityValues(PublicModel): + type: Literal[ResponseType.UNITY] | None + + class _SingleSelectionValue(PublicModel): id: str | None = None text: str @@ -406,6 +411,7 @@ class PhrasalTemplateValues(PublicModel): FlankerValues, StabilityTrackerValues, ABTrailsValues, + UnityValues, PhrasalTemplateValues, ] @@ -422,6 +428,7 @@ class PhrasalTemplateValues(PublicModel): | AudioValues | AudioPlayerValues | TimeValues + | UnityValues | PhrasalTemplateValues ) @@ -477,6 +484,7 @@ def validate_none_option_flag(options): FlankerConfig, StabilityTrackerConfig, ABTrailsConfig, + UnityConfig, PhrasalTemplateConfig, ] diff --git a/src/apps/activities/tests/fixtures/activities.py b/src/apps/activities/tests/fixtures/activities.py index e89bf4d592e..8c554c18123 100644 --- a/src/apps/activities/tests/fixtures/activities.py +++ b/src/apps/activities/tests/fixtures/activities.py @@ -19,6 +19,7 @@ StabilityTrackerConfig, StimulusConfigId, StimulusConfiguration, + UnityConfig, ) from apps.shared.enums import Language @@ -609,3 +610,24 @@ def actvitiy_cst_touch_create() -> ActivityCreate: ), ], ) + + +@pytest.fixture +def activity_unity_create() -> ActivityCreate: + return ActivityCreate( + name="Unity", + description={Language.ENGLISH: "Unity"}, + is_hidden=False, + report_included_item_name="", + key=uuid.uuid4(), + items=[ + ActivityItemCreate( + question={"en": "File"}, + response_type=ResponseType.UNITY, + response_values=None, + config=UnityConfig(), + name="Unity_Item", + is_hidden=False, + ), + ], + ) diff --git a/src/apps/activities/tests/fixtures/configs.py b/src/apps/activities/tests/fixtures/configs.py index 4cf8ac15b7d..65010229630 100644 --- a/src/apps/activities/tests/fixtures/configs.py +++ b/src/apps/activities/tests/fixtures/configs.py @@ -23,6 +23,7 @@ TextConfig, TimeConfig, TimeRangeConfig, + UnityConfig, VideoConfig, ) @@ -179,3 +180,8 @@ def audio_player_config(default_config: DefaultConfig) -> AudioPlayerConfig: @pytest.fixture def phrasal_template_config(default_config: DefaultConfig) -> PhrasalTemplateConfig: return PhrasalTemplateConfig(**default_config.dict(), type=ResponseType.PHRASAL_TEMPLATE) + + +@pytest.fixture +def unity_config(default_config: DefaultConfig) -> UnityConfig: + return UnityConfig(**default_config.dict(), type=ResponseType.UNITY) diff --git a/src/apps/applets/tests/fixtures/applets.py b/src/apps/applets/tests/fixtures/applets.py index 6866729a037..611f880d7cb 100644 --- a/src/apps/applets/tests/fixtures/applets.py +++ b/src/apps/applets/tests/fixtures/applets.py @@ -337,6 +337,7 @@ async def applet_with_all_performance_tasks( activity_flanker_create: ActivityCreate, actvitiy_cst_gyroscope_create: ActivityCreate, actvitiy_cst_touch_create: ActivityCreate, + actvitiy_unity: ActivityCreate, ) -> AppletFull: data = applet_minimal_data.copy(deep=True) data.activities = [ @@ -345,6 +346,7 @@ async def applet_with_all_performance_tasks( activity_flanker_create, actvitiy_cst_gyroscope_create, actvitiy_cst_touch_create, + actvitiy_unity, ] applet = await AppletService(session, tom.id).create(data) return applet diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index dba8a1ac4db..adf07922333 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -170,6 +170,7 @@ async def test_create_applet_with_phrasal_template( ("activity_flanker_create", PerformanceTaskType.FLANKER), ("actvitiy_cst_gyroscope_create", PerformanceTaskType.GYROSCOPE), ("actvitiy_cst_touch_create", PerformanceTaskType.TOUCH), + ("activity_unity_create", PerformanceTaskType.UNITY), ), ) async def test_create_applet_with_performance_task( From ca2a65ba5b25506d2434b975e87707a25233e1e0 Mon Sep 17 00:00:00 2001 From: Rodrigo Colao Merlo Date: Wed, 11 Sep 2024 14:01:51 -0300 Subject: [PATCH 15/41] Fix: tests for Unity activity item (#1597) --- compose/fastapi/Dockerfile | 6 +----- src/apps/activities/tests/fixtures/activities.py | 2 +- src/apps/applets/tests/fixtures/applets.py | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/compose/fastapi/Dockerfile b/compose/fastapi/Dockerfile index 477d602936a..5d4aae974ff 100644 --- a/compose/fastapi/Dockerfile +++ b/compose/fastapi/Dockerfile @@ -18,7 +18,7 @@ RUN groupadd --gid $USER_GID $USERNAME \ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME -s /bin/bash # Install Python dependencies -RUN pip install --no-cache-dir pipenv==2023.11.15 +RUN pip install --no-cache-dir pipenv COPY Pipfile Pipfile.lock ./ ARG PIPENV_EXTRA_ARGS @@ -72,7 +72,3 @@ RUN apt-get update -y && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/ USER code - - - - diff --git a/src/apps/activities/tests/fixtures/activities.py b/src/apps/activities/tests/fixtures/activities.py index 8c554c18123..08c4d71ba14 100644 --- a/src/apps/activities/tests/fixtures/activities.py +++ b/src/apps/activities/tests/fixtures/activities.py @@ -625,7 +625,7 @@ def activity_unity_create() -> ActivityCreate: question={"en": "File"}, response_type=ResponseType.UNITY, response_values=None, - config=UnityConfig(), + config=UnityConfig(type=ResponseType.UNITY), name="Unity_Item", is_hidden=False, ), diff --git a/src/apps/applets/tests/fixtures/applets.py b/src/apps/applets/tests/fixtures/applets.py index 611f880d7cb..0bab6921e29 100644 --- a/src/apps/applets/tests/fixtures/applets.py +++ b/src/apps/applets/tests/fixtures/applets.py @@ -337,7 +337,7 @@ async def applet_with_all_performance_tasks( activity_flanker_create: ActivityCreate, actvitiy_cst_gyroscope_create: ActivityCreate, actvitiy_cst_touch_create: ActivityCreate, - actvitiy_unity: ActivityCreate, + activity_unity_create: ActivityCreate, ) -> AppletFull: data = applet_minimal_data.copy(deep=True) data.activities = [ @@ -346,7 +346,7 @@ async def applet_with_all_performance_tasks( activity_flanker_create, actvitiy_cst_gyroscope_create, actvitiy_cst_touch_create, - actvitiy_unity, + activity_unity_create, ] applet = await AppletService(session, tom.id).create(data) return applet From 17c3808f5058de9d24d321d21079f246c0c4182b Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Wed, 11 Sep 2024 12:14:10 -0500 Subject: [PATCH 16/41] feat: Add `severity` field to subscale lookup table (M2-7586) (#1596) This PR adds a field called `severity` to the subscale lookup table JSONB data. This allows subscale lookup tables to be saved with severity data --- src/apps/activities/domain/scores_reports.py | 1 + src/apps/activities/tests/fixtures/scores_reports.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apps/activities/domain/scores_reports.py b/src/apps/activities/domain/scores_reports.py index 55e1cf1c073..9e0bc859c5c 100644 --- a/src/apps/activities/domain/scores_reports.py +++ b/src/apps/activities/domain/scores_reports.py @@ -171,6 +171,7 @@ class SubScaleLookupTable(PublicModel): age: PositiveInt | None = None sex: str | None = Field(default=None, regex="^(M|F)$", description="M or F") optional_text: str | None = None + severity: str | None = Field(default=None, regex="^(Minimal|Mild|Moderate|Severe)$") @validator("raw_score") def validate_raw_score_lookup(cls, value): diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index 8b9a1d0c921..40b5c90e87f 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -122,6 +122,6 @@ def subscale_total_score_table() -> list[TotalScoreTable]: @pytest.fixture def subscale_lookup_table() -> list[SubScaleLookupTable]: return [ - SubScaleLookupTable(score="10", age=10, sex="M", raw_score="1", optional_text="some url"), - SubScaleLookupTable(score="20", age=10, sex="F", raw_score="2", optional_text="some url"), + SubScaleLookupTable(score="10", age=10, sex="M", raw_score="1", optional_text="some url", severity="Minimal"), + SubScaleLookupTable(score="20", age=10, sex="F", raw_score="2", optional_text="some url", severity="Mild"), ] From 633788d26c4ae882084eb49f84b7677dbabbd571 Mon Sep 17 00:00:00 2001 From: Rodrigo Colao Merlo Date: Thu, 12 Sep 2024 14:21:02 -0300 Subject: [PATCH 17/41] feature: Handle reassign an assignment in creation endpoint (M2-7461) (#1594) * chore: Update some dependencies to fix some security issues * feature: Handle reassign an assignment in creation endpoint (M2-7461) * Refatoring assignments service and crud to use upsert * chore: Move assignments tests to one file to remove duplicated fixtures --- .gitignore | 2 +- Pipfile | 123 +- Pipfile.lock | 2781 +++++++++-------- README.md | 2 +- alembic.ini | 20 +- alembic_arbitrary.ini | 18 +- compose/fastapi/entrypoint | 2 +- .../activities/crud/activity_item_history.py | 2 +- .../activity_assignments/crud/assignments.py | 61 +- src/apps/activity_assignments/db/schemas.py | 19 +- .../domain/assignments.py | 38 +- src/apps/activity_assignments/errors.py | 8 - src/apps/activity_assignments/service.py | 29 +- .../tests/test_assignments.py | 559 +++- .../tests/test_unassignments.py | 588 ---- src/apps/answers/crud/answers.py | 4 +- src/apps/applets/crud/applets.py | 2 +- src/apps/logs/crud/notification.py | 8 +- src/apps/subjects/crud/subject.py | 4 +- src/apps/users/cruds/user_device.py | 2 +- .../workspaces/crud/user_applet_access.py | 6 +- src/broker.py | 11 +- src/infrastructure/database/crud.py | 13 +- ...dd_unique_index_to_activity_assignments.py | 41 + src/infrastructure/utility/redis_client.py | 12 +- 25 files changed, 2200 insertions(+), 2155 deletions(-) delete mode 100644 src/apps/activity_assignments/tests/test_unassignments.py create mode 100644 src/infrastructure/database/migrations/versions/2024_09_11_11_56-add_unique_index_to_activity_assignments.py diff --git a/.gitignore b/.gitignore index 8bbbd056e8f..bdb7dc1d53f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ # VSCode .vscode/ - +allure-results/ ## Python stuff diff --git a/Pipfile b/Pipfile index 168771e9905..9438a2662f1 100644 --- a/Pipfile +++ b/Pipfile @@ -4,82 +4,81 @@ verify_ssl = true name = "pypi" [packages] -aioredis = "~=2.0.1" -alembic = "~=1.8.1" -asyncpg = "~=0.29.0" -boto3 = "==1.26.10" -fastapi = "==0.110.0" +redis = "==5.0.8" +alembic = "==1.13.2" +asyncpg = "==0.29.0" +boto3 = "==1.35.16" +fastapi = "==0.110.3" # The latest version of the fastapi is not taken because of the issue # with fastapi-mail that requires 0.21 < starlette < 0.22 # starlette version for those deps ==0.21.0 -fastapi-mail = "~=1.2.2" -httpx = "~=0.23" -jinja2 = "~=3.1.2" -bcrypt = "==4.0.1" -passlib = {version = "~=1.7.4", extras = ["bcrypt"]} -pyOpenSSL = "~=22.1.0" -pydantic = {extras = ["email"], version = "==1.10.15"} -python-jose = {version = "~=3.3.0", extras = ["cryptography"]} -python-multipart = "~=0.0.5" -sentry-sdk = "~=2.3" +fastapi-mail = "==1.2.9" +httpx = "==0.27.2" +jinja2 = "==3.1.4" +bcrypt = "==4.2.0" +passlib = { version = "==1.7.4", extras = ["bcrypt"] } +pyOpenSSL = "==24.2.1" +pydantic = { extras = ["email"], version = "==1.10.18" } +python-jose = { version = "==3.3.0", extras = ["cryptography"] } +python-multipart = "==0.0.9" +sentry-sdk = "~=2.13" sqlalchemy = { extras = ["asyncio"], version = "==1.4.53" } -uvicorn = {extras = ["standard"], version = "==0.29.0"} -taskiq = {extras = ["reload"], version = "==0.9.1"} -aiohttp = "==3.9.5" +uvicorn = { extras = ["standard"], version = "==0.30.6" } +taskiq = { extras = ["reload"], version = "==0.11.7" } +aiohttp = "==3.10.5" firebase-admin = "==6.5.0" -aio-pika = "==9.3.0" -azure-storage-blob = "==12.18.2" -taskiq-fastapi = "==0.3.0" -taskiq-redis = "==0.5.0" -taskiq-aio-pika = "==0.4.0" -sqlalchemy-utils = "==0.41.1" -typer = {extras = ["all"], version = "==0.9.0"} -aiofiles = "==23.2.1" -opentelemetry-api = "==1.24.0" -opentelemetry-distro = "==0.45b0" -opentelemetry-instrumentation = "==0.45b0" -opentelemetry-instrumentation-asgi = "==0.45b0" -opentelemetry-instrumentation-asyncio = "==0.45b0" -opentelemetry-instrumentation-aws-lambda = "==0.45b0" -opentelemetry-instrumentation-dbapi = "==0.45b0" -opentelemetry-instrumentation-fastapi = "==0.45b0" -opentelemetry-instrumentation-logging = "==0.45b0" -opentelemetry-instrumentation-sqlite3 = "==0.45b0" -opentelemetry-instrumentation-tortoiseorm = "==0.45b0" -opentelemetry-instrumentation-urllib = "==0.45b0" -opentelemetry-instrumentation-wsgi = "==0.45b0" -opentelemetry-propagator-aws-xray = "==1.0.1" -opentelemetry-sdk = "==1.24.0" -opentelemetry-semantic-conventions = "==0.45b0" -opentelemetry-test-utils = "==0.45b0" -opentelemetry-util-http = "==0.45b" -opentelemetry-exporter-otlp = "==1.24.0" -opentelemetry-sdk-extension-aws = "==2.0.1" -nh3 = "==0.2.17" +aio-pika = "==9.4.3" +azure-storage-blob = "==12.22.0" +taskiq-fastapi = "==0.3.2" +taskiq-redis = "==1.0.0" +taskiq-aio-pika = "==0.4.1" +sqlalchemy-utils = "==0.41.2" +typer = "==0.12.5" +aiofiles = "==24.1.0" +opentelemetry-api = "==1.27.0" +opentelemetry-sdk = "==1.27.0" +opentelemetry-exporter-otlp = "==1.27.0" +opentelemetry-distro = "==0.48b0" +opentelemetry-instrumentation = "==0.48b0" +opentelemetry-instrumentation-asgi = "==0.48b0" +opentelemetry-instrumentation-asyncio = "==0.48b0" +opentelemetry-instrumentation-dbapi = "==0.48b0" +opentelemetry-instrumentation-fastapi = "==0.48b0" +opentelemetry-instrumentation-logging = "==0.48b0" +opentelemetry-instrumentation-sqlite3 = "==0.48b0" +opentelemetry-instrumentation-tortoiseorm = "==0.48b0" +opentelemetry-instrumentation-urllib = "==0.48b0" +opentelemetry-instrumentation-wsgi = "==0.48b0" +opentelemetry-semantic-conventions = "==0.48b0" +opentelemetry-test-utils = "==0.48b0" +opentelemetry-util-http = "==0.48b0" +opentelemetry-propagator-aws-xray = "==1.0.2" +opentelemetry-sdk-extension-aws = "==2.0.2" +nh3 = "==0.2.18" pymongo = "*" [dev-packages] # Nobody knows for what its needed -ipdb = "~=0.13" -pudb = "~=2022.1" +ipdb = "==0.13.13" +pudb = "==2024.1.2" # Linters and Formatters -pre-commit = "~=2.7.1" -ruff = "=0.6.2" +pre-commit = "==3.8.0" +ruff = "==0.6.4" # Tests -allure-pytest = "*" -pydantic-factories = "~=1.17.0" -pytest = "~=7.1" +allure-pytest = "==2.13.5" +pydantic-factories = "==1.17.3" +pytest = "==8.3.3" pytest-asyncio = "~=0.19" -pytest-cov = "~=3.0" -pytest-env = "~=0.8.1" -pytest-lazy-fixture = "~=0.6" -pytest-mock = "~=3.8" +pytest-cov = "==5.0.0" +pytest-env = "==1.1.4" +pytest-lazy-fixtures = "==1.1.1" +pytest-mock = "==3.14.0" nest-asyncio = "==1.6.0" -gevent = "~=23.9" +gevent = "==24.2.1" # MyPy -mypy = "==1.10.0" +mypy = "==1.11.2" types-passlib = "==1.7.7.20240819" -types-python-dateutil = "==2.9.0.20240821" +types-python-dateutil = "==2.9.0.20240906" types-python-jose = "==3.3.4.20240106" typing-extensions = "==4.12.2" types-requests = "==2.32.0.20240712" @@ -87,7 +86,7 @@ types-pytz = "==2024.1.0.20240417" types-aiofiles = "==24.1.0.20240626" types-cachetools = "==5.5.0.20240820" # https://github.com/sqlalchemy/sqlalchemy/issues/7714 -greenlet = "~=2.0.1" +greenlet = "==3.1.0" # JSONLD deps only for dev reproschema = "*" cachetools = "==5.3.0" diff --git a/Pipfile.lock b/Pipfile.lock index b92f497f344..e41225c1e26 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5bfdc5aa73d20cef191f5f02d9883eb25c964f32736ee139f1939bc7dcb0b67f" + "sha256": "4176cbc56761e9bb9ae86678e121b87dc4381f6d4f4791bb3c92e7d66e354a3b" }, "pipfile-spec": 6, "requires": { @@ -18,121 +18,135 @@ "default": { "aio-pika": { "hashes": [ - "sha256:230c1087e089e62a590fae95b77ccda053331d3c745bca067d274d76ffceda27", - "sha256:3aeb60410403bb61c0c0483f6487a471bfcf1f2cc7b738d6f3f466b18641a8f0" + "sha256:f1423d2d5a8b7315d144efe1773763bf687ac17aa1535385982687e9e5ed49bb", + "sha256:fd2b1fce25f6ed5203ef1dd554dc03b90c9a46a64aaf758d032d78dc31e5295d" ], "index": "pypi", - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==9.3.0" + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==9.4.3" }, "aiofiles": { "hashes": [ - "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107", - "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a" + "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", + "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==23.2.1" + "markers": "python_version >= '3.8'", + "version": "==24.1.0" }, - "aiohttp": { + "aiohappyeyeballs": { "hashes": [ - "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8", - "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c", - "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475", - "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed", - "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf", - "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372", - "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81", - "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f", - "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1", - "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd", - "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a", - "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb", - "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46", - "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de", - "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78", - "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c", - "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771", - "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb", - "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430", - "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233", - "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156", - "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9", - "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59", - "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888", - "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c", - "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c", - "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da", - "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424", - "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2", - "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb", - "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8", - "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a", - "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10", - "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0", - "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09", - "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031", - "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4", - "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3", - "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa", - "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a", - "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe", - "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a", - "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2", - "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1", - "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323", - "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b", - "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b", - "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106", - "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac", - "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6", - "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832", - "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75", - "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6", - "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d", - "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72", - "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db", - "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a", - "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da", - "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678", - "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b", - "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24", - "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed", - "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f", - "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e", - "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58", - "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a", - "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342", - "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558", - "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2", - "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551", - "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595", - "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee", - "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11", - "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d", - "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7", - "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.9.5" - }, - "aioredis": { - "hashes": [ - "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6", - "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e" + "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2", + "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd" ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==2.4.0" + }, + "aiohttp": { + "hashes": [ + "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277", + "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1", + "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe", + "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb", + "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca", + "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91", + "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972", + "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a", + "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3", + "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa", + "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77", + "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b", + "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8", + "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599", + "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc", + "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf", + "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511", + "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699", + "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487", + "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987", + "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff", + "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db", + "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022", + "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce", + "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a", + "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5", + "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7", + "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820", + "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf", + "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e", + "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf", + "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5", + "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6", + "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6", + "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91", + "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3", + "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a", + "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d", + "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088", + "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc", + "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f", + "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75", + "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471", + "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e", + "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697", + "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092", + "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69", + "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3", + "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32", + "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589", + "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178", + "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92", + "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2", + "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e", + "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058", + "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857", + "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1", + "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6", + "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22", + "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0", + "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b", + "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57", + "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f", + "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e", + "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16", + "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1", + "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f", + "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6", + "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04", + "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae", + "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d", + "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b", + "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f", + "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862", + "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689", + "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c", + "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683", + "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef", + "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f", + "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12", + "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73", + "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061", + "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072", + "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11", + "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691", + "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77", + "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385", + "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172", + "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569", + "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f", + "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.10.5" }, "aiormq": { "hashes": [ - "sha256:3b93f612f56989b2757a9a7b299dd94dd3227ce28ba43e81d5fbcded6341dfab", - "sha256:f5efbfcd7d703f3c05c08d4e74cfaa66ca7199840e2969d75ad41b0810026b0a" + "sha256:5da896c8624193708f9409ffad0b20395010e2747f22aa4150593837f40aa017", + "sha256:a964ab09634be1da1f9298ce225b310859763d5cf83ef3a7eae1a6dc6bd1da1a" ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==6.7.7" + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==6.8.1" }, "aiosignal": { "hashes": [ @@ -152,12 +166,12 @@ }, "alembic": { "hashes": [ - "sha256:0a024d7f2de88d738d7395ff866997314c837be6104e90c5724350313dee4da4", - "sha256:cd0b5e45b14b706426b833f06369b9a6d5ee03f826ec3238723ce8caaf6e5ffa" + "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef", + "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.8.1" + "markers": "python_version >= '3.8'", + "version": "==1.13.2" }, "anyio": { "hashes": [ @@ -180,7 +194,7 @@ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_version < '3.11'", + "markers": "python_full_version < '3.12.0'", "version": "==4.0.3" }, "asyncpg": { @@ -249,40 +263,46 @@ }, "azure-storage-blob": { "hashes": [ - "sha256:e11935348981ffc005b848b55db25c04f2d1f90e1ee33000659906b763cf14c8", - "sha256:ffd864bf9abf33dfc72c6ef37899a19bd9d585a946a2c61e288b4420c035df3a" + "sha256:b3804bb4fe8ab1c32771fa464053da772a682c2737b19da438a3f4e5e3b3736e", + "sha256:bb7d2d824ce3f11f14a27ee7d9281289f7e072ac8311c52e3652672455b7d5e8" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==12.18.2" + "markers": "python_version >= '3.8'", + "version": "==12.22.0" }, "bcrypt": { "hashes": [ - "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", - "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", - "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", - "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", - "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", - "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", - "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", - "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", - "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", - "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", - "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", - "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", - "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", - "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", - "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", - "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", - "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", - "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", - "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", - "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", - "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" + "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", + "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", + "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", + "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", + "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", + "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170", + "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", + "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", + "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", + "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184", + "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", + "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", + "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", + "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", + "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", + "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", + "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", + "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", + "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", + "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", + "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", + "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", + "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", + "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", + "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", + "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", + "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==4.0.1" + "markers": "python_version >= '3.7'", + "version": "==4.2.0" }, "blinker": { "hashes": [ @@ -294,20 +314,20 @@ }, "boto3": { "hashes": [ - "sha256:0e2444f1f653c2fa87e6e30b3ac983cba2961a98a27dd788753a34e198c9e450", - "sha256:48e579088ec320f84266bb26434a14ab3e375456feb0f3bf043f78c485a3cee2" + "sha256:9b96c210678cf430b16b49dee87db30f46044602bb9a605a465e1900f468a43f", + "sha256:9c5b0ce4a25bb78d659478d1c552f1dbb7ff275aab3263bb41cdbef8bca28693" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.26.10" + "markers": "python_version >= '3.8'", + "version": "==1.35.16" }, "botocore": { "hashes": [ - "sha256:6f35d59e230095aed7cd747604fe248fa384bebb7d09549077892f936a8ca3df", - "sha256:988b948be685006b43c4bbd8f5c0cb93e77c66deb70561994e0c5b31b5a67210" + "sha256:0d35d03ea647b5d464c7f77bdab6fb23ae5d49752b13cf97ab84444518c7b1bd", + "sha256:a93f773ca93139529b5d36730b382dbee63ab4c7f26129aa5c84835255ca999d" ], - "markers": "python_version >= '3.7'", - "version": "==1.29.165" + "markers": "python_version >= '3.8'", + "version": "==1.35.17" }, "cachecontrol": { "hashes": [ @@ -327,84 +347,84 @@ }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "cffi": { "hashes": [ - "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", - "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab", - "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499", - "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058", - "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693", - "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb", - "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", - "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", - "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2", - "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401", - "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4", - "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b", - "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", - "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f", - "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c", - "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", - "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa", - "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", - "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", - "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2", - "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", - "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e", - "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", - "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82", - "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", - "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759", - "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc", - "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", - "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf", - "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932", - "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", - "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29", - "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206", - "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2", - "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c", - "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c", - "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", - "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a", - "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", - "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", - "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", - "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", - "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", - "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", - "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", - "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", - "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", - "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", - "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", - "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42", - "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", - "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", - "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d", - "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", - "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4", - "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", - "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b", - "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", - "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e", - "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", - "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3", - "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", - "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", - "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", - "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", - "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb", - "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91" - ], - "markers": "python_version >= '3.8'", - "version": "==1.17.0" + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.17.1" }, "charset-normalizer": { "hashes": [ @@ -510,44 +530,38 @@ "markers": "python_version >= '3.7'", "version": "==8.1.7" }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "version": "==0.4.6" - }, "cryptography": { "hashes": [ - "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd", - "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", - "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", - "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", - "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", - "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", - "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", - "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b", - "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876", - "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083", - "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6", - "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", - "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", - "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b", - "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b", - "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285", - "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9", - "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", - "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", - "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2", - "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", - "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee", - "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", - "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7", - "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353", - "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c" + "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", + "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", + "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", + "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", + "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", + "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", + "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", + "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", + "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", + "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", + "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", + "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", + "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", + "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", + "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", + "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", + "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", + "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", + "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", + "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", + "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", + "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", + "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", + "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", + "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", + "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", + "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], - "markers": "python_version >= '3.6'", - "version": "==38.0.4" + "markers": "python_version >= '3.7'", + "version": "==43.0.1" }, "deprecated": { "hashes": [ @@ -578,6 +592,7 @@ "sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda", "sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2" ], + "markers": "python_version >= '3.5'", "version": "==1.3.1" }, "exceptiongroup": { @@ -590,12 +605,12 @@ }, "fastapi": { "hashes": [ - "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3", - "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b" + "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626", + "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.110.0" + "version": "==0.110.3" }, "fastapi-mail": { "hashes": [ @@ -709,19 +724,19 @@ "grpc" ], "hashes": [ - "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125", - "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd" + "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", + "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==2.19.1" + "version": "==2.19.2" }, "google-api-python-client": { "hashes": [ - "sha256:266799082bb8301f423ec204dffbffb470b502abbf29efd1f83e644d36eb5a8f", - "sha256:a1101ac9e24356557ca22f07ff48b7f61fa5d4b4e7feeef3bda16e5dcb86350e" + "sha256:8b84dde11aaccadc127e4846f5cd932331d804ea324e353131595e3f25376e97", + "sha256:d74da1358f3f2d63daf3c6f26bd96d89652051183bc87cf10a56ceb2a70beb50" ], "markers": "python_version >= '3.7'", - "version": "==2.142.0" + "version": "==2.145.0" }, "google-auth": { "hashes": [ @@ -748,11 +763,11 @@ }, "google-cloud-firestore": { "hashes": [ - "sha256:98ff885f67ca8d1f4589235a3322591a83b7890d97763ae7549c5c2bebc3b134", - "sha256:9ea3c78f8df87e92c0be7c3fe598ab687c3093429ff9d41dbb8c433835367c40" + "sha256:3db5dd42334b9904d82b3786703a5a4b576810fb50f61b8fa83ecf4f17b7fdae", + "sha256:9a735860b692f39f93f900dd3390713ceb9b47ea82cda98360bb551f03d2b916" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==2.17.2" + "version": "==2.18.0" }, "google-cloud-storage": { "hashes": [ @@ -764,77 +779,36 @@ }, "google-crc32c": { "hashes": [ - "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a", - "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876", - "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c", - "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289", - "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298", - "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02", - "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f", - "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2", - "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a", - "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb", - "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210", - "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5", - "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee", - "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c", - "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a", - "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314", - "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd", - "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65", - "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37", - "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4", - "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13", - "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894", - "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31", - "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e", - "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709", - "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740", - "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc", - "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d", - "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c", - "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c", - "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d", - "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906", - "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61", - "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57", - "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c", - "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a", - "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438", - "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946", - "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7", - "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96", - "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091", - "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae", - "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d", - "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88", - "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2", - "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd", - "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541", - "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728", - "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178", - "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968", - "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346", - "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8", - "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93", - "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7", - "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273", - "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462", - "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94", - "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd", - "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e", - "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57", - "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b", - "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9", - "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a", - "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100", - "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325", - "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183", - "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556", - "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4" - ], - "markers": "python_version >= '3.7'", - "version": "==1.5.0" + "sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24", + "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d", + "sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e", + "sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57", + "sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2", + "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8", + "sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc", + "sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42", + "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f", + "sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa", + "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b", + "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc", + "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760", + "sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d", + "sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7", + "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d", + "sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0", + "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3", + "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3", + "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00", + "sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871", + "sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c", + "sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9", + "sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205", + "sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc", + "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d", + "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" }, "google-resumable-media": { "hashes": [ @@ -846,127 +820,135 @@ }, "googleapis-common-protos": { "hashes": [ - "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945", - "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87" + "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", + "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" ], "markers": "python_version >= '3.7'", - "version": "==1.63.2" + "version": "==1.65.0" }, "greenlet": { "hashes": [ - "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", - "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", - "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", - "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", - "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", - "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", - "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", - "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", - "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", - "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", - "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", - "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", - "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", - "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", - "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", - "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", - "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", - "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", - "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", - "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", - "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", - "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", - "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", - "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", - "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", - "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", - "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", - "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", - "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", - "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", - "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", - "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", - "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", - "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", - "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", - "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", - "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", - "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", - "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", - "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", - "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", - "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", - "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", - "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", - "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", - "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", - "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", - "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", - "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", - "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", - "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", - "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", - "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", - "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", - "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", - "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", - "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", - "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9", + "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17", + "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc", + "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637", + "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2", + "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3", + "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6", + "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b", + "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf", + "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27", + "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1", + "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc", + "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a", + "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b", + "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d", + "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28", + "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303", + "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99", + "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f", + "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7", + "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6", + "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a", + "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc", + "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0", + "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8", + "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a", + "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca", + "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b", + "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989", + "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19", + "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6", + "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484", + "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd", + "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25", + "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b", + "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910", + "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0", + "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5", + "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345", + "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6", + "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00", + "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df", + "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811", + "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca", + "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8", + "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33", + "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97", + "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0", + "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b", + "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682", + "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39", + "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64", + "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f", + "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665", + "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f", + "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc", + "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d", + "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a", + "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0", + "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09", + "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b", + "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491", + "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7", + "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954", + "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501", + "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54" ], "markers": "python_version >= '3.7'", - "version": "==3.0.3" + "version": "==3.1.0" }, "grpcio": { "hashes": [ - "sha256:0f3010bf46b2a01c9e40644cb9ed91b4b8435e5c500a275da5f9f62580e31e80", - "sha256:1c5466222470cb7fbc9cc898af1d48eefd297cb2e2f59af6d4a851c862fa90ac", - "sha256:1eb03524d0f55b965d6c86aa44e5db9e5eaa15f9ed3b164621e652e5b927f4b8", - "sha256:230cdd696751e7eb1395718cd308234749daa217bb8d128f00357dc4df102558", - "sha256:245b08f9b3c645a6a623f3ed4fa43dcfcd6ad701eb9c32511c1bb7380e8c3d23", - "sha256:296a45ea835e12a1cc35ab0c57e455346c272af7b0d178e29c67742167262b4c", - "sha256:37514b68a42e9cf24536345d3cf9e580ffd29117c158b4eeea34625200256067", - "sha256:375b58892301a5fc6ca7d7ff689c9dc9d00895f5d560604ace9f4f0573013c63", - "sha256:423ae18637cd99ddcf2e5a6851c61828c49e9b9d022d0442d979b4f230109787", - "sha256:49234580a073ce7ac490112f6c67c874cbcb27804c4525978cdb21ba7f3f193c", - "sha256:508411df1f2b7cfa05d4d7dbf3d576fe4f949cd61c03f3a6f0378c84e3d7b963", - "sha256:50cea8ce2552865b87e3dffbb85eb21e6b98d928621600c0feda2f02449cd837", - "sha256:516fdbc8e156db71a004bc431a6303bca24cfde186babe96dde7bd01e8f0cc70", - "sha256:526d4f6ca19f31b25606d5c470ecba55c0b22707b524e4de8987919e8920437d", - "sha256:53d4c6706b49e358a2a33345dbe9b6b3bb047cecd7e8c07ba383bd09349bfef8", - "sha256:5b15ef1b296c4e78f15f64fc65bf8081f8774480ffcac45642f69d9d753d9c6b", - "sha256:5e8140b39f10d7be2263afa2838112de29374c5c740eb0afd99146cb5bdbd990", - "sha256:5ea27f4ce8c0daccfdd2c7961e6ba404b6599f47c948415c4cca5728739107a3", - "sha256:5f4b3357e59dfba9140a51597287297bc638710d6a163f99ee14efc19967a821", - "sha256:5f93fc84b72bbc7b84a42f3ca9dc055fa00d2303d9803be011ebf7a10a4eb833", - "sha256:643d8d9632a688ae69661e924b862e23c83a3575b24e52917ec5bcc59543d212", - "sha256:684a4c07883cbd4ac864f0d08d927267404f5f0c76f31c85f9bbe05f2daae2f2", - "sha256:6d586a95c05c82a5354be48bb4537e1accaf2472d8eb7e9086d844cbff934482", - "sha256:6ed35bf7da3fb3b1949e32bdf47a8b5ffe0aed11722d948933bd068531cd4682", - "sha256:748452dbd5a047475d5413bdef08b0b9ceb2c0c0e249d4ee905a5fb82c6328dc", - "sha256:7bc9d823e05d63a87511fb456dcc48dc0fced86c282bf60229675e7ee7aac1a1", - "sha256:8096a922eb91bc97c839f675c3efa1257c6ef181ae1b25d3fb97f2cae4c57c01", - "sha256:832945e64176520520317b50d64ec7d79924429528d5747669b52d0bf2c7bd78", - "sha256:8fc5c710ddd51b5a0dc36ef1b6663430aa620e0ce029b87b150dafd313b978c3", - "sha256:921b8f7f25d5300d7c6837a1e0639ef145fbdbfb728e0a5db2dbccc9fc0fd891", - "sha256:9d5251578767fe44602688c851c2373b5513048ac84c21a0fe946590a8e7933d", - "sha256:a639d3866bfb5a678b5c0b92cd7ab543033ed8988854290fd86145e71731fd4c", - "sha256:aaf30c75cbaf30e561ca45f21eb1f729f0fab3f15c592c1074795ed43e3ff96f", - "sha256:ad7256f224437b2c29c2bef98ddd3130454c5b1ab1f0471fc11794cefd4dbd3d", - "sha256:ba18cfdc09312eb2eea6fa0ce5d2eec3cf345ea78f6528b2eaed6432105e0bd0", - "sha256:ba60ae3b465b3e85080ae3bfbc36fd0305ae495ab16fcf8022fc7d7a23aac846", - "sha256:bc008c6afa1e7c8df99bd9154abc4f0470d26b7730ca2521122e99e771baa8c7", - "sha256:c072f90a1f0409f827ae86266984cba65e89c5831a0726b9fc7f4b5fb940b853", - "sha256:c1ea4c528e7db6660718e4165fd1b5ac24b79a70c870a7bc0b7bdb9babab7c1e", - "sha256:c3084e590e857ba7585ae91078e4c9b6ef55aaf1dc343ce26400ba59a146eada", - "sha256:c3f6feb0dc8456d025e566709f7dd02885add99bedaac50229013069242a1bfd", - "sha256:d0439a970d65327de21c299ea0e0c2ad0987cdaf18ba5066621dea5f427f922b", - "sha256:dd614370e939f9fceeeb2915111a0795271b4c11dfb5fc0f58449bee40c726a5", - "sha256:de9e20a0acb709dcfa15a622c91f584f12c9739a79c47999f73435d2b3cc8a3b", - "sha256:e36fa838ac1d6c87198ca149cbfcc92e1af06bb8c8cd852622f8e58f33ea3324", - "sha256:e8d20308eeae15b3e182f47876f05acbdec1eebd9473a9814a44e46ec4a84c04" - ], - "markers": "python_version >= '3.8'", - "version": "==1.66.0" + "sha256:0e6c9b42ded5d02b6b1fea3a25f036a2236eeb75d0579bfd43c0018c88bf0a3e", + "sha256:161d5c535c2bdf61b95080e7f0f017a1dfcb812bf54093e71e5562b16225b4ce", + "sha256:17663598aadbedc3cacd7bbde432f541c8e07d2496564e22b214b22c7523dac8", + "sha256:1c17ebcec157cfb8dd445890a03e20caf6209a5bd4ac5b040ae9dbc59eef091d", + "sha256:292a846b92cdcd40ecca46e694997dd6b9be6c4c01a94a0dfb3fcb75d20da858", + "sha256:2ca2559692d8e7e245d456877a85ee41525f3ed425aa97eb7a70fc9a79df91a0", + "sha256:307b1d538140f19ccbd3aed7a93d8f71103c5d525f3c96f8616111614b14bf2a", + "sha256:30a1c2cf9390c894c90bbc70147f2372130ad189cffef161f0432d0157973f45", + "sha256:31a049daa428f928f21090403e5d18ea02670e3d5d172581670be006100db9ef", + "sha256:35334f9c9745add3e357e3372756fd32d925bd52c41da97f4dfdafbde0bf0ee2", + "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac", + "sha256:3885f037eb11f1cacc41f207b705f38a44b69478086f40608959bf5ad85826dd", + "sha256:4573608e23f7e091acfbe3e84ac2045680b69751d8d67685ffa193a4429fedb1", + "sha256:4825a3aa5648010842e1c9d35a082187746aa0cdbf1b7a2a930595a94fb10fce", + "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492", + "sha256:48b0d92d45ce3be2084b92fb5bae2f64c208fea8ceed7fccf6a7b524d3c4942e", + "sha256:4d813316d1a752be6f5c4360c49f55b06d4fe212d7df03253dfdae90c8a402bb", + "sha256:5dd67ed9da78e5121efc5c510f0122a972216808d6de70953a740560c572eb44", + "sha256:6f914386e52cbdeb5d2a7ce3bf1fdfacbe9d818dd81b6099a05b741aaf3848bb", + "sha256:7101db1bd4cd9b880294dec41a93fcdce465bdbb602cd8dc5bd2d6362b618759", + "sha256:7e06aa1f764ec8265b19d8f00140b8c4b6ca179a6dc67aa9413867c47e1fb04e", + "sha256:84ca1be089fb4446490dd1135828bd42a7c7f8421e74fa581611f7afdf7ab761", + "sha256:8a1e224ce6f740dbb6b24c58f885422deebd7eb724aff0671a847f8951857c26", + "sha256:97ae7edd3f3f91480e48ede5d3e7d431ad6005bfdbd65c1b56913799ec79e791", + "sha256:9c9bebc6627873ec27a70fc800f6083a13c70b23a5564788754b9ee52c5aef6c", + "sha256:a013c5fbb12bfb5f927444b477a26f1080755a931d5d362e6a9a720ca7dbae60", + "sha256:a66fe4dc35d2330c185cfbb42959f57ad36f257e0cc4557d11d9f0a3f14311df", + "sha256:a92c4f58c01c77205df6ff999faa008540475c39b835277fb8883b11cada127a", + "sha256:aa8ba945c96e73de29d25331b26f3e416e0c0f621e984a3ebdb2d0d0b596a3b3", + "sha256:b0aa03d240b5539648d996cc60438f128c7f46050989e35b25f5c18286c86734", + "sha256:b1b24c23d51a1e8790b25514157d43f0a4dce1ac12b3f0b8e9f66a5e2c4c132f", + "sha256:b7ffb8ea674d68de4cac6f57d2498fef477cef582f1fa849e9f844863af50083", + "sha256:b9feb4e5ec8dc2d15709f4d5fc367794d69277f5d680baf1910fc9915c633524", + "sha256:bff2096bdba686019fb32d2dde45b95981f0d1490e054400f70fc9a8af34b49d", + "sha256:c30aeceeaff11cd5ddbc348f37c58bcb96da8d5aa93fed78ab329de5f37a0d7a", + "sha256:c9f80f9fad93a8cf71c7f161778ba47fd730d13a343a46258065c4deb4b550c0", + "sha256:cfd349de4158d797db2bd82d2020554a121674e98fbe6b15328456b3bf2495bb", + "sha256:d0cd7050397b3609ea51727b1811e663ffda8bda39c6a5bb69525ef12414b503", + "sha256:d639c939ad7c440c7b2819a28d559179a4508783f7e5b991166f8d7a34b52815", + "sha256:e3ba04659e4fce609de2658fe4dbf7d6ed21987a94460f5f92df7579fd5d0e22", + "sha256:ecfe735e7a59e5a98208447293ff8580e9db1e890e232b8b292dc8bd15afc0d2", + "sha256:ef82d361ed5849d34cf09105d00b94b6728d289d6b9235513cb2fcc79f7c432c", + "sha256:f03a5884c56256e08fd9e262e11b5cfacf1af96e2ce78dc095d2c41ccae2c80d", + "sha256:f1fe60d0772831d96d263b53d83fb9a3d050a94b0e94b6d004a5ad111faa5b5b", + "sha256:f517fd7259fe823ef3bd21e508b653d5492e706e9f0ef82c16ce3347a8a5620c", + "sha256:fdb14bad0835914f325349ed34a51940bc2ad965142eb3090081593c6e347be9" + ], + "markers": "python_version >= '3.8'", + "version": "==1.66.1" }, "grpcio-status": { "hashes": [ @@ -1042,28 +1024,28 @@ }, "httpx": { "hashes": [ - "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", - "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5" + "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", + "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.27.0" + "version": "==0.27.2" }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.8" }, "importlib-metadata": { "hashes": [ - "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7", - "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67" + "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", + "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], "markers": "python_version >= '3.8'", - "version": "==7.0.0" + "version": "==8.4.0" }, "isodate": { "hashes": [ @@ -1181,395 +1163,396 @@ }, "msgpack": { "hashes": [ - "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", - "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", - "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", - "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", - "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", - "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", - "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", - "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", - "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", - "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", - "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", - "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", - "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", - "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", - "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", - "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", - "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", - "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", - "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", - "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", - "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", - "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", - "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", - "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", - "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", - "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", - "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", - "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", - "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", - "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", - "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", - "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", - "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", - "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", - "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", - "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", - "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", - "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", - "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", - "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", - "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", - "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", - "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", - "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", - "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", - "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", - "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", - "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", - "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", - "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", - "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", - "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", - "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", - "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", - "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", - "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" - ], - "markers": "python_version >= '3.8'", - "version": "==1.0.8" + "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", + "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", + "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", + "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", + "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", + "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", + "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", + "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", + "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", + "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", + "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", + "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", + "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", + "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", + "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", + "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", + "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", + "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", + "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", + "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", + "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", + "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", + "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", + "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", + "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", + "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", + "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", + "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", + "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", + "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", + "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", + "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", + "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", + "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", + "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", + "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", + "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", + "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", + "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", + "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", + "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", + "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", + "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", + "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", + "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", + "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", + "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", + "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", + "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", + "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", + "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", + "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", + "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", + "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", + "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", + "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", + "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", + "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", + "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", + "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", + "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", + "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", + "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", + "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" }, "multidict": { "hashes": [ - "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", - "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", - "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", - "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", - "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", - "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", - "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", - "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", - "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", - "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", - "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", - "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", - "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", - "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", - "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", - "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", - "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", - "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", - "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", - "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", - "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", - "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", - "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", - "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", - "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", - "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", - "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", - "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", - "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", - "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", - "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", - "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", - "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", - "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", - "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", - "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", - "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", - "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", - "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", - "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", - "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", - "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", - "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", - "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", - "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", - "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", - "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", - "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", - "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", - "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", - "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", - "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", - "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", - "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", - "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", - "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", - "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", - "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", - "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", - "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", - "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", - "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", - "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", - "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", - "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", - "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", - "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", - "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", - "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", - "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", - "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", - "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", - "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", - "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", - "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", - "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", - "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", - "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", - "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", - "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", - "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", - "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", - "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", - "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", - "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", - "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", - "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", - "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", - "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", - "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.5" + "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", + "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", + "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", + "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", + "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", + "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", + "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", + "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", + "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", + "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", + "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", + "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", + "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", + "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", + "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", + "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", + "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", + "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", + "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", + "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", + "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", + "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", + "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", + "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", + "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", + "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", + "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", + "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", + "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", + "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", + "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", + "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", + "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", + "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", + "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", + "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", + "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", + "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", + "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", + "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", + "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", + "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", + "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", + "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", + "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", + "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", + "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", + "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", + "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", + "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", + "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", + "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", + "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", + "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", + "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", + "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", + "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", + "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", + "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", + "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", + "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", + "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", + "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", + "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", + "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", + "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", + "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", + "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", + "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", + "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", + "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", + "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", + "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", + "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", + "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", + "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", + "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", + "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", + "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", + "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", + "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", + "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", + "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", + "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", + "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", + "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", + "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", + "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", + "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", + "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", + "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", + "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db" + ], + "markers": "python_version >= '3.8'", + "version": "==6.1.0" }, "nh3": { "hashes": [ - "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a", - "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911", - "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb", - "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a", - "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc", - "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028", - "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9", - "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3", - "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351", - "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10", - "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71", - "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f", - "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b", - "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a", - "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062", - "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a" - ], - "index": "pypi", - "version": "==0.2.17" + "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", + "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", + "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", + "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", + "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", + "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", + "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", + "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", + "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", + "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", + "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", + "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", + "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", + "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", + "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", + "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe" + ], + "index": "pypi", + "version": "==0.2.18" }, "opentelemetry-api": { "hashes": [ - "sha256:0f2c363d98d10d1ce93330015ca7fd3a65f60be64e05e30f557c61de52c80ca2", - "sha256:42719f10ce7b5a9a73b10a4baf620574fb8ad495a9cbe5c18d76b75d8689c67e" + "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7", + "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-distro": { "hashes": [ - "sha256:81864219f1ec5656973ef42074d27772b3e510c1bcfff09bf34f0c447251a867", - "sha256:e57c670d1820ccaf2857064a392e006942deed340beecba75fc7ba4b7dfe789f" + "sha256:5cb15915780ac4972583286a56683d43bd4ca95371d72f5f3f179c8b0b2ddc91", + "sha256:b2f8fce114325b020769af3b9bf503efb8af07efc190bd1b9deac7843171664a" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-exporter-otlp": { "hashes": [ - "sha256:1dfe2e4befe1f0efc193a896837740407669b2929233b406ac0a813151200cac", - "sha256:649c6e249e55cbdebe99ba2846e3851c04c9f328570328c35b3af9c094314b55" + "sha256:4a599459e623868cc95d933c301199c2367e530f089750e115599fccd67cb2a1", + "sha256:7688791cbdd951d71eb6445951d1cfbb7b6b2d7ee5948fac805d404802931145" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-exporter-otlp-proto-common": { "hashes": [ - "sha256:5d31fa1ff976cacc38be1ec4e3279a3f88435c75b38b1f7a099a1faffc302461", - "sha256:e51f2c9735054d598ad2df5d3eca830fecfb5b0bda0a2fa742c9c7718e12f641" + "sha256:159d27cf49f359e3798c4c3eb8da6ef4020e292571bd8c5604a2a573231dd5c8", + "sha256:675db7fffcb60946f3a5c43e17d1168a3307a94a930ecf8d2ea1f286f3d4f79a" ], "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-exporter-otlp-proto-grpc": { "hashes": [ - "sha256:217c6e30634f2c9797999ea9da29f7300479a94a610139b9df17433f915e7baa", - "sha256:f40d62aa30a0a43cc1657428e59fcf82ad5f7ea8fff75de0f9d9cb6f739e0a3b" + "sha256:56b5bbd5d61aab05e300d9d62a6b3c134827bbd28d0b12f2649c2da368006c9e", + "sha256:af6f72f76bcf425dfb5ad11c1a6d6eca2863b91e63575f89bb7b4b55099d968f" ], "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-exporter-otlp-proto-http": { "hashes": [ - "sha256:25af10e46fdf4cd3833175e42f4879a1255fc01655fe14c876183a2903949836", - "sha256:704c066cc96f5131881b75c0eac286cd73fc735c490b054838b4513254bd7850" + "sha256:2103479092d8eb18f61f3fbff084f67cc7f2d4a7d37e75304b8b56c1d09ebef5", + "sha256:688027575c9da42e179a69fe17e2d1eba9b14d81de8d13553a21d3114f3b4d75" ], "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-instrumentation": { "hashes": [ - "sha256:06c02e2c952c1b076e8eaedf1b82f715e2937ba7eeacab55913dd434fbcec258", - "sha256:6c47120a7970bbeb458e6a73686ee9ba84b106329a79e4a4a66761f933709c7e" + "sha256:94929685d906380743a71c3970f76b5f07476eea1834abd5dd9d17abfe23cc35", + "sha256:a69750dc4ba6a5c3eb67986a337185a25b739966d80479befe37b546fc870b44" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-asgi": { "hashes": [ - "sha256:8be1157ed62f0db24e45fdf7933c530c4338bd025c5d4af7830e903c0756021b", - "sha256:97f55620f163fd3d20323e9fd8dc3aacc826c03397213ff36b877e0f4b6b08a6" + "sha256:04c32174b23c7fa72ddfe192dad874954968a6a924608079af9952964ecdf785", + "sha256:ddb1b5fc800ae66e85a4e2eca4d9ecd66367a8c7b556169d9e7b57e10676e44d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-asyncio": { "hashes": [ - "sha256:063aaa04b83b3fe10b44b44624b8d090268ca49c0ec14c024523cd28631eda29", - "sha256:38e766ebc1132c6d58cd0eecd750a0b30e84004c48cd9065c683c217c59d0388" + "sha256:dfa8e80ba4c6ba9c17a9f1a0f7d9a8d09787a5407254efd2c9ed5ab7c478b027", + "sha256:f932eb49cb6050eb85905f600124e06868d4712b117ad7ac7c0af048f03b8fd4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" - }, - "opentelemetry-instrumentation-aws-lambda": { - "hashes": [ - "sha256:12c2b89465ade23427279c18a274c91d24c52f47827852aef9d132f51c28a180", - "sha256:ac09a0c99e56dd8e90ddadb9ec4fca6f0951d3bde682d4e50cd2f79bb9d85723" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-dbapi": { "hashes": [ - "sha256:0678578d6a98300841b8ed743724ad17a9fb3a555a7cfc0f6bb61e8441c94618", - "sha256:f6753e13548e45a9cf86f92eaa6e9cd9a8803a56376819c7f7e6ea1aa7ff984c" + "sha256:0d11a73ecbf55b11e8fbc93e9e97366958b98ccb4b691c776b32e4b20b3ce8bb", + "sha256:89821288199f4f5225e74543bf14addf9b1824b8b5f1e83ad0d9dafa844f33b0" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-fastapi": { "hashes": [ - "sha256:5a6b91e1c08a01601845fcfcfdefd0a2aecdb3c356d4a436a3210cb58c21487e", - "sha256:77d9c123a363129148f5f66d44094f3d67aaaa2b201396d94782b4a7f9ce4314" + "sha256:21a72563ea412c0b535815aeed75fc580240f1f02ebc72381cfab672648637a2", + "sha256:afeb820a59e139d3e5d96619600f11ce0187658b8ae9e3480857dd790bc024f2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-logging": { "hashes": [ - "sha256:48bfb6161a09f210c28a30295c4d217c4703e2d05d1df04fd3ab19ea30837978", - "sha256:bfaaca6862e84bb41b434178fba69afdb622f226cfdee243acb3959b65c97b48" + "sha256:529eb13eedf57d6b2f94e20e996271db2957b817b9457fe4796365d6d4238dec", + "sha256:75e5357d9b8c12071a19e1fef664dc1f430ef45874445c324ba4439a00972dc0" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-sqlite3": { "hashes": [ - "sha256:ceffafe7501c6831f466a9e2bd6f9178c632c2011f8ef7883298242dcd84dfb9", - "sha256:dc574337ffe9b8d9e4b46017d483b3d494046cb340e7a58dabb44619d27e453c" + "sha256:483b973a197890d69a25d17956d6fa66c540fc0f9f73190c93c98d2dabb3188b", + "sha256:558ff8e7b78d0647cdffb1496c5e92f72d1f459e9ae9c6d3ae9eab3517d481e5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-tortoiseorm": { "hashes": [ - "sha256:877907790aa55a61c31904db941497dedd27e103fae11a423309b1f18d6e5ecf", - "sha256:f8b619b64129d14c91e54807aa49ce333912ede7b9521f89e0b90fe65fc2c2b6" + "sha256:1f76172f75141598f374f6c2c67b127f8fcc7ea24ba9ee846c538b1a85f0d2f1", + "sha256:780d02073013b3e9d551b5c1d79e70cb079139038dbbde1c266b5bba72414bda" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-urllib": { "hashes": [ - "sha256:3effe2f1e10f77f5532a7f7f73f0719e5e5bf7e6c5e8557643f26f6749a82dc1", - "sha256:92b0843a2c087c6ab81d4ad37f2432884a7f9ec91bd21a77858805944ed13fbd" + "sha256:8115399fc786f5a46f30b158ab32a9cc77a248d421dcb0d411da657250388915", + "sha256:a9db839b4248efc9b01628dc8aa886c1269a81cec84bc375d344239037823d48" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-instrumentation-wsgi": { "hashes": [ - "sha256:7a6f9c71b25f5c5e112827540008882f6a9088447cb65745e7f2083749516663", - "sha256:f53a2a38e6582406e207d404e4c1b859b83bec11a68ad6c7366642d01c873ad0" + "sha256:1a1e752367b0df4397e0b835839225ef5c2c3c053743a261551af13434fc4d51", + "sha256:c6051124d741972090fe94b2fa302555e1e2a22e9cdda32dd39ed49a5b34e0c6" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-propagator-aws-xray": { "hashes": [ - "sha256:49267a1d72b3f04880ac75e24f9ef38fe323e2f3156c4531e0e00c71c0829c0f", - "sha256:6e8be667bbcf17c3d81d70b2a7cdec0b11257ff64d3829ffe75b810ba1b49f86" + "sha256:1c99181ee228e99bddb638a0c911a297fa21f1c3a0af951f841e79919b5f1934", + "sha256:6b2cee5479d2ef0172307b66ed2ed151f598a0fd29b3c01133ac87ca06326260" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==1.0.1" + "markers": "python_version >= '3.8'", + "version": "==1.0.2" }, "opentelemetry-proto": { "hashes": [ - "sha256:bcb80e1e78a003040db71ccf83f2ad2019273d1e0828089d183b18a1476527ce", - "sha256:ff551b8ad63c6cabb1845ce217a6709358dfaba0f75ea1fa21a61ceddc78cab8" + "sha256:33c9345d91dafd8a74fc3d7576c5a38f18b7fdf8d02983ac67485386132aedd6", + "sha256:b133873de5581a50063e1e4b29cdcf0c5e253a8c2d8dc1229add20a4c3830ace" ], "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-sdk": { "hashes": [ - "sha256:75bc0563affffa827700e0f4f4a68e1e257db0df13372344aebc6f8a64cde2e5", - "sha256:fa731e24efe832e98bcd90902085b359dcfef7d9c9c00eb5b9a18587dae3eb59" + "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d", + "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.24.0" + "version": "==1.27.0" }, "opentelemetry-sdk-extension-aws": { "hashes": [ - "sha256:dd7cf6fc0e7c8070dbe179348f2f194ca4555601b60efb7264d82fc8df53f4ba", - "sha256:f964b0598793ded268d3329c33829fad33f63a8d9299fe51bf3a743e81fd7c67" + "sha256:4c6e4b9fec01a4a9cfeac5272ce5aae6bc80e080a6bae1e52098746f53a7b32d", + "sha256:9faa9bdf480d1c5c53151dabee75735c94dbde09e4762c68ff5c7bd4aa3408f3" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==2.0.2" }, "opentelemetry-semantic-conventions": { "hashes": [ - "sha256:7c84215a44ac846bc4b8e32d5e78935c5c43482e491812a0bb8aaf87e4d92118", - "sha256:a4a6fb9a7bacd9167c082aa4681009e9acdbfa28ffb2387af50c2fef3d30c864" + "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a", + "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-test-utils": { "hashes": [ - "sha256:589891cff3e4b3637f610ddc321ea458c83af54bc99f5be564fff9e3d04e1a91", - "sha256:d22f0dd35506fedcc656f147860bc0b1848eb1d1d10ad0000ccfb99711657c3e" + "sha256:65780873544a6041700a7f225a95525330135699ba14db9732e74b45ff73b111", + "sha256:cd51052e70a3a189d0898d0afbb3420013c79f2e0c5b4932a48eb2c8c1fe7f64" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "opentelemetry-util-http": { "hashes": [ - "sha256:4ce08b6a7d52dd7c96b7705b5b4f06fdb6aa3eac1233b3b0bfef8a0cab9a92cd", - "sha256:6628868b501b3004e1860f976f410eeb3d3499e009719d818000f24ce17b6e33" + "sha256:60312015153580cc20f322e5cdc3d3ecad80a71743235bdb77716e742814623c", + "sha256:76f598af93aab50328d2a69c786beaedc8b6a7770f7a818cc307eb353debfffb" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.45b0" + "version": "==0.48b0" }, "packaging": { "hashes": [ @@ -1581,11 +1564,11 @@ }, "pamqp": { "hashes": [ - "sha256:15acef752356593ca569d13dfedc8ada9f17deeeb8cec4f7b77825e2b6c7de3e", - "sha256:22550ceb1ca50aafda65873e77e8c1c1b139fb5975e1a09860fae940cf8e970a" + "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", + "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0" ], "markers": "python_version >= '3.7'", - "version": "==3.2.1" + "version": "==3.3.0" }, "passlib": { "extras": [ @@ -1624,19 +1607,19 @@ }, "pyasn1": { "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", + "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0" + "version": "==0.6.1" }, "pyasn1-modules": { "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", + "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "version": "==0.4.1" }, "pycparser": { "hashes": [ @@ -1657,45 +1640,52 @@ "email" ], "hashes": [ - "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", - "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", - "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", - "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", - "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", - "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", - "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", - "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", - "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", - "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", - "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", - "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", - "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", - "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", - "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", - "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", - "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", - "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", - "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", - "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", - "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", - "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", - "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", - "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", - "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", - "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", - "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", - "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", - "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", - "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", - "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", - "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", - "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", - "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", - "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", - "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", + "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", + "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62", + "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c", + "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c", + "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682", + "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", + "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b", + "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03", + "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", + "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a", + "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1", + "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe", + "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", + "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", + "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518", + "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485", + "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f", + "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", + "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70", + "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86", + "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf", + "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d", + "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", + "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", + "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9", + "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", + "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab", + "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", + "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", + "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0", + "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc", + "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861", + "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", + "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", + "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3", + "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80", + "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02", + "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b", + "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", + "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", + "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", + "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f" ], "markers": "python_version >= '3.7'", - "version": "==1.10.15" + "version": "==1.10.18" }, "pygments": { "hashes": [ @@ -1775,19 +1765,20 @@ }, "pyopenssl": { "hashes": [ - "sha256:7a83b7b272dd595222d672f5ce29aa030f1fb837630ef229f62e72e395ce8968", - "sha256:b28437c9773bb6c6958628cf9c3bebe585de661dba6f63df17111966363dd15e" + "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95", + "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d" ], - "markers": "python_version >= '3.6'", - "version": "==22.1.0" + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==24.2.1" }, "pyparsing": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", + "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" ], "markers": "python_version >= '3.1'", - "version": "==3.1.2" + "version": "==3.1.4" }, "python-dateutil": { "hashes": [ @@ -1825,10 +1816,10 @@ }, "pytz": { "hashes": [ - "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", - "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319" + "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" ], - "version": "==2024.1" + "version": "==2024.2" }, "pyyaml": { "hashes": [ @@ -1893,6 +1884,7 @@ "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4" ], + "index": "pypi", "markers": "python_version >= '3.7'", "version": "==5.0.8" }, @@ -1906,10 +1898,11 @@ }, "rich": { "hashes": [ - "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", - "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" + "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", + "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" ], - "version": "==13.7.1" + "markers": "python_full_version >= '3.7.0'", + "version": "==13.8.1" }, "rsa": { "hashes": [ @@ -1921,34 +1914,35 @@ }, "s3transfer": { "hashes": [ - "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084", - "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861" + "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", + "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" ], - "markers": "python_version >= '3.7'", - "version": "==0.6.2" + "markers": "python_version >= '3.8'", + "version": "==0.10.2" }, "sentry-sdk": { "hashes": [ - "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", - "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260" + "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d", + "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.13.0" + "version": "==2.14.0" }, "setuptools": { "hashes": [ - "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e", - "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193" + "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", + "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" ], "markers": "python_version >= '3.8'", - "version": "==73.0.1" + "version": "==74.1.2" }, "shellingham": { "hashes": [ "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" ], + "markers": "python_version >= '3.7'", "version": "==1.5.4" }, "six": { @@ -2022,40 +2016,40 @@ }, "sqlalchemy-utils": { "hashes": [ - "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801", - "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74" + "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e", + "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==0.41.1" + "markers": "python_version >= '3.7'", + "version": "==0.41.2" }, "starlette": { "hashes": [ - "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044", - "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080" + "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", + "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823" ], "markers": "python_version >= '3.8'", - "version": "==0.36.3" + "version": "==0.37.2" }, "taskiq": { "extras": [ "reload" ], "hashes": [ - "sha256:a0ce2e28f76b87e2504693eca8dcd174013198a880fc209f831829dcf7fa6075", - "sha256:a3c4fab8959ac4bf3e8a7e372a677169de41128a2c756546555ece9f9669d3c9" + "sha256:15f741ca03e812724985333a327a2344ab720e7d54daaa932e1d3df7639558da", + "sha256:dcb43960de0309b10bda814ce4da3963e532d50c132687a43edffa3f60da440a" ], "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==0.9.1" + "version": "==0.11.7" }, "taskiq-aio-pika": { "hashes": [ - "sha256:3d96acf8ee5d9170bdc3f726b7a2f9d29f58e0fbc760fb13a50f40eeaa1368a3", - "sha256:9295e911ad2c808e10571adee262dcfe51344a2aebba0fbc89249e666bbe44a1" + "sha256:97b8eaba1a205c605c89d3a08aa3a43746b41d0485fa997b8316f0e219b85f7a", + "sha256:ddd9ef3315f651313581a984ea1c8df83489320d5eff3261dca729b56989ea33" ], "index": "pypi", "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==0.4.0" + "version": "==0.4.1" }, "taskiq-dependencies": { "hashes": [ @@ -2067,32 +2061,33 @@ }, "taskiq-fastapi": { "hashes": [ - "sha256:3beb52c389ebee528be6579794fde06809f3435fcd3af867f25add9b3f32576b", - "sha256:93eae839c0df9f24d5dcaef9c617b21fbe2396ce0490dacbb39c6ca37be3a997" + "sha256:85da394239801ca3b1142bf3d15ebc0a19e9e8f224f074c945b25a1c69f4e365", + "sha256:f6f24ce07d7b784211f0d7f564d18a3d2411a5d7fa4fbb154adac449c9496e49" ], "index": "pypi", "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==0.3.0" + "version": "==0.3.2" }, "taskiq-redis": { "hashes": [ - "sha256:2d591fb5c106d4908c6915a806fe28465626bce93204a92b6a2233c5ad786641", - "sha256:bda563f085eae21345a1365cb71b7a72acca73616ff979045e9d73f9ff69eaa9" + "sha256:45b2ac312a61725b4a5bdc5595f160d83986eac21eb5bc080ede5e6272d87d61", + "sha256:867949594e63402bdb8378fcc9f1e4e1a18360cb7d3da30fe06d8c33cb74822f" ], "index": "pypi", "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==0.5.0" + "version": "==1.0.0" }, "typer": { "extras": [ "all" ], "hashes": [ - "sha256:50922fd79aea2f4751a8e0408ff10d2662bd0c8bbfa84755a699f3bada2978b2", - "sha256:5d96d986a21493606a358cae4461bd8cdf83cbf33a5aa950ae629ca3b51467ee" + "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", + "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722" ], - "markers": "python_version >= '3.6'", - "version": "==0.9.0" + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.12.5" }, "typing-extensions": { "hashes": [ @@ -2112,22 +2107,22 @@ }, "urllib3": { "hashes": [ - "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", - "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.19" + "markers": "python_version >= '3.8'", + "version": "==2.2.2" }, "uvicorn": { "extras": [ "standard" ], "hashes": [ - "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", - "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0" + "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788", + "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5" ], "markers": "python_version >= '3.8'", - "version": "==0.29.0" + "version": "==0.30.6" }, "uvloop": { "hashes": [ @@ -2200,186 +2195,182 @@ }, "watchfiles": { "hashes": [ - "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d", - "sha256:02ff5d7bd066c6a7673b17c8879cd8ee903078d184802a7ee851449c43521bdd", - "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0", - "sha256:0eff099a4df36afaa0eea7a913aa64dcf2cbd4e7a4f319a73012210af4d23810", - "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef", - "sha256:11698bb2ea5e991d10f1f4f83a39a02f91e44e4bd05f01b5c1ec04c9342bf63c", - "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4", - "sha256:1733b9bc2c8098c6bdb0ff7a3d7cb211753fecb7bd99bdd6df995621ee1a574b", - "sha256:18e2de19801b0eaa4c5292a223effb7cfb43904cb742c5317a0ac686ed604765", - "sha256:1cf7f486169986c4b9d34087f08ce56a35126600b6fef3028f19ca16d5889071", - "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b", - "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5", - "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1", - "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed", - "sha256:20b423b58f5fdde704a226b598a2d78165fe29eb5621358fe57ea63f16f165c4", - "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220", - "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217", - "sha256:2537ef60596511df79b91613a5bb499b63f46f01a11a81b0a2b0dedf645d0a9c", - "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42", - "sha256:2aec5c29915caf08771d2507da3ac08e8de24a50f746eb1ed295584ba1820330", - "sha256:2dddc2487d33e92f8b6222b5fb74ae2cfde5e8e6c44e0248d24ec23befdc5366", - "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788", - "sha256:3af1b05361e1cc497bf1be654a664750ae61f5739e4bb094a2be86ec8c6db9b6", - "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1", - "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e", - "sha256:486bda18be5d25ab5d932699ceed918f68eb91f45d018b0343e3502e52866e5e", - "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0", - "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b", - "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75", - "sha256:524fcb8d59b0dbee2c9b32207084b67b2420f6431ed02c18bd191e6c575f5c48", - "sha256:532e1f2c491274d1333a814e4c5c2e8b92345d41b12dc806cf07aaff786beb66", - "sha256:556347b0abb4224c5ec688fc58214162e92a500323f50182f994f3ad33385dcb", - "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8", - "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b", - "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe", - "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1", - "sha256:6bb91fa4d0b392f0f7e27c40981e46dda9eb0fbc84162c7fb478fe115944f491", - "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8", - "sha256:7ca6b71dcc50d320c88fb2d88ecd63924934a8abc1673683a242a7ca7d39e781", - "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5", - "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f", - "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e", - "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320", - "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8", - "sha256:8ada449e22198c31fb013ae7e9add887e8d2bd2335401abd3cbc55f8c5083647", - "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c", - "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207", - "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b", - "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981", - "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b", - "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b", - "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1", - "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0", - "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e", - "sha256:a753993635eccf1ecb185dedcc69d220dab41804272f45e4aef0a67e790c3eb3", - "sha256:a8323daae27ea290ba3350c70c836c0d2b0fb47897fa3b0ca6a5375b952b90d3", - "sha256:a8f195338a5a7b50a058522b39517c50238358d9ad8284fd92943643144c0c03", - "sha256:a96ac14e184aa86dc43b8a22bb53854760a58b2966c2b41580de938e9bf26ed0", - "sha256:aafea64a3ae698695975251f4254df2225e2624185a69534e7fe70581066bc1b", - "sha256:aba037c1310dd108411d27b3d5815998ef0e83573e47d4219f45753c710f969f", - "sha256:b1f67312efa3902a8e8496bfa9824d3bec096ff83c4669ea555c6bdd213aa516", - "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3", - "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1", - "sha256:b98732ec893975455708d6fc9a6daab527fc8bbe65be354a3861f8c450a632a4", - "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9", - "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581", - "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4", - "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf", - "sha256:c846884b2e690ba62a51048a097acb6b5cd263d8bd91062cd6137e2880578472", - "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a", - "sha256:d2d42254b189a346249424fb9bb39182a19289a2409051ee432fb2926bad966a", - "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb", - "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77", - "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f", - "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab", - "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304", - "sha256:e75695cc952e825fa3e0684a7f4a302f9128721f13eedd8dbd3af2ba450932b8", - "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75", - "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6", - "sha256:ee1f5fcbf5bc33acc0be9dd31130bcba35d6d2302e4eceafafd7d9018c7755ab", - "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1", - "sha256:efadd40fca3a04063d40c4448c9303ce24dd6151dc162cfae4a2a060232ebdcb", - "sha256:f18de0f82c62c4197bea5ecf4389288ac755896aac734bd2cc44004c56e4ac47", - "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff", - "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b", - "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f", - "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee" - ], - "version": "==0.23.0" + "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a", + "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22", + "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a", + "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0", + "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", + "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1", + "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", + "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e", + "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188", + "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", + "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", + "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", + "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", + "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", + "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", + "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", + "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", + "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", + "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd", + "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91", + "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", + "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e", + "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4", + "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a", + "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", + "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1", + "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", + "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04", + "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", + "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f", + "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f", + "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", + "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735", + "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da", + "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", + "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", + "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3", + "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c", + "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", + "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361", + "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855", + "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", + "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5", + "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", + "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", + "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777", + "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b", + "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be", + "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", + "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b", + "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e", + "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b", + "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", + "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", + "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3", + "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", + "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f", + "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", + "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886", + "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571", + "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c", + "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", + "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", + "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", + "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", + "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", + "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9", + "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", + "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", + "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", + "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", + "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", + "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b", + "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c", + "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca", + "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b", + "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", + "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318", + "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", + "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430", + "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c", + "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83", + "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05" + ], + "version": "==0.24.0" }, "websockets": { "hashes": [ - "sha256:02cc9bb1a887dac0e08bf657c5d00aa3fac0d03215d35a599130c2034ae6663a", - "sha256:038e7a0f1bfafc7bf52915ab3506b7a03d1e06381e9f60440c856e8918138151", - "sha256:05c25f7b849702950b6fd0e233989bb73a0d2bc83faa3b7233313ca395205f6d", - "sha256:06b3186e97bf9a33921fa60734d5ed90f2a9b407cce8d23c7333a0984049ef61", - "sha256:06df8306c241c235075d2ae77367038e701e53bc8c1bb4f6644f4f53aa6dedd0", - "sha256:0a8f7d65358a25172db00c69bcc7df834155ee24229f560d035758fd6613111a", - "sha256:1f661a4205741bdc88ac9c2b2ec003c72cee97e4acd156eb733662ff004ba429", - "sha256:265e1f0d3f788ce8ef99dca591a1aec5263b26083ca0934467ad9a1d1181067c", - "sha256:2be1382a4daa61e2f3e2be3b3c86932a8db9d1f85297feb6e9df22f391f94452", - "sha256:2e1cf4e1eb84b4fd74a47688e8b0940c89a04ad9f6937afa43d468e71128cd68", - "sha256:337837ac788d955728b1ab01876d72b73da59819a3388e1c5e8e05c3999f1afa", - "sha256:358d37c5c431dd050ffb06b4b075505aae3f4f795d7fff9794e5ed96ce99b998", - "sha256:35c2221b539b360203f3f9ad168e527bf16d903e385068ae842c186efb13d0ea", - "sha256:3670def5d3dfd5af6f6e2b3b243ea8f1f72d8da1ef927322f0703f85c90d9603", - "sha256:372f46a0096cfda23c88f7e42349a33f8375e10912f712e6b496d3a9a557290f", - "sha256:376a43a4fd96725f13450d3d2e98f4f36c3525c562ab53d9a98dd2950dca9a8a", - "sha256:384129ad0490e06bab2b98c1da9b488acb35bb11e2464c728376c6f55f0d45f3", - "sha256:3a20cf14ba7b482c4a1924b5e061729afb89c890ca9ed44ac4127c6c5986e424", - "sha256:3e6566e79c8c7cbea75ec450f6e1828945fc5c9a4769ceb1c7b6e22470539712", - "sha256:4782ec789f059f888c1e8fdf94383d0e64b531cffebbf26dd55afd53ab487ca4", - "sha256:4d70c89e3d3b347a7c4d3c33f8d323f0584c9ceb69b82c2ef8a174ca84ea3d4a", - "sha256:516062a0a8ef5ecbfa4acbaec14b199fc070577834f9fe3d40800a99f92523ca", - "sha256:5575031472ca87302aeb2ce2c2349f4c6ea978c86a9d1289bc5d16058ad4c10a", - "sha256:587245f0704d0bb675f919898d7473e8827a6d578e5a122a21756ca44b811ec8", - "sha256:602cbd010d8c21c8475f1798b705bb18567eb189c533ab5ef568bc3033fdf417", - "sha256:6058b6be92743358885ad6dcdecb378fde4a4c74d4dd16a089d07580c75a0e80", - "sha256:63b702fb31e3f058f946ccdfa551f4d57a06f7729c369e8815eb18643099db37", - "sha256:6ad684cb7efce227d756bae3e8484f2e56aa128398753b54245efdfbd1108f2c", - "sha256:6fd757f313c13c34dae9f126d3ba4cf97175859c719e57c6a614b781c86b617e", - "sha256:7334752052532c156d28b8eaf3558137e115c7871ea82adff69b6d94a7bee273", - "sha256:788bc841d250beccff67a20a5a53a15657a60111ef9c0c0a97fbdd614fae0fe2", - "sha256:7d14901fdcf212804970c30ab9ee8f3f0212e620c7ea93079d6534863444fb4e", - "sha256:7ea9c9c7443a97ea4d84d3e4d42d0e8c4235834edae652993abcd2aff94affd7", - "sha256:81a11a1ddd5320429db47c04d35119c3e674d215173d87aaeb06ae80f6e9031f", - "sha256:851fd0afb3bc0b73f7c5b5858975d42769a5fdde5314f4ef2c106aec63100687", - "sha256:85a1f92a02f0b8c1bf02699731a70a8a74402bb3f82bee36e7768b19a8ed9709", - "sha256:89d795c1802d99a643bf689b277e8604c14b5af1bc0a31dade2cd7a678087212", - "sha256:9202c0010c78fad1041e1c5285232b6508d3633f92825687549540a70e9e5901", - "sha256:939a16849d71203628157a5e4a495da63967c744e1e32018e9b9e2689aca64d4", - "sha256:93b8c2008f372379fb6e5d2b3f7c9ec32f7b80316543fd3a5ace6610c5cde1b0", - "sha256:94c1c02721139fe9940b38d28fb15b4b782981d800d5f40f9966264fbf23dcc8", - "sha256:9895df6cd0bfe79d09bcd1dbdc03862846f26fbd93797153de954306620c1d00", - "sha256:9cc7f35dcb49a4e32db82a849fcc0714c4d4acc9d2273aded2d61f87d7f660b7", - "sha256:9ed02c604349068d46d87ef4c2012c112c791f2bec08671903a6bb2bd9c06784", - "sha256:a00e1e587c655749afb5b135d8d3edcfe84ec6db864201e40a882e64168610b3", - "sha256:a1ab8f0e0cadc5be5f3f9fa11a663957fecbf483d434762c8dfb8aa44948944a", - "sha256:a4de299c947a54fca9ce1c5fd4a08eb92ffce91961becb13bd9195f7c6e71b47", - "sha256:a7fbf2a8fe7556a8f4e68cb3e736884af7bf93653e79f6219f17ebb75e97d8f0", - "sha256:ad4fa707ff9e2ffee019e946257b5300a45137a58f41fbd9a4db8e684ab61528", - "sha256:ad818cdac37c0ad4c58e51cb4964eae4f18b43c4a83cb37170b0d90c31bd80cf", - "sha256:addf0a16e4983280efed272d8cb3b2e05f0051755372461e7d966b80a6554e16", - "sha256:ae7a519a56a714f64c3445cabde9fc2fc927e7eae44f413eae187cddd9e54178", - "sha256:b32f38bc81170fd56d0482d505b556e52bf9078b36819a8ba52624bd6667e39e", - "sha256:b5407c34776b9b77bd89a5f95eb0a34aaf91889e3f911c63f13035220eb50107", - "sha256:b7bf950234a482b7461afdb2ec99eee3548ec4d53f418c7990bb79c620476602", - "sha256:b89849171b590107f6724a7b0790736daead40926ddf47eadf998b4ff51d6414", - "sha256:bcea3eb58c09c3a31cc83b45c06d5907f02ddaf10920aaa6443975310f699b95", - "sha256:bd4ba86513430513e2aa25a441bb538f6f83734dc368a2c5d18afdd39097aa33", - "sha256:bf8eb5dca4f484a60f5327b044e842e0d7f7cdbf02ea6dc4a4f811259f1f1f0b", - "sha256:c026ee729c4ce55708a14b839ba35086dfae265fc12813b62d34ce33f4980c1c", - "sha256:c210d1460dc8d326ffdef9703c2f83269b7539a1690ad11ae04162bc1878d33d", - "sha256:c8feb8e19ef65c9994e652c5b0324abd657bedd0abeb946fb4f5163012c1e730", - "sha256:cbac2eb7ce0fac755fb983c9247c4a60c4019bcde4c0e4d167aeb17520cc7ef1", - "sha256:cbfe82a07596a044de78bb7a62519e71690c5812c26c5f1d4b877e64e4f46309", - "sha256:d3f3d2e20c442b58dbac593cb1e02bc02d149a86056cc4126d977ad902472e3b", - "sha256:d42a818e634f789350cd8fb413a3f5eec1cf0400a53d02062534c41519f5125c", - "sha256:d4b83cf7354cbbc058e97b3e545dceb75b8d9cf17fd5a19db419c319ddbaaf7a", - "sha256:d9726d2c9bd6aed8cb994d89b3910ca0079406edce3670886ec828a73e7bdd53", - "sha256:da7e501e59857e8e3e9d10586139dc196b80445a591451ca9998aafba1af5278", - "sha256:da7e918d82e7bdfc6f66d31febe1b2e28a1ca3387315f918de26f5e367f61572", - "sha256:dbbac01e80aee253d44c4f098ab3cc17c822518519e869b284cfbb8cd16cc9de", - "sha256:df5c0eff91f61b8205a6c9f7b255ff390cdb77b61c7b41f79ca10afcbb22b6cb", - "sha256:e07e76c49f39c5b45cbd7362b94f001ae209a3ea4905ae9a09cfd53b3c76373d", - "sha256:e1e10b3fbed7be4a59831d3a939900e50fcd34d93716e433d4193a4d0d1d335d", - "sha256:e39d393e0ab5b8bd01717cc26f2922026050188947ff54fe6a49dc489f7750b7", - "sha256:e5ba5e9b332267d0f2c33ede390061850f1ac3ee6cd1bdcf4c5ea33ead971966", - "sha256:e7a1963302947332c3039e3f66209ec73b1626f8a0191649e0713c391e9f5b0d", - "sha256:e7fcad070dcd9ad37a09d89a4cbc2a5e3e45080b88977c0da87b3090f9f55ead", - "sha256:eae368cac85adc4c7dc3b0d5f84ffcca609d658db6447387300478e44db70796", - "sha256:ede95125a30602b1691a4b1da88946bf27dae283cf30f22cd2cb8ca4b2e0d119", - "sha256:f5737c53eb2c8ed8f64b50d3dafd3c1dae739f78aa495a288421ac1b3de82717", - "sha256:f5f9d23fbbf96eefde836d9692670bfc89e2d159f456d499c5efcf6a6281c1af", - "sha256:f66e00e42f25ca7e91076366303e11c82572ca87cc5aae51e6e9c094f315ab41", - "sha256:f9af457ed593e35f467140d8b61d425495b127744a9d65d45a366f8678449a23", - "sha256:fa0839f35322f7b038d8adcf679e2698c3a483688cc92e3bd15ee4fb06669e9a", - "sha256:fd038bc9e2c134847f1e0ce3191797fad110756e690c2fdd9702ed34e7a43abb" - ], - "version": "==13.0" + "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026", + "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad", + "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99", + "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920", + "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448", + "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4", + "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c", + "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37", + "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3", + "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2", + "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4", + "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333", + "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543", + "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b", + "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8", + "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f", + "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c", + "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa", + "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9", + "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf", + "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc", + "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb", + "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb", + "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060", + "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f", + "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185", + "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc", + "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418", + "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab", + "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63", + "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e", + "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7", + "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36", + "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0", + "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2", + "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f", + "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f", + "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9", + "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e", + "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75", + "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b", + "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d", + "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075", + "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603", + "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d", + "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7", + "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491", + "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956", + "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376", + "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97", + "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f", + "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553", + "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0", + "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237", + "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f", + "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58", + "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980", + "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9", + "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4", + "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097", + "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df", + "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e", + "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32", + "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817", + "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501", + "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0", + "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d", + "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83", + "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c", + "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b", + "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4", + "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e", + "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870", + "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096", + "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231", + "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5", + "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae", + "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329", + "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a", + "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f", + "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2", + "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491", + "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af", + "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a", + "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462", + "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89" + ], + "version": "==13.0.1" }, "wrapt": { "hashes": [ @@ -2459,107 +2450,109 @@ }, "yarl": { "hashes": [ - "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", - "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", - "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", - "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", - "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", - "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", - "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", - "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", - "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", - "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", - "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", - "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", - "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", - "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", - "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", - "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", - "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", - "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", - "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", - "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", - "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", - "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", - "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", - "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", - "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", - "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", - "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", - "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", - "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", - "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", - "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", - "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", - "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", - "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", - "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", - "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", - "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", - "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", - "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", - "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", - "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", - "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", - "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", - "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", - "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", - "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", - "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", - "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", - "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", - "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", - "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", - "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", - "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", - "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", - "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", - "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", - "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", - "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", - "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", - "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", - "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", - "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", - "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", - "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", - "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", - "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", - "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", - "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", - "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", - "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", - "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", - "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", - "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", - "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", - "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", - "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", - "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", - "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", - "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", - "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", - "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", - "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", - "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", - "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", - "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", - "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", - "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", - "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", - "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", - "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.4" + "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", + "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", + "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", + "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", + "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", + "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", + "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", + "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", + "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", + "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", + "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", + "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", + "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", + "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", + "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", + "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", + "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", + "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", + "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", + "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", + "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", + "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", + "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", + "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", + "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", + "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", + "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", + "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", + "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", + "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", + "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", + "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", + "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", + "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", + "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", + "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", + "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", + "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", + "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", + "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", + "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", + "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", + "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", + "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", + "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", + "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", + "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", + "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", + "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", + "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", + "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", + "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", + "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", + "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", + "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", + "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", + "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", + "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", + "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", + "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", + "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", + "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", + "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", + "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", + "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", + "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", + "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", + "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", + "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", + "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", + "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", + "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", + "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", + "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", + "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", + "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", + "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", + "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", + "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", + "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", + "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", + "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", + "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", + "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", + "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", + "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", + "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", + "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", + "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", + "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", + "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", + "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" + ], + "markers": "python_version >= '3.8'", + "version": "==1.11.1" }, "zipp": { "hashes": [ - "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", - "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d" + "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", + "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" ], "markers": "python_version >= '3.8'", - "version": "==3.20.0" + "version": "==3.20.1" } }, "develop": { @@ -2604,19 +2597,19 @@ }, "cattrs": { "hashes": [ - "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108", - "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f" + "sha256:16e94a13f9aaf6438bd5be5df521e072b1b00481b4cf807bcb1acbd49f814c08", + "sha256:ec8ce8fdc725de9d07547cd616f968670687c6fa7a2e263b088370c46d834d97" ], "markers": "python_version >= '3.8'", - "version": "==23.2.3" + "version": "==24.1.1" }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "cfgv": { "hashes": [ @@ -2851,27 +2844,27 @@ }, "executing": { "hashes": [ - "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", - "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc" + "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", + "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab" ], - "markers": "python_version >= '3.5'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "faker": { "hashes": [ - "sha256:1c44d4bdcad7237516c9a829b6a0bcb031c6a4cb0506207c480c79f74d8922bf", - "sha256:4ce108fc96053bbba3abf848e3a2885f05faa938deb987f97e4420deaec541c4" + "sha256:4294d169255a045990720d6f3fa4134b764a4cdf46ef0d3c7553d2506f1adaa1", + "sha256:e59c01d1e8b8e20a83255ab8232c143cb2af3b4f5ab6a3f5ce495f385ad8ab4c" ], "markers": "python_version >= '3.8'", - "version": "==27.4.0" + "version": "==28.4.1" }, "filelock": { "hashes": [ - "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", - "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7" + "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec", + "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609" ], "markers": "python_version >= '3.8'", - "version": "==3.15.4" + "version": "==3.16.0" }, "frozendict": { "hashes": [ @@ -2914,114 +2907,123 @@ }, "gevent": { "hashes": [ - "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a", - "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2", - "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535", - "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e", - "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653", - "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1", - "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c", - "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648", - "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599", - "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea", - "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6", - "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f", - "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9", - "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e", - "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34", - "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397", - "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507", - "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b", - "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd", - "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe", - "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a", - "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b", - "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771", - "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e", - "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69", - "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a", - "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011", - "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7", - "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71", - "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5", - "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae", - "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7", - "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39", - "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d", - "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599", - "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07", - "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904", - "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a", - "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543", - "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==23.9.1" + "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5", + "sha256:117e5837bc74a1673605fb53f8bfe22feb6e5afa411f524c835b2ddf768db0de", + "sha256:141a2b24ad14f7b9576965c0c84927fc85f824a9bb19f6ec1e61e845d87c9cd8", + "sha256:14532a67f7cb29fb055a0e9b39f16b88ed22c66b96641df8c04bdc38c26b9ea5", + "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc", + "sha256:2955eea9c44c842c626feebf4459c42ce168685aa99594e049d03bedf53c2800", + "sha256:2ae3a25ecce0a5b0cd0808ab716bfca180230112bb4bc89b46ae0061d62d4afe", + "sha256:2e9ac06f225b696cdedbb22f9e805e2dd87bf82e8fa5e17756f94e88a9d37cf7", + "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9", + "sha256:3adfb96637f44010be8abd1b5e73b5070f851b817a0b182e601202f20fa06533", + "sha256:3d5325ccfadfd3dcf72ff88a92fb8fc0b56cacc7225f0f4b6dcf186c1a6eeabc", + "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056", + "sha256:44098038d5e2749b0784aabb27f1fcbb3f43edebedf64d0af0d26955611be8d6", + "sha256:5a1df555431f5cd5cc189a6ee3544d24f8c52f2529134685f1e878c4972ab026", + "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40", + "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07", + "sha256:782a771424fe74bc7e75c228a1da671578c2ba4ddb2ca09b8f959abdf787331e", + "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be", + "sha256:7b00f8c9065de3ad226f7979154a7b27f3b9151c8055c162332369262fc025d8", + "sha256:8f4b8e777d39013595a7740b4463e61b1cfe5f462f1b609b28fbc1e4c4ff01e5", + "sha256:90cbac1ec05b305a1b90ede61ef73126afdeb5a804ae04480d6da12c56378df1", + "sha256:918cdf8751b24986f915d743225ad6b702f83e1106e08a63b736e3a4c6ead789", + "sha256:9202f22ef811053077d01f43cc02b4aaf4472792f9fd0f5081b0b05c926cca19", + "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5", + "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7", + "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388", + "sha256:a7ceb59986456ce851160867ce4929edaffbd2f069ae25717150199f8e1548b8", + "sha256:b9913c45d1be52d7a5db0c63977eebb51f68a2d5e6fd922d1d9b5e5fd758cc98", + "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3", + "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7", + "sha256:ca80b121bbec76d7794fcb45e65a7eca660a76cc1a104ed439cdbd7df5f0b060", + "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d", + "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661", + "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c", + "sha256:dd23df885318391856415e20acfd51a985cba6919f0be78ed89f5db9ff3a31cb", + "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f", + "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91", + "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0", + "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f", + "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836", + "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.2.1" }, "greenlet": { "hashes": [ - "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", - "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", - "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", - "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", - "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", - "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", - "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", - "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", - "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", - "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", - "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", - "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", - "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", - "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", - "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", - "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", - "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", - "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", - "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", - "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", - "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", - "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", - "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", - "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", - "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", - "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", - "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", - "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", - "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", - "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", - "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", - "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", - "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", - "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", - "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", - "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", - "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", - "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", - "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", - "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", - "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", - "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", - "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", - "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", - "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", - "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", - "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", - "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", - "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", - "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", - "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", - "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", - "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", - "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", - "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", - "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", - "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", - "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9", + "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17", + "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc", + "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637", + "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2", + "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3", + "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6", + "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b", + "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf", + "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27", + "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1", + "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc", + "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a", + "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b", + "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d", + "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28", + "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303", + "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99", + "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f", + "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7", + "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6", + "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a", + "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc", + "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0", + "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8", + "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a", + "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca", + "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b", + "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989", + "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19", + "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6", + "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484", + "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd", + "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25", + "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b", + "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910", + "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0", + "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5", + "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345", + "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6", + "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00", + "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df", + "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811", + "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca", + "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8", + "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33", + "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97", + "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0", + "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b", + "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682", + "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39", + "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64", + "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f", + "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665", + "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f", + "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc", + "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d", + "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a", + "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0", + "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09", + "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b", + "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491", + "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7", + "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954", + "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501", + "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54" ], "markers": "python_version >= '3.7'", - "version": "==3.0.3" + "version": "==3.1.0" }, "html5lib": { "hashes": [ @@ -3041,19 +3043,19 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.8" }, "importlib-metadata": { "hashes": [ - "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7", - "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67" + "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", + "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" ], "markers": "python_version >= '3.8'", - "version": "==7.0.0" + "version": "==8.4.0" }, "iniconfig": { "hashes": [ @@ -3074,11 +3076,11 @@ }, "ipython": { "hashes": [ - "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c", - "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff" + "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e", + "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c" ], "markers": "python_version < '3.11' and python_version >= '3.7'", - "version": "==8.26.0" + "version": "==8.27.0" }, "isodate": { "hashes": [ @@ -3249,37 +3251,37 @@ }, "mypy": { "hashes": [ - "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061", - "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99", - "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de", - "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a", - "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9", - "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec", - "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1", - "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131", - "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f", - "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821", - "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5", - "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee", - "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e", - "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746", - "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2", - "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0", - "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b", - "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53", - "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30", - "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda", - "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051", - "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2", - "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7", - "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee", - "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727", - "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976", - "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.10.0" + "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", + "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", + "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", + "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", + "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", + "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", + "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", + "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", + "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", + "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", + "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", + "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", + "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", + "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", + "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", + "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", + "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", + "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", + "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", + "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", + "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", + "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", + "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", + "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", + "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", + "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", + "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.11.2" }, "mypy-extensions": { "hashes": [ @@ -3339,11 +3341,11 @@ }, "platformdirs": { "hashes": [ - "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", + "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617" ], "markers": "python_version >= '3.8'", - "version": "==4.2.2" + "version": "==4.3.2" }, "pluggy": { "hashes": [ @@ -3355,12 +3357,12 @@ }, "pre-commit": { "hashes": [ - "sha256:810aef2a2ba4f31eed1941fc270e72696a1ad5590b9751839c90807d0fff6b9a", - "sha256:c54fd3e574565fe128ecc5e7d2f91279772ddb03f8729645fa812fe809084a70" + "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", + "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f" ], "index": "pypi", - "markers": "python_full_version >= '3.6.1'", - "version": "==2.7.1" + "markers": "python_version >= '3.9'", + "version": "==3.8.0" }, "prettytable": { "hashes": [ @@ -3387,11 +3389,12 @@ }, "pudb": { "hashes": [ - "sha256:58e83ada9e19ffe92c1fdc78ae5458ef91aeb892a5b8f0e7379e6fa61e0e664a" + "sha256:4726c288d9f57845b8dba706c70eb6faaddff9d86e5208eda82216ef5e79cc2e", + "sha256:adc9b00042ba8367117df0a6c0dc62fa9609abd21c3bf8e5b73d620907c5b43e" ], "index": "pypi", - "markers": "python_version ~= '3.6'", - "version": "==2022.1.3" + "markers": "python_version ~= '3.8'", + "version": "==2024.1.2" }, "pure-eval": { "hashes": [ @@ -3405,45 +3408,52 @@ "email" ], "hashes": [ - "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", - "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", - "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", - "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", - "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", - "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", - "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", - "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", - "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", - "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", - "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", - "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", - "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", - "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", - "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", - "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", - "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", - "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", - "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", - "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", - "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", - "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", - "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", - "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", - "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", - "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", - "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", - "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", - "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", - "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", - "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", - "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", - "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", - "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", - "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", - "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", + "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", + "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62", + "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c", + "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c", + "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682", + "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", + "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b", + "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03", + "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", + "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a", + "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1", + "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe", + "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", + "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", + "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518", + "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485", + "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f", + "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", + "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70", + "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86", + "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf", + "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d", + "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", + "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", + "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9", + "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", + "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab", + "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", + "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", + "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0", + "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc", + "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861", + "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", + "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", + "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3", + "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80", + "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02", + "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b", + "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", + "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", + "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", + "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f" ], "markers": "python_version >= '3.7'", - "version": "==1.10.15" + "version": "==1.10.18" }, "pydantic-factories": { "hashes": [ @@ -3472,11 +3482,11 @@ }, "pyparsing": { "hashes": [ - "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad", - "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742" + "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", + "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" ], "markers": "python_version >= '3.1'", - "version": "==3.1.2" + "version": "==3.1.4" }, "pyshacl": { "hashes": [ @@ -3488,47 +3498,48 @@ }, "pytest": { "hashes": [ - "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", - "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==7.4.4" + "markers": "python_version >= '3.8'", + "version": "==8.3.3" }, "pytest-asyncio": { "hashes": [ - "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", - "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3" + "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", + "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==0.23.8" + "version": "==0.24.0" }, "pytest-cov": { "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" + "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", + "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==3.0.0" + "markers": "python_version >= '3.8'", + "version": "==5.0.0" }, "pytest-env": { "hashes": [ - "sha256:5e533273f4d9e6a41c3a3120e0c7944aae5674fa773b329f00a5eb1f23c53a38", - "sha256:baed9b3b6bae77bd75b9238e0ed1ee6903a42806ae9d6aeffb8754cd5584d4ff" + "sha256:86653658da8f11c6844975db955746c458a9c09f1e64957603161e2ff93f5133", + "sha256:a4212056d4d440febef311a98fdca56c31256d58fb453d103cba4e8a532b721d" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==0.8.2" + "markers": "python_version >= '3.8'", + "version": "==1.1.4" }, - "pytest-lazy-fixture": { + "pytest-lazy-fixtures": { "hashes": [ - "sha256:0e7d0c7f74ba33e6e80905e9bfd81f9d15ef9a790de97993e34213deb5ad10ac", - "sha256:e0b379f38299ff27a653f03eaa69b08a6fd4484e46fd1c9907d984b9f9daeda6" + "sha256:0c561f0d29eea5b55cf29b9264a3241999ffdb74c6b6e8c4ccc0bd2c934d01ed", + "sha256:a4b396a361faf56c6305535fd0175ce82902ca7cf668c4d812a25ed2bcde8183" ], "index": "pypi", - "version": "==0.6.3" + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==1.1.1" }, "pytest-mock": { "hashes": [ @@ -3639,36 +3650,36 @@ }, "ruff": { "hashes": [ - "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534", - "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23", - "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570", - "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", - "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", - "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66", - "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b", - "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158", - "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a", - "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", - "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56", - "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1", - "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1", - "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c", - "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8", - "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d", - "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", - "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9" + "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6", + "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa", + "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6", + "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1", + "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e", + "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58", + "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa", + "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc", + "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d", + "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408", + "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212", + "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14", + "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60", + "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818", + "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258", + "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f", + "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617", + "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.6.2" + "version": "==0.6.4" }, "setuptools": { "hashes": [ - "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e", - "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193" + "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", + "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" ], "markers": "python_version >= '3.8'", - "version": "==73.0.1" + "version": "==74.1.2" }, "six": { "hashes": [ @@ -3685,14 +3696,6 @@ ], "version": "==0.6.3" }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, "tomli": { "hashes": [ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", @@ -3738,20 +3741,20 @@ }, "types-pyasn1": { "hashes": [ - "sha256:5d54dcb33f69dd269071ca098e923ac20c5f03c814631fa7f3ed9ee035a5da3a", - "sha256:848d01e7313c200acc035a8b3d377fe7b2aecbe77f2be49eb160a7f82835aaaf" + "sha256:2cee8bfddf06d88e25ea122f7fb3b9d127a9f4a532e4d1415ef99ee0c2f902ea", + "sha256:40873dbd960e8ddb4bebda5195d3aa5e9f9c7c19d461af2f6c8540aa97e8055d" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0.20240402" + "version": "==0.6.0.20240824" }, "types-python-dateutil": { "hashes": [ - "sha256:9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98", - "sha256:f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57" + "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6", + "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.9.0.20240821" + "version": "==2.9.0.20240906" }, "types-python-jose": { "hashes": [ @@ -3798,11 +3801,11 @@ }, "urllib3": { "hashes": [ - "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", - "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429" + "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", + "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.19" + "markers": "python_version >= '3.8'", + "version": "==2.2.2" }, "urwid": { "hashes": [ @@ -3820,11 +3823,11 @@ }, "virtualenv": { "hashes": [ - "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a", - "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589" + "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55", + "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c" ], "markers": "python_version >= '3.7'", - "version": "==20.26.3" + "version": "==20.26.4" }, "wcwidth": { "hashes": [ @@ -3842,11 +3845,11 @@ }, "zipp": { "hashes": [ - "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", - "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d" + "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", + "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" ], "markers": "python_version >= '3.8'", - "version": "==3.20.0" + "version": "==3.20.1" }, "zope.event": { "hashes": [ @@ -3858,43 +3861,43 @@ }, "zope.interface": { "hashes": [ - "sha256:03bd5c0db82237bbc47833a8b25f1cc090646e212f86b601903d79d7e6b37031", - "sha256:03f1452d5d1f279184d5bdb663a3dc39902d9320eceb63276240791e849054b6", - "sha256:10ebac566dd0cec66f942dc759d46a994a2b3ba7179420f0e2130f88f8a5f400", - "sha256:192b7a792e3145ed880ff6b1a206fdb783697cfdb4915083bfca7065ec845e60", - "sha256:19c829d52e921b9fe0b2c0c6a8f9a2508c49678ee1be598f87d143335b6a35dc", - "sha256:3f3495462bc0438b76536a0e10d765b168ae636092082531b88340dc40dcd118", - "sha256:3f52050c6a10d4a039ec6f2c58e5b3ade5cc570d16cf9d102711e6b8413c90e6", - "sha256:400d06c9ec8dbcc96f56e79376297e7be07a315605c9a2208720da263d44d76f", - "sha256:4ec212037becf6d2f705b7ed4538d56980b1e7bba237df0d8995cbbed29961dc", - "sha256:51d5713e8e38f2d3ec26e0dfdca398ed0c20abda2eb49ffc15a15a23eb8e5f6d", - "sha256:52f5253cca1b35eaeefa51abd366b87f48f8714097c99b131ba61f3fdbbb58e7", - "sha256:5566fd9271c89ad03d81b0831c37d46ae5e2ed211122c998637130159a120cf1", - "sha256:55bbcc74dc0c7ab489c315c28b61d7a1d03cf938cc99cc58092eb065f120c3a5", - "sha256:696c2a381fc7876b3056711717dba5eddd07c2c9e5ccd50da54029a1293b6e43", - "sha256:6ba4b3638d014918b918aa90a9c8370bd74a03abf8fcf9deb353b3a461a59a84", - "sha256:7039e624bcb820f77cc2ff3d1adcce531932990eee16121077eb51d9c76b6c14", - "sha256:88d108d004e0df25224de77ce349a7e73494ea2cb194031f7c9687e68a88ec9b", - "sha256:8c1dff87b30fd150c61367d0e2cdc49bb55f8b9fd2a303560bbc24b951573ae1", - "sha256:9a8195b99e650e6f329ce4e5eb22d448bdfef0406404080812bc96e2a05674cb", - "sha256:af0b33f04677b57843d529b9257a475d2865403300b48c67654c40abac2f9f24", - "sha256:b419f2144e1762ab845f20316f1df36b15431f2622ebae8a6d5f7e8e712b413c", - "sha256:b59deb0ddc7b431e41d720c00f99d68b52cb9bd1d5605a085dc18f502fe9c47f", - "sha256:bc0615351221926a36a0fbcb2520fb52e0b23e8c22a43754d9cb8f21358c33c0", - "sha256:c203d82069ba31e1f3bc7ba530b2461ec86366cd4bfc9b95ec6ce58b1b559c34", - "sha256:ce6cbb852fb8f2f9bb7b9cdca44e2e37bce783b5f4c167ff82cb5f5128163c8f", - "sha256:d33cb526efdc235a2531433fc1287fcb80d807d5b401f9b801b78bf22df560dd", - "sha256:da0cef4d7e3f19c3bd1d71658d6900321af0492fee36ec01b550a10924cffb9c", - "sha256:da21e7eec49252df34d426c2ee9cf0361c923026d37c24728b0fa4cc0599fd03", - "sha256:ea8d51e5eb29e57d34744369cd08267637aa5a0fefc9b5d33775ab7ff2ebf2e3", - "sha256:ec4e87e6fdc511a535254daa122c20e11959ce043b4e3425494b237692a34f1c", - "sha256:f0f5fda7cbf890371a59ab1d06512da4f2c89a6ea194e595808123c863c38eff", - "sha256:f32ca483e6ade23c7caaee9d5ee5d550cf4146e9b68d2fb6c68bac183aa41c37", - "sha256:f749ca804648d00eda62fe1098f229b082dfca930d8bad8386e572a6eafa7525", - "sha256:f89a420cf5a6f2aa7849dd59e1ff0e477f562d97cf8d6a1ee03461e1eec39887" - ], - "markers": "python_version >= '3.8'", - "version": "==7.0.1" + "sha256:01e6e58078ad2799130c14a1d34ec89044ada0e1495329d72ee0407b9ae5100d", + "sha256:064ade95cb54c840647205987c7b557f75d2b2f7d1a84bfab4cf81822ef6e7d1", + "sha256:11fa1382c3efb34abf16becff8cb214b0b2e3144057c90611621f2d186b7e1b7", + "sha256:1bee1b722077d08721005e8da493ef3adf0b7908e0cd85cc7dc836ac117d6f32", + "sha256:1eeeb92cb7d95c45e726e3c1afe7707919370addae7ed14f614e22217a536958", + "sha256:21a207c6b2c58def5011768140861a73f5240f4f39800625072ba84e76c9da0b", + "sha256:2545d6d7aac425d528cd9bf0d9e55fcd47ab7fd15f41a64b1c4bf4c6b24946dc", + "sha256:2c4316a30e216f51acbd9fb318aa5af2e362b716596d82cbb92f9101c8f8d2e7", + "sha256:35062d93bc49bd9b191331c897a96155ffdad10744ab812485b6bad5b588d7e4", + "sha256:382d31d1e68877061daaa6499468e9eb38eb7625d4369b1615ac08d3860fe896", + "sha256:3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d", + "sha256:3d4b91821305c8d8f6e6207639abcbdaf186db682e521af7855d0bea3047c8ca", + "sha256:3de1d553ce72868b77a7e9d598c9bff6d3816ad2b4cc81c04f9d8914603814f3", + "sha256:3fcdc76d0cde1c09c37b7c6b0f8beba2d857d8417b055d4f47df9c34ec518bdd", + "sha256:5112c530fa8aa2108a3196b9c2f078f5738c1c37cfc716970edc0df0414acda8", + "sha256:53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386", + "sha256:6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58", + "sha256:6d04b11ea47c9c369d66340dbe51e9031df2a0de97d68f442305ed7625ad6493", + "sha256:6dd647fcd765030638577fe6984284e0ebba1a1008244c8a38824be096e37fe3", + "sha256:799ef7a444aebbad5a145c3b34bff012b54453cddbde3332d47ca07225792ea4", + "sha256:7d92920416f31786bc1b2f34cc4fc4263a35a407425319572cbf96b51e835cd3", + "sha256:7e0c151a6c204f3830237c59ee4770cc346868a7a1af6925e5e38650141a7f05", + "sha256:84f8794bd59ca7d09d8fce43ae1b571be22f52748169d01a13d3ece8394d8b5b", + "sha256:95e5913ec718010dc0e7c215d79a9683b4990e7026828eedfda5268e74e73e11", + "sha256:9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b", + "sha256:ab985c566a99cc5f73bc2741d93f1ed24a2cc9da3890144d37b9582965aff996", + "sha256:af94e429f9d57b36e71ef4e6865182090648aada0cb2d397ae2b3f7fc478493a", + "sha256:c96b3e6b0d4f6ddfec4e947130ec30bd2c7b19db6aa633777e46c8eecf1d6afd", + "sha256:cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1", + "sha256:d3b7ce6d46fb0e60897d62d1ff370790ce50a57d40a651db91a3dde74f73b738", + "sha256:d976fa7b5faf5396eb18ce6c132c98e05504b52b60784e3401f4ef0b2e66709b", + "sha256:db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca", + "sha256:ecd32f30f40bfd8511b17666895831a51b532e93fc106bfa97f366589d3e4e0e", + "sha256:f418c88f09c3ba159b95a9d1cfcdbe58f208443abb1f3109f4b9b12fd60b187c" + ], + "markers": "python_version >= '3.8'", + "version": "==7.0.3" } } } diff --git a/README.md b/README.md index 51043a9fc61..2a0903a7af0 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ And ### Prerequisites -- Python 3.10 - This project requires Python 3.10 as `aioredis` is [incompatible with 3.11+](https://github.com/aio-libs-abandoned/aioredis-py/issues/1409) +- Python 3.10 - [Docker](https://docs.docker.com/get-docker/) #### Recommended Extras diff --git a/alembic.ini b/alembic.ini index f8d03e68ffc..47bc9b45709 100644 --- a/alembic.ini +++ b/alembic.ini @@ -58,15 +58,17 @@ sqlalchemy.url = driver://user:pass@localhost/dbname [post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run -# on newly generated revision scripts. See the documentation for further -# detail and examples - -# format using "black" - use the console_scripts runner, against the "black" entrypoint -hooks = black -black.type = console_scripts -black.entrypoint = black -black.options = -l 79 REVISION_SCRIPT_FILENAME +hooks = ruff_format, ruff + +# lint with attempts to fix using "ruff" +ruff.type = exec +ruff.executable = ruff +ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# format using "ruff" - use the exec runner, execute a binary +ruff_format.type = exec +ruff_format.executable = ruff +ruff_format.options = format REVISION_SCRIPT_FILENAME # Logging configuration [loggers] diff --git a/alembic_arbitrary.ini b/alembic_arbitrary.ini index 67193dde43e..3140169bb59 100644 --- a/alembic_arbitrary.ini +++ b/alembic_arbitrary.ini @@ -3,10 +3,18 @@ script_location = ./src/infrastructure/database/migrations_arbitrary sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/test_arbitrary [post_write_hooks] -hooks = black -black.type = console_scripts -black.entrypoint = black -black.options = -l 79 REVISION_SCRIPT_FILENAME +hooks = ruff_format, ruff + +# lint with attempts to fix using "ruff" +ruff.type = exec +ruff.executable = ruff +ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# format using "ruff" - use the exec runner, execute a binary +ruff_format.type = exec +ruff_format.executable = ruff +ruff_format.options = format REVISION_SCRIPT_FILENAME + [loggers] keys = root,sqlalchemy,alembic @@ -40,4 +48,4 @@ formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S \ No newline at end of file +datefmt = %H:%M:%S diff --git a/compose/fastapi/entrypoint b/compose/fastapi/entrypoint index 38aedefe954..5cebdf7ade7 100644 --- a/compose/fastapi/entrypoint +++ b/compose/fastapi/entrypoint @@ -10,7 +10,7 @@ import asyncio import sys import os -from aioredis import from_url +from redis.asyncio import from_url from config import settings from infrastructure.logger import logger diff --git a/src/apps/activities/crud/activity_item_history.py b/src/apps/activities/crud/activity_item_history.py index 6da5f8cad50..b53695ecbe8 100644 --- a/src/apps/activities/crud/activity_item_history.py +++ b/src/apps/activities/crud/activity_item_history.py @@ -72,7 +72,7 @@ async def get_applets_assessments( query = query.where(ActivitySchema.applet_id == applet_id) query = query.where( ActivityHistorySchema.is_reviewable == True, # noqa: E712 - ActivityHistorySchema.id_version.in_(subquery), + ActivityHistorySchema.id_version.in_(select(subquery)), ) query = query.order_by(ActivityItemHistorySchema.order.asc()) db_result = await self._execute(query) diff --git a/src/apps/activity_assignments/crud/assignments.py b/src/apps/activity_assignments/crud/assignments.py index 9a0946d8699..e42a4d77d60 100644 --- a/src/apps/activity_assignments/crud/assignments.py +++ b/src/apps/activity_assignments/crud/assignments.py @@ -1,6 +1,8 @@ +import datetime import uuid from sqlalchemy import and_, or_, select, tuple_, update +from sqlalchemy.dialects.postgresql import insert from sqlalchemy.orm import Query, aliased from apps.activities.db.schemas import ActivitySchema @@ -71,9 +73,12 @@ async def create_many(self, schemas: list[ActivityAssigmentSchema]) -> list[Acti bulk insertion operations. - Ensures that all new assignments are created in a single database transaction. """ + if len(schemas) == 0: + return [] + return await self._create_many(schemas) - async def already_exists(self, schema: ActivityAssigmentSchema) -> bool: + async def already_exists(self, schema: ActivityAssigmentSchema) -> ActivityAssigmentSchema: """ Checks if an activity assignment already exists in the database. @@ -102,17 +107,16 @@ async def already_exists(self, schema: ActivityAssigmentSchema) -> bool: query = query.where(ActivityAssigmentSchema.respondent_subject_id == schema.respondent_subject_id) query = query.where(ActivityAssigmentSchema.target_subject_id == schema.target_subject_id) query = query.where(ActivityAssigmentSchema.activity_flow_id == schema.activity_flow_id) - query = query.where(ActivityAssigmentSchema.soft_exists()) - query = query.exists() - db_result = await self._execute(select(query)) - return db_result.scalars().first() or False - async def unassign_many( + db_result = await self._execute(query) + return db_result.scalars().first() + + async def delete_many( self, activity_or_flow_ids: list[uuid.UUID], respondent_subject_ids: list[uuid.UUID], target_subject_ids: list[uuid.UUID], - ) -> None: + ): """ Marks the `is_deleted` field as True for all matching assignments based on the provided activity or flow IDs, respondent subject IDs, and target subject IDs. The method ensures @@ -138,11 +142,10 @@ async def unassign_many( AssertionError If the lengths of the provided ID lists do not match. """ - # Ensure all lists are of equal length assert len(activity_or_flow_ids) == len(respondent_subject_ids) == len(target_subject_ids) - query: Query = ( + stmt = ( update(ActivityAssigmentSchema) .where( or_( @@ -160,7 +163,7 @@ async def unassign_many( ) .values(is_deleted=True) ) - await self._execute(query) + await self._execute(stmt) async def get_by_applet(self, applet_id: uuid.UUID, query_params: QueryParams) -> list[ActivityAssigmentSchema]: respondent_schema = aliased(SubjectSchema) @@ -184,8 +187,44 @@ async def get_by_applet(self, applet_id: uuid.UUID, query_params: QueryParams) - flows_clause = _ActivityAssignmentFlowsFilter().get_clauses(**query_params.filters) subject_clauses = _ActivityAssignmentSubjectFilter().get_clauses(**query_params.filters) - query = query.where(and_(or_(*activities_clause, *flows_clause), or_(*subject_clauses))) + query = query.where( + and_( + or_(*activities_clause, *flows_clause) + if len(activities_clause) > 0 or len(flows_clause) > 0 + else True, + or_(*subject_clauses) if len(subject_clauses) > 0 else True, + ) + ) db_result = await self._execute(query) return db_result.scalars().all() + + async def upsert(self, values: dict) -> ActivityAssigmentSchema | None: + stmt = ( + insert(ActivityAssigmentSchema) + .values(values) + .on_conflict_do_update( + index_elements=[ + ActivityAssigmentSchema.respondent_subject_id, + ActivityAssigmentSchema.target_subject_id, + ActivityAssigmentSchema.activity_id + if values.get("activity_id") + else ActivityAssigmentSchema.activity_flow_id, + ], + set_={ + "updated_at": datetime.datetime.utcnow(), + "is_deleted": False, + }, + where=(ActivityAssigmentSchema.soft_exists(exists=False)), + ) + .returning(ActivityAssigmentSchema.id) + ) + + result = await self._execute(stmt) + model_id = result.scalar_one_or_none() + updated_schema = None + if model_id: + updated_schema = await self._get("id", model_id) + + return updated_schema diff --git a/src/apps/activity_assignments/db/schemas.py b/src/apps/activity_assignments/db/schemas.py index d5d3d589889..d5bda6b5fb0 100644 --- a/src/apps/activity_assignments/db/schemas.py +++ b/src/apps/activity_assignments/db/schemas.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, ForeignKey +from sqlalchemy import Column, ForeignKey, Index from infrastructure.database import Base @@ -12,3 +12,20 @@ class ActivityAssigmentSchema(Base): activity_id = Column(ForeignKey("activities.id", ondelete="RESTRICT"), nullable=True) respondent_subject_id = Column(ForeignKey("subjects.id", ondelete="RESTRICT"), nullable=False) target_subject_id = Column(ForeignKey("subjects.id", ondelete="RESTRICT"), nullable=False) + + __table_args__ = ( + Index( + "uq_activity_assignments_activity_respondent_target", + "activity_id", + "respondent_subject_id", + "target_subject_id", + unique=True, + ), + Index( + "uq_activity_assignments_activity_flow_respondent_target", + "activity_flow_id", + "respondent_subject_id", + "target_subject_id", + unique=True, + ), + ) diff --git a/src/apps/activity_assignments/domain/assignments.py b/src/apps/activity_assignments/domain/assignments.py index 8b4ba2ab172..91a313a46a3 100644 --- a/src/apps/activity_assignments/domain/assignments.py +++ b/src/apps/activity_assignments/domain/assignments.py @@ -4,14 +4,22 @@ from apps.activity_assignments.errors import ( ActivityAssignmentActivityOrFlowError, - ActivityAssignmentMissingRespondentError, - ActivityAssignmentMissingTargetError, ActivityAssignmentNotActivityAndFlowError, ) from apps.shared.domain import InternalModel, PublicModel from apps.subjects.domain import SubjectReadResponse +def _validate_assignments(values): + if not values.get("activity_id") and not values.get("activity_flow_id"): + raise ActivityAssignmentNotActivityAndFlowError() + + if values.get("activity_id") and values.get("activity_flow_id"): + raise ActivityAssignmentActivityOrFlowError("Only one of activity_id or activity_flow_id must be provided") + + return values + + class ActivityAssignmentCreate(BaseModel): activity_id: UUID | None activity_flow_id: UUID | None @@ -20,13 +28,7 @@ class ActivityAssignmentCreate(BaseModel): @root_validator def validate_assignments(cls, values): - if not values.get("activity_id") and not values.get("activity_flow_id"): - raise ActivityAssignmentNotActivityAndFlowError() - - if values.get("activity_id") and values.get("activity_flow_id"): - raise ActivityAssignmentActivityOrFlowError("Only one of activity_id or activity_flow_id must be provided") - - return values + return _validate_assignments(values) class ActivitiesAssignmentsCreate(InternalModel): @@ -72,23 +74,7 @@ class ActivityAssignmentDelete(BaseModel): @root_validator def validate_assignments(cls, values): - # Validate that exactly one of activity_id or activity_flow_id is provided - if not values.get("activity_id") and not values.get("activity_flow_id"): - raise ActivityAssignmentNotActivityAndFlowError() - - # Validate that respondent_subject_id is provided - if not values.get("respondent_subject_id"): - raise ActivityAssignmentMissingRespondentError() - - # Validate that target_subject_id is provided - if not values.get("target_subject_id"): - raise ActivityAssignmentMissingTargetError() - - # Ensure that only one of activity_id or activity_flow_id is provided - if values.get("activity_id") and values.get("activity_flow_id"): - raise ActivityAssignmentActivityOrFlowError() - - return values + return _validate_assignments(values) class ActivitiesAssignmentsDelete(InternalModel): diff --git a/src/apps/activity_assignments/errors.py b/src/apps/activity_assignments/errors.py index c1dc3d2c0b1..4120b4b79d1 100644 --- a/src/apps/activity_assignments/errors.py +++ b/src/apps/activity_assignments/errors.py @@ -9,11 +9,3 @@ class ActivityAssignmentActivityOrFlowError(ValidationError): class ActivityAssignmentNotActivityAndFlowError(ValidationError): message = _("Either activity_id or activity_flow_id must be provided") - - -class ActivityAssignmentMissingRespondentError(ValidationError): - message = _("Respondent subject ID must be provided") - - -class ActivityAssignmentMissingTargetError(ValidationError): - message = _("Target subject ID must be provided") diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index 0d7b5baedf2..0b9b4a12c50 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -80,20 +80,21 @@ async def create_many( entities = await self._get_assignments_entities(applet_id, assignments_create) respondent_activities: dict[uuid.UUID, set[str]] = defaultdict(set) - schemas = [] + assignment_schemas = [] for assignment in assignments_create: activity_or_flow_name: str = self._validate_assignment_and_get_activity_or_flow_name(assignment, entities) - schema = ActivityAssigmentSchema( - id=uuid.uuid4(), - activity_id=assignment.activity_id, + data = dict( activity_flow_id=assignment.activity_flow_id, + activity_id=assignment.activity_id, respondent_subject_id=assignment.respondent_subject_id, target_subject_id=assignment.target_subject_id, ) - if await ActivityAssigmentCRUD(self.session).already_exists(schema): + + schema: ActivityAssigmentSchema | None = await ActivityAssigmentCRUD(self.session).upsert(data) + if schema is None: continue - schemas.append(schema) + assignment_schemas.append(schema) pending_invitation = await InvitationCRUD(self.session).get_pending_subject_invitation( applet_id, assignment.respondent_subject_id @@ -101,10 +102,6 @@ async def create_many( if not pending_invitation: respondent_activities[assignment.respondent_subject_id].add(activity_or_flow_name) - assignment_schemas: list[ActivityAssigmentSchema] = await ActivityAssigmentCRUD(self.session).create_many( - schemas - ) - await self.send_email_notification(applet_id, entities.respondent_subjects, respondent_activities) return [ @@ -325,18 +322,12 @@ async def unassign_many(self, assignments_unassign: list[ActivityAssignmentDelet target_subject_ids = [] for assignment in assignments_unassign: - # Append only non-None values to the activity_or_flow_ids list - if assignment.activity_id is not None: - activity_or_flow_ids.append(assignment.activity_id) - elif assignment.activity_flow_id is not None: - activity_or_flow_ids.append(assignment.activity_flow_id) - - # Append other necessary IDs + activity_or_flow_ids.append(assignment.activity_id or assignment.activity_flow_id) target_subject_ids.append(assignment.target_subject_id) respondent_subject_ids.append(assignment.respondent_subject_id) - await ActivityAssigmentCRUD(self.session).unassign_many( - activity_or_flow_ids=activity_or_flow_ids, + await ActivityAssigmentCRUD(self.session).delete_many( + activity_or_flow_ids=activity_or_flow_ids, # type: ignore respondent_subject_ids=respondent_subject_ids, target_subject_ids=target_subject_ids, ) diff --git a/src/apps/activity_assignments/tests/test_assignments.py b/src/apps/activity_assignments/tests/test_assignments.py index 1a4de23dcdd..24c50037b35 100644 --- a/src/apps/activity_assignments/tests/test_assignments.py +++ b/src/apps/activity_assignments/tests/test_assignments.py @@ -2,16 +2,22 @@ import uuid import pytest -from sqlalchemy import select +from pydantic import EmailStr +from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from apps.activity_assignments.db.schemas import ActivityAssigmentSchema -from apps.activity_assignments.domain.assignments import ActivitiesAssignmentsCreate, ActivityAssignmentCreate +from apps.activity_assignments.domain.assignments import ( + ActivitiesAssignmentsCreate, + ActivitiesAssignmentsDelete, + ActivityAssignmentCreate, + ActivityAssignmentDelete, +) from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate from apps.applets.domain.applet_create_update import AppletUpdate from apps.applets.domain.applet_full import AppletFull from apps.applets.service import AppletService -from apps.invitations.domain import InvitationRespondentRequest +from apps.invitations.domain import InvitationLanguage, InvitationRespondentRequest from apps.mailing.services import TestMail from apps.shared.enums import Language from apps.shared.test import BaseTest @@ -25,10 +31,10 @@ @pytest.fixture def invitation_respondent_data(bill_bronson: User) -> InvitationRespondentRequest: return InvitationRespondentRequest( - email=bill_bronson.email_encrypted, + email=EmailStr(bill_bronson.email_encrypted), first_name=bill_bronson.first_name, last_name=bill_bronson.last_name, - language="en", + language=InvitationLanguage.EN, secret_user_id=str(uuid.uuid4()), nickname=str(uuid.uuid4()), tag="respondentTag", @@ -114,6 +120,7 @@ async def applet_one_shell_account(session: AsyncSession, applet_one: AppletFull class TestActivityAssignments(BaseTest): activities_assignments_applet = "/assignments/applet/{applet_id}" user_activities_assignments = "/users/me/assignments/{applet_id}" + activities_assign_unassign_applet = "/assignments/applet/{applet_id}" async def test_create_one_assignment( self, @@ -892,3 +899,545 @@ async def test_assignment_list_by_respondent_success( assert assignment["respondentSubject"]["id"] == str(lucy_applet_one_subject.id) assert assignment["respondentSubject"]["firstName"] == lucy_applet_one_subject.first_name assert assignment["respondentSubject"]["lastName"] == lucy_applet_one_subject.last_name + + async def test_reassign_creation( + self, + client: TestClient, + applet_one_with_flow: AppletFull, + tom: User, + lucy_applet_one_subject: SubjectFull, + tom_applet_one_subject: SubjectFull, + mailbox: TestMail, + ): + client.login(tom) + + assignments_create = dict( + assignments=[ + dict( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + dict( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ) + + response = await client.post( + self.activities_assignments_applet.format(applet_id=applet_one_with_flow.id), data=assignments_create + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + assignments = response.json()["result"]["assignments"] + assert len(assignments) == 2 + assignment_activity_created = [ + a for a in assignments if a["activityId"] == str(applet_one_with_flow.activities[0].id) + ][0] + assignment_flow_created = [ + a for a in assignments if a["activityFlowId"] == str(applet_one_with_flow.activity_flows[0].id) + ][0] + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=dict( + assignments=[ + dict( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + dict( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ), + ) + + assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT + + assignments_create = dict( + assignments=[ + dict( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + dict( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ) + + response = await client.post( + self.activities_assignments_applet.format(applet_id=applet_one_with_flow.id), data=assignments_create + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + assignments = response.json()["result"]["assignments"] + assert len(assignments) == 2 + + assignment_activity = [a for a in assignments if a["activityId"] == str(applet_one_with_flow.activities[0].id)][ + 0 + ] + assignment_flow = [ + a for a in assignments if a["activityFlowId"] == str(applet_one_with_flow.activity_flows[0].id) + ][0] + + assert assignment_activity["id"] == assignment_activity_created["id"] + assert assignment_activity["activityId"] == str(applet_one_with_flow.activities[0].id) + assert assignment_activity["respondentSubjectId"] == str(tom_applet_one_subject.id) + assert assignment_activity["targetSubjectId"] == str(tom_applet_one_subject.id) + assert assignment_activity["activityFlowId"] is None + + assert assignment_flow["id"] == assignment_flow_created["id"] + assert assignment_flow["activityId"] is None + assert assignment_flow["respondentSubjectId"] == str(tom_applet_one_subject.id) + assert assignment_flow["targetSubjectId"] == str(lucy_applet_one_subject.id) + assert assignment_flow["activityFlowId"] == str(applet_one_with_flow.activity_flows[0].id) + + async def test_create_one_unassignment( + self, + client: TestClient, + applet_one: AppletFull, + tom: User, + lucy_applet_one_subject: SubjectFull, + tom_applet_one_subject, + session: AsyncSession, + ): + client.login(tom) + activity_assignment_id = applet_one.activities[0].id + subject_id = lucy_applet_one_subject.id + target_id = tom_applet_one_subject.id + # Use the same details as the existing assignment + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=activity_assignment_id, + respondent_subject_id=subject_id, + target_subject_id=target_id, + ) + ] + ) + + assignment_response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert assignment_response.status_code == http.HTTPStatus.CREATED, assignment_response.json() + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create.dict(), + ) + + assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT + + # Query based on activity_id, respondent_subject_id, and target_subject_id + query = select(ActivityAssigmentSchema).where( + ActivityAssigmentSchema.activity_id == activity_assignment_id, + ActivityAssigmentSchema.respondent_subject_id == subject_id, + ActivityAssigmentSchema.target_subject_id == target_id, + ) + + res = await session.execute(query) + model = res.scalars().one() + + assert model.activity_id == activity_assignment_id + assert model.respondent_subject_id == subject_id + assert model.target_subject_id == target_id + assert model.is_deleted is True + + async def test_unassign_no_effect_with_wrong_activity( + self, + client: TestClient, + applet_one: AppletFull, + applet_two: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + session: AsyncSession, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign it using the wrong applet_id (applet_one) + assignment_delete = ActivitiesAssignmentsDelete( + assignments=[ + ActivityAssignmentDelete( + activity_id=applet_two.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete.dict(), + ) + + # Expect a 204 No Content because no assignments match the given applet_id + assert response.status_code == http.HTTPStatus.NO_CONTENT + + # Verify that the assignment in applet_two still exists and is not marked as deleted + query = select(ActivityAssigmentSchema).where( + ActivityAssigmentSchema.activity_id == applet_one.activities[0].id, + ActivityAssigmentSchema.respondent_subject_id == tom_applet_one_subject.id, + ActivityAssigmentSchema.target_subject_id == tom_applet_one_subject.id, + ) + res = await session.execute(query) + assignment = res.scalars().first() + + assert assignment is not None # The assignment should still exist + assert assignment.is_deleted is False + + async def test_unassign_fail_missing_activity_and_flow( + self, + client: TestClient, + applet_one: AppletFull, + applet_two: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign without providing activity_id or activity_flow_id + assignment_delete = dict( + assignments=[ + dict( + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, + ) + + # Expect a 400 Bad Request because neither activity_id nor activity_flow_id was provided + assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST + result = unassign_response.json()["result"][0] + assert result["message"] == "Either activity_id or activity_flow_id must be provided" + + async def test_unassign_fail_both_activity_and_flow( + self, + client: TestClient, + applet_one: AppletFull, + applet_two: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign with both activity_id and activity_flow_id provided + assignment_delete = dict( + assignments=[ + dict( + activity_id=applet_one.activities[0].id, + activity_flow_id=applet_two.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, + ) + + assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST, unassign_response.json() + result = unassign_response.json()["result"][0] + assert result["message"] == "Only one of activity_id or activity_flow_id must be provided" + + async def test_unassign_multiple_assignments_with_flow( + self, + client: TestClient, + applet_one_with_flow: AppletFull, + tom: User, + lucy_applet_one_subject: SubjectFull, + tom_applet_one_subject: SubjectFull, + session: AsyncSession, + ): + client.login(tom) + + # Step 1: Assign multiple activities/flows + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ActivityAssignmentCreate( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ) + + # Create the assignments + assignment_response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignments_create, + ) + + assert assignment_response.status_code == http.HTTPStatus.CREATED, assignment_response.json() + + # Step 2: Unassign the previously assigned activities/flows + assignment_delete = ActivitiesAssignmentsDelete( + assignments=[ + ActivityAssignmentDelete( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ActivityAssignmentDelete( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=lucy_applet_one_subject.id, + ), + ] + ) + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignment_delete.dict(), + ) + + assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT, unassign_response.json() + + # Validate that the assignments have been unassigned + for assignment in assignment_delete.assignments: + query = select(ActivityAssigmentSchema).where( + or_( + ActivityAssigmentSchema.activity_id == assignment.activity_id, + ActivityAssigmentSchema.activity_flow_id == assignment.activity_flow_id, + ), + ActivityAssigmentSchema.respondent_subject_id == assignment.respondent_subject_id, + ActivityAssigmentSchema.target_subject_id == assignment.target_subject_id, + ) + res = await session.execute(query) + model = res.scalars().first() + + assert model is not None + assert model.is_deleted is True + + async def test_unassign_no_effect_with_wrong_flow( + self, + client: TestClient, + applet_one_with_flow: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + session: AsyncSession, + ): + client.login(tom) + + # Step 1: Assign an activity flow first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignments_create, + ) + + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign it using a wrong flow_id from a different applet + wrong_applet_id = uuid.UUID("7db2b7fe-3eba-4c70-8d02-dcf55b74d1c3") + assignment_delete = ActivitiesAssignmentsDelete( + assignments=[ + ActivityAssignmentDelete( + activity_flow_id=wrong_applet_id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), + data=assignment_delete.dict(), + ) + + # Expect a 204 No Content because no assignments match the given flow_id + assert response.status_code == http.HTTPStatus.NO_CONTENT + + # Verify that the assignment in applet_one_with_flow still exists and is not marked as deleted + query = select(ActivityAssigmentSchema).where( + ActivityAssigmentSchema.activity_flow_id == applet_one_with_flow.activity_flows[0].id, + ActivityAssigmentSchema.respondent_subject_id == tom_applet_one_subject.id, + ActivityAssigmentSchema.target_subject_id == tom_applet_one_subject.id, + ) + res = await session.execute(query) + assignment = res.scalars().first() + + assert assignment is not None # The assignment should still exist + assert assignment.is_deleted is False + + async def test_unassign_fail_missing_respondent( + self, + client: TestClient, + applet_one: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create.dict(), + ) + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign without providing target_subject_id using a dictionary + assignment_delete = { + "assignments": [ + { + "activity_id": str(applet_one.activities[0].id), + "respondent_subject_id": str(tom_applet_one_subject.id), + "target_subject_id": None, # Missing target_subject_id + } + ] + } + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, + ) + + # Expect a 400 Bad Request due to missing target_subject_id + assert unassign_response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + + async def test_unassign_fail_missing_target( + self, + client: TestClient, + applet_one: AppletFull, + tom: User, + tom_applet_one_subject: SubjectFull, + ): + client.login(tom) + + # Step 1: Assign an activity first + assignments_create = ActivitiesAssignmentsCreate( + assignments=[ + ActivityAssignmentCreate( + activity_id=applet_one.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + # Create the assignment + response = await client.post( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignments_create.dict(), + ) + assert response.status_code == http.HTTPStatus.CREATED, response.json() + + # Step 2: Attempt to unassign without providing target_subject_id using a dictionary + assignment_delete = { + "assignments": [ + { + "activity_id": str(applet_one.activities[0].id), + "respondent_subject_id": str(tom_applet_one_subject.id), + "target_subject_id": None, # Missing target_subject_id + } + ] + } + + unassign_response = await client.delete( + self.activities_assign_unassign_applet.format(applet_id=applet_one.id), + data=assignment_delete, # Use json= to pass the dictionary as JSON + ) + + # Expect a 400 Bad Request due to missing target_subject_id + assert unassign_response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY diff --git a/src/apps/activity_assignments/tests/test_unassignments.py b/src/apps/activity_assignments/tests/test_unassignments.py deleted file mode 100644 index ecabcc8c531..00000000000 --- a/src/apps/activity_assignments/tests/test_unassignments.py +++ /dev/null @@ -1,588 +0,0 @@ -import http -import uuid - -import pytest -from pydantic import EmailStr -from sqlalchemy import or_, select -from sqlalchemy.ext.asyncio import AsyncSession - -from apps.activity_assignments.db.schemas import ActivityAssigmentSchema -from apps.activity_assignments.domain.assignments import ( - ActivitiesAssignmentsCreate, - ActivitiesAssignmentsDelete, - ActivityAssignmentCreate, - ActivityAssignmentDelete, -) -from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate -from apps.applets.domain.applet_create_update import AppletUpdate -from apps.applets.domain.applet_full import AppletFull -from apps.applets.service import AppletService -from apps.invitations.domain import InvitationRespondentRequest -from apps.shared.enums import Language -from apps.shared.test import BaseTest -from apps.shared.test.client import TestClient -from apps.subjects.db.schemas import SubjectSchema -from apps.subjects.domain import Subject, SubjectCreate, SubjectFull -from apps.subjects.services import SubjectsService -from apps.users import User - - -@pytest.fixture -def invitation_respondent_data() -> InvitationRespondentRequest: - return InvitationRespondentRequest( - email=EmailStr("pending@example.com"), - first_name="User", - last_name="pending", - language="en", - secret_user_id=str(uuid.uuid4()), - nickname=str(uuid.uuid4()), - tag="respondentTag", - ) - - -@pytest.fixture -async def lucy_applet_one_subject(session: AsyncSession, lucy: User, applet_one_lucy_respondent: AppletFull) -> Subject: - applet_id = applet_one_lucy_respondent.id - query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) - res = await session.execute(query, execution_options={"synchronize_session": False}) - model = res.scalars().one() - return Subject.from_orm(model) - - -@pytest.fixture -async def lucy_applet_two_subject(session: AsyncSession, lucy: User, applet_two_lucy_respondent: AppletFull) -> Subject: - applet_id = applet_two_lucy_respondent.id - query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) - res = await session.execute(query, execution_options={"synchronize_session": False}) - model = res.scalars().one() - return Subject.from_orm(model) - - -@pytest.fixture -async def applet_one_pending_subject( - client, - tom: User, - invitation_respondent_data, - applet_one: AppletFull, - session: AsyncSession, -) -> Subject: - # invite a new respondent - client.login(tom) - response = await client.post( - "/invitations/{applet_id}/respondent".format(applet_id=str(applet_one.id)), - invitation_respondent_data, - ) - assert response.status_code == http.HTTPStatus.OK - - query = select(SubjectSchema).where( - SubjectSchema.applet_id == applet_one.id, - SubjectSchema.email == invitation_respondent_data.email, - ) - res = await session.execute(query, execution_options={"synchronize_session": False}) - model = res.scalars().one() - return Subject.from_orm(model) - - -@pytest.fixture -async def applet_one_with_flow( - session: AsyncSession, - applet_one: AppletFull, - applet_minimal_data: AppletFull, - tom: User, -): - data = AppletUpdate(**applet_minimal_data.dict()) - flow = FlowUpdate( - name="flow", - items=[ActivityFlowItemUpdate(id=None, activity_key=data.activities[0].key)], - description={Language.ENGLISH: "description"}, - id=None, - ) - data.activity_flows = [flow] - srv = AppletService(session, tom.id) - await srv.update(applet_one.id, data) - applet = await srv.get_full_applet(applet_one.id) - return applet - - -@pytest.fixture -async def applet_two_with_flow( - session: AsyncSession, - applet_two: AppletFull, - applet_minimal_data: AppletFull, - tom: User, -): - data = AppletUpdate(**applet_minimal_data.dict()) - flow = FlowUpdate( - name="flow_two", - items=[ActivityFlowItemUpdate(id=None, activity_key=data.activities[0].key)], - description={Language.ENGLISH: "description for flow two"}, - id=None, - ) - data.activity_flows = [flow] - srv = AppletService(session, tom.id) - await srv.update(applet_two.id, data) - applet = await srv.get_full_applet(applet_two.id) - return applet - - -@pytest.fixture -async def applet_one_shell_account(session: AsyncSession, applet_one: AppletFull, tom: User) -> Subject: - return await SubjectsService(session, tom.id).create( - SubjectCreate( - applet_id=applet_one.id, - creator_id=tom.id, - first_name="Shell", - last_name="Account", - nickname="shell-account-0", - tag="shell-account-0-tag", - secret_user_id=f"{uuid.uuid4()}", - ) - ) - - -class TestActivityUnassignments(BaseTest): - activities_assign_unassign_applet = "/assignments/applet/{applet_id}" - - async def test_create_one_unassignment( - self, - client: TestClient, - applet_one: AppletFull, - tom: User, - lucy_applet_one_subject: SubjectFull, - tom_applet_one_subject, - session: AsyncSession, - ): - client.login(tom) - activity_assignment_id = applet_one.activities[0].id - subject_id = lucy_applet_one_subject.id - target_id = tom_applet_one_subject.id - # Use the same details as the existing assignment - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=activity_assignment_id, - respondent_subject_id=subject_id, - target_subject_id=target_id, - ) - ] - ) - - assignment_response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create, - ) - - assert assignment_response.status_code == http.HTTPStatus.CREATED, assignment_response.json() - - unassign_response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create.dict(), - ) - - assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT - - # Query based on activity_id, respondent_subject_id, and target_subject_id - query = select(ActivityAssigmentSchema).where( - ActivityAssigmentSchema.activity_id == activity_assignment_id, - ActivityAssigmentSchema.respondent_subject_id == subject_id, - ActivityAssigmentSchema.target_subject_id == target_id, - ) - - res = await session.execute(query) - model = res.scalars().one() - - assert model.activity_id == activity_assignment_id - assert model.respondent_subject_id == subject_id - assert model.target_subject_id == target_id - assert model.is_deleted is True - - async def test_unassign_no_effect_with_wrong_activity( - self, - client: TestClient, - applet_one: AppletFull, - applet_two: AppletFull, - tom: User, - tom_applet_one_subject: SubjectFull, - session: AsyncSession, - ): - client.login(tom) - - # Step 1: Assign an activity first - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=applet_one.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - # Create the assignment - response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create, - ) - - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - # Step 2: Attempt to unassign it using the wrong applet_id (applet_one) - assignment_delete = ActivitiesAssignmentsDelete( - assignments=[ - ActivityAssignmentDelete( - activity_id=applet_two.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignment_delete.dict(), - ) - - # Expect a 204 No Content because no assignments match the given applet_id - assert response.status_code == http.HTTPStatus.NO_CONTENT - - # Verify that the assignment in applet_two still exists and is not marked as deleted - query = select(ActivityAssigmentSchema).where( - ActivityAssigmentSchema.activity_id == applet_one.activities[0].id, - ActivityAssigmentSchema.respondent_subject_id == tom_applet_one_subject.id, - ActivityAssigmentSchema.target_subject_id == tom_applet_one_subject.id, - ) - res = await session.execute(query) - assignment = res.scalars().first() - - assert assignment is not None # The assignment should still exist - assert assignment.is_deleted is False - - async def test_unassign_fail_missing_activity_and_flow( - self, - client: TestClient, - applet_one: AppletFull, - applet_two: AppletFull, - tom: User, - tom_applet_one_subject: SubjectFull, - ): - client.login(tom) - - # Step 1: Assign an activity first - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=applet_one.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - # Create the assignment - response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create, - ) - - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - # Step 2: Attempt to unassign without providing activity_id or activity_flow_id - assignment_delete = dict( - assignments=[ - dict( - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - unassign_response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignment_delete, - ) - - # Expect a 400 Bad Request because neither activity_id nor activity_flow_id was provided - assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST - result = unassign_response.json()["result"][0] - assert result["message"] == "Either activity_id or activity_flow_id must be provided" - - async def test_unassign_fail_both_activity_and_flow( - self, - client: TestClient, - applet_one: AppletFull, - applet_two: AppletFull, - tom: User, - tom_applet_one_subject: SubjectFull, - ): - client.login(tom) - - # Step 1: Assign an activity first - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=applet_one.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - # Create the assignment - response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create, - ) - - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - # Step 2: Attempt to unassign with both activity_id and activity_flow_id provided - assignment_delete = dict( - assignments=[ - dict( - activity_id=applet_one.activities[0].id, - activity_flow_id=applet_two.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - unassign_response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignment_delete, - ) - - assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST, unassign_response.json() - result = unassign_response.json()["result"][0] - assert result["message"] == "Either activity_id or activity_flow_id must be provided, but not both" - - async def test_unassign_multiple_assignments_with_flow( - self, - client: TestClient, - applet_one_with_flow: AppletFull, - tom: User, - lucy_applet_one_subject: SubjectFull, - tom_applet_one_subject: SubjectFull, - session: AsyncSession, - ): - client.login(tom) - - # Step 1: Assign multiple activities/flows - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=applet_one_with_flow.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ), - ActivityAssignmentCreate( - activity_flow_id=applet_one_with_flow.activity_flows[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=lucy_applet_one_subject.id, - ), - ] - ) - - # Create the assignments - assignment_response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), - data=assignments_create, - ) - - assert assignment_response.status_code == http.HTTPStatus.CREATED, assignment_response.json() - - # Step 2: Unassign the previously assigned activities/flows - assignment_delete = ActivitiesAssignmentsDelete( - assignments=[ - ActivityAssignmentDelete( - activity_id=applet_one_with_flow.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ), - ActivityAssignmentDelete( - activity_flow_id=applet_one_with_flow.activity_flows[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=lucy_applet_one_subject.id, - ), - ] - ) - - unassign_response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), - data=assignment_delete.dict(), - ) - - assert unassign_response.status_code == http.HTTPStatus.NO_CONTENT, unassign_response.json() - - # Validate that the assignments have been unassigned - for assignment in assignment_delete.assignments: - query = select(ActivityAssigmentSchema).where( - or_( - ActivityAssigmentSchema.activity_id == assignment.activity_id, - ActivityAssigmentSchema.activity_flow_id == assignment.activity_flow_id, - ), - ActivityAssigmentSchema.respondent_subject_id == assignment.respondent_subject_id, - ActivityAssigmentSchema.target_subject_id == assignment.target_subject_id, - ) - res = await session.execute(query) - model = res.scalars().first() - - assert model is not None - assert model.is_deleted is True - - async def test_unassign_no_effect_with_wrong_flow( - self, - client: TestClient, - applet_one_with_flow: AppletFull, - tom: User, - tom_applet_one_subject: SubjectFull, - session: AsyncSession, - ): - client.login(tom) - - # Step 1: Assign an activity flow first - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_flow_id=applet_one_with_flow.activity_flows[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - # Create the assignment - response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), - data=assignments_create, - ) - - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - # Step 2: Attempt to unassign it using a wrong flow_id from a different applet - wrong_applet_id = "7db2b7fe-3eba-4c70-8d02-dcf55b74d1c3" - assignment_delete = ActivitiesAssignmentsDelete( - assignments=[ - ActivityAssignmentDelete( - activity_flow_id=wrong_applet_id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one_with_flow.id), - data=assignment_delete.dict(), - ) - - # Expect a 204 No Content because no assignments match the given flow_id - assert response.status_code == http.HTTPStatus.NO_CONTENT - - # Verify that the assignment in applet_one_with_flow still exists and is not marked as deleted - query = select(ActivityAssigmentSchema).where( - ActivityAssigmentSchema.activity_flow_id == applet_one_with_flow.activity_flows[0].id, - ActivityAssigmentSchema.respondent_subject_id == tom_applet_one_subject.id, - ActivityAssigmentSchema.target_subject_id == tom_applet_one_subject.id, - ) - res = await session.execute(query) - assignment = res.scalars().first() - - assert assignment is not None # The assignment should still exist - assert assignment.is_deleted is False - - async def test_unassign_fail_missing_respondent( - self, - client: TestClient, - applet_one: AppletFull, - tom: User, - tom_applet_one_subject: SubjectFull, - ): - client.login(tom) - - # Step 1: Assign an activity first - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=applet_one.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - # Create the assignment - response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create.dict(), - ) - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - # Step 2: Attempt to unassign without providing target_subject_id using a dictionary - assignment_delete = { - "assignments": [ - { - "activity_id": str(applet_one.activities[0].id), - "respondent_subject_id": str(tom_applet_one_subject.id), - "target_subject_id": None, # Missing target_subject_id - } - ] - } - - unassign_response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignment_delete, - ) - - # Expect a 400 Bad Request due to missing target_subject_id - assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST - result = unassign_response.json()["result"][0] - assert result["message"] == "Target subject ID must be provided" - - async def test_unassign_fail_missing_target( - self, - client: TestClient, - applet_one: AppletFull, - tom: User, - tom_applet_one_subject: SubjectFull, - ): - client.login(tom) - - # Step 1: Assign an activity first - assignments_create = ActivitiesAssignmentsCreate( - assignments=[ - ActivityAssignmentCreate( - activity_id=applet_one.activities[0].id, - respondent_subject_id=tom_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, - ) - ] - ) - - # Create the assignment - response = await client.post( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignments_create.dict(), - ) - assert response.status_code == http.HTTPStatus.CREATED, response.json() - - # Step 2: Attempt to unassign without providing target_subject_id using a dictionary - assignment_delete = { - "assignments": [ - { - "activity_id": str(applet_one.activities[0].id), - "respondent_subject_id": str(tom_applet_one_subject.id), - "target_subject_id": None, # Missing target_subject_id - } - ] - } - - unassign_response = await client.delete( - self.activities_assign_unassign_applet.format(applet_id=applet_one.id), - data=assignment_delete, # Use json= to pass the dictionary as JSON - ) - - # Expect a 400 Bad Request due to missing target_subject_id - assert unassign_response.status_code == http.HTTPStatus.BAD_REQUEST - result = unassign_response.json()["result"][0] - assert result["message"] == "Target subject ID must be provided" diff --git a/src/apps/answers/crud/answers.py b/src/apps/answers/crud/answers.py index be3420cf66c..62481ba0a54 100644 --- a/src/apps/answers/crud/answers.py +++ b/src/apps/answers/crud/answers.py @@ -600,7 +600,7 @@ async def get_completed_answers_data( ) db_result = await self._execute(query) - data = db_result.all() + data = db_result.mappings().all() activities = [] flows = [] @@ -674,7 +674,7 @@ async def get_completed_answers_data_list( ) db_result = await self._execute(query) - data = db_result.all() + data = db_result.mappings().all() applet_activities_flows_map: dict[uuid.UUID, dict[str, list]] = dict() for row in data: diff --git a/src/apps/applets/crud/applets.py b/src/apps/applets/crud/applets.py index 05a380b0063..4500c4618da 100644 --- a/src/apps/applets/crud/applets.py +++ b/src/apps/applets/crud/applets.py @@ -692,7 +692,7 @@ async def get_respondents_device_ids( @staticmethod def _get_activity_subquery() -> Query: return ( - select([func.count().label("count")]) + select(func.count().label("count")) .where( AppletSchema.id == ActivitySchema.applet_id, ) diff --git a/src/apps/logs/crud/notification.py b/src/apps/logs/crud/notification.py index 64bdc860553..f4227366601 100644 --- a/src/apps/logs/crud/notification.py +++ b/src/apps/logs/crud/notification.py @@ -77,7 +77,7 @@ async def _get_previous( self, user_id: str, device_id: str, - field: list[InstrumentedAttribute], + field: InstrumentedAttribute, flt: list[ColumnOperators], ) -> NotificationLogSchema | None: query: Query = select(field) @@ -97,7 +97,7 @@ async def get_previous_description( return await self._get_previous( user_id, schema.device_id, - [NotificationLogSchema.notification_descriptions], + NotificationLogSchema.notification_descriptions, [NotificationLogSchema.notification_descriptions.isnot(None)], ) @@ -105,7 +105,7 @@ async def get_previous_in_queue(self, user_id: str, schema: NotificationLogCreat return await self._get_previous( user_id, schema.device_id, - [NotificationLogSchema.notification_in_queue], + NotificationLogSchema.notification_in_queue, [NotificationLogSchema.notification_in_queue.isnot(None)], ) @@ -115,6 +115,6 @@ async def get_previous_scheduled_notifications( return await self._get_previous( user_id, schema.device_id, - [NotificationLogSchema.scheduled_notifications], + NotificationLogSchema.scheduled_notifications, [NotificationLogSchema.scheduled_notifications.isnot(None)], ) diff --git a/src/apps/subjects/crud/subject.py b/src/apps/subjects/crud/subject.py index ff420d1716a..57971b667aa 100644 --- a/src/apps/subjects/crud/subject.py +++ b/src/apps/subjects/crud/subject.py @@ -32,9 +32,9 @@ async def update_by_id(self, id_, **values): update(self.schema_class).where(self.schema_class.id == id_).values(**values).returning(self.schema_class) ) db_result = await self._execute(query) # TODO test - result = db_result.first() + result = db_result.mappings().first() - return self.schema_class(**dict(zip(result.keys(), result))) + return self.schema_class(**result) async def get_by_id(self, _id: uuid.UUID) -> SubjectSchema | None: return await self._get("id", _id) diff --git a/src/apps/users/cruds/user_device.py b/src/apps/users/cruds/user_device.py index 6ed2c79a304..8898de7f73f 100644 --- a/src/apps/users/cruds/user_device.py +++ b/src/apps/users/cruds/user_device.py @@ -34,7 +34,7 @@ async def upsert(self, user_id: uuid.UUID, device_id: str, **data): ) result = await self._execute(stmt) - row = result.one() + row = result.mappings().first() model = UserDeviceSchema(**row) return model diff --git a/src/apps/workspaces/crud/user_applet_access.py b/src/apps/workspaces/crud/user_applet_access.py index 4fd4b3003f2..793caead9f0 100644 --- a/src/apps/workspaces/crud/user_applet_access.py +++ b/src/apps/workspaces/crud/user_applet_access.py @@ -437,7 +437,7 @@ async def get_workspace_respondents( UserAppletAccessSchema.role.in_([Role.OWNER, Role.MANAGER, Role.COORDINATOR]), and_( UserAppletAccessSchema.role == Role.REVIEWER, - SubjectSchema.id == any_(assigned_subjects), + SubjectSchema.id == any_(assigned_subjects.scalar_subquery()), ), ), ) @@ -449,7 +449,7 @@ async def get_workspace_respondents( select(InvitationSchema.id) .where( InvitationSchema.status == InvitationStatus.PENDING, - InvitationSchema.applet_id.in_(workspace_applets_sq), + InvitationSchema.applet_id.in_(select(workspace_applets_sq)), InvitationSchema.meta.has_key("subject_id"), func.cast(InvitationSchema.meta["subject_id"].astext, UUID(as_uuid=True)) == any_(func.array_agg(SubjectSchema.id)), @@ -537,7 +537,7 @@ async def get_workspace_respondents( query = query.where( has_access, - SubjectSchema.applet_id.in_(workspace_applets_sq), + SubjectSchema.applet_id.in_(select(workspace_applets_sq)), SubjectSchema.applet_id == applet_id if applet_id else True, SubjectSchema.soft_exists(), ) diff --git a/src/broker.py b/src/broker.py index d48d80a7419..1a8b34e7105 100644 --- a/src/broker.py +++ b/src/broker.py @@ -1,13 +1,18 @@ import taskiq_fastapi -from taskiq import InMemoryBroker +from taskiq import AsyncBroker, InMemoryBroker +from taskiq.formatters.json_formatter import JSONFormatter from taskiq_aio_pika import AioPikaBroker from taskiq_redis import RedisAsyncResultBackend from config import settings -broker = AioPikaBroker(settings.rabbitmq.url).with_result_backend(RedisAsyncResultBackend(settings.redis.url)) +broker: AsyncBroker = ( + AioPikaBroker(settings.rabbitmq.url) + .with_result_backend(RedisAsyncResultBackend(settings.redis.url)) + .with_formatter(JSONFormatter()) +) if settings.env == "testing": - broker = InMemoryBroker() + broker = InMemoryBroker().with_formatter(JSONFormatter()) taskiq_fastapi.init(broker, "main:app") diff --git a/src/infrastructure/database/crud.py b/src/infrastructure/database/crud.py index fa10792eb8e..06c41d29291 100644 --- a/src/infrastructure/database/crud.py +++ b/src/infrastructure/database/crud.py @@ -43,12 +43,13 @@ async def _update_one(self, lookup: str, value: Any, schema: ConcreteSchema) -> query = query.values(**dict(schema)) query = query.returning(self.schema_class) db_result = await self._execute(query) - results = db_result.fetchall() - if len(results) == 0: + rows_as_dict = db_result.mappings().all() + if len(rows_as_dict) == 0: raise NoResultFound() - elif len(results) > 1: + elif len(rows_as_dict) > 1: raise MultipleResultsFound() - return self.schema_class(**dict(zip(results[0].keys(), results[0]))) + + return self.schema_class(**rows_as_dict[0]) async def _update( self, @@ -66,8 +67,8 @@ async def _update( query = query.returning(self.schema_class) db_result = await self._execute(query) - results = db_result.fetchall() - return [self.schema_class(**dict(zip(result.keys(), result))) for result in results] + rows_as_dict = db_result.mappings().all() + return [self.schema_class(**row_dict) for row_dict in rows_as_dict] async def _get(self, key: str, value: Any) -> ConcreteSchema | None: """Return only one result by filters""" diff --git a/src/infrastructure/database/migrations/versions/2024_09_11_11_56-add_unique_index_to_activity_assignments.py b/src/infrastructure/database/migrations/versions/2024_09_11_11_56-add_unique_index_to_activity_assignments.py new file mode 100644 index 00000000000..03c0c544765 --- /dev/null +++ b/src/infrastructure/database/migrations/versions/2024_09_11_11_56-add_unique_index_to_activity_assignments.py @@ -0,0 +1,41 @@ +"""Add unique index to ActivityAssignments table + +Revision ID: 9cc4ba6a211a +Revises: 769a83b9c24f +Create Date: 2024-09-11 11:56:49.556116 + +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9cc4ba6a211a" +down_revision = "769a83b9c24f" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_index( + "uq_activity_assignments_activity_flow_respondent_target", + "activity_assignments", + ["activity_flow_id", "respondent_subject_id", "target_subject_id"], + unique=True, + ) + op.create_index( + "uq_activity_assignments_activity_respondent_target", + "activity_assignments", + ["activity_id", "respondent_subject_id", "target_subject_id"], + unique=True, + ) + + +def downgrade() -> None: + op.drop_index( + "uq_activity_assignments_activity_flow_respondent_target", + table_name="activity_assignments", + ) + op.drop_index( + "uq_activity_assignments_activity_respondent_target", + table_name="activity_assignments", + ) diff --git a/src/infrastructure/utility/redis_client.py b/src/infrastructure/utility/redis_client.py index 5f747c848dd..89e3d8d0747 100644 --- a/src/infrastructure/utility/redis_client.py +++ b/src/infrastructure/utility/redis_client.py @@ -3,8 +3,8 @@ import re import typing -import aioredis -from aioredis.connection import EncodableT +import redis.asyncio as redis +from redis.typing import EncodableT from sentry_sdk import capture_exception from config import settings @@ -71,7 +71,7 @@ class RedisCache: _initialized: bool = False _instance = None configuration: dict = {} - _cache: typing.Optional[aioredis.Redis] = None + _cache: typing.Optional[redis.Redis] = None host: str port: int db: int @@ -108,13 +108,13 @@ def _start(self): if not self.host: return try: - self._cache = aioredis.client.Redis( + self._cache = redis.client.Redis( host=self.host, port=self.port, db=self.db, **self.configuration, ) - except aioredis.exceptions.ConnectionError as e: + except redis.exceptions.ConnectionError as e: try: capture_exception(e) except ImportError: @@ -126,7 +126,7 @@ async def get(self, key: str) -> typing.Optional[str]: try: value = await self._cache.get(key) return value - except aioredis.RedisError: + except redis.RedisError: return None async def set(self, key: str, value: EncodableT, ex=None) -> bool: From 709e6f4b999504291ba4f4477612f27fe34bad8d Mon Sep 17 00:00:00 2001 From: Phillipe Bojorquez Date: Fri, 13 Sep 2024 10:41:36 -0400 Subject: [PATCH 18/41] add passed tests to the slack output steps --- .github/workflows/e2e-tests.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 69b88a4cd10..77a45866e91 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -85,6 +85,13 @@ jobs: with: script: | core.setFailed('E2E tests failed') + + - name: Pass if tests pass + if: steps.e2e-tests.outcome = 'success' + uses: actions/github-script@v7 + with: + script: | + core.setCommandEcho('E2E tests passed') publish-report: name: Publish Report From 2c60a0234135a5177c603958e0a2fb491d084df6 Mon Sep 17 00:00:00 2001 From: Rodrigo Colao Merlo Date: Mon, 16 Sep 2024 13:19:54 -0300 Subject: [PATCH 19/41] feature: Soft delete activity assignments on activity/flow deletion (#1598) --- src/apps/activities/services/activity.py | 2 + .../activity_assignments/crud/assignments.py | 35 +++++++++++ src/apps/activity_assignments/service.py | 3 + src/apps/activity_flows/service/flow.py | 2 + src/apps/applets/tests/conftest.py | 36 +++++++++++ src/apps/applets/tests/test_applet.py | 61 +++++++++++++++++++ 6 files changed, 139 insertions(+) diff --git a/src/apps/activities/services/activity.py b/src/apps/activities/services/activity.py index 6f973f0a003..ecdb19e205a 100644 --- a/src/apps/activities/services/activity.py +++ b/src/apps/activities/services/activity.py @@ -18,6 +18,7 @@ ) from apps.activities.errors import ActivityAccessDeniedError, ActivityDoeNotExist from apps.activities.services.activity_item import ActivityItemService +from apps.activity_assignments.service import ActivityAssignmentService from apps.applets.crud import AppletsCRUD, UserAppletAccessCRUD from apps.schedule.crud.events import ActivityEventsCRUD, EventCRUD from apps.schedule.service.schedule import ScheduleService @@ -189,6 +190,7 @@ async def update_create(self, applet_id: uuid.UUID, activities_create: list[Acti await ScheduleService(self.session).delete_by_activity_ids( applet_id=applet_id, activity_ids=list(deleted_activity_ids) ) + await ActivityAssignmentService(self.session).delete_by_activity_or_flow_ids(list(deleted_activity_ids)) # Create default events for new activities if new_activities: diff --git a/src/apps/activity_assignments/crud/assignments.py b/src/apps/activity_assignments/crud/assignments.py index e42a4d77d60..3d9108c13df 100644 --- a/src/apps/activity_assignments/crud/assignments.py +++ b/src/apps/activity_assignments/crud/assignments.py @@ -111,6 +111,41 @@ async def already_exists(self, schema: ActivityAssigmentSchema) -> ActivityAssig db_result = await self._execute(query) return db_result.scalars().first() + async def delete_by_activity_or_flow_ids(self, activity_or_flow_ids: list[uuid.UUID]): + """ + Marks the `is_deleted` field as True for all matching assignments based on the provided + activity or flow IDs. The method ensures that each ID corresponds to a unique record by + treating the ID as a unique combination. + + Parameters: + ---------- + activity_or_flow_ids : list[uuid.UUID] + List of activity or flow IDs to search for. These IDs may correspond to either + `activity_id` or `activity_flow_id` fields. + + Returns: + ------- + None + + Raises: + ------ + AssertionError + If the provided ID list is empty. + """ + assert len(activity_or_flow_ids) > 0 + + stmt = ( + update(ActivityAssigmentSchema) + .where( + or_( + ActivityAssigmentSchema.activity_id.in_(activity_or_flow_ids), + ActivityAssigmentSchema.activity_flow_id.in_(activity_or_flow_ids), + ) + ) + .values(is_deleted=True) + ) + await self._execute(stmt) + async def delete_many( self, activity_or_flow_ids: list[uuid.UUID], diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index 0b9b4a12c50..a49b01077f6 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -332,6 +332,9 @@ async def unassign_many(self, assignments_unassign: list[ActivityAssignmentDelet target_subject_ids=target_subject_ids, ) + async def delete_by_activity_or_flow_ids(self, activity_or_flow_ids: list[uuid.UUID]) -> None: + await ActivityAssigmentCRUD(self.session).delete_by_activity_or_flow_ids(activity_or_flow_ids) + async def get_all(self, applet_id: uuid.UUID, query_params: QueryParams) -> list[ActivityAssignment]: """ Returns assignments for given applet ID and matching any filters provided in query_params. diff --git a/src/apps/activity_flows/service/flow.py b/src/apps/activity_flows/service/flow.py index ad09ba947b1..894e7d02658 100644 --- a/src/apps/activity_flows/service/flow.py +++ b/src/apps/activity_flows/service/flow.py @@ -1,5 +1,6 @@ import uuid +from apps.activity_assignments.service import ActivityAssignmentService from apps.activity_flows.crud import FlowItemsCRUD, FlowsCRUD, FlowsHistoryCRUD from apps.activity_flows.db.schemas import ActivityFlowSchema from apps.activity_flows.domain.flow import ( @@ -143,6 +144,7 @@ async def update_create( deleted_flow_ids = set(all_flows) - set(existing_flows) if deleted_flow_ids: await ScheduleService(self.session).delete_by_flow_ids(applet_id=applet_id, flow_ids=list(deleted_flow_ids)) + await ActivityAssignmentService(self.session).delete_by_activity_or_flow_ids(list(deleted_flow_ids)) # Create default events for new activities if new_flows: diff --git a/src/apps/applets/tests/conftest.py b/src/apps/applets/tests/conftest.py index 4468df71ddd..0cb7dddea09 100644 --- a/src/apps/applets/tests/conftest.py +++ b/src/apps/applets/tests/conftest.py @@ -1,6 +1,9 @@ import pytest from sqlalchemy.ext.asyncio import AsyncSession +from apps.activities.domain.activity_update import ActivityUpdate +from apps.activity_assignments.domain.assignments import ActivityAssignmentCreate +from apps.activity_assignments.service import ActivityAssignmentService from apps.activity_flows.domain.flow_create import FlowCreate, FlowItemCreate from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate from apps.applets.crud.applets import AppletsCRUD @@ -10,6 +13,7 @@ from apps.applets.domain.applet_link import CreateAccessLink from apps.applets.service.applet import AppletService from apps.shared.enums import Language +from apps.subjects.domain import SubjectFull from apps.users.domain import User from apps.workspaces.domain.constants import Role from apps.workspaces.service.user_applet_access import UserAppletAccessService @@ -70,11 +74,43 @@ async def applet_one_with_flow( return applet +@pytest.fixture +async def applet_one_with_flow_and_assignments( + session: AsyncSession, applet_one_with_flow: AppletFull, tom_applet_one_subject: SubjectFull +) -> AppletFull: + assignments = [ + ActivityAssignmentCreate( + activity_id=applet_one_with_flow.activities[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ActivityAssignmentCreate( + activity_flow_id=applet_one_with_flow.activity_flows[0].id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ] + + await ActivityAssignmentService(session).create_many(applet_one_with_flow.id, assignments) + + return applet_one_with_flow + + @pytest.fixture def applet_one_update_data(applet_one: AppletFull) -> AppletUpdate: return AppletUpdate(**applet_one.dict()) +@pytest.fixture +def applet_one_change_activities_ids(applet_one: AppletFull) -> AppletUpdate: + data = applet_one.dict() + data["activities"] = [ + ActivityUpdate(**activity.dict(exclude={"name", "id"}), name="New Activity") + for activity in applet_one.activities[1:] + ] + return AppletUpdate(**data) + + @pytest.fixture def applet_create_with_flow(applet_minimal_data: AppletCreate) -> AppletCreate: data = applet_minimal_data.copy(deep=True) diff --git a/src/apps/applets/tests/test_applet.py b/src/apps/applets/tests/test_applet.py index 0060aa66a13..fca45e772ee 100644 --- a/src/apps/applets/tests/test_applet.py +++ b/src/apps/applets/tests/test_applet.py @@ -7,6 +7,7 @@ import pytest from firebase_admin.exceptions import NotFoundError as FireBaseNotFoundError from pytest_mock import MockerFixture +from sqlalchemy.ext.asyncio import AsyncSession from apps.activities.domain.activity_create import ActivityItemCreate from apps.activities.domain.activity_update import ActivityItemUpdate @@ -20,6 +21,8 @@ DuplicatedActivityFlowsError, FlowItemActivityKeyNotFoundError, ) +from apps.activity_assignments.crud.assignments import ActivityAssigmentCRUD +from apps.activity_assignments.db.schemas import ActivityAssigmentSchema from apps.applets.domain.applet_create_update import AppletCreate, AppletUpdate from apps.applets.domain.applet_full import AppletFull from apps.applets.domain.base import AppletReportConfigurationBase, Encryption @@ -878,6 +881,64 @@ async def test_update_applet_change_flow_auto_assign( updateResult = updateResp.json()["result"] assert updateResult["activityFlows"][0]["autoAssign"] is False + async def test_update_applet_delete_activity_with_assignments( + self, + client: TestClient, + tom: User, + applet_one_with_flow_and_assignments: AppletFull, + applet_one_change_activities_ids: AppletUpdate, + session: AsyncSession, + tom_applet_one_subject, + ): + client.login(tom) + + assignment: ActivityAssigmentSchema | None = await ActivityAssigmentCRUD(session)._get( + "activity_id", applet_one_with_flow_and_assignments.activities[0].id + ) + assert assignment is not None + assert assignment.soft_exists() is True + + updateResp = await client.put( + self.applet_detail_url.format(pk=applet_one_with_flow_and_assignments.id), + data=applet_one_change_activities_ids, + ) + assert updateResp.status_code == http.HTTPStatus.OK, updateResp.json() + + assignment = await ActivityAssigmentCRUD(session)._get( + "activity_id", applet_one_with_flow_and_assignments.activities[0].id + ) + assert assignment is not None + assert assignment.soft_exists() is False + + async def test_update_applet_delete_flow_with_assignments( + self, + client: TestClient, + tom: User, + applet_one_with_flow_and_assignments: AppletFull, + applet_one_change_activities_ids: AppletUpdate, + session: AsyncSession, + tom_applet_one_subject, + ): + client.login(tom) + + assignment: ActivityAssigmentSchema | None = await ActivityAssigmentCRUD(session)._get( + "activity_flow_id", applet_one_with_flow_and_assignments.activity_flows[0].id + ) + assert assignment is not None + assert assignment.soft_exists() is True + + updateResp = await client.put( + self.applet_detail_url.format(pk=applet_one_with_flow_and_assignments.id), + data=applet_one_change_activities_ids, + ) + assert updateResp.status_code == http.HTTPStatus.OK, updateResp.json() + + assignment = await ActivityAssigmentCRUD(session)._get( + "activity_flow_id", applet_one_with_flow_and_assignments.activity_flows[0].id + ) + assert assignment is not None + assert assignment.soft_exists() is False + async def test_update_applet_keep_flow_auto_assign( self, client: TestClient, tom: User, applet_create_with_flow: AppletCreate ): From 4703bcc02181dc8ae976ca60b9d1446d0edc98d0 Mon Sep 17 00:00:00 2001 From: AlejandroCoronadoN Date: Tue, 17 Sep 2024 09:09:05 -0700 Subject: [PATCH 20/41] fix: Modified answers applet_validate_multiinformant_assessment api (M2-6978) (#1581) Modified answers applet_validate_multiinformant_assessment api to return BAD REQUEST instead of 200 OK response using pydantic validation and modified the answers tests script to validate new responses. --- src/apps/answers/filters.py | 6 +- src/apps/answers/tests/test_answers.py | 149 ++++++++++++++++++------- 2 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/apps/answers/filters.py b/src/apps/answers/filters.py index 2d612ba3801..ef8cf4d32c4 100644 --- a/src/apps/answers/filters.py +++ b/src/apps/answers/filters.py @@ -61,6 +61,6 @@ class AnswerIdentifierVersionFilter(BaseQueryParams): class AppletMultiinformantAssessmentParams(InternalModel): - target_subject_id: uuid.UUID | None - source_subject_id: uuid.UUID | None - activity_or_flow_id: uuid.UUID | None + target_subject_id: uuid.UUID + source_subject_id: uuid.UUID + activity_or_flow_id: uuid.UUID diff --git a/src/apps/answers/tests/test_answers.py b/src/apps/answers/tests/test_answers.py index a250035b674..a8c621c3d87 100644 --- a/src/apps/answers/tests/test_answers.py +++ b/src/apps/answers/tests/test_answers.py @@ -3501,19 +3501,72 @@ async def test_validate_multiinformant_assessment_success_with_flow( assert response.status_code == http.HTTPStatus.OK assert response.json()["result"]["valid"] is True - async def test_validat_multiinformant_assessment_fail_not_manager(self, client, lucy: User, applet_one: AppletFull): + async def test_validat_multiinformant_assessment_fail_not_manager( + self, client, applet_with_flow, session: AsyncSession, lucy: User, tom: User, applet_one: AppletFull + ): + subject_service = SubjectsService(session, tom.id) + + source_subject = await subject_service.create( + SubjectCreate( + applet_id=applet_with_flow.id, + creator_id=tom.id, + first_name="source", + last_name="subject", + secret_user_id=f"{uuid.uuid4()}", + ) + ) + + target_subject = await subject_service.create( + SubjectCreate( + applet_id=applet_with_flow.id, + creator_id=tom.id, + first_name="target", + last_name="subject", + secret_user_id=f"{uuid.uuid4()}", + ) + ) + client.login(lucy) url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one.id) - + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) response = await client.get(url) assert response.status_code == http.HTTPStatus.FORBIDDEN - async def test_validate_multiinformant_assessment_fail_no_applet(self, client, lucy: User): + async def test_validate_multiinformant_assessment_fail_no_applet( + self, client, session: AsyncSession, applet_one: AppletFull, lucy: User + ): client.login(lucy) + subject_service = SubjectsService(session, lucy.id) + + source_subject = await subject_service.create( + SubjectCreate( + applet_id=applet_one.id, + creator_id=lucy.id, + first_name="source", + last_name="subject", + secret_user_id=f"{uuid.uuid4()}", + ) + ) + target_subject = await subject_service.create( + SubjectCreate( + applet_id=applet_one.id, + creator_id=lucy.id, + first_name="target", + last_name="subject", + secret_user_id=f"{uuid.uuid4()}", + ) + ) url = self.multiinformat_assessment_validate_url.format(applet_id=uuid.uuid4()) + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) response = await client.get(url) @@ -3528,8 +3581,8 @@ async def test_validate_multiinformant_assessment_success_no_params( response = await client.get(url) - assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["valid"] is True + assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + assert response.json()["result"][0]["message"] == "field required" async def test_validate_multiinformant_assessment_fail_source_subject_not_found( self, client, tom: User, applet_one: AppletFull, applet_two: AppletFull, session: AsyncSession @@ -3538,16 +3591,6 @@ async def test_validate_multiinformant_assessment_fail_source_subject_not_found( subject_service = SubjectsService(session, tom.id) - source_subject = await subject_service.create( - SubjectCreate( - applet_id=applet_two.id, - creator_id=tom.id, - first_name="source", - last_name="subject", - secret_user_id=f"{uuid.uuid4()}", - ) - ) - target_subject = await subject_service.create( SubjectCreate( applet_id=applet_one.id, @@ -3559,13 +3602,14 @@ async def test_validate_multiinformant_assessment_fail_source_subject_not_found( ) url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one.id) - - url = f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" - + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={uuid.uuid4()}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) response = await client.get(url) assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["valid"] is False + assert response.json()["result"]["message"] == "Source subject not found" assert response.json()["result"]["code"] == "invalid_source_subject" async def test_validate_multiinformant_assessment_fail_target_subject_not_found( @@ -3585,24 +3629,15 @@ async def test_validate_multiinformant_assessment_fail_target_subject_not_found( ) ) - target_subject = await subject_service.create( - SubjectCreate( - applet_id=applet_two.id, - creator_id=tom.id, - first_name="target", - last_name="subject", - secret_user_id=f"{uuid.uuid4()}", - ) - ) - url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one.id) - - url = f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" - + url = ( + f"{url}?targetSubjectId={uuid.uuid4()}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) response = await client.get(url) assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["valid"] is False + assert response.json()["result"]["message"] == "Target subject not found" assert response.json()["result"]["code"] == "invalid_target_subject" async def test_validate_multiinformant_assessment_fail_temporary_relation_expired( @@ -3658,12 +3693,14 @@ async def test_validate_multiinformant_assessment_fail_temporary_relation_expire ) url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one.id) - url = f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" - + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) response = await client.get(url) assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["valid"] is False + assert response.json()["result"]["message"] == "Subject relation not found" assert response.json()["result"]["code"] == "no_access_to_applet" async def test_validate_multiinformant_assessment_success_temporary_relation_not_expired( @@ -3719,8 +3756,11 @@ async def test_validate_multiinformant_assessment_success_temporary_relation_not ) url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one.id) - url = f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" - + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) + # activity_or_flow_id response = await client.get(url) assert response.status_code == http.HTTPStatus.OK assert response.json()["result"]["valid"] is True @@ -3768,7 +3808,10 @@ async def test_validate_multiinformant_assessment_success_permanent_non_take_now ) url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one.id) - url = f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one.activities[0].id}" + ) response = await client.get(url) assert response.status_code == http.HTTPStatus.OK @@ -3778,11 +3821,39 @@ async def test_validate_multiinformant_assessment_fail_no_permissions( self, client, lucy: User, + sam: User, + tom: User, + session: AsyncSession, + applet_one: AppletFull, applet_one_lucy_manager: AppletFull, ): client.login(lucy) + subject_service = SubjectsService(session, sam.id) + + source_subject = await subject_service.create( + SubjectCreate( + applet_id=applet_one.id, + creator_id=tom.id, + first_name="source", + last_name="subject", + secret_user_id=f"{uuid.uuid4()}", + ) + ) + target_subject = await subject_service.create( + SubjectCreate( + applet_id=applet_one.id, + creator_id=tom.id, + first_name="target", + last_name="subject", + secret_user_id=f"{uuid.uuid4()}", + ) + ) url = self.multiinformat_assessment_validate_url.format(applet_id=applet_one_lucy_manager.id) + url = ( + f"{url}?targetSubjectId={target_subject.id}&sourceSubjectId={source_subject.id}" + f"&activityOrFlowId={applet_one_lucy_manager.activities[0].id}" + ) response = await client.get(url) From d9e74bc88a50ca3c36b7b9ff7b78854b99219ec3 Mon Sep 17 00:00:00 2001 From: Shaunna Samuels Date: Thu, 19 Sep 2024 12:05:53 -0400 Subject: [PATCH 21/41] Update: README and .env template in relation to onboarding (#1606) * Update CORS ALLOW ORIGIN env variables to match any localhost pattern. Update MAILING__MAIL__SERVER to 'mailhog'. * Update README with steps related to running API locally. * Additional notes * spelling --- .env.default | 6 +++--- README.md | 29 ++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.env.default b/.env.default index 5e7d7b5fb29..bccdc69cc7e 100644 --- a/.env.default +++ b/.env.default @@ -17,8 +17,8 @@ REDIS__HOST=redis # Application configurations # CORS -CORS__ALLOW_ORIGINS=* -#CORS__ALLOW_ORIGINS_REGEX= +CORS__ALLOW_ORIGINS=https://localhost +CORS__ALLOW_ORIGIN_REGEX=https?://localhost:\d+ CORS__ALLOW_CREDENTIALS=true CORS__ALLOW_METHODS=* CORS__ALLOW_HEADERS=* @@ -40,7 +40,7 @@ AUTHENTICATION__REFRESH_TOKEN__TRANSITION_KEY= # Mailing MAILING__MAIL__USERNAME=mailhog MAILING__MAIL__PASSWORD=mailhog -MAILING__MAIL__SERVER=fcm.mail.server +MAILING__MAIL__SERVER=mailhog MAILING__MAIL__PORT=1025 MAILING__MAIL_STARTTLS= MAILING__MAIL_SSL_TLS= diff --git a/README.md b/README.md index 2a0903a7af0..e7a73cf36b0 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,12 @@ For manual installation refer to each service's documentation: Pipenv used as a default dependencies manager Create your virtual environment: + +> **NOTE:** +> Pipenv used as a default dependencies manager. +> When developing on the API be sure to work from within an active shell. +> If using VScode, open the terminal from w/in the active shell. Ideally, avoid using the integrated terminal during this process. + ```bash # Activate your environment pipenv shell @@ -197,7 +203,7 @@ Install all dependencies ```bash # Install all deps from Pipfile.lock # to install venv to current directory use `export PIPENV_VENV_IN_PROJECT=1` -pipenv sync --dev +pipenv sync --dev --system ``` > 🛑 **NOTE:** if you don't use `pipenv` for some reason remember that you will not have automatically exported variables from your `.env` file. @@ -233,11 +239,14 @@ pipenv install greenlet alembic upgrade head ``` +> 🛑 **NOTE:** If you run into role based errors e.g. `role "postgres" does not exist`, check to see if that program is running anywhere else (e.g. Homebrew), run... `ps -ef | grep {program-that-errored}` +> You can attempt to kill the process with the following command `kill -9 {PID-to-program-that-errored}`, followed by rerunning the previous check to confirm if the program has stopped. + ## Running the app ### Running locally -This option allows you to run the app for development purposes without having to manually build the Docker image. +This option allows you to run the app for development purposes without having to manually build the Docker image (i.e. When developing on the Web or Admin project). - Make sure all [required services](#required-services) are properly setup - If you're running required services using Docker, disable the `app` service from `docker-compose` before running: @@ -245,12 +254,26 @@ This option allows you to run the app for development purposes without having to docker-compose up -d ``` - Alternatively, you may run these services using [make](#running-using-makefile): + Alternatively, you may run these services using [make](#running-using-makefile) (i.e. When developing the API): + + - You'll need to sudo into `/etc/hosts` and append the following changes. + + ``` + #mindlogger + 127.0.0.1 postgres + 127.0.0.1 rabbitmq + 127.0.0.1 redis + 127.0.0.1 mailhog + ``` + + Then run the following command from within the active virtual environment shell... + ```bash make run_local ``` > 🛑 **NOTE:** Don't forget to set the `PYTHONPATH` environment variable, e.g: export PYTHONPATH=src/ +- To test that the API is up and running navigate to `http://localhost:8000/docs` in a browser. In project we use simplified version of imports: `from apps.application_name import class_name, function_name, module_nanme`. From a79644c8f44f7dc80590838ced74ba40d6f9d9a8 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Fri, 20 Sep 2024 11:15:10 -0500 Subject: [PATCH 22/41] feat: Update Subscale Lookup Table to support age ranges (M2-7586) (#1604) This PR updates the validation for the subscale lookup table to support ranges in the `age` field using the tilde character (~), similar to the `score` and `rawScore` fields. --- .../domain/custom_validation_subscale.py | 34 ++++++++++++++++++- src/apps/activities/domain/scores_reports.py | 12 +++++-- src/apps/activities/errors.py | 4 +++ .../tests/fixtures/scores_reports.py | 8 +++-- .../tests/test_applet_activity_items.py | 26 ++++++++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/apps/activities/domain/custom_validation_subscale.py b/src/apps/activities/domain/custom_validation_subscale.py index f06291a049d..7060ad56cc2 100644 --- a/src/apps/activities/domain/custom_validation_subscale.py +++ b/src/apps/activities/domain/custom_validation_subscale.py @@ -1,4 +1,6 @@ -from apps.activities.errors import InvalidRawScoreSubscaleError, InvalidScoreSubscaleError +from pydantic import PositiveInt, ValidationError + +from apps.activities.errors import InvalidAgeSubscaleError, InvalidRawScoreSubscaleError, InvalidScoreSubscaleError def validate_score_subscale_table(value: str): @@ -30,6 +32,36 @@ def validate_score_subscale_table(value: str): return value +def validate_age_subscale(value: PositiveInt | str | None): + # make sure its format is "x~y" or "x" + + def validate_non_negative_int(maybe_int: str): + try: + int_value = int(maybe_int) + if int_value < 0: + raise InvalidAgeSubscaleError() + except (ValueError, ValidationError): + raise InvalidAgeSubscaleError() + + if value is None: + return value + elif isinstance(value, int): + if value < 0: + raise InvalidAgeSubscaleError() + elif "~" not in value: + # make sure value is a positive integer + validate_non_negative_int(value) + else: + # make sure x and y are positive integers + x: str + y: str + x, y = value.split("~") + validate_non_negative_int(x) + validate_non_negative_int(y) + + return value + + def validate_raw_score_subscale(value: str): # make sure it's format is "x~y" or "x" if "~" not in value: diff --git a/src/apps/activities/domain/scores_reports.py b/src/apps/activities/domain/scores_reports.py index 9e0bc859c5c..ea24fac2fe7 100644 --- a/src/apps/activities/domain/scores_reports.py +++ b/src/apps/activities/domain/scores_reports.py @@ -5,7 +5,11 @@ from apps.activities.domain.conditional_logic import Match from apps.activities.domain.conditions import ScoreCondition, SectionCondition -from apps.activities.domain.custom_validation_subscale import validate_raw_score_subscale, validate_score_subscale_table +from apps.activities.domain.custom_validation_subscale import ( + validate_age_subscale, + validate_raw_score_subscale, + validate_score_subscale_table, +) from apps.activities.errors import ( DuplicateScoreConditionIdError, DuplicateScoreConditionNameError, @@ -168,7 +172,7 @@ class SubscaleCalculationType(str, Enum): class SubScaleLookupTable(PublicModel): score: str raw_score: str - age: PositiveInt | None = None + age: PositiveInt | str | None = None sex: str | None = Field(default=None, regex="^(M|F)$", description="M or F") optional_text: str | None = None severity: str | None = Field(default=None, regex="^(Minimal|Mild|Moderate|Severe)$") @@ -181,6 +185,10 @@ def validate_raw_score_lookup(cls, value): def validate_score_lookup(cls, value): return validate_score_subscale_table(value) + @validator("age") + def validate_age_lookup(cls, value): + return validate_age_subscale(value) + class SubscaleItemType(str, Enum): ITEM = "item" diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index 1565a593d5d..ec1036dd679 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -220,6 +220,10 @@ class InvalidScoreSubscaleError(ValidationError): message = _("Score in subscale lookup table is invalid.") +class InvalidAgeSubscaleError(ValidationError): + message = _("Age in subscale lookup table is invalid.") + + class IncorrectSubscaleItemError(ValidationError): message = _("Activity item inside subscale does not exist.") diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index 40b5c90e87f..cf73250dfba 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -122,6 +122,10 @@ def subscale_total_score_table() -> list[TotalScoreTable]: @pytest.fixture def subscale_lookup_table() -> list[SubScaleLookupTable]: return [ - SubScaleLookupTable(score="10", age=10, sex="M", raw_score="1", optional_text="some url", severity="Minimal"), - SubScaleLookupTable(score="20", age=10, sex="F", raw_score="2", optional_text="some url", severity="Mild"), + SubScaleLookupTable(score="10", age="10", sex="M", raw_score="1", optional_text="some url", severity="Minimal"), + SubScaleLookupTable(score="20", age="10", sex="F", raw_score="2", optional_text="some url", severity="Mild"), + SubScaleLookupTable(score="20", age=15, sex="F", raw_score="2", optional_text="some url", severity="Mild"), + SubScaleLookupTable(score="20", sex="F", raw_score="2", optional_text="some url", severity="Mild"), + SubScaleLookupTable(score="20", age="10~15", sex="F", raw_score="2", optional_text="some url", severity="Mild"), + SubScaleLookupTable(score="20", age="0~5", sex="F", raw_score="2", optional_text="some url", severity="Mild"), ] diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index adf07922333..edb9f546506 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -22,6 +22,7 @@ SubscaleSetting, TotalScoreTable, ) +from apps.activities.errors import InvalidAgeSubscaleError from apps.applets.domain.applet_create_update import AppletCreate, AppletUpdate from apps.applets.domain.applet_full import AppletFull from apps.shared.enums import Language @@ -521,6 +522,31 @@ async def test_create_applet__activity_with_subscale_settings_with_subscale_look result = resp.json()["result"] assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + async def test_create_applet__activity_with_subscale_settings_with_invalid_subscale_lookup_table_age( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + single_select_item_create_with_score: ActivityItemCreate, + tom: User, + subscale_setting: SubscaleSetting, + ): + client.login(tom) + data = applet_minimal_data.copy(deep=True).dict() + sub_setting = subscale_setting.copy(deep=True).dict() + sub_setting["subscales"][0]["items"][0]["name"] = single_select_item_create_with_score.name + data["activities"][0]["items"] = [single_select_item_create_with_score] + data["activities"][0]["subscale_setting"] = sub_setting + + invalid_ages = ["-1", "-1~10", "~", "x~y", "1~", "~10"] + for age in invalid_ages: + sub_setting["subscales"][0]["subscale_table_data"] = [ + dict(score="20", age=age, sex="F", raw_score="2", optional_text="some url", severity="Mild") + ] + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) + assert resp.status_code == http.HTTPStatus.BAD_REQUEST + result = resp.json()["result"] + assert result[0]["message"] == InvalidAgeSubscaleError.message + async def test_create_applet__activity_with_score_and_reports__score_and_section( self, client: TestClient, From 2abf0206a7896cd48101b86c1d9bc100f0dee222 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Wed, 25 Sep 2024 20:49:10 -0500 Subject: [PATCH 23/41] feat: Get Subjects by Respondent Subject: Submissions + Assignments (M2-7919) (#1611) This PR creates the new endpoint `/subjects/respondent/{respondent_subject_id}/activity-or-flow/{activity_or_flow_id}`, which returns a list of subjects based on an association with the respondent subject and activity/flow specified. The endpoint is accessible by users with the following applet roles: - Owner - Manager - Coordinator - Reviewer (who is assigned the `respondent_subject_id` subject) Each subject in the list returned will be the target subject of at least one: - Assignment where `respondent_subject_id` is the respondent subject (including auto-assigned activities/flows) - Submission where `respondent_subject_id` is the source subject In addition to the usual `SubjectReadResponse` properties, there are two extra ones that I'd like to highlight: - `submissionCount`: This is the number of answers that the respondent has submitted for a given subject and activity/flow - `currentlyAssigned`: Whether the activity/flow is currently assigned for the respondent to complete about a given subject Activities/flows that have been unassigned and don't have any submissions aren't included --- .../activity_assignments/crud/assignments.py | 50 +++ src/apps/activity_assignments/service.py | 31 ++ src/apps/answers/crud/answers.py | 21 ++ src/apps/answers/service.py | 7 + src/apps/subjects/api.py | 86 ++++- src/apps/subjects/domain.py | 5 + src/apps/subjects/router.py | 23 +- src/apps/subjects/services/subjects.py | 4 + src/apps/subjects/tests/tests.py | 356 ++++++++++++++++++ src/apps/workspaces/service/check_access.py | 4 + 10 files changed, 584 insertions(+), 3 deletions(-) diff --git a/src/apps/activity_assignments/crud/assignments.py b/src/apps/activity_assignments/crud/assignments.py index 3d9108c13df..2cb7b1baa14 100644 --- a/src/apps/activity_assignments/crud/assignments.py +++ b/src/apps/activity_assignments/crud/assignments.py @@ -111,6 +111,44 @@ async def already_exists(self, schema: ActivityAssigmentSchema) -> ActivityAssig db_result = await self._execute(query) return db_result.scalars().first() + async def get_target_subject_ids_by_activity_or_flow_ids( + self, + respondent_subject_id: uuid.UUID, + activity_or_flow_ids: list[uuid.UUID] = [], + ) -> list[uuid.UUID]: + """ + Retrieves the IDs of target subjects that have assignments to be completed by the provided respondent. + + Parameters: + ---------- + respondent_subject_id : uuid.UUID + The ID of the respondent subject to search for. This parameter is required. + activity_or_flow_ids : list[uuid.UUID] + Optional list of activity or flow IDs to narrow the search. These IDs may correspond to either + `activity_id` or `activity_flow_id` fields + + Returns: + ------- + list[uuid.UUID] + List of target subject IDs associated with the provided activity or flow IDs. + """ + query = select(ActivityAssigmentSchema.target_subject_id).where( + ActivityAssigmentSchema.respondent_subject_id == respondent_subject_id, + ActivityAssigmentSchema.soft_exists(), + ) + + if len(activity_or_flow_ids) > 0: + query = query.where( + or_( + ActivityAssigmentSchema.activity_id.in_(activity_or_flow_ids), + ActivityAssigmentSchema.activity_flow_id.in_(activity_or_flow_ids), + ) + ) + + db_result = await self._execute(query.distinct()) + + return db_result.scalars().all() + async def delete_by_activity_or_flow_ids(self, activity_or_flow_ids: list[uuid.UUID]): """ Marks the `is_deleted` field as True for all matching assignments based on the provided @@ -263,3 +301,15 @@ async def upsert(self, values: dict) -> ActivityAssigmentSchema | None: updated_schema = await self._get("id", model_id) return updated_schema + + async def check_if_auto_assigned(self, activity_or_flow_id: uuid.UUID) -> bool | None: + """ + Checks if the activity or flow is currently set to auto-assign. + """ + activities_query = select(ActivitySchema.auto_assign).where(ActivitySchema.id == activity_or_flow_id) + flows_query = select(ActivityFlowSchema.auto_assign).where(ActivityFlowSchema.id == activity_or_flow_id) + + union_query = activities_query.union_all(flows_query).limit(1) + + db_result = await self._execute(union_query) + return db_result.scalar_one_or_none() diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index a49b01077f6..10136eda9b5 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -399,6 +399,37 @@ async def get_all_with_subject_entities( for assignment in assignments ] + async def get_target_subject_ids_by_respondent( + self, + respondent_subject_id: uuid.UUID, + activity_or_flow_ids: list[uuid.UUID] = [], + ) -> list[uuid.UUID]: + """ + Retrieves the IDs of target subjects that have assignments to be completed by the provided respondent. + + Parameters: + ---------- + respondent_subject_id : uuid.UUID + The ID of the respondent subject to search for. This parameter is required. + activity_or_flow_ids : list[uuid.UUID] + Optional list of activity or flow IDs to narrow the search. These IDs may correspond to either + `activity_id` or `activity_flow_id` fields + + Returns: + ------- + list[uuid.UUID] + List of target subject IDs associated with the provided activity or flow IDs. + """ + return await ActivityAssigmentCRUD(self.session).get_target_subject_ids_by_activity_or_flow_ids( + respondent_subject_id, activity_or_flow_ids + ) + + async def check_if_auto_assigned(self, activity_or_flow_id: uuid.UUID) -> bool | None: + """ + Checks if the activity or flow is currently set to auto-assign. + """ + return await ActivityAssigmentCRUD(self.session).check_if_auto_assigned(activity_or_flow_id) + @staticmethod def _get_email_template_name(language: str) -> str: return f"new_activity_assignments_{language}" diff --git a/src/apps/answers/crud/answers.py b/src/apps/answers/crud/answers.py index 62481ba0a54..313831d5668 100644 --- a/src/apps/answers/crud/answers.py +++ b/src/apps/answers/crud/answers.py @@ -937,3 +937,24 @@ async def delete_by_ids(self, ids: list[uuid.UUID]): query: Query = delete(AnswerSchema) query = query.where(AnswerSchema.id.in_(ids)) await self._execute(query) + + async def get_target_subject_ids_by_respondent( + self, respondent_subject_id: uuid.UUID, activity_or_flow_id: uuid.UUID + ): + query: Query = ( + select( + AnswerSchema.target_subject_id, + func.count(func.distinct(AnswerSchema.submit_id)).label("submission_count"), + ) + .where( + AnswerSchema.source_subject_id == respondent_subject_id, + or_( + AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id) == str(activity_or_flow_id), + AnswerSchema.id_from_history_id(AnswerSchema.flow_history_id) == str(activity_or_flow_id), + ), + ) + .group_by(AnswerSchema.target_subject_id) + ) + + res = await self._execute(query) + return res.all() diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 6f101c8e3d5..eb243b945bb 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1853,6 +1853,13 @@ async def _prepare_answer_reviews( ) return results + async def get_target_subject_ids_by_respondent_and_activity_or_flow( + self, respondent_subject_id: uuid.UUID, activity_or_flow_id: uuid.UUID + ) -> list[tuple[uuid.UUID, int]]: + return await AnswersCRUD(self.answer_session).get_target_subject_ids_by_respondent( + respondent_subject_id, activity_or_flow_id + ) + class ReportServerService: def __init__(self, session, arbitrary_session=None): diff --git a/src/apps/subjects/api.py b/src/apps/subjects/api.py index 18439744401..a60eab62e33 100644 --- a/src/apps/subjects/api.py +++ b/src/apps/subjects/api.py @@ -1,18 +1,20 @@ import uuid from datetime import datetime, timedelta +from typing import TypedDict from fastapi import Body, Depends from fastapi.exceptions import RequestValidationError from pydantic.error_wrappers import ErrorWrapper from sqlalchemy.ext.asyncio import AsyncSession +from apps.activity_assignments.service import ActivityAssignmentService from apps.answers.deps.preprocess_arbitrary import get_answer_session, get_answer_session_by_subject from apps.answers.service import AnswerService from apps.applets.service import AppletService from apps.authentication.deps import get_current_user from apps.invitations.errors import NonUniqueValue from apps.invitations.services import InvitationsService -from apps.shared.domain import Response +from apps.shared.domain import Response, ResponseMulti from apps.shared.exception import NotFoundError, ValidationError from apps.shared.response import EmptyResponse from apps.shared.subjects import is_take_now_relation, is_valid_take_now_relation @@ -24,6 +26,7 @@ SubjectReadResponse, SubjectRelationCreate, SubjectUpdateRequest, + TargetSubjectByRespondentResponse, ) from apps.subjects.errors import SecretIDUniqueViolationError from apps.subjects.services import SubjectsService @@ -296,3 +299,84 @@ async def get_my_subject( last_name=subject.last_name, ) ) + + +async def get_target_subjects_by_respondent( + respondent_subject_id: uuid.UUID, + activity_or_flow_id: uuid.UUID, + user: User = Depends(get_current_user), + session: AsyncSession = Depends(get_session), + answer_session=Depends(get_answer_session), +) -> ResponseMulti[TargetSubjectByRespondentResponse]: + subjects_service = SubjectsService(session, user.id) + respondent_subject = await subjects_service.get(respondent_subject_id) + if not respondent_subject: + raise NotFoundError(f"Subject with id {respondent_subject_id} not found") + + if respondent_subject.user_id is None: + # Return a generic bad request error to avoid leaking information + raise ValidationError(f"Subject {respondent_subject_id} is not a valid respondent") + + # Make sure the authenticated user has access to the subject + await CheckAccessService(session, user.id).check_subject_subject_access( + respondent_subject.applet_id, respondent_subject.id + ) + + assignment_service = ActivityAssignmentService(session) + assignment_subject_ids = await assignment_service.get_target_subject_ids_by_respondent( + respondent_subject_id=respondent_subject_id, activity_or_flow_ids=[activity_or_flow_id] + ) + + is_auto_assigned = await assignment_service.check_if_auto_assigned(activity_or_flow_id) + if is_auto_assigned: + assignment_subject_ids.append(respondent_subject_id) + + submission_data: list[tuple[uuid.UUID, int]] = await AnswerService( + user_id=user.id, session=session, arbitrary_session=answer_session + ).get_target_subject_ids_by_respondent_and_activity_or_flow( + respondent_subject_id=respondent_subject_id, activity_or_flow_id=activity_or_flow_id + ) + + class SubjectInfo(TypedDict): + currently_assigned: bool + submission_count: int + + subject_info: dict[uuid.UUID, SubjectInfo] = {} + for subject_id, submission_count in submission_data: + subject_info[subject_id] = {"currently_assigned": False, "submission_count": submission_count} + + for subject_id in assignment_subject_ids: + if subject_id not in subject_info: + subject_info[subject_id] = {"currently_assigned": True, "submission_count": 0} + else: + subject_info[subject_id]["currently_assigned"] = True + + subjects: list[Subject] = await subjects_service.get_by_ids(list(subject_info.keys())) + result: list[TargetSubjectByRespondentResponse] = [] + + # Find the respondent subject in the list of subjects + respondent_target_subject: TargetSubjectByRespondentResponse | None = None + for subject in subjects: + target_subject = TargetSubjectByRespondentResponse( + secret_user_id=subject.secret_user_id, + nickname=subject.nickname, + tag=subject.tag, + id=subject.id, + applet_id=subject.applet_id, + user_id=subject.user_id, + first_name=subject.first_name, + last_name=subject.last_name, + submission_count=subject_info[subject.id]["submission_count"], + currently_assigned=subject_info[subject.id]["currently_assigned"], + ) + + if subject.id == respondent_subject_id: + respondent_target_subject = target_subject + else: + result.append(target_subject) + + if respondent_target_subject: + # TODO: If this endpoint is ever paginated, this logic will need to be moved to the query level + result.insert(0, respondent_target_subject) + + return ResponseMulti(result=result, count=len(result)) diff --git a/src/apps/subjects/domain.py b/src/apps/subjects/domain.py index f117dd0a43e..f6d30739790 100644 --- a/src/apps/subjects/domain.py +++ b/src/apps/subjects/domain.py @@ -88,6 +88,11 @@ class SubjectReadResponse(SubjectUpdateRequest): last_name: str +class TargetSubjectByRespondentResponse(SubjectReadResponse): + submission_count: int = 0 + currently_assigned: bool = False + + class SubjectRelation(InternalModel): source_subject_id: uuid.UUID target_subject_id: uuid.UUID diff --git a/src/apps/subjects/router.py b/src/apps/subjects/router.py index 4c4e7a964eb..2a490e22979 100644 --- a/src/apps/subjects/router.py +++ b/src/apps/subjects/router.py @@ -4,7 +4,7 @@ from starlette import status from apps.authentication.deps import get_current_user -from apps.shared.domain import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE, Response +from apps.shared.domain import AUTHENTICATION_ERROR_RESPONSES, DEFAULT_OPENAPI_RESPONSE, Response, ResponseMulti from apps.subjects.api import ( create_relation, create_subject, @@ -12,9 +12,17 @@ delete_relation, delete_subject, get_subject, + get_target_subjects_by_respondent, update_subject, ) -from apps.subjects.domain import Subject, SubjectCreateRequest, SubjectCreateResponse, SubjectFull, SubjectReadResponse +from apps.subjects.domain import ( + Subject, + SubjectCreateRequest, + SubjectCreateResponse, + SubjectFull, + SubjectReadResponse, + TargetSubjectByRespondentResponse, +) from apps.users import User from infrastructure.database.deps import get_session @@ -104,3 +112,14 @@ async def create_shell_account( **AUTHENTICATION_ERROR_RESPONSES, }, )(delete_relation) + +router.get( + "/respondent/{respondent_subject_id}/activity-or-flow/{activity_or_flow_id}", + response_model=ResponseMulti[TargetSubjectByRespondentResponse], + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": ResponseMulti[TargetSubjectByRespondentResponse]}, + **DEFAULT_OPENAPI_RESPONSE, + **AUTHENTICATION_ERROR_RESPONSES, + }, +)(get_target_subjects_by_respondent) diff --git a/src/apps/subjects/services/subjects.py b/src/apps/subjects/services/subjects.py index 05775076ee0..d0fc9ca6116 100644 --- a/src/apps/subjects/services/subjects.py +++ b/src/apps/subjects/services/subjects.py @@ -69,6 +69,10 @@ async def get(self, id_: uuid.UUID) -> Subject | None: schema = await SubjectsCrud(self.session).get_by_id(id_) return Subject.from_orm(schema) if schema else None + async def get_by_ids(self, ids: list[uuid.UUID], include_deleted=False) -> list[Subject]: + subjects = await SubjectsCrud(self.session).get_by_ids(ids, include_deleted) + return [Subject.from_orm(subject) for subject in subjects] + async def get_if_soft_exist(self, id_: uuid.UUID) -> Subject | None: schema = await SubjectsCrud(self.session).get_by_id(id_) if schema and schema.soft_exists(): diff --git a/src/apps/subjects/tests/tests.py b/src/apps/subjects/tests/tests.py index 9365ca3aa3f..a2042a4c665 100644 --- a/src/apps/subjects/tests/tests.py +++ b/src/apps/subjects/tests/tests.py @@ -9,7 +9,13 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from apps.activities.domain.activity_update import ActivityUpdate +from apps.activities.services.activity import ActivityService +from apps.activity_assignments.domain.assignments import ActivityAssignmentCreate, ActivityAssignmentDelete +from apps.activity_assignments.service import ActivityAssignmentService from apps.answers.crud.answers import AnswersCRUD +from apps.answers.domain import AppletAnswerCreate +from apps.answers.service import AnswerService from apps.applets.domain.applet_full import AppletFull from apps.shared.test import BaseTest from apps.shared.test.client import TestClient @@ -201,6 +207,12 @@ async def applet_one_lucy_respondent(session: AsyncSession, applet_one: AppletFu return applet_one +@pytest.fixture +async def applet_one_pit_respondent(session: AsyncSession, applet_one: AppletFull, tom: User, pit: User) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_one.id).add_role(pit.id, Role.RESPONDENT) + return applet_one + + @pytest.fixture async def lucy_applet_one_subject(session: AsyncSession, lucy: User, applet_one_lucy_respondent: AppletFull) -> Subject: applet_id = applet_one_lucy_respondent.id @@ -211,6 +223,16 @@ async def lucy_applet_one_subject(session: AsyncSession, lucy: User, applet_one_ return Subject.from_orm(model) +@pytest.fixture +async def pit_applet_one_subject(session: AsyncSession, pit: User, applet_one_pit_respondent: AppletFull) -> Subject: + applet_id = applet_one_pit_respondent.id + user_id = pit.id + query = select(SubjectSchema).where(SubjectSchema.user_id == user_id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + @pytest.fixture async def applet_one_lucy_reviewer_with_subject( session: AsyncSession, applet_one: AppletFull, tom_applet_one_subject, tom, lucy @@ -267,6 +289,9 @@ class TestSubjects(BaseTest): subject_temporary_multiinformant_relation_url = ( "/subjects/{subject_id}/relations/{source_subject_id}/multiinformant-assessment" ) + subject_target_by_respondent_url = ( + "/subjects/respondent/{respondent_subject_id}/activity-or-flow/{activity_or_flow_id}" + ) answer_url = "/answers" async def test_create_subject(self, client, tom: User, applet_one: AppletFull, create_shell_body): @@ -770,3 +795,334 @@ async def test_error_try_get_subject_by_not_inviter( client.login(request.getfixturevalue(user_fixture)) res = await client.get(self.subject_detail_url.format(subject_id=subject_id)) assert res.status_code == expected + + async def test_get_target_subjects_by_respondent_invalid_respondent( + self, client, tom: User, applet_one: AppletFull + ): + invalid_respondent_subject_id = str(uuid.uuid4()) + activity_or_flow_id = str(applet_one.activities[0].id) + client.login(tom) + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=invalid_respondent_subject_id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.NOT_FOUND + + async def test_get_target_subjects_by_respondent_limited_account_respondent( + self, client, tom: User, applet_one: AppletFull, applet_one_shell_account: Subject + ): + activity_or_flow_id = str(applet_one.activities[0].id) + client.login(tom) + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=applet_one_shell_account.id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + async def test_get_target_subjects_by_respondent_editor_user( + self, client, applet_one_pit_editor: AppletFull, pit: User, tom_applet_one_subject: Subject + ): + activity_or_flow_id = str(applet_one_pit_editor.activities[0].id) + client.login(pit) + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=tom_applet_one_subject.id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.FORBIDDEN + + async def test_get_target_subjects_by_respondent_respondent_user( + self, client, lucy: User, tom_applet_one_subject: Subject, applet_one_lucy_respondent: AppletFull + ): + activity_or_flow_id = str(applet_one_lucy_respondent.activities[0].id) + client.login(lucy) + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=tom_applet_one_subject.id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.FORBIDDEN + + async def test_get_target_subjects_by_respondent_reviewer_without_assignment( + self, client, lucy: User, tom_applet_one_subject: Subject, applet_one_pit_reviewer: AppletFull + ): + activity_or_flow_id = str(applet_one_pit_reviewer.activities[0].id) + client.login(lucy) + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=tom_applet_one_subject.id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.FORBIDDEN + + async def test_get_target_subjects_by_respondent_reviewer_with_assignment( + self, client, lucy: User, tom_applet_one_subject: Subject, applet_one_lucy_reviewer_with_subject: AppletFull + ): + activity_or_flow_id = str(applet_one_lucy_reviewer_with_subject.activities[0].id) + client.login(lucy) + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=tom_applet_one_subject.id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.OK + + async def test_get_target_subjects_by_respondent_no_assignments_or_submissions( + self, + client, + tom: User, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, + applet_one_lucy_respondent: AppletFull, + session, + ): + activity_or_flow_id = str(applet_one_lucy_respondent.activities[0].id) + client.login(tom) + + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=lucy_applet_one_subject.id, activity_or_flow_id=activity_or_flow_id + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.OK + + result = response.json()["result"] + + assert len(result) == 1 + + subject_result = result[0] + + assert subject_result["id"] == str(lucy_applet_one_subject.id) + assert subject_result["submissionCount"] == 0 + assert subject_result["currentlyAssigned"] is True + + async def test_get_target_subjects_by_respondent_manual_assignment( + self, + client, + tom: User, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, + applet_one_lucy_respondent: AppletFull, + session, + ): + activity = applet_one_lucy_respondent.activities[0] + + # Turn off auto-assignment + activity_service = ActivityService(session, tom.id) + await activity_service.remove_applet_activities(applet_one_lucy_respondent.id) + await activity_service.update_create( + applet_one_lucy_respondent.id, + [ + ActivityUpdate( + **activity.dict(exclude={"auto_assign"}), + auto_assign=False, + ) + ], + ) + + # Create a manual assignment + await ActivityAssignmentService(session).create_many( + applet_one_lucy_respondent.id, + [ + ActivityAssignmentCreate( + activity_id=activity.id, + respondent_subject_id=lucy_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ], + ) + + client.login(tom) + + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=lucy_applet_one_subject.id, activity_or_flow_id=str(activity.id) + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.OK + + result = response.json()["result"] + + assert len(result) == 1 + + subject_result = result[0] + + assert subject_result["id"] == str(tom_applet_one_subject.id) + assert subject_result["submissionCount"] == 0 + assert subject_result["currentlyAssigned"] is True + + async def test_get_target_subjects_by_respondent_excludes_deleted_assignment( + self, + client, + tom: User, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, + applet_one_lucy_respondent: AppletFull, + session, + ): + activity = applet_one_lucy_respondent.activities[0] + + # Turn off auto-assignment + activity_service = ActivityService(session, tom.id) + await activity_service.remove_applet_activities(applet_one_lucy_respondent.id) + await activity_service.update_create( + applet_one_lucy_respondent.id, + [ + ActivityUpdate( + **activity.dict(exclude={"auto_assign"}), + auto_assign=False, + ) + ], + ) + + # Create a deleted assignment + assignment_service = ActivityAssignmentService(session) + await assignment_service.create_many( + applet_one_lucy_respondent.id, + [ + ActivityAssignmentCreate( + activity_id=activity.id, + respondent_subject_id=lucy_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ], + ) + await assignment_service.unassign_many( + [ + ActivityAssignmentDelete( + activity_id=activity.id, + respondent_subject_id=lucy_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ] + ) + + client.login(tom) + + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=lucy_applet_one_subject.id, activity_or_flow_id=str(activity.id) + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.OK + + assert response.json()["result"] == [] + + async def test_get_target_subjects_by_respondent_multiple_assignments( + self, + client, + tom: User, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, + pit_applet_one_subject: Subject, + applet_one_shell_account: Subject, + applet_one_lucy_respondent: AppletFull, + applet_one_pit_respondent: AppletFull, + session, + ): + activity = applet_one_lucy_respondent.activities[0] + + # Turn off auto-assignment + activity_service = ActivityService(session, tom.id) + await activity_service.remove_applet_activities(applet_one_lucy_respondent.id) + await activity_service.update_create( + applet_one_lucy_respondent.id, + [ + ActivityUpdate( + **activity.dict(exclude={"auto_assign"}), + auto_assign=False, + ) + ], + ) + + # Create a manual assignment + await ActivityAssignmentService(session).create_many( + applet_one_lucy_respondent.id, + [ + ActivityAssignmentCreate( + activity_id=activity.id, + respondent_subject_id=lucy_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ), + ActivityAssignmentCreate( + activity_id=activity.id, + respondent_subject_id=lucy_applet_one_subject.id, + target_subject_id=applet_one_shell_account.id, + ), + ActivityAssignmentCreate( + activity_id=activity.id, + respondent_subject_id=tom_applet_one_subject.id, + target_subject_id=pit_applet_one_subject.id, + ), + ], + ) + + client.login(tom) + + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=lucy_applet_one_subject.id, activity_or_flow_id=str(activity.id) + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.OK + + result = response.json()["result"] + + assert len(result) == 2 + + tom_result = result[0] + + assert tom_result["id"] == str(tom_applet_one_subject.id) + assert tom_result["submissionCount"] == 0 + assert tom_result["currentlyAssigned"] is True + + shell_account_result = result[1] + + assert shell_account_result["id"] == str(applet_one_shell_account.id) + assert shell_account_result["submissionCount"] == 0 + assert shell_account_result["currentlyAssigned"] is True + + async def test_get_target_subjects_by_respondent_via_submission( + self, + client, + tom: User, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, + applet_one_lucy_respondent: AppletFull, + answer_create_payload: dict, + session: AsyncSession, + ): + activity = applet_one_lucy_respondent.activities[0] + + # Turn off auto-assignment + activity_service = ActivityService(session, tom.id) + await activity_service.remove_applet_activities(applet_one_lucy_respondent.id) + await activity_service.update_create( + applet_one_lucy_respondent.id, + [ + ActivityUpdate( + **activity.dict(exclude={"auto_assign"}), + auto_assign=False, + ) + ], + ) + + # Create an answer + await AnswerService(session, tom.id).create_answer( + AppletAnswerCreate( + **answer_create_payload, + input_subject_id=lucy_applet_one_subject.id, + source_subject_id=lucy_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ) + + client.login(tom) + + url = self.subject_target_by_respondent_url.format( + respondent_subject_id=lucy_applet_one_subject.id, activity_or_flow_id=str(activity.id) + ) + response = await client.get(url) + assert response.status_code == http.HTTPStatus.OK + + result = response.json()["result"] + + assert len(result) == 1 + + tom_result = result[0] + + assert tom_result["id"] == str(tom_applet_one_subject.id) + assert tom_result["submissionCount"] == 1 + assert tom_result["currentlyAssigned"] is False diff --git a/src/apps/workspaces/service/check_access.py b/src/apps/workspaces/service/check_access.py index 6a809aa42fb..b56a5a1bfb3 100644 --- a/src/apps/workspaces/service/check_access.py +++ b/src/apps/workspaces/service/check_access.py @@ -234,6 +234,10 @@ async def check_answer_access( await self.check_answer_review_access(applet_id) async def check_subject_subject_access(self, applet_id: uuid.UUID, subject_id: uuid.UUID | None): + """ + Check if the current authenticated user has access to the subject within this applet. The user must be an + owner, manager, coordinator, or a reviewer who was assigned the subject. + """ access = await AppletAccessCRUD(self.session).get_priority_access(applet_id, self.user_id) role = getattr(access, "role", None) if not access: From a6b09ec1f6b95df02f3a475713573cb2027e93f4 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Fri, 4 Oct 2024 10:09:57 -0500 Subject: [PATCH 24/41] feat: Add endpoint to get activities/flows assigned to or submitted for target (M2-7853) (#1614) This PR creates the new endpoint `GET /activities/applet/{applet_id}/target/{subject_id}`, which returns a list of activities and flows that are associated with the specified target subject within the applet. The endpoint is accessible by users with the following applet roles: - Owner - Manager - Coordinator - Reviewer (who is assigned the target subject) Each activity/flow in the list will be returned based on one of the following conditions - The activity/flow is set to auto assign - The activity/flow is manually assigned to some respondent, with the specified target subject as its target - Answers have been submitted at some point in the past for the activity/flow with the specified target subject as the target The return type is `ActivityOrFlowWithAssignmentsPublic` --------- Co-authored-by: Rodrigo Merlo --- src/apps/activities/api/activities.py | 60 +- src/apps/activities/crud/activity.py | 84 ++- src/apps/activities/domain/activity.py | 50 ++ src/apps/activities/router.py | 18 +- src/apps/activities/services/activity.py | 8 + src/apps/activities/tests/test_activities.py | 743 ++++++++++++++++++- src/apps/answers/crud/answers.py | 23 + src/apps/answers/service.py | 9 + src/apps/subjects/api.py | 4 +- src/apps/subjects/errors.py | 10 + src/apps/subjects/services/subjects.py | 13 +- 11 files changed, 1013 insertions(+), 9 deletions(-) diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index 8c033428a12..016dfef8cbd 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -6,6 +6,7 @@ from apps.activities.crud import ActivitiesCRUD from apps.activities.domain.activity import ( ActivityLanguageWithItemsMobileDetailPublic, + ActivityOrFlowWithAssignmentsPublic, ActivitySingleLanguageWithItemsDetailPublic, ActivityWithAssignmentDetailsPublic, ) @@ -24,7 +25,7 @@ ) from apps.applets.service import AppletService from apps.authentication.deps import get_current_user -from apps.shared.domain import Response +from apps.shared.domain import Response, ResponseMulti from apps.shared.query_params import QueryParams, parse_query_params from apps.subjects.services import SubjectsService from apps.users import User @@ -157,6 +158,63 @@ async def applet_activities_for_subject( return Response(result=result) +async def applet_activities_for_target_subject( + applet_id: uuid.UUID, + subject_id: uuid.UUID, + user: User = Depends(get_current_user), + language: str = Depends(get_language), + session=Depends(get_session), + answer_session=Depends(get_answer_session), +) -> ResponseMulti[ActivityOrFlowWithAssignmentsPublic]: + applet_service = AppletService(session, user.id) + await applet_service.exist_by_id(applet_id) + + await SubjectsService(session, user.id).exist_by_id(subject_id) + + # Restrict the endpoint access to owners, managers, coordinators, and assigned reviewers + await CheckAccessService(session, user.id).check_subject_subject_access(applet_id, subject_id) + + assignments = await ActivityAssignmentService(session).get_all_with_subject_entities( + applet_id, QueryParams(filters={"target_subject_id": subject_id}) + ) + + # Only one of these IDs will be `None` at a time, so the resulting type will be a list of UUIDs + activity_and_flow_ids_from_assignments: list[uuid.UUID] = [ + assignment.activity_id or assignment.activity_flow_id # type: ignore + for assignment in assignments + ] + + activity_and_flow_ids_from_submissions = await AnswerService( + session, user.id, answer_session + ).get_activity_and_flow_ids_by_target_subject(subject_id) + + activities_and_flows = await ActivityService(session, user.id).get_activity_and_flow_basic_info_by_ids_or_auto( + applet_id, activity_and_flow_ids_from_submissions + activity_and_flow_ids_from_assignments, language + ) + + result = [] + for activity_or_flow in activities_and_flows: + activity_or_flow_assignments = [ + assignment + for assignment in assignments + if assignment.activity_id == activity_or_flow.id or assignment.activity_flow_id == activity_or_flow.id + ] + + activity_or_flow.set_status(activity_or_flow_assignments) + + result.append( + ActivityOrFlowWithAssignmentsPublic( + **activity_or_flow.dict(), + assignments=activity_or_flow_assignments, + ) + ) + + return ResponseMulti( + result=result, + count=len(result), + ) + + async def applet_activities_and_flows( applet_id: uuid.UUID, user: User = Depends(get_current_user), diff --git a/src/apps/activities/crud/activity.py b/src/apps/activities/crud/activity.py index c2f7635cfbf..abacbbf53f1 100644 --- a/src/apps/activities/crud/activity.py +++ b/src/apps/activities/crud/activity.py @@ -1,10 +1,13 @@ import uuid +from operator import and_ from typing import cast -from sqlalchemy import delete, select, update -from sqlalchemy.orm import Query +from sqlalchemy import String, delete, func, literal, or_, select, text, union, update +from sqlalchemy.orm import Query, aliased from apps.activities.db.schemas import ActivitySchema +from apps.activities.domain.activity import ActivityOrFlowBasicInfoInternal +from apps.activity_flows.db.schemas import ActivityFlowItemSchema, ActivityFlowSchema from infrastructure.database import BaseCRUD __all__ = ["ActivitiesCRUD"] @@ -103,3 +106,80 @@ async def get_ids_by_applet_id(self, applet_id: uuid.UUID) -> list[uuid.UUID]: query = query.where(ActivitySchema.applet_id == applet_id) result = await self._execute(query) return result.scalars().all() + + async def get_activity_and_flow_basic_info_by_ids_or_auto( + self, applet_id: uuid.UUID, ids: list[uuid.UUID], language: str + ) -> list[ActivityOrFlowBasicInfoInternal]: + activities_query: Query = select( + ActivitySchema.id, + ActivitySchema.name, + ActivitySchema.description, + ActivitySchema.image.label("images"), + literal("").label("activity_ids"), + literal(False).label("is_flow"), + ActivitySchema.auto_assign, + ActivitySchema.is_hidden, + ActivitySchema.is_performance_task, + ActivitySchema.performance_task_type, + ActivitySchema.order, + ).where( + ActivitySchema.applet_id == applet_id, + or_( + ActivitySchema.id.in_(ids), + ActivitySchema.auto_assign.is_(True), + ), + ) + + flow_alias = aliased(ActivityFlowSchema) + flow_items_alias = aliased(ActivityFlowItemSchema) + activities_alias = aliased(ActivitySchema) + + flows_query = ( + select( + flow_alias.id, + flow_alias.name, + flow_alias.description, + func.coalesce( + func.string_agg(activities_alias.image, ",").filter( + and_(activities_alias.image.isnot(None), activities_alias.image != "") + ), + "", + ).label("images"), + func.string_agg(activities_alias.id.cast(String), ",").label("activity_ids"), + literal(True).label("is_flow"), + flow_alias.auto_assign, + flow_alias.is_hidden, + literal(None).label("is_performance_task"), + literal(None).label("performance_task_type"), + flow_alias.order, + ) + .join(flow_items_alias, flow_alias.id == flow_items_alias.activity_flow_id) + .join(activities_alias, flow_items_alias.activity_id == activities_alias.id) + .where( + flow_alias.applet_id == applet_id, + or_( + flow_alias.id.in_(ids), + flow_alias.auto_assign.is_(True), + ), + ) + .group_by(flow_alias.id, flow_alias.name, flow_alias.description, flow_alias.auto_assign) + ) + + union_query = union(activities_query, flows_query).order_by(text("is_flow DESC"), text('"order" ASC')) + + result = await self.session.execute(union_query) + return [ + ActivityOrFlowBasicInfoInternal( + id=row[0], + name=row[1], + description=row[2][language], + images=row[3].split(","), + activity_ids=row[4].split(",") if row[4] else None, + is_flow=row[5], + auto_assign=row[6], + is_hidden=row[7], + is_performance_task=row[8], + performance_task_type=row[9], + ) + for row in result.fetchall() + ] diff --git a/src/apps/activities/domain/activity.py b/src/apps/activities/domain/activity.py index 74e5f76402e..3403a7aa81a 100644 --- a/src/apps/activities/domain/activity.py +++ b/src/apps/activities/domain/activity.py @@ -1,5 +1,6 @@ import uuid from datetime import datetime +from enum import Enum from pydantic import Field @@ -15,6 +16,51 @@ from apps.shared.domain import InternalModel, PublicModel +class ActivityOrFlowStatusEnum(Enum): + ACTIVE = "active" + INACTIVE = "inactive" + HIDDEN = "hidden" + DELETED = "deleted" + + +class ActivityOrFlowBasicInfoPublic(PublicModel): + id: uuid.UUID + name: str + description: str + images: list[str] = Field(default_factory=list) + is_flow: bool = False + status: ActivityOrFlowStatusEnum + auto_assign: bool = True + activity_ids: list[uuid.UUID] | None = None + performance_task_type: PerformanceTaskType | None = None + is_performance_task: bool | None = None + + +class ActivityOrFlowBasicInfoInternal(InternalModel): + id: uuid.UUID + name: str + description: str + images: list[str] = Field(default_factory=list) + is_flow: bool = False + status: ActivityOrFlowStatusEnum | None + auto_assign: bool = True + is_hidden: bool = False + activity_ids: list[uuid.UUID] | None = None + performance_task_type: PerformanceTaskType | None = None + is_performance_task: bool | None = None + + def set_status(self, assignments: list[ActivityAssignmentWithSubject]): + """ + Determine and set the value of the status field + """ + if self.is_hidden: + self.status = ActivityOrFlowStatusEnum.HIDDEN + elif assignments or self.auto_assign: + self.status = ActivityOrFlowStatusEnum.ACTIVE + else: + self.status = ActivityOrFlowStatusEnum.INACTIVE + + class Activity(ActivityBase, InternalModel): id: uuid.UUID order: int @@ -102,6 +148,10 @@ class ActivityWithAssignmentDetailsPublic(ActivityLanguageWithItemsMobileDetailP assignments: list[ActivityAssignmentWithSubject] = Field(default_factory=list) +class ActivityOrFlowWithAssignmentsPublic(ActivityOrFlowBasicInfoPublic): + assignments: list[ActivityAssignmentWithSubject] = Field(default_factory=list) + + class ActivityBaseInfo(ActivityMinimumInfo, InternalModel): contains_response_types: list[ResponseType] item_count: int diff --git a/src/apps/activities/router.py b/src/apps/activities/router.py index 87c8c9a35af..8687c2bea62 100644 --- a/src/apps/activities/router.py +++ b/src/apps/activities/router.py @@ -6,10 +6,14 @@ applet_activities, applet_activities_and_flows, applet_activities_for_subject, + applet_activities_for_target_subject, public_activity_retrieve, ) from apps.activities.api.reusable_item_choices import item_choice_create, item_choice_delete, item_choice_retrieve -from apps.activities.domain.activity import ActivitySingleLanguageWithItemsDetailPublic +from apps.activities.domain.activity import ( + ActivityOrFlowWithAssignmentsPublic, + ActivitySingleLanguageWithItemsDetailPublic, +) from apps.activities.domain.reusable_item_choices import PublicReusableItemChoice from apps.applets.domain.applet import ( ActivitiesAndFlowsWithAssignmentDetailsPublic, @@ -104,3 +108,15 @@ **DEFAULT_OPENAPI_RESPONSE, }, )(applet_activities_for_subject) + +router.get( + "/applet/{applet_id}/target/{subject_id}", + description="""Get all assigned activities and activity flows for a target subject. + """, + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": ResponseMulti[ActivityOrFlowWithAssignmentsPublic]}, + **AUTHENTICATION_ERROR_RESPONSES, + **DEFAULT_OPENAPI_RESPONSE, + }, +)(applet_activities_for_target_subject) diff --git a/src/apps/activities/services/activity.py b/src/apps/activities/services/activity.py index ecdb19e205a..82a82ce2259 100644 --- a/src/apps/activities/services/activity.py +++ b/src/apps/activities/services/activity.py @@ -6,6 +6,7 @@ ActivityBaseInfo, ActivityDuplicate, ActivityLanguageWithItemsMobileDetailPublic, + ActivityOrFlowBasicInfoInternal, ActivitySingleLanguageDetail, ActivitySingleLanguageWithItemsDetail, ) @@ -453,3 +454,10 @@ async def get_info_by_applet_id(self, applet_id: uuid.UUID, language: str) -> li activity.contains_response_types = list(set(activity_items_map.get(activity.id, list()))) activity.item_count = len(activity_items_map.get(activity.id, list())) return activities + + async def get_activity_and_flow_basic_info_by_ids_or_auto( + self, applet_id: uuid.UUID, ids: list[uuid.UUID], language: str + ) -> list[ActivityOrFlowBasicInfoInternal]: + return await ActivitiesCRUD(self.session).get_activity_and_flow_basic_info_by_ids_or_auto( + applet_id, ids, language + ) diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index a124194c80e..227123cb52d 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -9,15 +9,18 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from apps.activities.domain.activity import ActivityOrFlowStatusEnum from apps.activities.domain.activity_create import ActivityCreate from apps.activities.domain.activity_update import ActivityUpdate -from apps.activities.domain.response_type_config import SingleSelectionConfig +from apps.activities.domain.response_type_config import PerformanceTaskType, SingleSelectionConfig from apps.activities.domain.response_values import SingleSelectionValues from apps.activities.services.activity import ActivityService from apps.activity_assignments.domain.assignments import ActivityAssignmentCreate from apps.activity_assignments.service import ActivityAssignmentService from apps.activity_flows.domain.flow_update import ActivityFlowItemUpdate, FlowUpdate from apps.activity_flows.service.flow import FlowService +from apps.answers.domain import AppletAnswerCreate +from apps.answers.service import AnswerService from apps.applets.domain.applet_create_update import AppletCreate from apps.applets.domain.applet_full import AppletFull from apps.applets.domain.applet_link import CreateAccessLink @@ -108,6 +111,14 @@ async def empty_applet_lucy_manager( return empty_applet +@pytest.fixture +async def applet_with_all_performance_tasks_lucy_manager( + session: AsyncSession, applet_with_all_performance_tasks: AppletFull, tom: User, lucy: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_with_all_performance_tasks.id).add_role(lucy.id, Role.MANAGER) + return applet_with_all_performance_tasks + + @pytest.fixture async def applet_activity_flow_lucy_manager( session: AsyncSession, applet_activity_flow: AppletFull, tom: User, lucy: User @@ -124,6 +135,16 @@ async def empty_applet_lucy_respondent( return empty_applet +@pytest.fixture +async def applet_with_all_performance_tasks_lucy_respondent( + session: AsyncSession, applet_with_all_performance_tasks: AppletFull, tom: User, lucy: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_with_all_performance_tasks.id).add_role( + lucy.id, Role.RESPONDENT + ) + return applet_with_all_performance_tasks + + @pytest.fixture async def lucy_empty_applet_subject( session: AsyncSession, lucy: User, empty_applet_lucy_respondent: AppletFull @@ -135,12 +156,33 @@ async def lucy_empty_applet_subject( return Subject.from_orm(model) +@pytest.fixture +async def lucy_applet_with_all_performance_tasks_subject( + session: AsyncSession, lucy: User, applet_with_all_performance_tasks_lucy_respondent: AppletFull +) -> Subject: + applet_id = applet_with_all_performance_tasks_lucy_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == lucy.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + @pytest.fixture async def empty_applet_user_respondent(session: AsyncSession, empty_applet: AppletFull, tom, user) -> AppletFull: await UserAppletAccessService(session, tom.id, empty_applet.id).add_role(user.id, Role.RESPONDENT) return empty_applet +@pytest.fixture +async def applet_with_all_performance_tasks_user_respondent( + session: AsyncSession, applet_with_all_performance_tasks: AppletFull, tom: User, user: User +) -> AppletFull: + await UserAppletAccessService(session, tom.id, applet_with_all_performance_tasks.id).add_role( + user.id, Role.RESPONDENT + ) + return applet_with_all_performance_tasks + + @pytest.fixture async def user_empty_applet_subject( session: AsyncSession, user: User, empty_applet_user_respondent: AppletFull @@ -152,6 +194,17 @@ async def user_empty_applet_subject( return Subject.from_orm(model) +@pytest.fixture +async def user_applet_with_all_performance_tasks_subject( + session: AsyncSession, user: User, applet_with_all_performance_tasks_user_respondent: AppletFull +) -> Subject: + applet_id = applet_with_all_performance_tasks_user_respondent.id + query = select(SubjectSchema).where(SubjectSchema.user_id == user.id, SubjectSchema.applet_id == applet_id) + res = await session.execute(query, execution_options={"synchronize_session": False}) + model = res.scalars().one() + return Subject.from_orm(model) + + @pytest.fixture async def applet_activity_flow_lucy_respondent( session: AsyncSession, applet_activity_flow: AppletFull, tom: User, lucy: User @@ -171,6 +224,49 @@ async def lucy_applet_activity_flow_subject( return Subject.from_orm(model) +@pytest.fixture +def answer_create_payload(applet_one: AppletFull): + return dict( + submit_id=str(uuid.uuid4()), + applet_id=str(applet_one.id), + activity_id=str(applet_one.activities[0].id), + version=applet_one.version, + created_at=1690188731636, + answer=dict( + user_public_key="user key", + answer=json.dumps( + dict( + value="2ba4bb83-ed1c-4140-a225-c2c9b4db66d2", + additional_text=None, + ) + ), + events=json.dumps(dict(events=["event1", "event2"])), + item_ids=[ + str(applet_one.activities[0].items[0].id), + ], + identifier="encrypted_identifier", + scheduled_time=1690188679657, + start_time=1690188679657, + end_time=1690188731636, + scheduledEventId="eventId", + localEndDate="2022-10-01", + localEndTime="12:35:00", + ), + alerts=[ + dict( + activity_item_id=str(applet_one.activities[0].items[0].id), + message="hello world", + ) + ], + client=dict( + appId="mindlogger-mobile", + appVersion="0.21.48", + width=819, + height=1080, + ), + ) + + class TestActivities: login_url = "/auth/login" activity_detail = "/activities/{pk}" @@ -180,6 +276,7 @@ class TestActivities: answer_url = "/answers" applet_update_url = "applets/{applet_id}" subject_assigned_activities_url = "/activities/applet/{applet_id}/subject/{subject_id}" + target_assigned_activities_url = "/activities/applet/{applet_id}/target/{subject_id}" async def test_activity_detail(self, client, applet_one: AppletFull, tom: User): activity = applet_one.activities[0] @@ -938,3 +1035,647 @@ async def test_subject_assigned_activities_auto_and_manually_assigned( assert flow_assignment["activityFlowId"] == str(manual_flow.id) assert flow_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + async def test_target_assigned_activities_editor( + self, client, applet_one_lucy_editor, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_one_lucy_editor.id, subject_id=lucy_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied." + + async def test_target_assigned_activities_incorrect_reviewer( + self, client, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_one_lucy_reviewer.id, subject_id=lucy_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied." + + async def test_target_assigned_activities_participant( + self, client, applet_one_lucy_respondent, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_one_lucy_respondent.id, subject_id=lucy_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied." + + async def test_target_assigned_activities_participant_other( + self, client, applet_one_lucy_respondent, lucy, applet_one_user_respondent, user_applet_one_subject + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_one_lucy_respondent.id, subject_id=user_applet_one_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.FORBIDDEN + result = response.json()["result"] + + assert result[0]["type"] == "ACCESS_DENIED" + assert result[0]["message"] == "Access denied." + + async def test_target_assigned_activities_invalid_applet( + self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + applet_id = uuid.uuid4() + + response = await client.get( + self.target_assigned_activities_url.format(applet_id=applet_id, subject_id=lucy_applet_one_subject.id) + ) + + assert response.status_code == http.HTTPStatus.NOT_FOUND + result = response.json()["result"] + + assert result[0]["type"] == "NOT_FOUND" + assert result[0]["message"] == f"No such applets with id={applet_id}." + + async def test_target_assigned_activities_invalid_subject( + self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + ): + client.login(lucy) + + subject_id = uuid.uuid4() + + response = await client.get( + self.target_assigned_activities_url.format(applet_id=applet_one_lucy_manager.id, subject_id=subject_id) + ) + + assert response.status_code == http.HTTPStatus.NOT_FOUND + result = response.json()["result"] + + assert result[0]["type"] == "NOT_FOUND" + assert result[0]["message"] == f"Subject with id {subject_id} not found" + + async def test_target_assigned_activities_empty_applet( + self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=lucy_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert result == [] + + async def test_target_assigned_activities_auto_assigned( + self, client, applet_activity_flow_lucy_manager, lucy, lucy_applet_activity_flow_subject + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_activity_flow_lucy_manager.id, subject_id=lucy_applet_activity_flow_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result) == 2 + + flow = applet_activity_flow_lucy_manager.activity_flows[0] + flow_result = result[0] + + assert flow_result["id"] == str(flow.id) + assert flow_result["name"] == flow.name + assert flow_result["description"] == flow.description[Language.ENGLISH] + assert flow_result["isFlow"] is True + assert flow_result["autoAssign"] == flow.auto_assign + assert flow_result["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert len(flow_result["activityIds"]) == 1 + assert flow_result["activityIds"][0] == str(flow.items[0].activity_id) + assert len(flow_result["assignments"]) == 0 + assert flow_result["isPerformanceTask"] is None + assert flow_result["performanceTaskType"] is None + + activity = applet_activity_flow_lucy_manager.activities[0] + activity_result = result[1] + + assert activity_result["id"] == str(activity.id) + assert activity_result["name"] == activity.name + assert activity_result["description"] == activity.description[Language.ENGLISH] + assert activity_result["isFlow"] is False + assert activity_result["autoAssign"] == activity.auto_assign + assert activity_result["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert activity_result["activityIds"] is None + assert len(activity_result["assignments"]) == 0 + assert activity_result["isPerformanceTask"] is False + assert activity_result["performanceTaskType"] is None + + async def test_target_assigned_activities_manually_assigned( + self, + session, + client, + empty_applet_lucy_manager, + lucy, + lucy_empty_applet_subject, + user_empty_applet_subject, + activity_create_session: ActivityCreate, + ): + client.login(lucy) + + activities = await ActivityService(session, lucy.id).update_create( + empty_applet_lucy_manager.id, + [ + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity 1", + auto_assign=False, + ), + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity 2", + auto_assign=False, + ), + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity 3", + auto_assign=False, + ), + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign"}), + name="Manual Activity 4", + auto_assign=False, + ), + ], + ) + manual_activity_1 = next((activity for activity in activities if activity.name == "Manual Activity 1")) + manual_activity_2 = next((activity for activity in activities if activity.name == "Manual Activity 2")) + manual_activity_3 = next((activity for activity in activities if activity.name == "Manual Activity 3")) + manual_activity_4 = next((activity for activity in activities if activity.name == "Manual Activity 4")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=manual_activity_1.id, + activity_flow_id=None, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=manual_activity_2.id, + activity_flow_id=None, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=manual_activity_3.id, + activity_flow_id=None, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=manual_activity_4.id, + activity_flow_id=None, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), + ], + ) + + flows = await FlowService(session).update_create( + empty_applet_lucy_manager.id, + [ + FlowUpdate( + name="Manual Flow 1", + description={Language.ENGLISH: "Manual Flow"}, + auto_assign=False, + items=[ActivityFlowItemUpdate(activity_key=manual_activity_1.key)], + ), + FlowUpdate( + name="Manual Flow 2", + description={Language.ENGLISH: "Manual Flow"}, + auto_assign=False, + items=[ActivityFlowItemUpdate(activity_key=manual_activity_2.key)], + ), + FlowUpdate( + name="Manual Flow 3", + description={Language.ENGLISH: "Manual Flow"}, + auto_assign=False, + items=[ActivityFlowItemUpdate(activity_key=manual_activity_3.key)], + ), + FlowUpdate( + name="Manual Flow 4", + description={Language.ENGLISH: "Manual Flow"}, + auto_assign=False, + items=[ActivityFlowItemUpdate(activity_key=manual_activity_4.key)], + ), + ], + { + manual_activity_1.key: manual_activity_1.id, + manual_activity_2.key: manual_activity_2.id, + manual_activity_3.key: manual_activity_3.id, + manual_activity_4.key: manual_activity_4.id, + }, + ) + + manual_flow_1 = next((flow for flow in flows if flow.name == "Manual Flow 1")) + manual_flow_2 = next((flow for flow in flows if flow.name == "Manual Flow 2")) + manual_flow_3 = next((flow for flow in flows if flow.name == "Manual Flow 3")) + manual_flow_4 = next((flow for flow in flows if flow.name == "Manual Flow 4")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow_1.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow_2.id, + respondent_subject_id=user_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow_3.id, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow_4.id, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=lucy_empty_applet_subject.id, + ), + ], + ) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result) == 4 + + flow_result_1 = next( + (flow_result for flow_result in [result[0], result[1]] if flow_result["id"] == str(manual_flow_1.id)) + ) + assert flow_result_1["id"] == str(manual_flow_1.id) + assert flow_result_1["name"] == manual_flow_1.name + assert flow_result_1["description"] == manual_flow_1.description[Language.ENGLISH] + assert flow_result_1["isFlow"] is True + assert flow_result_1["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert flow_result_1["autoAssign"] is False + assert flow_result_1["activityIds"][0] == str(manual_flow_1.items[0].activity_id) + assert flow_result_1["isPerformanceTask"] is None + assert flow_result_1["performanceTaskType"] is None + + assert len(flow_result_1["assignments"]) == 1 + flow_assignment_1 = flow_result_1["assignments"][0] + assert flow_assignment_1["activityFlowId"] == str(manual_flow_1.id) + assert flow_assignment_1["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + flow_result_2 = next( + (flow_result for flow_result in [result[0], result[1]] if flow_result["id"] == str(manual_flow_3.id)) + ) + assert flow_result_2["id"] == str(manual_flow_3.id) + assert flow_result_2["name"] == manual_flow_3.name + assert flow_result_2["description"] == manual_flow_3.description[Language.ENGLISH] + assert flow_result_2["isFlow"] is True + assert flow_result_2["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert flow_result_2["autoAssign"] is False + assert flow_result_2["activityIds"][0] == str(manual_flow_3.items[0].activity_id) + assert flow_result_2["isPerformanceTask"] is None + assert flow_result_2["performanceTaskType"] is None + + assert len(flow_result_2["assignments"]) == 1 + flow_assignment_2 = flow_result_2["assignments"][0] + assert flow_assignment_2["activityFlowId"] == str(manual_flow_3.id) + assert flow_assignment_2["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + activity_result_1 = next( + ( + activity_result + for activity_result in [result[2], result[3]] + if activity_result["id"] == str(manual_activity_1.id) + ) + ) + assert activity_result_1["id"] == str(manual_activity_1.id) + assert activity_result_1["name"] == manual_activity_1.name + assert activity_result_1["description"] == manual_activity_1.description[Language.ENGLISH] + assert activity_result_1["isFlow"] is False + assert activity_result_1["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert activity_result_1["autoAssign"] is False + assert activity_result_1["activityIds"] is None + assert activity_result_1["isPerformanceTask"] is False + assert activity_result_1["performanceTaskType"] is None + + assert len(activity_result_1["assignments"]) == 1 + activity_assignment = activity_result_1["assignments"][0] + assert activity_assignment["activityId"] == str(manual_activity_1.id) + assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + activity_result_2 = next( + ( + activity_result + for activity_result in [result[2], result[3]] + if activity_result["id"] == str(manual_activity_3.id) + ) + ) + assert activity_result_2["id"] == str(manual_activity_3.id) + assert activity_result_2["name"] == manual_activity_3.name + assert activity_result_2["description"] == manual_activity_3.description[Language.ENGLISH] + assert activity_result_2["isFlow"] is False + assert activity_result_2["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert activity_result_2["autoAssign"] is False + assert activity_result_2["activityIds"] is None + assert activity_result_2["isPerformanceTask"] is False + assert activity_result_2["performanceTaskType"] is None + + assert len(activity_result_2["assignments"]) == 1 + activity_assignment = activity_result_2["assignments"][0] + assert activity_assignment["activityId"] == str(manual_activity_3.id) + assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + async def test_target_assigned_activity_from_submission( + self, + session, + client, + tom: User, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, + applet_one_lucy_respondent: AppletFull, + answer_create_payload: dict, + ): + activity = applet_one_lucy_respondent.activities[0] + + activity_service = ActivityService(session, tom.id) + await activity_service.remove_applet_activities(applet_one_lucy_respondent.id) + await activity_service.update_create( + applet_one_lucy_respondent.id, + [ + ActivityUpdate( + **activity.dict(exclude={"auto_assign"}), + auto_assign=False, + ), + ], + ) + + # Create an activity answer + await AnswerService(session, tom.id).create_answer( + AppletAnswerCreate( + **answer_create_payload, + input_subject_id=lucy_applet_one_subject.id, + source_subject_id=lucy_applet_one_subject.id, + target_subject_id=tom_applet_one_subject.id, + ) + ) + + client.login(tom) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_one_lucy_respondent.id, subject_id=tom_applet_one_subject.id + ) + ) + assert response.status_code == http.HTTPStatus.OK + + result = response.json()["result"] + assert len(result) == 1 + + activity_result = result[0] + assert activity_result["id"] == str(activity.id) + assert activity_result["name"] == activity.name + assert activity_result["description"] == activity.description[Language.ENGLISH] + assert activity_result["status"] == ActivityOrFlowStatusEnum.INACTIVE.value + assert activity_result["isFlow"] is False + assert activity_result["autoAssign"] is False + assert activity_result["activityIds"] is None + assert activity_result["isPerformanceTask"] is False + assert activity_result["performanceTaskType"] is None + assert len(activity_result["assignments"]) == 0 + + async def test_target_assigned_hidden_activity( + self, + session, + client, + empty_applet_lucy_manager, + lucy, + lucy_empty_applet_subject, + user_empty_applet_subject, + activity_create_session: ActivityCreate, + ): + client.login(lucy) + + activities = await ActivityService(session, lucy.id).update_create( + empty_applet_lucy_manager.id, + [ + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign", "is_hidden"}), + name="Auto Activity", + auto_assign=True, + is_hidden=True, + ), + ActivityUpdate( + **activity_create_session.dict(exclude={"name", "auto_assign", "is_hidden"}), + name="Manual Activity", + auto_assign=False, + is_hidden=True, + ), + ], + ) + auto_activity = next((activity for activity in activities if activity.name == "Auto Activity")) + manual_activity = next((activity for activity in activities if activity.name == "Manual Activity")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=manual_activity.id, + activity_flow_id=None, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ], + ) + + flows = await FlowService(session).update_create( + empty_applet_lucy_manager.id, + [ + FlowUpdate( + name="Auto Flow", + description={Language.ENGLISH: "Auto Flow"}, + auto_assign=True, + items=[ActivityFlowItemUpdate(activity_key=auto_activity.key)], + is_hidden=True, + ), + FlowUpdate( + name="Manual Flow", + description={Language.ENGLISH: "Manual Flow"}, + auto_assign=False, + items=[ActivityFlowItemUpdate(activity_key=manual_activity.key)], + is_hidden=True, + ), + ], + { + auto_activity.key: auto_activity.id, + manual_activity.key: manual_activity.id, + }, + ) + + auto_flow = next((flow for flow in flows if flow.name == "Auto Flow")) + manual_flow = next((flow for flow in flows if flow.name == "Manual Flow")) + + await ActivityAssignmentService(session).create_many( + empty_applet_lucy_manager.id, + [ + ActivityAssignmentCreate( + activity_id=None, + activity_flow_id=manual_flow.id, + respondent_subject_id=lucy_empty_applet_subject.id, + target_subject_id=user_empty_applet_subject.id, + ), + ], + ) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result) == 4 + + flow_result_1 = result[0] + flow_result_2 = result[1] + + manual_flow_result = flow_result_1 if not flow_result_1["autoAssign"] else flow_result_2 + assert manual_flow_result["id"] == str(manual_flow.id) + assert manual_flow_result["name"] == manual_flow.name + assert manual_flow_result["description"] == manual_flow.description[Language.ENGLISH] + assert manual_flow_result["isFlow"] is True + assert manual_flow_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert manual_flow_result["autoAssign"] is False + assert manual_flow_result["activityIds"][0] == str(manual_flow.items[0].activity_id) + assert manual_flow_result["isPerformanceTask"] is None + assert manual_flow_result["performanceTaskType"] is None + + assert len(manual_flow_result["assignments"]) == 1 + flow_assignment = manual_flow_result["assignments"][0] + assert flow_assignment["activityFlowId"] == str(manual_flow.id) + assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + auto_flow_result = flow_result_1 if flow_result_1["autoAssign"] else flow_result_2 + assert auto_flow_result["id"] == str(auto_flow.id) + assert auto_flow_result["name"] == auto_flow.name + assert auto_flow_result["description"] == auto_flow.description[Language.ENGLISH] + assert auto_flow_result["isFlow"] is True + assert auto_flow_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert auto_flow_result["autoAssign"] is True + assert auto_flow_result["activityIds"][0] == str(auto_flow.items[0].activity_id) + assert auto_flow_result["isPerformanceTask"] is None + assert auto_flow_result["performanceTaskType"] is None + assert len(auto_flow_result["assignments"]) == 0 + + activity_result_1 = result[2] + activity_result_2 = result[3] + + manual_activity_result = activity_result_1 if not activity_result_1["autoAssign"] else activity_result_2 + assert manual_activity_result["id"] == str(manual_activity.id) + assert manual_activity_result["name"] == manual_activity.name + assert manual_activity_result["description"] == manual_activity.description[Language.ENGLISH] + assert manual_activity_result["isFlow"] is False + assert manual_activity_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert manual_activity_result["autoAssign"] is False + assert manual_activity_result["activityIds"] is None + assert manual_activity_result["isPerformanceTask"] is False + assert manual_activity_result["performanceTaskType"] is None + + assert len(manual_activity_result["assignments"]) == 1 + activity_assignment = manual_activity_result["assignments"][0] + assert activity_assignment["activityId"] == str(manual_activity.id) + assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + + auto_activity_result = activity_result_1 if activity_result_1["autoAssign"] else activity_result_2 + assert auto_activity_result["id"] == str(auto_activity.id) + assert auto_activity_result["name"] == auto_activity.name + assert auto_activity_result["description"] == auto_activity.description[Language.ENGLISH] + assert auto_activity_result["isFlow"] is False + assert auto_activity_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert auto_activity_result["autoAssign"] is True + assert auto_activity_result["activityIds"] is None + assert auto_activity_result["isPerformanceTask"] is False + assert auto_activity_result["performanceTaskType"] is None + assert len(auto_activity_result["assignments"]) == 0 + + async def test_target_assigned_performance_task( + self, + session, + client, + applet_with_all_performance_tasks_lucy_manager, + lucy, + lucy_applet_with_all_performance_tasks_subject, + user_applet_with_all_performance_tasks_subject, + ): + client.login(lucy) + + response = await client.get( + self.target_assigned_activities_url.format( + applet_id=applet_with_all_performance_tasks_lucy_manager.id, + subject_id=user_applet_with_all_performance_tasks_subject.id, + ) + ) + + assert response.status_code == http.HTTPStatus.OK + result = response.json()["result"] + + assert len(result) == 6 + + for activity_result in result: + assert activity_result["isPerformanceTask"] is True + assert activity_result["performanceTaskType"] in [ + PerformanceTaskType.FLANKER.value, + PerformanceTaskType.GYROSCOPE.value, + PerformanceTaskType.TOUCH.value, + PerformanceTaskType.ABTRAILS.value, + PerformanceTaskType.UNITY.value, + ] diff --git a/src/apps/answers/crud/answers.py b/src/apps/answers/crud/answers.py index 313831d5668..3bfacefe611 100644 --- a/src/apps/answers/crud/answers.py +++ b/src/apps/answers/crud/answers.py @@ -958,3 +958,26 @@ async def get_target_subject_ids_by_respondent( res = await self._execute(query) return res.all() + + async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[uuid.UUID]: + """ + Get a list of activity and flow IDs based on answers submitted for a target subject + """ + query: Query = ( + select( + case( + ( + AnswerSchema.flow_history_id.isnot(None), + AnswerSchema.id_from_history_id(AnswerSchema.flow_history_id), + ), + else_=AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id), + ).label("id") + ) + .where( + AnswerSchema.target_subject_id == target_subject_id, + ) + .distinct() + ) + + res = await self._execute(query) + return res.scalars().all() diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index eb243b945bb..be6f964552d 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1860,6 +1860,15 @@ async def get_target_subject_ids_by_respondent_and_activity_or_flow( respondent_subject_id, activity_or_flow_id ) + async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[uuid.UUID]: + """ + Get a list of activity and flow IDs based on answers submitted for a target subject + + The data returned is just a combined list of activity and flow IDs, without any + distinction between the two + """ + return await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_target_subject(target_subject_id) + class ReportServerService: def __init__(self, session, arbitrary_session=None): diff --git a/src/apps/subjects/api.py b/src/apps/subjects/api.py index a60eab62e33..f7275240e48 100644 --- a/src/apps/subjects/api.py +++ b/src/apps/subjects/api.py @@ -309,9 +309,7 @@ async def get_target_subjects_by_respondent( answer_session=Depends(get_answer_session), ) -> ResponseMulti[TargetSubjectByRespondentResponse]: subjects_service = SubjectsService(session, user.id) - respondent_subject = await subjects_service.get(respondent_subject_id) - if not respondent_subject: - raise NotFoundError(f"Subject with id {respondent_subject_id} not found") + respondent_subject = await subjects_service.exist_by_id(respondent_subject_id) if respondent_subject.user_id is None: # Return a generic bad request error to avoid leaking information diff --git a/src/apps/subjects/errors.py b/src/apps/subjects/errors.py index 354ff86cc26..0cec93c6ac5 100644 --- a/src/apps/subjects/errors.py +++ b/src/apps/subjects/errors.py @@ -1,5 +1,15 @@ +from gettext import gettext as _ + +from apps.shared.exception import NotFoundError + + class SecretIDUniqueViolationError(Exception): pass class AppletUserViolationError(Exception): ... + + +class SubjectNotFoundError(NotFoundError): + message_is_template: bool = True + message = _("Subject with id {subject_id} not found") diff --git a/src/apps/subjects/services/subjects.py b/src/apps/subjects/services/subjects.py index d0fc9ca6116..b46cd6f4a9a 100644 --- a/src/apps/subjects/services/subjects.py +++ b/src/apps/subjects/services/subjects.py @@ -12,7 +12,7 @@ __all__ = ["SubjectsService"] -from apps.subjects.errors import SecretIDUniqueViolationError +from apps.subjects.errors import SecretIDUniqueViolationError, SubjectNotFoundError class SubjectsService: @@ -79,6 +79,17 @@ async def get_if_soft_exist(self, id_: uuid.UUID) -> Subject | None: return Subject.from_orm(schema) return None + async def exist_by_id(self, subject_id: uuid.UUID) -> Subject: + """ + Checks if a subject exists and returns subject. If the subject does not exist, + SubjectNotFoundError will be raised + """ + subject = await self.get(subject_id) + if not subject: + raise SubjectNotFoundError(subject_id=str(subject_id)) + + return subject + async def create_relation( self, subject_id: uuid.UUID, source_subject_id: uuid.UUID, relation: str, meta: dict[str, Any] = {} ): From 7e8222053fd28dee6fea2e08e5c9ffe7aa2c28c7 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Tue, 8 Oct 2024 09:06:32 -0500 Subject: [PATCH 25/41] chore: Fix syntax error in e2e-tests.yaml --- .github/workflows/e2e-tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 77a45866e91..718c2f67698 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -87,7 +87,7 @@ jobs: core.setFailed('E2E tests failed') - name: Pass if tests pass - if: steps.e2e-tests.outcome = 'success' + if: steps.e2e-tests.outcome == 'success' uses: actions/github-script@v7 with: script: | From be21d8d2e8a8a83d20f25cb9153986d354b25b99 Mon Sep 17 00:00:00 2001 From: Billie He Date: Sun, 6 Oct 2024 10:58:55 -0700 Subject: [PATCH 26/41] feat: implement endpoints for respondent activities --- Makefile | 5 + docker-compose.yaml | 5 +- src/apps/activities/api/activities.py | 57 +++ src/apps/activities/router.py | 13 + src/apps/activities/tests/test_activities.py | 477 ++++++++++--------- src/apps/answers/crud/answers.py | 30 +- src/apps/answers/service.py | 9 + 7 files changed, 371 insertions(+), 225 deletions(-) diff --git a/Makefile b/Makefile index cd4e7071777..8b956c77d7d 100644 --- a/Makefile +++ b/Makefile @@ -51,6 +51,11 @@ migrate-arbitrary: cq: ${RUFF_COMMAND} check . && ${RUFF_COMMAND} format . && ${MYPY_COMMAND} . +# NOTE: cq == "Code quality fix" +.PHONY: cqf +cqf: + ${RUFF_COMMAND} format . && ${RUFF_COMMAND} check --fix . && ${MYPY_COMMAND} . + # ############### # Docker # ############### diff --git a/docker-compose.yaml b/docker-compose.yaml index cda70cef220..41b80843da8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,6 +28,7 @@ services: redis: image: redis + container_name: mindlogger_redis ports: - 6379:6379 @@ -35,13 +36,13 @@ services: stdin_open: true tty: true image: mindlogger_webapp + container_name: mindlogger_app build: context: . dockerfile: ./compose/fastapi/Dockerfile target: base args: - PIPENV_EXTRA_ARGS=--dev - container_name: mindlogger_app entrypoint: /fastapi-entrypoint command: /fastapi-start env_file: .env @@ -128,8 +129,8 @@ services: entrypoint: /etc/minio/create_bucket.sh opentelemetry: - container_name: mindlogger_opentelemetry image: otel/opentelemetry-collector + container_name: mindlogger_opentelemetry command: ["--config=/etc/otel-collector-config.yaml"] volumes: - "./compose/opentelemetry/otel-collector-config.yaml:/etc/otel-collector-config.yaml" diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index 016dfef8cbd..04ef5670c35 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -215,6 +215,63 @@ async def applet_activities_for_target_subject( ) +async def applet_activities_for_respondent_subject( + applet_id: uuid.UUID, + subject_id: uuid.UUID, + user: User = Depends(get_current_user), + language: str = Depends(get_language), + session=Depends(get_session), + answer_session=Depends(get_answer_session), +) -> ResponseMulti[ActivityOrFlowWithAssignmentsPublic]: + applet_service = AppletService(session, user.id) + await applet_service.exist_by_id(applet_id) + + await SubjectsService(session, user.id).exist_by_id(subject_id) + + # Restrict the endpoint access to owners, managers, coordinators, and assigned reviewers + await CheckAccessService(session, user.id).check_subject_subject_access(applet_id, subject_id) + + assignments = await ActivityAssignmentService(session).get_all_with_subject_entities( + applet_id, QueryParams(filters={"respondent_subject_id": subject_id}) + ) + + # Only one of these IDs will be `None` at a time, so the resulting type will be a list of UUIDs + activity_and_flow_ids_from_assignments: list[uuid.UUID] = [ + assignment.activity_id or assignment.activity_flow_id # type: ignore + for assignment in assignments + ] + + activity_and_flow_ids_from_submissions = await AnswerService( + session, user.id, answer_session + ).get_activity_and_flow_ids_by_source_subject(subject_id) + + activities_and_flows = await ActivityService(session, user.id).get_activity_and_flow_basic_info_by_ids_or_auto( + applet_id, activity_and_flow_ids_from_submissions + activity_and_flow_ids_from_assignments, language + ) + + result: list[ActivityOrFlowWithAssignmentsPublic] = [] + for activity_or_flow in activities_and_flows: + activity_or_flow_assignments = [ + assignment + for assignment in assignments + if assignment.activity_id == activity_or_flow.id or assignment.activity_flow_id == activity_or_flow.id + ] + + activity_or_flow.set_status(activity_or_flow_assignments) + + result.append( + ActivityOrFlowWithAssignmentsPublic( + **activity_or_flow.dict(), + assignments=activity_or_flow_assignments, + ) + ) + + return ResponseMulti( + result=result, + count=len(result), + ) + + async def applet_activities_and_flows( applet_id: uuid.UUID, user: User = Depends(get_current_user), diff --git a/src/apps/activities/router.py b/src/apps/activities/router.py index 8687c2bea62..c54145866dc 100644 --- a/src/apps/activities/router.py +++ b/src/apps/activities/router.py @@ -5,6 +5,7 @@ activity_retrieve, applet_activities, applet_activities_and_flows, + applet_activities_for_respondent_subject, applet_activities_for_subject, applet_activities_for_target_subject, public_activity_retrieve, @@ -120,3 +121,15 @@ **DEFAULT_OPENAPI_RESPONSE, }, )(applet_activities_for_target_subject) + +router.get( + "/applet/{applet_id}/respondent/{subject_id}", + description="""Get all assigned activities and activity flows for a respondent subject. + """, + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_200_OK: {"model": ResponseMulti[ActivityOrFlowWithAssignmentsPublic]}, + **AUTHENTICATION_ERROR_RESPONSES, + **DEFAULT_OPENAPI_RESPONSE, + }, +)(applet_activities_for_respondent_subject) diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 227123cb52d..8f3be5c5c03 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -277,6 +277,7 @@ class TestActivities: applet_update_url = "applets/{applet_id}" subject_assigned_activities_url = "/activities/applet/{applet_id}/subject/{subject_id}" target_assigned_activities_url = "/activities/applet/{applet_id}/target/{subject_id}" + respondent_assigned_activities_url = "/activities/applet/{applet_id}/respondent/{subject_id}" async def test_activity_detail(self, client, applet_one: AppletFull, tom: User): activity = applet_one.activities[0] @@ -1036,13 +1037,22 @@ async def test_subject_assigned_activities_auto_and_manually_assigned( assert flow_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) - async def test_target_assigned_activities_editor( - self, client, applet_one_lucy_editor, lucy, lucy_applet_one_subject + def _get_assigned_activities_rul(self, subject_type: str): + if subject_type == "target": + return self.target_assigned_activities_url + elif subject_type == "respondent": + return self.respondent_assigned_activities_url + else: + raise Exception(f"Invalid subject_type: {subject_type}") + + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_editor( + self, client, applet_one_lucy_editor, lucy, lucy_applet_one_subject, subject_type: str ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_one_lucy_editor.id, subject_id=lucy_applet_one_subject.id ) ) @@ -1053,13 +1063,14 @@ async def test_target_assigned_activities_editor( assert result[0]["type"] == "ACCESS_DENIED" assert result[0]["message"] == "Access denied." - async def test_target_assigned_activities_incorrect_reviewer( - self, client, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_incorrect_reviewer( + self, client, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject, subject_type: str ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_one_lucy_reviewer.id, subject_id=lucy_applet_one_subject.id ) ) @@ -1070,13 +1081,14 @@ async def test_target_assigned_activities_incorrect_reviewer( assert result[0]["type"] == "ACCESS_DENIED" assert result[0]["message"] == "Access denied." - async def test_target_assigned_activities_participant( - self, client, applet_one_lucy_respondent, lucy, lucy_applet_one_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_participant( + self, client, applet_one_lucy_respondent, lucy, lucy_applet_one_subject, subject_type: str ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_one_lucy_respondent.id, subject_id=lucy_applet_one_subject.id ) ) @@ -1087,13 +1099,20 @@ async def test_target_assigned_activities_participant( assert result[0]["type"] == "ACCESS_DENIED" assert result[0]["message"] == "Access denied." - async def test_target_assigned_activities_participant_other( - self, client, applet_one_lucy_respondent, lucy, applet_one_user_respondent, user_applet_one_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_participant_other( + self, + client, + applet_one_lucy_respondent, + lucy, + applet_one_user_respondent, + user_applet_one_subject, + subject_type: str, ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_one_lucy_respondent.id, subject_id=user_applet_one_subject.id ) ) @@ -1104,15 +1123,18 @@ async def test_target_assigned_activities_participant_other( assert result[0]["type"] == "ACCESS_DENIED" assert result[0]["message"] == "Access denied." - async def test_target_assigned_activities_invalid_applet( - self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_invalid_applet( + self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject, subject_type: str ): - client.login(lucy) - applet_id = uuid.uuid4() + client.login(lucy) + response = await client.get( - self.target_assigned_activities_url.format(applet_id=applet_id, subject_id=lucy_applet_one_subject.id) + self._get_assigned_activities_rul(subject_type).format( + applet_id=applet_id, subject_id=lucy_applet_one_subject.id + ) ) assert response.status_code == http.HTTPStatus.NOT_FOUND @@ -1121,15 +1143,18 @@ async def test_target_assigned_activities_invalid_applet( assert result[0]["type"] == "NOT_FOUND" assert result[0]["message"] == f"No such applets with id={applet_id}." - async def test_target_assigned_activities_invalid_subject( - self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_invalid_subject( + self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject, subject_type: str ): - client.login(lucy) - subject_id = uuid.uuid4() + client.login(lucy) + response = await client.get( - self.target_assigned_activities_url.format(applet_id=applet_one_lucy_manager.id, subject_id=subject_id) + self._get_assigned_activities_rul(subject_type).format( + applet_id=applet_one_lucy_manager.id, subject_id=subject_id + ) ) assert response.status_code == http.HTTPStatus.NOT_FOUND @@ -1138,13 +1163,14 @@ async def test_target_assigned_activities_invalid_subject( assert result[0]["type"] == "NOT_FOUND" assert result[0]["message"] == f"Subject with id {subject_id} not found" - async def test_target_assigned_activities_empty_applet( - self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_empty_applet( + self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject, subject_type: str ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=empty_applet_lucy_manager.id, subject_id=lucy_empty_applet_subject.id ) ) @@ -1154,13 +1180,14 @@ async def test_target_assigned_activities_empty_applet( assert result == [] - async def test_target_assigned_activities_auto_assigned( - self, client, applet_activity_flow_lucy_manager, lucy, lucy_applet_activity_flow_subject + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_auto_assigned( + self, client, applet_activity_flow_lucy_manager, lucy, lucy_applet_activity_flow_subject, subject_type: str ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_activity_flow_lucy_manager.id, subject_id=lucy_applet_activity_flow_subject.id ) ) @@ -1199,7 +1226,14 @@ async def test_target_assigned_activities_auto_assigned( assert activity_result["isPerformanceTask"] is False assert activity_result["performanceTaskType"] is None - async def test_target_assigned_activities_manually_assigned( + @pytest.mark.parametrize( + "subject_type,result_order", + [ + ("target", ["flow-1", "flow-3", "activity-1", "activity-3"]), + ("respondent", ["flow-1", "flow-2", "activity-1", "activity-2"]), + ], + ) + async def test_assigned_activities_manually_assigned( self, session, client, @@ -1208,9 +1242,9 @@ async def test_target_assigned_activities_manually_assigned( lucy_empty_applet_subject, user_empty_applet_subject, activity_create_session: ActivityCreate, + subject_type: str, + result_order: list[str], ): - client.login(lucy) - activities = await ActivityService(session, lucy.id).update_create( empty_applet_lucy_manager.id, [ @@ -1236,34 +1270,37 @@ async def test_target_assigned_activities_manually_assigned( ), ], ) - manual_activity_1 = next((activity for activity in activities if activity.name == "Manual Activity 1")) - manual_activity_2 = next((activity for activity in activities if activity.name == "Manual Activity 2")) - manual_activity_3 = next((activity for activity in activities if activity.name == "Manual Activity 3")) - manual_activity_4 = next((activity for activity in activities if activity.name == "Manual Activity 4")) + + activity_by_number = { + 1: next((activity for activity in activities if activity.name == "Manual Activity 1")), + 2: next((activity for activity in activities if activity.name == "Manual Activity 2")), + 3: next((activity for activity in activities if activity.name == "Manual Activity 3")), + 4: next((activity for activity in activities if activity.name == "Manual Activity 4")), + } await ActivityAssignmentService(session).create_many( empty_applet_lucy_manager.id, [ ActivityAssignmentCreate( - activity_id=manual_activity_1.id, + activity_id=activity_by_number[1].id, activity_flow_id=None, respondent_subject_id=user_empty_applet_subject.id, target_subject_id=user_empty_applet_subject.id, ), ActivityAssignmentCreate( - activity_id=manual_activity_2.id, + activity_id=activity_by_number[2].id, activity_flow_id=None, respondent_subject_id=user_empty_applet_subject.id, target_subject_id=lucy_empty_applet_subject.id, ), ActivityAssignmentCreate( - activity_id=manual_activity_3.id, + activity_id=activity_by_number[3].id, activity_flow_id=None, respondent_subject_id=lucy_empty_applet_subject.id, target_subject_id=user_empty_applet_subject.id, ), ActivityAssignmentCreate( - activity_id=manual_activity_4.id, + activity_id=activity_by_number[4].id, activity_flow_id=None, respondent_subject_id=lucy_empty_applet_subject.id, target_subject_id=lucy_empty_applet_subject.id, @@ -1278,72 +1315,76 @@ async def test_target_assigned_activities_manually_assigned( name="Manual Flow 1", description={Language.ENGLISH: "Manual Flow"}, auto_assign=False, - items=[ActivityFlowItemUpdate(activity_key=manual_activity_1.key)], + items=[ActivityFlowItemUpdate(activity_key=activity_by_number[1].key)], ), FlowUpdate( name="Manual Flow 2", description={Language.ENGLISH: "Manual Flow"}, auto_assign=False, - items=[ActivityFlowItemUpdate(activity_key=manual_activity_2.key)], + items=[ActivityFlowItemUpdate(activity_key=activity_by_number[2].key)], ), FlowUpdate( name="Manual Flow 3", description={Language.ENGLISH: "Manual Flow"}, auto_assign=False, - items=[ActivityFlowItemUpdate(activity_key=manual_activity_3.key)], + items=[ActivityFlowItemUpdate(activity_key=activity_by_number[3].key)], ), FlowUpdate( name="Manual Flow 4", description={Language.ENGLISH: "Manual Flow"}, auto_assign=False, - items=[ActivityFlowItemUpdate(activity_key=manual_activity_4.key)], + items=[ActivityFlowItemUpdate(activity_key=activity_by_number[4].key)], ), ], { - manual_activity_1.key: manual_activity_1.id, - manual_activity_2.key: manual_activity_2.id, - manual_activity_3.key: manual_activity_3.id, - manual_activity_4.key: manual_activity_4.id, + activity_by_number[1].key: activity_by_number[1].id, + activity_by_number[2].key: activity_by_number[2].id, + activity_by_number[3].key: activity_by_number[3].id, + activity_by_number[4].key: activity_by_number[4].id, }, ) - manual_flow_1 = next((flow for flow in flows if flow.name == "Manual Flow 1")) - manual_flow_2 = next((flow for flow in flows if flow.name == "Manual Flow 2")) - manual_flow_3 = next((flow for flow in flows if flow.name == "Manual Flow 3")) - manual_flow_4 = next((flow for flow in flows if flow.name == "Manual Flow 4")) + flow_by_number = { + 1: next((flow for flow in flows if flow.name == "Manual Flow 1")), + 2: next((flow for flow in flows if flow.name == "Manual Flow 2")), + 3: next((flow for flow in flows if flow.name == "Manual Flow 3")), + 4: next((flow for flow in flows if flow.name == "Manual Flow 4")), + } await ActivityAssignmentService(session).create_many( empty_applet_lucy_manager.id, [ ActivityAssignmentCreate( activity_id=None, - activity_flow_id=manual_flow_1.id, + activity_flow_id=flow_by_number[1].id, respondent_subject_id=user_empty_applet_subject.id, target_subject_id=user_empty_applet_subject.id, ), ActivityAssignmentCreate( activity_id=None, - activity_flow_id=manual_flow_2.id, + activity_flow_id=flow_by_number[2].id, respondent_subject_id=user_empty_applet_subject.id, target_subject_id=lucy_empty_applet_subject.id, ), ActivityAssignmentCreate( activity_id=None, - activity_flow_id=manual_flow_3.id, + activity_flow_id=flow_by_number[3].id, respondent_subject_id=lucy_empty_applet_subject.id, target_subject_id=user_empty_applet_subject.id, ), ActivityAssignmentCreate( activity_id=None, - activity_flow_id=manual_flow_4.id, + activity_flow_id=flow_by_number[4].id, respondent_subject_id=lucy_empty_applet_subject.id, target_subject_id=lucy_empty_applet_subject.id, ), ], ) + client.login(lucy) + response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id ) ) @@ -1353,95 +1394,100 @@ async def test_target_assigned_activities_manually_assigned( assert len(result) == 4 - flow_result_1 = next( - (flow_result for flow_result in [result[0], result[1]] if flow_result["id"] == str(manual_flow_1.id)) - ) - assert flow_result_1["id"] == str(manual_flow_1.id) - assert flow_result_1["name"] == manual_flow_1.name - assert flow_result_1["description"] == manual_flow_1.description[Language.ENGLISH] - assert flow_result_1["isFlow"] is True - assert flow_result_1["status"] == ActivityOrFlowStatusEnum.ACTIVE.value - assert flow_result_1["autoAssign"] is False - assert flow_result_1["activityIds"][0] == str(manual_flow_1.items[0].activity_id) - assert flow_result_1["isPerformanceTask"] is None - assert flow_result_1["performanceTaskType"] is None - - assert len(flow_result_1["assignments"]) == 1 - flow_assignment_1 = flow_result_1["assignments"][0] - assert flow_assignment_1["activityFlowId"] == str(manual_flow_1.id) - assert flow_assignment_1["targetSubject"]["id"] == str(user_empty_applet_subject.id) - - flow_result_2 = next( - (flow_result for flow_result in [result[0], result[1]] if flow_result["id"] == str(manual_flow_3.id)) - ) - assert flow_result_2["id"] == str(manual_flow_3.id) - assert flow_result_2["name"] == manual_flow_3.name - assert flow_result_2["description"] == manual_flow_3.description[Language.ENGLISH] - assert flow_result_2["isFlow"] is True - assert flow_result_2["status"] == ActivityOrFlowStatusEnum.ACTIVE.value - assert flow_result_2["autoAssign"] is False - assert flow_result_2["activityIds"][0] == str(manual_flow_3.items[0].activity_id) - assert flow_result_2["isPerformanceTask"] is None - assert flow_result_2["performanceTaskType"] is None - - assert len(flow_result_2["assignments"]) == 1 - flow_assignment_2 = flow_result_2["assignments"][0] - assert flow_assignment_2["activityFlowId"] == str(manual_flow_3.id) - assert flow_assignment_2["targetSubject"]["id"] == str(user_empty_applet_subject.id) - - activity_result_1 = next( - ( - activity_result - for activity_result in [result[2], result[3]] - if activity_result["id"] == str(manual_activity_1.id) + spec_activity_numbers: list[int] = [] + spec_flow_numbers: list[int] = [] + spec_entity_identifiers: list[str] = [] + + for spec_order in result_order: + order_spec_parts = spec_order.split("-", 1) + order_spec_type = order_spec_parts[0] + order_spec_number = int(order_spec_parts[1]) + order_spec_id: uuid.UUID + if order_spec_type == "flow": + order_spec_id = flow_by_number[order_spec_number].id + spec_flow_numbers.append(order_spec_number) + elif order_spec_type == "activity": + order_spec_id = activity_by_number[order_spec_number].id + spec_activity_numbers.append(order_spec_number) + else: + raise Exception(f"Invalid order spec type: {order_spec_type}") + entity_identifier = f"{order_spec_type}-{order_spec_id}" + spec_entity_identifiers.append(entity_identifier) + + result_entity_identifiers: list[str] = [] + for entity in result: + entity_type = "flow" if entity["isFlow"] else "activity" + entity_id = entity["id"] + entity_identifier = f"{entity_type}-{entity_id}" + result_entity_identifiers.append(entity_identifier) + + # The `==` operator will deeply compare the 2 lists + assert result_entity_identifiers == spec_entity_identifiers + + for spec_activity_number in spec_activity_numbers: + spec_activity = activity_by_number[spec_activity_number] + + result_activity = next( + (entity for entity in result if not entity["isFlow"] and entity["id"] == str(spec_activity.id)) ) - ) - assert activity_result_1["id"] == str(manual_activity_1.id) - assert activity_result_1["name"] == manual_activity_1.name - assert activity_result_1["description"] == manual_activity_1.description[Language.ENGLISH] - assert activity_result_1["isFlow"] is False - assert activity_result_1["status"] == ActivityOrFlowStatusEnum.ACTIVE.value - assert activity_result_1["autoAssign"] is False - assert activity_result_1["activityIds"] is None - assert activity_result_1["isPerformanceTask"] is False - assert activity_result_1["performanceTaskType"] is None - - assert len(activity_result_1["assignments"]) == 1 - activity_assignment = activity_result_1["assignments"][0] - assert activity_assignment["activityId"] == str(manual_activity_1.id) - assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) - activity_result_2 = next( - ( - activity_result - for activity_result in [result[2], result[3]] - if activity_result["id"] == str(manual_activity_3.id) - ) - ) - assert activity_result_2["id"] == str(manual_activity_3.id) - assert activity_result_2["name"] == manual_activity_3.name - assert activity_result_2["description"] == manual_activity_3.description[Language.ENGLISH] - assert activity_result_2["isFlow"] is False - assert activity_result_2["status"] == ActivityOrFlowStatusEnum.ACTIVE.value - assert activity_result_2["autoAssign"] is False - assert activity_result_2["activityIds"] is None - assert activity_result_2["isPerformanceTask"] is False - assert activity_result_2["performanceTaskType"] is None - - assert len(activity_result_2["assignments"]) == 1 - activity_assignment = activity_result_2["assignments"][0] - assert activity_assignment["activityId"] == str(manual_activity_3.id) - assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) + assert result_activity is not None + + assert result_activity["id"] == str(spec_activity.id) + assert result_activity["name"] == spec_activity.name + assert result_activity["description"] == spec_activity.description[Language.ENGLISH] + assert result_activity["isFlow"] is False + assert result_activity["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert result_activity["autoAssign"] is False + assert result_activity["activityIds"] is None + assert result_activity["isPerformanceTask"] is False + assert result_activity["performanceTaskType"] is None + + assert len(result_activity["assignments"]) == 1 + + activity_assignment = result_activity["assignments"][0] + assert activity_assignment["activityId"] == str(spec_activity.id) + + subject_attr = "targetSubject" if subject_type == "target" else "respondentSubject" + assert activity_assignment[subject_attr]["id"] == str(user_empty_applet_subject.id) + + for spec_flow_number in spec_flow_numbers: + spec_flow = flow_by_number[spec_flow_number] + + result_flow = next((entity for entity in result if entity["isFlow"] and entity["id"] == str(spec_flow.id))) - async def test_target_assigned_activity_from_submission( + assert result_flow is not None + + assert result_flow["id"] == str(spec_flow.id) + assert result_flow["name"] == spec_flow.name + assert result_flow["description"] == spec_flow.description[Language.ENGLISH] + assert result_flow["isFlow"] is True + assert result_flow["status"] == ActivityOrFlowStatusEnum.ACTIVE.value + assert result_flow["autoAssign"] is False + assert result_flow["activityIds"][0] == str(spec_flow.items[0].activity_id) + assert result_flow["isPerformanceTask"] is None + assert result_flow["performanceTaskType"] is None + + assert len(result_flow["assignments"]) == 1 + + flow_assignment = result_flow["assignments"][0] + assert flow_assignment["activityFlowId"] == str(spec_flow.id) + + subject_attr = "targetSubject" if subject_type == "target" else "respondentSubject" + assert flow_assignment[subject_attr]["id"] == str(user_empty_applet_subject.id) + + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_activities_from_submission( self, session, client, tom: User, tom_applet_one_subject: Subject, + lucy: User, lucy_applet_one_subject: Subject, applet_one_lucy_respondent: AppletFull, answer_create_payload: dict, + subject_type: str, ): activity = applet_one_lucy_respondent.activities[0] @@ -1457,41 +1503,49 @@ async def test_target_assigned_activity_from_submission( ], ) + target_subject_id, respondent_subject_id = ( + [tom_applet_one_subject.id, lucy_applet_one_subject.id] + if subject_type == "target" + else [lucy_applet_one_subject.id, tom_applet_one_subject.id] + ) + # Create an activity answer await AnswerService(session, tom.id).create_answer( AppletAnswerCreate( **answer_create_payload, - input_subject_id=lucy_applet_one_subject.id, - source_subject_id=lucy_applet_one_subject.id, - target_subject_id=tom_applet_one_subject.id, + input_subject_id=respondent_subject_id, + source_subject_id=respondent_subject_id, + target_subject_id=target_subject_id, ) ) client.login(tom) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_one_lucy_respondent.id, subject_id=tom_applet_one_subject.id ) ) + assert response.status_code == http.HTTPStatus.OK result = response.json()["result"] assert len(result) == 1 - activity_result = result[0] - assert activity_result["id"] == str(activity.id) - assert activity_result["name"] == activity.name - assert activity_result["description"] == activity.description[Language.ENGLISH] - assert activity_result["status"] == ActivityOrFlowStatusEnum.INACTIVE.value - assert activity_result["isFlow"] is False - assert activity_result["autoAssign"] is False - assert activity_result["activityIds"] is None - assert activity_result["isPerformanceTask"] is False - assert activity_result["performanceTaskType"] is None - assert len(activity_result["assignments"]) == 0 - - async def test_target_assigned_hidden_activity( + result_activity = result[0] + assert result_activity["id"] == str(activity.id) + assert result_activity["name"] == activity.name + assert result_activity["description"] == activity.description[Language.ENGLISH] + assert result_activity["status"] == ActivityOrFlowStatusEnum.INACTIVE.value + assert result_activity["isFlow"] is False + assert result_activity["autoAssign"] is False + assert result_activity["activityIds"] is None + assert result_activity["isPerformanceTask"] is False + assert result_activity["performanceTaskType"] is None + assert len(result_activity["assignments"]) == 0 + + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_hidden_activities( self, session, client, @@ -1500,9 +1554,8 @@ async def test_target_assigned_hidden_activity( lucy_empty_applet_subject, user_empty_applet_subject, activity_create_session: ActivityCreate, + subject_type: str, ): - client.login(lucy) - activities = await ActivityService(session, lucy.id).update_create( empty_applet_lucy_manager.id, [ @@ -1558,7 +1611,6 @@ async def test_target_assigned_hidden_activity( manual_activity.key: manual_activity.id, }, ) - auto_flow = next((flow for flow in flows if flow.name == "Auto Flow")) manual_flow = next((flow for flow in flows if flow.name == "Manual Flow")) @@ -1574,9 +1626,12 @@ async def test_target_assigned_hidden_activity( ], ) + client.login(lucy) + response = await client.get( - self.target_assigned_activities_url.format( - applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id + self._get_assigned_activities_rul(subject_type).format( + applet_id=empty_applet_lucy_manager.id, + subject_id=user_empty_applet_subject.id if subject_type == "target" else lucy_empty_applet_subject.id, ) ) @@ -1585,69 +1640,62 @@ async def test_target_assigned_hidden_activity( assert len(result) == 4 - flow_result_1 = result[0] - flow_result_2 = result[1] - - manual_flow_result = flow_result_1 if not flow_result_1["autoAssign"] else flow_result_2 - assert manual_flow_result["id"] == str(manual_flow.id) - assert manual_flow_result["name"] == manual_flow.name - assert manual_flow_result["description"] == manual_flow.description[Language.ENGLISH] - assert manual_flow_result["isFlow"] is True - assert manual_flow_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value - assert manual_flow_result["autoAssign"] is False - assert manual_flow_result["activityIds"][0] == str(manual_flow.items[0].activity_id) - assert manual_flow_result["isPerformanceTask"] is None - assert manual_flow_result["performanceTaskType"] is None - - assert len(manual_flow_result["assignments"]) == 1 - flow_assignment = manual_flow_result["assignments"][0] + result_flow_manual = next((entity for entity in result if entity["isFlow"] and not entity["autoAssign"])) + assert result_flow_manual + assert result_flow_manual["id"] == str(manual_flow.id) + assert result_flow_manual["name"] == manual_flow.name + assert result_flow_manual["description"] == manual_flow.description[Language.ENGLISH] + assert result_flow_manual["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert result_flow_manual["activityIds"][0] == str(manual_flow.items[0].activity_id) + assert result_flow_manual["isPerformanceTask"] is None + assert result_flow_manual["performanceTaskType"] is None + + assert len(result_flow_manual["assignments"]) == 1 + flow_assignment = result_flow_manual["assignments"][0] assert flow_assignment["activityFlowId"] == str(manual_flow.id) assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) - auto_flow_result = flow_result_1 if flow_result_1["autoAssign"] else flow_result_2 - assert auto_flow_result["id"] == str(auto_flow.id) - assert auto_flow_result["name"] == auto_flow.name - assert auto_flow_result["description"] == auto_flow.description[Language.ENGLISH] - assert auto_flow_result["isFlow"] is True - assert auto_flow_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value - assert auto_flow_result["autoAssign"] is True - assert auto_flow_result["activityIds"][0] == str(auto_flow.items[0].activity_id) - assert auto_flow_result["isPerformanceTask"] is None - assert auto_flow_result["performanceTaskType"] is None - assert len(auto_flow_result["assignments"]) == 0 - - activity_result_1 = result[2] - activity_result_2 = result[3] - - manual_activity_result = activity_result_1 if not activity_result_1["autoAssign"] else activity_result_2 - assert manual_activity_result["id"] == str(manual_activity.id) - assert manual_activity_result["name"] == manual_activity.name - assert manual_activity_result["description"] == manual_activity.description[Language.ENGLISH] - assert manual_activity_result["isFlow"] is False - assert manual_activity_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value - assert manual_activity_result["autoAssign"] is False - assert manual_activity_result["activityIds"] is None - assert manual_activity_result["isPerformanceTask"] is False - assert manual_activity_result["performanceTaskType"] is None - - assert len(manual_activity_result["assignments"]) == 1 - activity_assignment = manual_activity_result["assignments"][0] + result_flow_auto = next((entity for entity in result if entity["isFlow"] and entity["autoAssign"])) + assert result_flow_auto + assert result_flow_auto["id"] == str(auto_flow.id) + assert result_flow_auto["name"] == auto_flow.name + assert result_flow_auto["description"] == auto_flow.description[Language.ENGLISH] + assert result_flow_auto["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert result_flow_auto["activityIds"][0] == str(auto_flow.items[0].activity_id) + assert result_flow_auto["isPerformanceTask"] is None + assert result_flow_auto["performanceTaskType"] is None + assert len(result_flow_auto["assignments"]) == 0 + + result_activity_manual = next( + (entity for entity in result if not entity["isFlow"] and not entity["autoAssign"]) + ) + assert result_activity_manual + assert result_activity_manual["id"] == str(manual_activity.id) + assert result_activity_manual["name"] == manual_activity.name + assert result_activity_manual["description"] == manual_activity.description[Language.ENGLISH] + assert result_activity_manual["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert result_activity_manual["activityIds"] is None + assert result_activity_manual["isPerformanceTask"] is False + assert result_activity_manual["performanceTaskType"] is None + + assert len(result_activity_manual["assignments"]) == 1 + activity_assignment = result_activity_manual["assignments"][0] assert activity_assignment["activityId"] == str(manual_activity.id) assert activity_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) - auto_activity_result = activity_result_1 if activity_result_1["autoAssign"] else activity_result_2 - assert auto_activity_result["id"] == str(auto_activity.id) - assert auto_activity_result["name"] == auto_activity.name - assert auto_activity_result["description"] == auto_activity.description[Language.ENGLISH] - assert auto_activity_result["isFlow"] is False - assert auto_activity_result["status"] == ActivityOrFlowStatusEnum.HIDDEN.value - assert auto_activity_result["autoAssign"] is True - assert auto_activity_result["activityIds"] is None - assert auto_activity_result["isPerformanceTask"] is False - assert auto_activity_result["performanceTaskType"] is None - assert len(auto_activity_result["assignments"]) == 0 - - async def test_target_assigned_performance_task( + result_activity_auto = next((entity for entity in result if not entity["isFlow"] and entity["autoAssign"])) + assert result_activity_auto + assert result_activity_auto["id"] == str(auto_activity.id) + assert result_activity_auto["name"] == auto_activity.name + assert result_activity_auto["description"] == auto_activity.description[Language.ENGLISH] + assert result_activity_auto["status"] == ActivityOrFlowStatusEnum.HIDDEN.value + assert result_activity_auto["activityIds"] is None + assert result_activity_auto["isPerformanceTask"] is False + assert result_activity_auto["performanceTaskType"] is None + assert len(result_activity_auto["assignments"]) == 0 + + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) + async def test_assigned_performance_tasks( self, session, client, @@ -1655,11 +1703,12 @@ async def test_target_assigned_performance_task( lucy, lucy_applet_with_all_performance_tasks_subject, user_applet_with_all_performance_tasks_subject, + subject_type: str, ): client.login(lucy) response = await client.get( - self.target_assigned_activities_url.format( + self._get_assigned_activities_rul(subject_type).format( applet_id=applet_with_all_performance_tasks_lucy_manager.id, subject_id=user_applet_with_all_performance_tasks_subject.id, ) diff --git a/src/apps/answers/crud/answers.py b/src/apps/answers/crud/answers.py index 3bfacefe611..14b5b1bce76 100644 --- a/src/apps/answers/crud/answers.py +++ b/src/apps/answers/crud/answers.py @@ -6,7 +6,7 @@ from pydantic import parse_obj_as from sqlalchemy import Text, and_, case, column, delete, func, null, or_, select, text, update from sqlalchemy.dialects.postgresql import UUID, insert -from sqlalchemy.orm import Query, aliased, contains_eager +from sqlalchemy.orm import InstrumentedAttribute, Query, aliased, contains_eager from sqlalchemy.sql import Values from sqlalchemy.sql.elements import BooleanClauseList @@ -959,10 +959,8 @@ async def get_target_subject_ids_by_respondent( res = await self._execute(query) return res.all() - async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[uuid.UUID]: - """ - Get a list of activity and flow IDs based on answers submitted for a target subject - """ + @staticmethod + def __activity_and_flow_ids_by_subject_query(subject_column: InstrumentedAttribute, subject_id: uuid.UUID) -> Query: query: Query = ( select( case( @@ -973,11 +971,25 @@ async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: u else_=AnswerSchema.id_from_history_id(AnswerSchema.activity_history_id), ).label("id") ) - .where( - AnswerSchema.target_subject_id == target_subject_id, - ) + .where(subject_column == subject_id) .distinct() ) + return query - res = await self._execute(query) + async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: uuid.UUID) -> list[uuid.UUID]: + """ + Get a list of activity and flow IDs based on answers submitted for a target subject + """ + res = await self._execute( + self.__activity_and_flow_ids_by_subject_query(AnswerSchema.target_subject_id, target_subject_id) + ) + return res.scalars().all() + + async def get_activity_and_flow_ids_by_source_subject(self, source_subject_id: uuid.UUID) -> list[uuid.UUID]: + """ + Get a list of activity and flow IDs based on answers submitted for a source subject + """ + res = await self._execute( + self.__activity_and_flow_ids_by_subject_query(AnswerSchema.source_subject_id, source_subject_id) + ) return res.scalars().all() diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index be6f964552d..37b1a05bbc0 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1869,6 +1869,15 @@ async def get_activity_and_flow_ids_by_target_subject(self, target_subject_id: u """ return await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_target_subject(target_subject_id) + async def get_activity_and_flow_ids_by_source_subject(self, source_subject_id: uuid.UUID) -> list[uuid.UUID]: + """ + Get a list of activity and flow IDs based on answers submitted for a source subject + + The data returned is just a combined list of activity and flow IDs, without any + distinction between the two + """ + return await AnswersCRUD(self.answer_session).get_activity_and_flow_ids_by_source_subject(source_subject_id) + class ReportServerService: def __init__(self, session, arbitrary_session=None): From a5d8cb0617d0649cc9dd45ffe079cda81f7937d7 Mon Sep 17 00:00:00 2001 From: Billie He Date: Mon, 7 Oct 2024 10:11:14 -0700 Subject: [PATCH 27/41] fix: some typo --- Makefile | 2 +- src/apps/activities/tests/test_activities.py | 26 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 8b956c77d7d..438676997ab 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ migrate-arbitrary: cq: ${RUFF_COMMAND} check . && ${RUFF_COMMAND} format . && ${MYPY_COMMAND} . -# NOTE: cq == "Code quality fix" +# NOTE: cqf == "Code quality fix" .PHONY: cqf cqf: ${RUFF_COMMAND} format . && ${RUFF_COMMAND} check --fix . && ${MYPY_COMMAND} . diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 8f3be5c5c03..6fbcba09bb2 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -1037,7 +1037,7 @@ async def test_subject_assigned_activities_auto_and_manually_assigned( assert flow_assignment["respondentSubject"]["id"] == str(user_empty_applet_subject.id) assert flow_assignment["targetSubject"]["id"] == str(user_empty_applet_subject.id) - def _get_assigned_activities_rul(self, subject_type: str): + def _get_assigned_activities_url(self, subject_type: str): if subject_type == "target": return self.target_assigned_activities_url elif subject_type == "respondent": @@ -1052,7 +1052,7 @@ async def test_assigned_activities_editor( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_one_lucy_editor.id, subject_id=lucy_applet_one_subject.id ) ) @@ -1070,7 +1070,7 @@ async def test_assigned_activities_incorrect_reviewer( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_one_lucy_reviewer.id, subject_id=lucy_applet_one_subject.id ) ) @@ -1088,7 +1088,7 @@ async def test_assigned_activities_participant( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_one_lucy_respondent.id, subject_id=lucy_applet_one_subject.id ) ) @@ -1112,7 +1112,7 @@ async def test_assigned_activities_participant_other( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_one_lucy_respondent.id, subject_id=user_applet_one_subject.id ) ) @@ -1132,7 +1132,7 @@ async def test_assigned_activities_invalid_applet( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_id, subject_id=lucy_applet_one_subject.id ) ) @@ -1152,7 +1152,7 @@ async def test_assigned_activities_invalid_subject( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_one_lucy_manager.id, subject_id=subject_id ) ) @@ -1170,7 +1170,7 @@ async def test_assigned_activities_empty_applet( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=empty_applet_lucy_manager.id, subject_id=lucy_empty_applet_subject.id ) ) @@ -1187,7 +1187,7 @@ async def test_assigned_activities_auto_assigned( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_activity_flow_lucy_manager.id, subject_id=lucy_applet_activity_flow_subject.id ) ) @@ -1384,7 +1384,7 @@ async def test_assigned_activities_manually_assigned( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id ) ) @@ -1522,7 +1522,7 @@ async def test_assigned_activities_from_submission( client.login(tom) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_one_lucy_respondent.id, subject_id=tom_applet_one_subject.id ) ) @@ -1629,7 +1629,7 @@ async def test_assigned_hidden_activities( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=empty_applet_lucy_manager.id, subject_id=user_empty_applet_subject.id if subject_type == "target" else lucy_empty_applet_subject.id, ) @@ -1708,7 +1708,7 @@ async def test_assigned_performance_tasks( client.login(lucy) response = await client.get( - self._get_assigned_activities_rul(subject_type).format( + self._get_assigned_activities_url(subject_type).format( applet_id=applet_with_all_performance_tasks_lucy_manager.id, subject_id=user_applet_with_all_performance_tasks_subject.id, ) From ae0f51aad8d6b9dbad1300a3ebad4fd254f0701c Mon Sep 17 00:00:00 2001 From: Billie He Date: Mon, 7 Oct 2024 10:55:10 -0700 Subject: [PATCH 28/41] fix: check for limited account respondents --- src/apps/activities/api/activities.py | 8 +- src/apps/activities/tests/test_activities.py | 85 ++++++++++++++------ src/apps/applets/tests/fixtures/applets.py | 18 ++++- src/apps/subjects/api.py | 1 + 4 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/apps/activities/api/activities.py b/src/apps/activities/api/activities.py index 04ef5670c35..1a2e807b62a 100644 --- a/src/apps/activities/api/activities.py +++ b/src/apps/activities/api/activities.py @@ -26,6 +26,7 @@ from apps.applets.service import AppletService from apps.authentication.deps import get_current_user from apps.shared.domain import Response, ResponseMulti +from apps.shared.exception import ValidationError from apps.shared.query_params import QueryParams, parse_query_params from apps.subjects.services import SubjectsService from apps.users import User @@ -226,7 +227,12 @@ async def applet_activities_for_respondent_subject( applet_service = AppletService(session, user.id) await applet_service.exist_by_id(applet_id) - await SubjectsService(session, user.id).exist_by_id(subject_id) + subject = await SubjectsService(session, user.id).exist_by_id(subject_id) + + # Ensure the respondent is not a limited account + if subject.user_id is None: + # Return a generic bad request error to avoid leaking information + raise ValidationError(f"Subject {subject_id} is not a valid respondent") # Restrict the endpoint access to owners, managers, coordinators, and assigned reviewers await CheckAccessService(session, user.id).check_subject_subject_access(applet_id, subject_id) diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 6fbcba09bb2..59a33e16968 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -29,6 +29,7 @@ from apps.applets.tests.fixtures.applets import _get_or_create_applet from apps.applets.tests.utils import teardown_applet from apps.shared.enums import Language +from apps.shared.test.client import TestClient from apps.subjects.db.schemas import SubjectSchema from apps.subjects.domain import Subject from apps.themes.domain import Theme @@ -1047,7 +1048,12 @@ def _get_assigned_activities_url(self, subject_type: str): @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_editor( - self, client, applet_one_lucy_editor, lucy, lucy_applet_one_subject, subject_type: str + self, + client: TestClient, + applet_one_lucy_editor: AppletFull, + lucy: User, + lucy_applet_one_subject: AppletFull, + subject_type: str, ): client.login(lucy) @@ -1065,7 +1071,7 @@ async def test_assigned_activities_editor( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_incorrect_reviewer( - self, client, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject, subject_type: str + self, client: TestClient, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject, subject_type: str ): client.login(lucy) @@ -1083,7 +1089,7 @@ async def test_assigned_activities_incorrect_reviewer( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_participant( - self, client, applet_one_lucy_respondent, lucy, lucy_applet_one_subject, subject_type: str + self, client: TestClient, applet_one_lucy_respondent, lucy, lucy_applet_one_subject, subject_type: str ): client.login(lucy) @@ -1102,7 +1108,7 @@ async def test_assigned_activities_participant( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_participant_other( self, - client, + client: TestClient, applet_one_lucy_respondent, lucy, applet_one_user_respondent, @@ -1125,7 +1131,7 @@ async def test_assigned_activities_participant_other( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_invalid_applet( - self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject, subject_type: str + self, client: TestClient, applet_one_lucy_manager, lucy, lucy_applet_one_subject, subject_type: str ): applet_id = uuid.uuid4() @@ -1145,7 +1151,7 @@ async def test_assigned_activities_invalid_applet( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_invalid_subject( - self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject, subject_type: str + self, client: TestClient, applet_one_lucy_manager, lucy, lucy_applet_one_subject, subject_type: str ): subject_id = uuid.uuid4() @@ -1165,7 +1171,7 @@ async def test_assigned_activities_invalid_subject( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_empty_applet( - self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject, subject_type: str + self, client: TestClient, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject, subject_type: str ): client.login(lucy) @@ -1180,9 +1186,36 @@ async def test_assigned_activities_empty_applet( assert result == [] + async def test_assigned_activities_limited_respondent( + self, + session, + client: TestClient, + tom: User, + applet_one: AppletFull, + applet_one_shell_account: Subject, + ): + client.login(tom) + + response = await client.get( + self.respondent_assigned_activities_url.format( + applet_id=applet_one.id, subject_id=applet_one_shell_account.id + ) + ) + + assert response.status_code == http.HTTPStatus.BAD_REQUEST + result = response.json()["result"] + + assert result[0]["type"] == "BAD_REQUEST" + assert result[0]["message"] == f"Subject {applet_one_shell_account.id} is not a valid respondent" + @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_auto_assigned( - self, client, applet_activity_flow_lucy_manager, lucy, lucy_applet_activity_flow_subject, subject_type: str + self, + client: TestClient, + lucy: User, + applet_activity_flow_lucy_manager: AppletFull, + lucy_applet_activity_flow_subject: Subject, + subject_type: str, ): client.login(lucy) @@ -1236,11 +1269,11 @@ async def test_assigned_activities_auto_assigned( async def test_assigned_activities_manually_assigned( self, session, - client, - empty_applet_lucy_manager, - lucy, - lucy_empty_applet_subject, - user_empty_applet_subject, + client: TestClient, + lucy: User, + empty_applet_lucy_manager: AppletFull, + lucy_empty_applet_subject: Subject, + user_empty_applet_subject: Subject, activity_create_session: ActivityCreate, subject_type: str, result_order: list[str], @@ -1480,12 +1513,12 @@ async def test_assigned_activities_manually_assigned( async def test_assigned_activities_from_submission( self, session, - client, + client: TestClient, tom: User, - tom_applet_one_subject: Subject, lucy: User, - lucy_applet_one_subject: Subject, applet_one_lucy_respondent: AppletFull, + tom_applet_one_subject: Subject, + lucy_applet_one_subject: Subject, answer_create_payload: dict, subject_type: str, ): @@ -1548,11 +1581,11 @@ async def test_assigned_activities_from_submission( async def test_assigned_hidden_activities( self, session, - client, - empty_applet_lucy_manager, - lucy, - lucy_empty_applet_subject, - user_empty_applet_subject, + client: TestClient, + lucy: User, + empty_applet_lucy_manager: AppletFull, + lucy_empty_applet_subject: Subject, + user_empty_applet_subject: Subject, activity_create_session: ActivityCreate, subject_type: str, ): @@ -1698,11 +1731,11 @@ async def test_assigned_hidden_activities( async def test_assigned_performance_tasks( self, session, - client, - applet_with_all_performance_tasks_lucy_manager, - lucy, - lucy_applet_with_all_performance_tasks_subject, - user_applet_with_all_performance_tasks_subject, + client: TestClient, + lucy: User, + applet_with_all_performance_tasks_lucy_manager: AppletFull, + lucy_applet_with_all_performance_tasks_subject: Subject, + user_applet_with_all_performance_tasks_subject: Subject, subject_type: str, ): client.login(lucy) diff --git a/src/apps/applets/tests/fixtures/applets.py b/src/apps/applets/tests/fixtures/applets.py index 0bab6921e29..21130e4685f 100644 --- a/src/apps/applets/tests/fixtures/applets.py +++ b/src/apps/applets/tests/fixtures/applets.py @@ -20,7 +20,8 @@ from apps.applets.tests.utils import teardown_applet from apps.shared.enums import Language from apps.subjects.db.schemas import SubjectSchema -from apps.subjects.domain import Subject +from apps.subjects.domain import Subject, SubjectCreate +from apps.subjects.services import SubjectsService from apps.themes.service import ThemeService from apps.users.domain import User from apps.workspaces.domain.constants import Role @@ -292,6 +293,21 @@ async def applet_two_lucy_respondent( return applet_two +@pytest.fixture +async def applet_one_shell_account(session: AsyncSession, applet_one: AppletFull, tom: User) -> Subject: + return await SubjectsService(session, tom.id).create( + SubjectCreate( + applet_id=applet_one.id, + creator_id=tom.id, + first_name="Shell", + last_name="Account", + nickname="shell-account-0", + secret_user_id=f"{uuid.uuid4()}", + email="shell@mail.com", + ) + ) + + @pytest.fixture async def applet_lucy_respondent(session: AsyncSession, applet: AppletFull, user: User, lucy: User) -> AppletFull: await UserAppletAccessService(session, user.id, applet.id).add_role(lucy.id, Role.RESPONDENT) diff --git a/src/apps/subjects/api.py b/src/apps/subjects/api.py index f7275240e48..ae6551ee3fb 100644 --- a/src/apps/subjects/api.py +++ b/src/apps/subjects/api.py @@ -311,6 +311,7 @@ async def get_target_subjects_by_respondent( subjects_service = SubjectsService(session, user.id) respondent_subject = await subjects_service.exist_by_id(respondent_subject_id) + # Ensure the respondent is not a limited account if respondent_subject.user_id is None: # Return a generic bad request error to avoid leaking information raise ValidationError(f"Subject {respondent_subject_id} is not a valid respondent") From 1df864c3b6d6d9343f46a5fb427ba55de2627de5 Mon Sep 17 00:00:00 2001 From: Billie He Date: Mon, 7 Oct 2024 11:07:10 -0700 Subject: [PATCH 29/41] chore: fix typing and formatting --- src/apps/activities/tests/test_activities.py | 64 ++++++++++++-------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/apps/activities/tests/test_activities.py b/src/apps/activities/tests/test_activities.py index 59a33e16968..7265c2d351a 100644 --- a/src/apps/activities/tests/test_activities.py +++ b/src/apps/activities/tests/test_activities.py @@ -39,7 +39,7 @@ @pytest.fixture -async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletFull, tom): +async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletFull, tom: User): srv = AppletService(session, tom.id) await srv.create_access_link(applet_one.id, CreateAccessLink(require_login=False)) applet = await srv.get_full_applet(applet_one.id) @@ -57,7 +57,9 @@ async def lucy_applet_one_subject(session: AsyncSession, lucy: User, applet_one_ @pytest.fixture -async def applet_one_user_respondent(session: AsyncSession, applet_one: AppletFull, tom, user) -> AppletFull: +async def applet_one_user_respondent( + session: AsyncSession, applet_one: AppletFull, tom: User, user: User +) -> AppletFull: await UserAppletAccessService(session, tom.id, applet_one.id).add_role(user.id, Role.RESPONDENT) return applet_one @@ -169,7 +171,9 @@ async def lucy_applet_with_all_performance_tasks_subject( @pytest.fixture -async def empty_applet_user_respondent(session: AsyncSession, empty_applet: AppletFull, tom, user) -> AppletFull: +async def empty_applet_user_respondent( + session: AsyncSession, empty_applet: AppletFull, tom: User, user: User +) -> AppletFull: await UserAppletAccessService(session, tom.id, empty_applet.id).add_role(user.id, Role.RESPONDENT) return empty_applet @@ -280,7 +284,7 @@ class TestActivities: target_assigned_activities_url = "/activities/applet/{applet_id}/target/{subject_id}" respondent_assigned_activities_url = "/activities/applet/{applet_id}/respondent/{subject_id}" - async def test_activity_detail(self, client, applet_one: AppletFull, tom: User): + async def test_activity_detail(self, client: TestClient, applet_one: AppletFull, tom: User): activity = applet_one.activities[0] client.login(tom) response = await client.get(self.activity_detail.format(pk=activity.id)) @@ -294,7 +298,12 @@ async def test_activity_detail(self, client, applet_one: AppletFull, tom: User): assert result["items"][0]["question"] == activity.items[0].question[Language.ENGLISH] async def test_activities_applet( - self, client, applet_one: AppletFull, default_theme: Theme, tom: User, tom_applet_one_subject + self, + client: TestClient, + applet_one: AppletFull, + default_theme: Theme, + tom: User, + tom_applet_one_subject: Subject, ): client.login(tom) response = await client.get(self.activities_applet.format(applet_id=applet_one.id)) @@ -399,7 +408,7 @@ async def test_activities_applet( } async def test_activities_flows_applet( - self, client, applet_activity_flow: AppletFull, default_theme: Theme, tom: User + self, client: TestClient, applet_activity_flow: AppletFull, default_theme: Theme, tom: User ): client.login(tom) response = await client.get(self.activities_flows_applet.format(applet_id=applet_activity_flow.id)) @@ -486,7 +495,7 @@ async def test_activities_flows_applet( assert item["activityId"] == str(flow_item.activity_id) assert item["order"] == flow_item.order - async def test_public_activity_detail(self, client, applet_one_with_public_link: AppletFull): + async def test_public_activity_detail(self, client: TestClient, applet_one_with_public_link: AppletFull): activity = applet_one_with_public_link.activities[0] response = await client.get(self.public_activity_detail.format(pk=activity.id)) @@ -500,7 +509,7 @@ async def test_public_activity_detail(self, client, applet_one_with_public_link: # Get only applet activities with submitted answers async def test_activities_applet_has_submitted( - self, client, applet_one: AppletFull, default_theme: Theme, tom: User + self, client: TestClient, applet_one: AppletFull, default_theme: Theme, tom: User ): client.login(tom) @@ -546,7 +555,9 @@ async def test_activities_applet_has_submitted( assert result["activitiesDetails"][0]["name"] == activity.name # Get only applet activities with score - async def test_activities_applet_has_score(self, client, applet_one: AppletFull, default_theme: Theme, tom: User): + async def test_activities_applet_has_score( + self, client: TestClient, applet_one: AppletFull, default_theme: Theme, tom: User + ): client.login(tom) create_data = dict( @@ -644,7 +655,7 @@ async def test_activities_applet_has_score(self, client, applet_one: AppletFull, assert option["score"] > 0 async def test_subject_assigned_activities_editor( - self, client, applet_one_lucy_editor, lucy, lucy_applet_one_subject + self, client: TestClient, applet_one_lucy_editor: AppletFull, lucy: User, lucy_applet_one_subject: Subject ): client.login(lucy) @@ -661,7 +672,7 @@ async def test_subject_assigned_activities_editor( assert result[0]["message"] == "Access denied to applet." async def test_subject_assigned_activities_incorrect_reviewer( - self, client, applet_one_lucy_reviewer, lucy, lucy_applet_one_subject + self, client: TestClient, applet_one_lucy_reviewer: AppletFull, lucy: User, lucy_applet_one_subject: Subject ): client.login(lucy) @@ -678,7 +689,7 @@ async def test_subject_assigned_activities_incorrect_reviewer( assert result[0]["message"] == "Access denied." async def test_subject_assigned_activities_participant( - self, client, applet_one_lucy_respondent, lucy, lucy_applet_one_subject + self, client: TestClient, applet_one_lucy_respondent: AppletFull, lucy: User, lucy_applet_one_subject: Subject ): client.login(lucy) @@ -695,7 +706,12 @@ async def test_subject_assigned_activities_participant( assert result[0]["message"] == "Access denied to applet." async def test_subject_assigned_activities_participant_other( - self, client, applet_one_lucy_respondent, lucy, applet_one_user_respondent, user_applet_one_subject + self, + client: TestClient, + applet_one_lucy_respondent: AppletFull, + lucy: User, + applet_one_user_respondent: AppletFull, + user_applet_one_subject: Subject, ): client.login(lucy) @@ -712,7 +728,7 @@ async def test_subject_assigned_activities_participant_other( assert result[0]["message"] == "Access denied to applet." async def test_subject_assigned_activities_invalid_applet( - self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + self, client: TestClient, applet_one_lucy_manager: AppletFull, lucy: User, lucy_applet_one_subject: AppletFull ): client.login(lucy) @@ -729,7 +745,7 @@ async def test_subject_assigned_activities_invalid_applet( assert result[0]["message"] == f"No such applets with id={applet_id}." async def test_subject_assigned_activities_invalid_subject( - self, client, applet_one_lucy_manager, lucy, lucy_applet_one_subject + self, client: TestClient, applet_one_lucy_manager: AppletFull, lucy: User, lucy_applet_one_subject: AppletFull ): client.login(lucy) @@ -746,7 +762,7 @@ async def test_subject_assigned_activities_invalid_subject( assert result[0]["message"] == f"Subject with id {subject_id} not found" async def test_subject_assigned_activities_empty_applet( - self, client, empty_applet_lucy_manager, lucy, lucy_empty_applet_subject + self, client: TestClient, empty_applet_lucy_manager: AppletFull, lucy: User, lucy_empty_applet_subject: Subject ): client.login(lucy) @@ -763,7 +779,11 @@ async def test_subject_assigned_activities_empty_applet( assert result["activityFlows"] == [] async def test_subject_assigned_activities_auto_assigned( - self, client, applet_activity_flow_lucy_manager, lucy, lucy_applet_activity_flow_subject + self, + client: TestClient, + applet_activity_flow_lucy_manager: AppletFull, + lucy: User, + lucy_applet_activity_flow_subject: Subject, ): client.login(lucy) @@ -1188,7 +1208,6 @@ async def test_assigned_activities_empty_applet( async def test_assigned_activities_limited_respondent( self, - session, client: TestClient, tom: User, applet_one: AppletFull, @@ -1268,7 +1287,7 @@ async def test_assigned_activities_auto_assigned( ) async def test_assigned_activities_manually_assigned( self, - session, + session: AsyncSession, client: TestClient, lucy: User, empty_applet_lucy_manager: AppletFull, @@ -1512,10 +1531,9 @@ async def test_assigned_activities_manually_assigned( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_activities_from_submission( self, - session, + session: AsyncSession, client: TestClient, tom: User, - lucy: User, applet_one_lucy_respondent: AppletFull, tom_applet_one_subject: Subject, lucy_applet_one_subject: Subject, @@ -1580,7 +1598,7 @@ async def test_assigned_activities_from_submission( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_hidden_activities( self, - session, + session: AsyncSession, client: TestClient, lucy: User, empty_applet_lucy_manager: AppletFull, @@ -1730,11 +1748,9 @@ async def test_assigned_hidden_activities( @pytest.mark.parametrize("subject_type", ["target", "respondent"]) async def test_assigned_performance_tasks( self, - session, client: TestClient, lucy: User, applet_with_all_performance_tasks_lucy_manager: AppletFull, - lucy_applet_with_all_performance_tasks_subject: Subject, user_applet_with_all_performance_tasks_subject: Subject, subject_type: str, ): From 36dca7cb52bbc543e70fbc98791165b41ba749fd Mon Sep 17 00:00:00 2001 From: Billie He Date: Tue, 8 Oct 2024 12:03:09 -0700 Subject: [PATCH 30/41] chore: change cq target to only check and not format --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 438676997ab..4ae865fff30 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ migrate-arbitrary: # NOTE: cq == "Code quality" .PHONY: cq cq: - ${RUFF_COMMAND} check . && ${RUFF_COMMAND} format . && ${MYPY_COMMAND} . + ${RUFF_COMMAND} check . && ${RUFF_COMMAND} format --check . && ${MYPY_COMMAND} . # NOTE: cqf == "Code quality fix" .PHONY: cqf From ba0a6097746ef7f2b325b784ff0c4a93cb84ad74 Mon Sep 17 00:00:00 2001 From: felipeMetaLab Date: Fri, 11 Oct 2024 07:48:10 -0700 Subject: [PATCH 31/41] feature: extend the list of supported item types * feature(conditionalLogic): Accept new activity items * wip(conditionalLogic): DateItem support * wip(conditionalLogic): time and timerange added * wip(conditionalLogic) Fix validation errors for time an date * wip(sliderRow): Add slider row validation * wip(conditionalLogic) Remove type from tume renge * wip(conditionalLogic): allow all activity items types * wip(conditionalLogic): Split score and reports and conditionalLogic * wip(conditionalLogic): Add special public model * wip(conditionalLogic): Change index type * wip(conditionalLogic): Change option based items validation * feature/new_items_types * chore/fixing code quality * chore/fixing singleTimePayload, minMaxTimePayload, activity_item_change on code quality and fixing date test on test_custom_validation * chore/cleaning test_custom_validation comment * chore/ruff format condition.py and test_custom_validation --------- Co-authored-by: ivan koryshkin Co-authored-by: iwankrshkin <140184081+iwankrshkin@users.noreply.github.com> Co-authored-by: Felipe Imperio --- .../activities/domain/activity_item_base.py | 29 - src/apps/activities/domain/conditions.py | 634 ++++++++++++++++-- .../activities/domain/custom_validation.py | 31 +- .../activities/domain/response_type_config.py | 33 + src/apps/activities/errors.py | 4 + .../services/activity_item_change.py | 32 +- .../tests/fixtures/conditional_logic.py | 36 +- .../unit/domain/test_activity_item_create.py | 70 +- .../unit/domain/test_custom_validation.py | 107 ++- src/apps/shared/domain/base.py | 6 + 10 files changed, 862 insertions(+), 120 deletions(-) diff --git a/src/apps/activities/domain/activity_item_base.py b/src/apps/activities/domain/activity_item_base.py index a6c1c1428ea..a8d49043287 100644 --- a/src/apps/activities/domain/activity_item_base.py +++ b/src/apps/activities/domain/activity_item_base.py @@ -8,7 +8,6 @@ AlertFlagMissingSliderItemError, DataMatrixRequiredError, HiddenWhenConditionalLogicSetError, - IncorrectConditionLogicItemTypeError, IncorrectConfigError, IncorrectNameCharactersError, IncorrectResponseValueError, @@ -154,34 +153,6 @@ def validate_score_required(cls, values): return values - @validator("conditional_logic") - def validate_conditional_logic(cls, value, values): - response_type = values.get("response_type") - if value is not None and response_type not in [ - ResponseType.SINGLESELECT, - ResponseType.MULTISELECT, - ResponseType.SINGLESELECTROWS, - ResponseType.MULTISELECTROWS, - ResponseType.SLIDER, - ResponseType.SLIDERROWS, - ResponseType.TEXT, - ResponseType.PARAGRAPHTEXT, - ResponseType.TIME, - ResponseType.TIMERANGE, - ResponseType.DATE, - ResponseType.NUMBERSELECT, - ResponseType.DRAWING, - ResponseType.PHOTO, - ResponseType.VIDEO, - ResponseType.GEOLOCATION, - ResponseType.AUDIO, - ResponseType.MESSAGE, - ResponseType.AUDIOPLAYER, - ]: - raise IncorrectConditionLogicItemTypeError() - - return value - @root_validator(skip_on_failure=True) def validate_is_hidden(cls, values): # cannot hide if conditional logic is set diff --git a/src/apps/activities/domain/conditions.py b/src/apps/activities/domain/conditions.py index 9c7452fa1d5..98c35cbb81d 100644 --- a/src/apps/activities/domain/conditions.py +++ b/src/apps/activities/domain/conditions.py @@ -1,8 +1,10 @@ +import datetime from enum import Enum +from typing import Any, Dict, Optional -from pydantic import Field, validator +from pydantic import Field, root_validator, validator -from apps.shared.domain import PublicModel +from apps.shared.domain import PublicModel, PublicModelNoExtra class ConditionType(str, Enum): @@ -19,16 +21,62 @@ class ConditionType(str, Enum): EQUAL_TO_SCORE = "EQUAL_TO_SCORE" +class DateConditionType(str, Enum): + GREATER_THAN_DATE = "GREATER_THAN_DATE" + LESS_THAN_DATE = "LESS_THAN_DATE" + EQUAL_TO_DATE = "EQUAL_TO_DATE" + NOT_EQUAL_TO_DATE = "NOT_EQUAL_TO_DATE" + BETWEEN_DATES = "BETWEEN_DATES" + OUTSIDE_OF_DATES = "OUTSIDE_OF_DATES" + + +class TimeRangeConditionType(str, Enum): + GREATER_THAN_TIME_RANGE = "GREATER_THAN_TIME_RANGE" + LESS_THAN_TIMES_RANGE = "LESS_THAN_TIME_RANGE" + BETWEEN_TIMES_RANGE = "BETWEEN_TIMES_RANGE" + EQUAL_TO_TIMES_RANGE = "EQUAL_TO_TIME_RANGE" + NOT_EQUAL_TO_TIMES_RANGE = "NOT_EQUAL_TO_TIME_RANGE" + OUTSIDE_OF_TIMES_RANGE = "OUTSIDE_OF_TIMES_RANGE" + + +class TimeConditionType(str, Enum): + GREATER_THAN_TIME = "GREATER_THAN_TIME" + LESS_THAN_TIME = "LESS_THAN_TIME" + BETWEEN_TIMES = "BETWEEN_TIMES" + EQUAL_TO_TIME = "EQUAL_TO_TIME" + NOT_EQUAL_TO_TIMES = "NOT_EQUAL_TO_TIME" + OUTSIDE_OF_TIMES = "OUTSIDE_OF_TIMES" + + class MultiSelectConditionType(str, Enum): INCLUDES_OPTION = "INCLUDES_OPTION" NOT_INCLUDES_OPTION = "NOT_INCLUDES_OPTION" +class MultiSelectionsPerRowConditionType(str, Enum): + INCLUDES_ROW_OPTION = "INCLUDES_ROW_OPTION" + NOT_INCLUDES_ROW_OPTION = "NOT_INCLUDES_ROW_OPTION" + + +class SingleSelectionPerRowConditionType(str, Enum): + EQUAL_TO_ROW_OPTION = "EQUAL_TO_ROW_OPTION" + NOT_EQUAL_TO_ROW_OPTION = "NOT_EQUAL_TO_ROW_OPTION" + + class SingleSelectConditionType(str, Enum): EQUAL_TO_OPTION = "EQUAL_TO_OPTION" NOT_EQUAL_TO_OPTION = "NOT_EQUAL_TO_OPTION" +class SliderRowConditionType(str, Enum): + GREATER_THAN_SLIDER_ROWS = "GREATER_THAN_SLIDER_ROWS" + LESS_THAN_SLIDER_ROWS = "LESS_THAN_SLIDER_ROWS" + EQUAL_TO_SLIDER_ROWS = "EQUAL_TO_SLIDER_ROWS" + NOT_EQUAL_TO_SLIDER_ROWS = "NOT_EQUAL_TO_SLIDER_ROWS" + BETWEEN_SLIDER_ROWS = "BETWEEN_SLIDER_ROWS" + OUTSIDE_OF_SLIDER_ROWS = "OUTSIDE_OF_SLIDER_ROWS" + + class SliderConditionType(str, Enum): GREATER_THAN = "GREATER_THAN" LESS_THAN = "LESS_THAN" @@ -38,11 +86,32 @@ class SliderConditionType(str, Enum): OUTSIDE_OF = "OUTSIDE_OF" -class OptionPayload(PublicModel): +class TimePayloadType(str, Enum): + START_TIME = "startTime" + END_TIME = "endTime" + + +OPTION_BASED_CONDITIONS = [ + MultiSelectConditionType.INCLUDES_OPTION, + MultiSelectConditionType.NOT_INCLUDES_OPTION, + SingleSelectConditionType.EQUAL_TO_OPTION, + SingleSelectConditionType.NOT_EQUAL_TO_OPTION, + SingleSelectionPerRowConditionType.EQUAL_TO_ROW_OPTION, + SingleSelectionPerRowConditionType.NOT_EQUAL_TO_ROW_OPTION, + MultiSelectionsPerRowConditionType.INCLUDES_ROW_OPTION, + MultiSelectionsPerRowConditionType.NOT_INCLUDES_ROW_OPTION, +] + + +class OptionPayload(PublicModelNoExtra): option_value: str -class ValuePayload(PublicModel): +class OptionIndexPayload(OptionPayload): + row_index: str + + +class ValuePayload(PublicModelNoExtra): value: float @validator("value") @@ -50,7 +119,144 @@ def validate_score(cls, value): return round(value, 2) -class MinMaxPayload(PublicModel): +class ValueIndexPayload(ValuePayload): + row_index: str + + +class SingleDatePayload(PublicModel): + date: datetime.date + + def dict(self, *args, **kwargs): + d = super().dict(*args, **kwargs) + d["date"] = self.date.isoformat() + return d + + +class DateRangePayload(PublicModel): + minDate: datetime.date + maxDate: datetime.date + + @root_validator(pre=True) + def validate_dates(cls, values): + min_date = values.get("minDate") + max_date = values.get("maxDate") + if min_date and max_date and min_date > max_date: + raise ValueError("minDate cannot be later than maxDate") + return values + + def dict(self, *args, **kwargs): + d = super().dict(*args, **kwargs) + d["minDate"] = self.minDate.isoformat() + d["maxDate"] = self.maxDate.isoformat() + return d + + +class TimePayload(PublicModel): + type: str | None = None + value: datetime.time + + def dict(self, *args, **kwargs): + d = super().dict(*args, **kwargs) + d["value"] = self.value.strftime("%H:%M") + return d + + +class SingleTimePayload(PublicModel): + time: Optional[datetime.time] = None + + @root_validator(pre=True) + def validate_time(cls, values: Dict[str, Any]) -> Dict[str, Any]: + time_value = values.get("time") + if isinstance(time_value, dict): + values["time"] = cls._dict_to_time(time_value) + elif isinstance(time_value, str): + values["time"] = cls._string_to_time(time_value) + return values + + def dict(self, *args, **kwargs) -> Dict[str, Any]: + d = super().dict(*args, **kwargs) + if self.time: + d["time"] = self.time.strftime("%H:%M") + return d + + @staticmethod + def _dict_to_time(time_dict: Dict[str, Any]) -> datetime.time: + if "hours" in time_dict and "minutes" in time_dict: + return datetime.time(hour=int(time_dict["hours"]), minute=int(time_dict["minutes"])) + raise ValueError("Invalid time dictionary structure") + + @staticmethod + def _string_to_time(time_string: str) -> datetime.time: + try: + return datetime.datetime.strptime(time_string, "%H:%M").time() + except ValueError: + raise ValueError("Invalid time string format. Expected 'HH:MM'.") + + @staticmethod + def _time_to_dict(time: datetime.time) -> Dict[str, int]: + return {"hours": time.hour, "minutes": time.minute} + + +class MinMaxTimePayload(PublicModel): + minTime: Optional[datetime.time] = None + maxTime: Optional[datetime.time] = None + + @root_validator(pre=True) + def validate_times(cls, values: Dict[str, Any]) -> Dict[str, Any]: + min_time_dict = values.get("minTime") + max_time_dict = values.get("maxTime") + + if isinstance(min_time_dict, dict): + values["minTime"] = cls._dict_to_time(min_time_dict) + if isinstance(max_time_dict, dict): + values["maxTime"] = cls._dict_to_time(max_time_dict) + + return values + + def dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + d = super().dict(*args, **kwargs) + if self.minTime: + d["minTime"] = self._time_to_dict(self.minTime) + if self.maxTime: + d["maxTime"] = self._time_to_dict(self.maxTime) + return {key: value for key, value in d.items() if value is not None} + + @staticmethod + def _dict_to_time(time_dict: Dict[str, int]) -> datetime.time: + if "hours" in time_dict and "minutes" in time_dict: + return datetime.time(hour=int(time_dict["hours"]), minute=int(time_dict["minutes"])) + raise ValueError("Invalid time dictionary structure") + + @staticmethod + def _time_to_dict(time: datetime.time) -> Dict[str, int]: + return {"hours": time.hour, "minutes": time.minute} + + def json_serialize(self) -> Dict[str, Any]: + data = self.dict() + if self.minTime: + data["minTime"] = self._time_to_dict(self.minTime) + if self.maxTime: + data["maxTime"] = self._time_to_dict(self.maxTime) + return data + + +class MinMaxSliderRowPayload(PublicModelNoExtra): + minValue: float + maxValue: float + rowIndex: str + + @validator("minValue", "maxValue") + def validate_score(cls, value): + return round(value, 2) + + def dict(self, *args, **kwargs): + d = super().dict(*args, **kwargs) + d["minValue"] = round(self.minValue, 2) + d["maxValue"] = round(self.maxValue, 2) + return d + + +class MinMaxPayload(PublicModelNoExtra): min_value: float max_value: float @@ -59,6 +265,10 @@ def validate_score(cls, value): return round(value, 2) +class MinMaxPayloadRow(MinMaxPayload): + row_index: str + + class ScoreConditionPayload(PublicModel): value: bool @@ -67,54 +277,308 @@ class BaseCondition(PublicModel): item_name: str -class IncludesOptionCondition(BaseCondition): +class _IncludesOptionCondition(BaseCondition): type: str = Field(ConditionType.INCLUDES_OPTION, const=True) - payload: OptionPayload -class NotIncludesOptionCondition(BaseCondition): +class _IncludesOptionPerRowCondition(BaseCondition): + type: str = Field(MultiSelectionsPerRowConditionType.INCLUDES_ROW_OPTION, const=True) + + +class _NotIncludesOptionPerRowCondition(BaseCondition): + type: str = Field(MultiSelectionsPerRowConditionType.NOT_INCLUDES_ROW_OPTION, const=True) + + +class _NotIncludesOptionCondition(BaseCondition): type: str = Field(ConditionType.NOT_INCLUDES_OPTION, const=True) - payload: OptionPayload -class EqualToOptionCondition(BaseCondition): +class _EqualToOptionCondition(BaseCondition): type: str = Field(ConditionType.EQUAL_TO_OPTION, const=True) - payload: OptionPayload -class NotEqualToOptionCondition(BaseCondition): +class _EqualToRowOptionCondition(BaseCondition): + type: str = Field(SingleSelectionPerRowConditionType.EQUAL_TO_ROW_OPTION, const=True) + + +class _NotEqualToRowOptionCondition(BaseCondition): + type: str = Field(SingleSelectionPerRowConditionType.NOT_EQUAL_TO_ROW_OPTION, const=True) + + +class _NotEqualToOptionCondition(BaseCondition): type: str = Field(ConditionType.NOT_EQUAL_TO_OPTION, const=True) - payload: OptionPayload -class GreaterThanCondition(BaseCondition): +class _GraterThanDateCondition(BaseCondition): + type: str = Field(DateConditionType.GREATER_THAN_DATE, const=True) + + +class _LessThanDateCondition(BaseCondition): + type: str = Field(DateConditionType.LESS_THAN_DATE, const=True) + + +class _GreaterThanSliderRowCondition(BaseCondition): + type: str = Field(SliderRowConditionType.GREATER_THAN_SLIDER_ROWS, const=True) + + +class _LessThanSliderRowCondition(BaseCondition): + type: str = Field(SliderRowConditionType.LESS_THAN_SLIDER_ROWS, const=True) + + +class _EqualToSliderRowCondition(BaseCondition): + type: str = Field(SliderRowConditionType.EQUAL_TO_SLIDER_ROWS, const=True) + + +class _NotEqualToSliderRowCondition(BaseCondition): + type: str = Field(SliderRowConditionType.NOT_EQUAL_TO_SLIDER_ROWS, const=True) + + +class _BetweenSliderRowCondition(BaseCondition): + type: str = Field(SliderRowConditionType.BETWEEN_SLIDER_ROWS, const=True) + + +class _OutsideOfSliderRowCondition(BaseCondition): + type: str = Field(SliderRowConditionType.OUTSIDE_OF_SLIDER_ROWS, const=True) + + +class _BetweenTimeRangeCondition(BaseCondition): + type: str = Field(TimeRangeConditionType.BETWEEN_TIMES_RANGE, const=True) + + +class _GreaterThanTimeRangeCondition(BaseCondition): + type: str = Field(TimeRangeConditionType.GREATER_THAN_TIME_RANGE, const=True) + + +class _EqualToTimeRangeCondition(BaseCondition): + type: str = Field(TimeRangeConditionType.EQUAL_TO_TIMES_RANGE, const=True) + + +class _EqualToTimeCondition(BaseCondition): + type: str = Field(TimeConditionType.EQUAL_TO_TIME, const=True) + + +class _NotEqualToTimeCondition(BaseCondition): + type: str = Field(TimeConditionType.NOT_EQUAL_TO_TIMES, const=True) + + +class _LessThanTimeRangeCondition(BaseCondition): + type: str = Field(TimeRangeConditionType.LESS_THAN_TIMES_RANGE, const=True) + + +class _LessThanTimeCondition(BaseCondition): + type: str = Field(TimeConditionType.LESS_THAN_TIME, const=True) + + +class _BetweenTimeCondition(BaseCondition): + type: str = Field(TimeConditionType.BETWEEN_TIMES, const=True) + + +class _NotEqualToTimeRangeCondition(BaseCondition): + type: str = Field(TimeRangeConditionType.NOT_EQUAL_TO_TIMES_RANGE, const=True) + + +class _OutsideOfTimeRangeCondition(BaseCondition): + type: str = Field(TimeRangeConditionType.OUTSIDE_OF_TIMES_RANGE, const=True) + + +class _GreaterThanTimeCondition(BaseCondition): + type: str = Field(TimeConditionType.GREATER_THAN_TIME, const=True) + + +class _OutsideOfTimeCondition(BaseCondition): + type: str = Field(TimeConditionType.OUTSIDE_OF_TIMES, const=True) + + +class _GreaterThanCondition(BaseCondition): type: str = Field(ConditionType.GREATER_THAN, const=True) - payload: ValuePayload -class LessThanCondition(BaseCondition): +class _LessThanCondition(BaseCondition): type: str = Field(ConditionType.LESS_THAN, const=True) - payload: ValuePayload -class EqualCondition(BaseCondition): +class _EqualCondition(BaseCondition): type: str = Field(ConditionType.EQUAL, const=True) - payload: ValuePayload -class NotEqualCondition(BaseCondition): +class _EqualToDateCondition(BaseCondition): + type: str = Field(DateConditionType.EQUAL_TO_DATE, const=True) + + +class _NotEqualToDateCondition(BaseCondition): + type: str = Field(DateConditionType.NOT_EQUAL_TO_DATE, const=True) + + +class _NotEqualCondition(BaseCondition): type: str = Field(ConditionType.NOT_EQUAL, const=True) - payload: ValuePayload -class BetweenCondition(BaseCondition): +class _BetweenCondition(BaseCondition): type: str = Field(ConditionType.BETWEEN, const=True) - payload: MinMaxPayload -class OutsideOfCondition(BaseCondition): +class _BetweenDatesCondition(BaseCondition): + type: str = Field(DateConditionType.BETWEEN_DATES, const=True) + + +class _OutsideOfDatesCondition(BaseCondition): + type: str = Field(DateConditionType.OUTSIDE_OF_DATES, const=True) + + +class _OutsideOfCondition(BaseCondition): type: str = Field(ConditionType.OUTSIDE_OF, const=True) - payload: MinMaxPayload + + +class IncludesOptionCondition(_IncludesOptionCondition): + payload: OptionPayload | OptionIndexPayload + + +class IncludesOptionPerRowCondition(_IncludesOptionPerRowCondition): + payload: OptionPayload | OptionIndexPayload + + +class NotIncludesOptionPerRowCondition(_NotIncludesOptionPerRowCondition): + payload: OptionPayload | OptionIndexPayload + + +class NotIncludesOptionCondition(_NotIncludesOptionCondition): + payload: OptionPayload | OptionIndexPayload + + +class EqualToOptionCondition(_EqualToOptionCondition): + payload: OptionPayload | OptionIndexPayload + + +class EqualToRowOptionCondition(_EqualToRowOptionCondition): + payload: OptionPayload | OptionIndexPayload + + +class NotEqualToRowOptionCondition(_NotEqualToRowOptionCondition): + payload: OptionPayload | OptionIndexPayload + + +class NotEqualToOptionCondition(_NotEqualToOptionCondition): + payload: OptionPayload | OptionIndexPayload + + +class GreaterThanDateCondition(_GraterThanDateCondition): + payload: SingleDatePayload + + +class GreaterThanSliderRowCondition(_GreaterThanSliderRowCondition): + payload: ValuePayload | ValueIndexPayload + + +class LessThanSliderRowCondition(_LessThanSliderRowCondition): + payload: ValuePayload | ValueIndexPayload + + +class EqualToSliderRowCondition(_EqualToSliderRowCondition): + payload: ValuePayload | ValueIndexPayload + + +class NotEqualToSliderRowCondition(_NotEqualToSliderRowCondition): + payload: ValuePayload | ValueIndexPayload + + +class BetweenSliderRowCondition(_BetweenSliderRowCondition): + payload: MinMaxSliderRowPayload | ValueIndexPayload + + +class OutsideOfSliderRowCondition(_OutsideOfSliderRowCondition): + payload: MinMaxSliderRowPayload | ValueIndexPayload + + +class LessThanDateCondition(_LessThanDateCondition): + payload: SingleDatePayload + + +class BetweenTimeRangeCondition(_BetweenTimeRangeCondition): + payload: MinMaxTimePayload + + +class GreaterThanTimeRangeCondition(_GreaterThanTimeRangeCondition): + payload: SingleTimePayload + + +class LessThanTimeRangeCondition(_LessThanTimeRangeCondition): + payload: SingleTimePayload + + +class EqualToTimeRangeCondition(_EqualToTimeRangeCondition): + payload: SingleTimePayload + + +class NotEqualToTimeRangeCondition(_NotEqualToTimeRangeCondition): + payload: SingleTimePayload + + +class OutsideOfTimeRangeCondition(_OutsideOfTimeRangeCondition): + payload: MinMaxTimePayload + + +class GreaterThanTimeCondition(_GreaterThanTimeCondition): + payload: SingleTimePayload + + +class LessThanTimeCondition(_LessThanTimeCondition): + payload: SingleTimePayload + + +class EqualToTimeCondition(_EqualToTimeCondition): + payload: SingleTimePayload + + +class NotEqualToTimeCondition(_NotEqualToTimeCondition): + payload: SingleTimePayload + + +class OutsideOfTimeCondition(_OutsideOfTimeCondition): + payload: MinMaxTimePayload + + +class BetweenTimeCondition(_BetweenTimeCondition): + payload: MinMaxTimePayload + + +class GreaterThanCondition(_GreaterThanCondition): + payload: ValuePayload | TimePayload + + +class LessThanCondition(_LessThanCondition): + payload: ValuePayload | ValueIndexPayload | TimePayload + + +class EqualToDateCondition(_EqualToDateCondition): + payload: SingleDatePayload + + +class EqualCondition(_EqualCondition): + payload: ValuePayload | ValueIndexPayload | TimePayload + + +class NotEqualToDateCondition(_NotEqualToDateCondition): + payload: SingleDatePayload + + +class NotEqualCondition(_NotEqualCondition): + payload: ValuePayload | ValueIndexPayload | TimePayload + + +class BetweenDatesCondition(_BetweenDatesCondition): + payload: DateRangePayload + + +class BetweenCondition(_BetweenCondition): + payload: MinMaxPayload | MinMaxPayloadRow | TimePayload + + +class OutsideOfCondition(_OutsideOfCondition): + payload: MinMaxPayload | MinMaxPayloadRow | TimePayload + + +class OutsideOfDatesCondition(_OutsideOfDatesCondition): + payload: DateRangePayload class ScoreBoolCondition(BaseCondition): @@ -122,6 +586,46 @@ class ScoreBoolCondition(BaseCondition): payload: ScoreConditionPayload +class ScoreGraterThanCondition(_GreaterThanCondition): + payload: ValuePayload + + +class ScoreLessThanCondition(_LessThanCondition): + payload: ValuePayload + + +class ScoreEqualCondition(_EqualCondition): + payload: ValuePayload + + +class ScoreNotEqualCondition(_NotEqualCondition): + payload: ValuePayload + + +class ScoreBetweenCondition(_BetweenCondition): + payload: MinMaxPayload + + +class ScoreOutsideOfCondition(_OutsideOfCondition): + payload: MinMaxPayload + + +class ScoreNotIncludesOptionCondition(_NotIncludesOptionCondition): + payload: OptionPayload + + +class ScoreIncludesOptionCondition(_IncludesOptionCondition): + payload: OptionPayload + + +class ScoreEqualToOptionCondition(_EqualToOptionCondition): + payload: OptionPayload + + +class ScoreNotEqualToOptionCondition(_NotEqualToOptionCondition): + payload: OptionPayload + + Condition = ( IncludesOptionCondition | NotIncludesOptionCondition @@ -133,39 +637,67 @@ class ScoreBoolCondition(BaseCondition): | NotEqualCondition | BetweenCondition | OutsideOfCondition + | GreaterThanDateCondition + | BetweenTimeRangeCondition + | LessThanDateCondition + | EqualToDateCondition + | BetweenDatesCondition + | NotEqualToDateCondition + | OutsideOfDatesCondition + | GreaterThanTimeRangeCondition + | LessThanTimeRangeCondition + | EqualToTimeRangeCondition + | NotEqualToTimeRangeCondition + | OutsideOfTimeRangeCondition + | EqualToRowOptionCondition + | NotEqualToRowOptionCondition + | IncludesOptionPerRowCondition + | NotIncludesOptionPerRowCondition + | GreaterThanSliderRowCondition + | LessThanSliderRowCondition + | EqualToSliderRowCondition + | NotEqualToSliderRowCondition + | BetweenSliderRowCondition + | OutsideOfSliderRowCondition + | GreaterThanTimeCondition + | LessThanTimeCondition + | EqualToTimeCondition + | NotEqualToTimeCondition + | BetweenTimeCondition + | OutsideOfTimeCondition ) ScoreCondition = ( - GreaterThanCondition - | LessThanCondition - | EqualCondition - | NotEqualCondition - | BetweenCondition - | OutsideOfCondition + ScoreGraterThanCondition + | ScoreLessThanCondition + | ScoreEqualCondition + | ScoreNotEqualCondition + | ScoreBetweenCondition + | ScoreOutsideOfCondition ) SectionCondition = ( - IncludesOptionCondition - | NotIncludesOptionCondition - | EqualToOptionCondition - | NotEqualToOptionCondition - | GreaterThanCondition - | LessThanCondition - | EqualCondition - | NotEqualCondition - | BetweenCondition - | OutsideOfCondition + ScoreIncludesOptionCondition + | ScoreNotIncludesOptionCondition + | ScoreEqualToOptionCondition + | ScoreNotEqualToOptionCondition + | ScoreGraterThanCondition + | ScoreGraterThanCondition + | ScoreEqualCondition + | ScoreNotEqualCondition + | ScoreBetweenCondition + | ScoreOutsideOfCondition | ScoreBoolCondition ) AnyCondition = ( - IncludesOptionCondition - | NotIncludesOptionCondition - | EqualToOptionCondition - | NotEqualToOptionCondition - | GreaterThanCondition - | LessThanCondition - | EqualCondition - | NotEqualCondition - | BetweenCondition - | OutsideOfCondition + ScoreIncludesOptionCondition + | ScoreNotIncludesOptionCondition + | ScoreEqualToOptionCondition + | ScoreNotEqualToOptionCondition + | ScoreGraterThanCondition + | ScoreGraterThanCondition + | ScoreEqualCondition + | ScoreNotEqualCondition + | ScoreBetweenCondition + | ScoreOutsideOfCondition | ScoreBoolCondition ) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 0fb6e6c3c5d..f2b44dd03d9 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -1,4 +1,3 @@ -from apps.activities.domain.conditions import MultiSelectConditionType, SingleSelectConditionType from apps.activities.domain.response_type_config import PerformanceTaskType, ResponseType from apps.activities.domain.response_values import PhrasalTemplateFieldType from apps.activities.domain.scores_reports import ReportType, SubscaleItemType @@ -40,26 +39,32 @@ def validate_item_flow(values: dict): else: # check if condition item order is less than current item order # noqa: E501 condition_item_index = item_names.index(condition.item_name) + condition_source_item = items[condition_item_index] + item_type = condition_source_item.config.type if condition_item_index > index: raise IncorrectConditionItemIndexError() # check if condition item type is correct - if items[condition_item_index].response_type not in [ - ResponseType.SINGLESELECT, - ResponseType.MULTISELECT, - ResponseType.SLIDER, - ]: + if condition_source_item.response_type not in ResponseType.conditional_logic_types(): raise IncorrectConditionLogicItemTypeError() # check if condition option ids are correct - if condition.type in list(SingleSelectConditionType) or condition.type in list( - MultiSelectConditionType - ): - option_values = [ - str(option.value) for option in items[condition_item_index].response_values.options - ] - if str(condition.payload.option_value) not in option_values: + if item_type in ResponseType.option_based(): + if item_type in ResponseType.options_mapped_on_value(): + option_value_attr = "value" + selected_option = str(condition.payload.option_value) + else: + option_value_attr = "id" + selected_option = str(condition.payload.option_value) + + option_values = [] + for option in condition_source_item.response_values.options: + option_value = getattr(option, option_value_attr) + option_values.append(str(option_value)) + + if selected_option not in option_values: raise IncorrectConditionOptionError() + return values diff --git a/src/apps/activities/domain/response_type_config.py b/src/apps/activities/domain/response_type_config.py index 5504df1138c..c29e6f15eb7 100644 --- a/src/apps/activities/domain/response_type_config.py +++ b/src/apps/activities/domain/response_type_config.py @@ -75,6 +75,39 @@ def get_non_response_types(cls): cls.UNITY, ) + @classmethod + def conditional_logic_types(cls): + return ( + cls.DATE, + cls.NUMBERSELECT, + cls.TIME, + cls.TIMERANGE, + cls.SINGLESELECTROWS, + cls.MULTISELECTROWS, + cls.SLIDERROWS, + cls.SINGLESELECT, + cls.MULTISELECT, + cls.SLIDER, + ) + + @classmethod + def options_mapped_on_value(cls) -> list[str]: + return [ + cls.SINGLESELECT, + cls.MULTISELECT, + ] + + @classmethod + def options_mapped_on_id(cls) -> list[str]: + return [ + cls.SINGLESELECTROWS, + cls.MULTISELECTROWS, + ] + + @classmethod + def option_based(cls) -> list[str]: + return cls.options_mapped_on_id() + cls.options_mapped_on_value() + class AdditionalResponseOption(PublicModel): text_input_option: bool diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index ec1036dd679..554f2e564df 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -256,6 +256,10 @@ class MultiSelectNoneOptionError(ValidationError): message = _("No more than 1 none option is not allowed for multiselect.") +class IncorrectTimeRange(ValidationError): + message = _("Incorrect timerange") + + class FlowDoesNotExist(NotFoundError): message = _("Flow does not exist.") diff --git a/src/apps/activities/services/activity_item_change.py b/src/apps/activities/services/activity_item_change.py index 96a23404ead..8eaad7e42b5 100644 --- a/src/apps/activities/services/activity_item_change.py +++ b/src/apps/activities/services/activity_item_change.py @@ -5,7 +5,17 @@ from apps.activities.domain.activity_history import ActivityHistoryFull, ActivityItemHistoryFull from apps.activities.domain.activity_item_history import ActivityItemHistoryChange from apps.activities.domain.conditional_logic import ConditionalLogic -from apps.activities.domain.conditions import Condition, MinMaxPayload, OptionPayload, ValuePayload +from apps.activities.domain.conditions import ( + Condition, + DateRangePayload, + MinMaxPayload, + MinMaxSliderRowPayload, + MinMaxTimePayload, + OptionPayload, + SingleDatePayload, + SingleTimePayload, + ValuePayload, +) from apps.activities.domain.response_type_config import AdditionalResponseOption, ResponseType from apps.shared.changes_generator import BaseChangeGenerator @@ -260,11 +270,29 @@ def __get_payload(condition: Condition) -> str: return condition.payload.option_value elif isinstance(condition.payload, ValuePayload): return str(condition.payload.value) + elif isinstance(condition.payload, SingleDatePayload): + return condition.payload.date.isoformat() + elif isinstance(condition.payload, DateRangePayload): + if condition.payload.minDate and condition.payload.maxDate: + return f"Between {condition.payload.minDate.isoformat()} and {condition.payload.maxDate.isoformat()}" + elif isinstance(condition.payload, MinMaxTimePayload): + if condition.payload.minTime and condition.payload.maxTime: + minTime = f"{condition.payload.minTime.hour}:{condition.payload.minTime.minute:02d}" + maxTime = f"{condition.payload.maxTime.hour}:{condition.payload.maxTime.minute:02d}" + return f"Between {minTime} and {maxTime}" + elif isinstance(condition.payload, SingleTimePayload): + if condition.payload.time: + return f"{condition.payload.time.hour}:{condition.payload.time.minute:02d}" elif isinstance(condition.payload, MinMaxPayload): min_value = condition.payload.min_value max_value = condition.payload.max_value return f"{min_value} and {max_value}" - return "true" if condition.payload.value else "false" + elif isinstance(condition.payload, MinMaxSliderRowPayload): + return f"Between {condition.payload.minValue} and {condition.payload.maxValue}" + elif hasattr(condition.payload, "value"): + return str(condition.payload.value) + + return "Unknown" class ActivityItemChangeService(BaseChangeGenerator): diff --git a/src/apps/activities/tests/fixtures/conditional_logic.py b/src/apps/activities/tests/fixtures/conditional_logic.py index 306efb7d83d..d656d397592 100644 --- a/src/apps/activities/tests/fixtures/conditional_logic.py +++ b/src/apps/activities/tests/fixtures/conditional_logic.py @@ -1,19 +1,43 @@ import pytest +from apps.activities.domain import conditions as cnd from apps.activities.domain.conditional_logic import ConditionalLogic, Match -from apps.activities.domain.conditions import ConditionType, EqualCondition, ValuePayload from apps.activities.tests.utils import DEFAULT_ITEM_NAME @pytest.fixture -def condition_equal() -> EqualCondition: - return EqualCondition( +def condition_equal() -> cnd.EqualCondition: + return cnd.EqualCondition( item_name=DEFAULT_ITEM_NAME, - type=ConditionType.EQUAL, - payload=ValuePayload(value=1), + type=cnd.ConditionType.EQUAL, + payload=cnd.ValuePayload(value=1), ) @pytest.fixture -def conditional_logic(condition_equal: EqualCondition) -> ConditionalLogic: +def condition_between() -> cnd.BetweenCondition: + return cnd.BetweenCondition( + item_name=DEFAULT_ITEM_NAME, type=cnd.ConditionType.BETWEEN, payload=cnd.MinMaxPayload(min_value=0, max_value=2) + ) + + +@pytest.fixture +def condition_rows_outside_of() -> cnd.OutsideOfCondition: + return cnd.OutsideOfCondition( + item_name=DEFAULT_ITEM_NAME, payload=cnd.MinMaxPayloadRow(min_value=0, max_value=10, row_index=0) + ) + + +@pytest.fixture +def conditional_logic_equal(condition_equal: cnd.EqualCondition) -> ConditionalLogic: return ConditionalLogic(match=Match.ALL, conditions=[condition_equal]) + + +@pytest.fixture +def conditional_logic_between(condition_between: cnd.BetweenCondition) -> ConditionalLogic: + return ConditionalLogic(math=Match.ALL, conditions=[condition_between]) + + +@pytest.fixture +def conditional_logic_rows_outside_of(condition_rows_outside_of: cnd.OutsideOfCondition) -> ConditionalLogic: + return ConditionalLogic(math=Match.ALL, conditions=[condition_rows_outside_of]) diff --git a/src/apps/activities/tests/unit/domain/test_activity_item_create.py b/src/apps/activities/tests/unit/domain/test_activity_item_create.py index 8158aecec4e..eaa4a4a8782 100644 --- a/src/apps/activities/tests/unit/domain/test_activity_item_create.py +++ b/src/apps/activities/tests/unit/domain/test_activity_item_create.py @@ -28,23 +28,10 @@ from apps.shared.domain.custom_validations import InvalidImageError -def test_create_activity_item_conditional_logic_not_valid_response_type_config( - base_item_data, phrasal_template_config, phrasal_template_with_text_response_values, conditional_logic -) -> None: - with pytest.raises(errors.IncorrectConditionLogicItemTypeError): - ActivityItemCreate( - **base_item_data.dict(), - config=phrasal_template_config, - response_type=ResponseType.PHRASAL_TEMPLATE, - conditional_logic=conditional_logic, - response_values=phrasal_template_with_text_response_values, - ) - - def test_create_activity_item_conditional_logic_can_not_be_hidden( base_item_data, single_select_config, - conditional_logic, + conditional_logic_equal, single_select_response_values, ) -> None: base_item_data.is_hidden = True @@ -52,7 +39,7 @@ def test_create_activity_item_conditional_logic_can_not_be_hidden( ActivityItemCreate( **base_item_data.dict(), config=single_select_config, - conditional_logic=conditional_logic, + conditional_logic=conditional_logic_equal, response_values=single_select_response_values, response_type=ResponseType.SINGLESELECT, ) @@ -610,3 +597,56 @@ def test_create_message_item__sanitize_question(message_item_create): data["question"] = {"en": "One Two"} item = ActivityItemCreate(**data) assert item.question["en"] == "One Two" + + +@pytest.mark.parametrize( + "response_type, config_fixture, cnd_logic_fixture, response_values_fixture", + ( + (ResponseType.SINGLESELECT, "single_select_config", "conditional_logic_equal", "single_select_response_values"), + (ResponseType.MULTISELECT, "multi_select_config", "conditional_logic_equal", "multi_select_response_values"), + (ResponseType.SLIDER, "slider_config", "conditional_logic_between", "slider_response_values"), + (ResponseType.TIME, "time_config", "conditional_logic_between", None), + (ResponseType.TIMERANGE, "time_range_config", "conditional_logic_between", None), + ( + ResponseType.NUMBERSELECT, + "number_selection_config", + "conditional_logic_between", + "number_selection_response_values", + ), + (ResponseType.DATE, "date_config", "conditional_logic_equal", None), + ( + ResponseType.SINGLESELECTROWS, + "single_select_row_config", + "conditional_logic_equal", + "single_select_row_response_values", + ), + ( + ResponseType.MULTISELECTROWS, + "multi_select_row_config", + "conditional_logic_equal", + "multi_select_row_response_values", + ), + ( + ResponseType.SLIDERROWS, + "slider_rows_config", + "conditional_logic_rows_outside_of", + "slider_rows_response_values", + ), + ), +) +def test_create_activity_item_conditional_logic( + base_item_data, request, response_type, config_fixture, cnd_logic_fixture, response_values_fixture +) -> None: + config = request.getfixturevalue(config_fixture) + cnd_logic = request.getfixturevalue(cnd_logic_fixture) + if response_values_fixture: + response_values = request.getfixturevalue(response_values_fixture) + else: + response_values = None + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=response_type, + conditional_logic=cnd_logic, + response_values=response_values, + ) diff --git a/src/apps/activities/tests/unit/domain/test_custom_validation.py b/src/apps/activities/tests/unit/domain/test_custom_validation.py index e914fea49aa..ac50cd77b8e 100644 --- a/src/apps/activities/tests/unit/domain/test_custom_validation.py +++ b/src/apps/activities/tests/unit/domain/test_custom_validation.py @@ -1,18 +1,32 @@ from typing import cast import pytest +from pydantic import ValidationError from apps.activities.domain.activity_create import ActivityItemCreate from apps.activities.domain.conditional_logic import ConditionalLogic from apps.activities.domain.conditions import ( + BetweenCondition, Condition, ConditionType, EqualCondition, EqualToOptionCondition, + GreaterThanCondition, + LessThanCondition, + NotEqualCondition, OptionPayload, + OutsideOfCondition, + SingleDatePayload, + SingleTimePayload, # Added for correct payload handling + TimePayload, + TimePayloadType, ValuePayload, ) -from apps.activities.domain.custom_validation import validate_item_flow, validate_score_and_sections, validate_subscales +from apps.activities.domain.custom_validation import ( + validate_item_flow, + validate_score_and_sections, + validate_subscales, +) from apps.activities.domain.response_type_config import ResponseType, SingleSelectionConfig from apps.activities.domain.scores_reports import ( ReportType, @@ -41,6 +55,7 @@ IncorrectSectionPrintItemTypeError, IncorrectSubscaleInsideSubscaleError, IncorrectSubscaleItemError, + IncorrectTimeRange, SubscaleInsideSubscaleError, SubscaleItemScoreError, SubscaleItemTypeError, @@ -75,10 +90,10 @@ def items() -> list[ActivityItemCreate]: is_hidden=False, conditional_logic=ConditionalLogic( conditions=[ - EqualCondition( + EqualToOptionCondition( item_name=item_name, - type=ConditionType.EQUAL, - payload=ValuePayload(value=1), + type=ConditionType.EQUAL_TO_OPTION, + payload=OptionPayload(option_value=1), ) ] ), @@ -139,6 +154,90 @@ def test_incorrect_conditional_option(self, items: list[ActivityItemCreate]): with pytest.raises(IncorrectConditionOptionError): validate_item_flow(values) + @pytest.mark.parametrize( + "payload", + (SingleTimePayload(time={"hours": 1, "minutes": 0}),), + ) + def test_validator_successful_create_eq_condition(self, payload): + EqualCondition( + item_name="test", + type=ConditionType.EQUAL, + payload=payload, + ) + + @pytest.mark.parametrize( + "payload", + (TimePayload(type=TimePayloadType.START_TIME, value="01:00"),), + ) + def test_validator_successful_create_ne_condition(self, payload): + NotEqualCondition( + item_name="test", + type=ConditionType.NOT_EQUAL, + payload=payload, + ) + + @pytest.mark.parametrize( + "payload", + (TimePayload(type=TimePayloadType.START_TIME, value="01:00"),), + ) + def test_validator_successful_create_lt_condition(self, payload): + LessThanCondition( + item_name="test", + type=ConditionType.LESS_THAN, + payload=payload, + ) + + @pytest.mark.parametrize( + "payload", + (TimePayload(type=TimePayloadType.START_TIME, value="01:00"),), + ) + def test_validator_successful_create_gt_condition(self, payload): + GreaterThanCondition( + item_name="test", + type=ConditionType.GREATER_THAN, + payload=payload, + ) + + @pytest.mark.parametrize( + "payload", + (TimePayload(type=TimePayloadType.START_TIME, value="01:00"),), + ) + def test_validator_successful_create_between_condition(self, payload): + BetweenCondition( + item_name="test", + type=ConditionType.BETWEEN, + payload=payload, + ) + + @pytest.mark.parametrize( + "payload", + (TimePayload(type=TimePayloadType.START_TIME, value="01:00"),), + ) + def test_validator_successful_create_outside_condition(self, payload): + OutsideOfCondition( + item_name="test", + type=ConditionType.OUTSIDE_OF, + payload=payload, + ) + + @pytest.mark.parametrize( + "payload_type,payload,exp_exception", + ( + (SingleDatePayload, dict(date="1970-99-01"), ValidationError), + (SingleTimePayload, dict(time={"hours": 80, "minutes": 0}), ValueError), # Adjusted time dict + ( + SingleTimePayload, + dict(time={"hours": 3, "minutes": 0}, min_value="03:00", max_value="02:00"), + IncorrectTimeRange, + ), + (SingleTimePayload, dict(time={"hours": 3, "minutes": 0}), ValidationError), + (SingleTimePayload, dict(time="unknown_item_type", min_value="01:00", max_value="02:00"), ValidationError), + ), + ) + def test_fails_create_outside_condition(self, payload_type, payload, exp_exception): + with pytest.raises(exp_exception): + payload_type(**payload) + class TestValidateScoreAndSections: def test_successful_validation( diff --git a/src/apps/shared/domain/base.py b/src/apps/shared/domain/base.py index 3d2b55db49a..b01d7aeb518 100644 --- a/src/apps/shared/domain/base.py +++ b/src/apps/shared/domain/base.py @@ -8,6 +8,7 @@ __all__ = [ "InternalModel", "PublicModel", + "PublicModelNoExtra", "to_camelcase", "list_items_to_camel_case", "dict_keys_to_camel_case", @@ -102,3 +103,8 @@ class Config: allow_population_by_field_name = True validate_assignment = True alias_generator = to_camelcase + + +class PublicModelNoExtra(PublicModel): + class Config(PublicModel.Config): + extra = Extra.forbid From 92d601eaf17480f949596f5a1fdcbe61d6821616 Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Fri, 11 Oct 2024 11:28:24 -0500 Subject: [PATCH 32/41] feat(applet-duplication): add optional report server config flag (M2-7830) (#1623) This PR updates the applet duplication endpoint to optionally include an optional `include_report_server` property that defaults to false (making it backwards compatible). The following properties are duplicated: - `reportServerIp` - `reportPublicKey` - `reportIncludeUserId` - `reportIncludeCaseId` - `reportEmailBody` --- src/apps/applets/api/applets.py | 4 +- .../applets/domain/applet_create_update.py | 1 + src/apps/applets/router.py | 2 +- src/apps/applets/service/applet.py | 22 +++++- src/apps/applets/tests/test_applet.py | 68 ++++++++++++++++++- 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/apps/applets/api/applets.py b/src/apps/applets/api/applets.py index 1e6e013ec0b..c674b76827e 100644 --- a/src/apps/applets/api/applets.py +++ b/src/apps/applets/api/applets.py @@ -188,7 +188,9 @@ async def applet_duplicate( await CheckAccessService(session, user.id).check_applet_duplicate_access(applet_id) applet_for_duplicate = await service.get_by_id_for_duplicate(applet_id) - applet = await service.duplicate(applet_for_duplicate, schema.display_name, schema.encryption) + applet = await service.duplicate( + applet_for_duplicate, schema.display_name, schema.encryption, schema.include_report_server + ) return Response(result=public_detail.Applet.from_orm(applet)) diff --git a/src/apps/applets/domain/applet_create_update.py b/src/apps/applets/domain/applet_create_update.py index bbe40e0df2c..dbff1ff7653 100644 --- a/src/apps/applets/domain/applet_create_update.py +++ b/src/apps/applets/domain/applet_create_update.py @@ -108,3 +108,4 @@ class AppletReportConfiguration(AppletReportConfigurationBase, InternalModel): class AppletDuplicateRequest(InternalModel): display_name: str encryption: Encryption + include_report_server: bool = False diff --git a/src/apps/applets/router.py b/src/apps/applets/router.py index ca7e01e8902..2484c6ad469 100644 --- a/src/apps/applets/router.py +++ b/src/apps/applets/router.py @@ -102,7 +102,7 @@ router.post( "/{applet_id}/duplicate", - description="""This endpoint using for duplicate existing applet""", + description="""Duplicate an existing applet, and optionally its report server configuration""", response_model_by_alias=True, response_model=Response[public_detail.Applet], status_code=status.HTTP_201_CREATED, diff --git a/src/apps/applets/service/applet.py b/src/apps/applets/service/applet.py index 33641b6ba8c..5996b0d1a8f 100644 --- a/src/apps/applets/service/applet.py +++ b/src/apps/applets/service/applet.py @@ -25,7 +25,7 @@ from apps.applets.domain.applet_duplicate import AppletDuplicate from apps.applets.domain.applet_full import AppletFull from apps.applets.domain.applet_link import AppletLink, CreateAccessLink -from apps.applets.domain.base import Encryption +from apps.applets.domain.base import AppletReportConfigurationBase, Encryption from apps.applets.errors import ( AccessLinkDoesNotExistError, AppletAlreadyExist, @@ -219,6 +219,7 @@ async def duplicate( applet_exist: AppletDuplicate, new_name: str, encryption: Encryption, + include_report_server: bool, ): activity_key_id_map = dict() @@ -232,7 +233,7 @@ async def duplicate( ) manager_role = Role.EDITOR if has_editor else Role.MANAGER - create_data = self._prepare_duplicate(applet_exist, new_name, encryption) + create_data = self._prepare_duplicate(applet_exist, new_name, encryption, include_report_server) applet = await self._create(create_data, self.user_id) @@ -252,7 +253,9 @@ async def duplicate( return applet @staticmethod - def _prepare_duplicate(applet_exist: AppletDuplicate, new_name: str, encryption: Encryption) -> AppletCreate: + def _prepare_duplicate( + applet_exist: AppletDuplicate, new_name: str, encryption: Encryption, include_report_server: bool + ) -> AppletCreate: activities = list() for activity in applet_exist.activities: activities.append( @@ -289,7 +292,20 @@ def _prepare_duplicate(applet_exist: AppletDuplicate, new_name: str, encryption: ) ) + report_server_config = ( + AppletReportConfigurationBase( + report_server_ip=applet_exist.report_server_ip, + report_public_key=applet_exist.report_public_key, + report_include_user_id=applet_exist.report_include_user_id, + report_include_case_id=applet_exist.report_include_case_id, + report_email_body=applet_exist.report_email_body, + ).dict() + if include_report_server + else {} + ) + return AppletCreate( + **report_server_config, display_name=new_name, description=applet_exist.description, about=applet_exist.about, diff --git a/src/apps/applets/tests/test_applet.py b/src/apps/applets/tests/test_applet.py index fca45e772ee..1d1e5befdca 100644 --- a/src/apps/applets/tests/test_applet.py +++ b/src/apps/applets/tests/test_applet.py @@ -23,7 +23,7 @@ ) from apps.activity_assignments.crud.assignments import ActivityAssigmentCRUD from apps.activity_assignments.db.schemas import ActivityAssigmentSchema -from apps.applets.domain.applet_create_update import AppletCreate, AppletUpdate +from apps.applets.domain.applet_create_update import AppletCreate, AppletReportConfiguration, AppletUpdate from apps.applets.domain.applet_full import AppletFull from apps.applets.domain.base import AppletReportConfigurationBase, Encryption from apps.applets.errors import AppletAlreadyExist, AppletVersionNotFoundError @@ -245,6 +245,72 @@ async def test_duplicate_applet( assert response.status_code == http.HTTPStatus.CREATED assert response.json()["result"]["displayName"] == new_name + async def test_duplicate_applet_default_exclude_report_server_config( + self, client: TestClient, tom: User, applet_one: AppletFull, encryption: Encryption, session: AsyncSession + ): + await AppletService(session, tom.id).set_report_configuration( + applet_one.id, + AppletReportConfiguration( + report_server_ip="ipaddress", + report_public_key="public key", + report_recipients=["recipient1", "recipient1"], + report_include_user_id=True, + report_include_case_id=True, + report_email_body="email body", + ), + ) + + client.login(tom) + new_name = "New Name" + response = await client.post( + self.applet_duplicate_url.format(pk=applet_one.id), + data=dict(display_name=new_name, encryption=encryption.dict()), + ) + assert response.status_code == http.HTTPStatus.CREATED + + result = response.json()["result"] + assert result["displayName"] == new_name + assert result["reportServerIp"] == "" + assert result["reportPublicKey"] == "" + assert result["reportRecipients"] == [] + assert result["reportIncludeUserId"] is False + assert result["reportIncludeCaseId"] is False + assert result["reportEmailBody"] == "" + + async def test_duplicate_applet_include_report_server_config( + self, client: TestClient, tom: User, applet_one: AppletFull, encryption: Encryption, session: AsyncSession + ): + await AppletService(session, tom.id).set_report_configuration( + applet_one.id, + AppletReportConfiguration( + report_server_ip="ipaddress", + report_public_key="public key", + report_recipients=["recipient1", "recipient1"], + report_include_user_id=True, + report_include_case_id=True, + report_email_body="email body", + ), + ) + + client.login(tom) + new_name = "New Name" + response = await client.post( + self.applet_duplicate_url.format(pk=applet_one.id), + data=dict(display_name=new_name, encryption=encryption.dict(), include_report_server=True), + ) + assert response.status_code == http.HTTPStatus.CREATED + + result = response.json()["result"] + assert result["displayName"] == new_name + assert result["reportServerIp"] == "ipaddress" + assert result["reportPublicKey"] == "public key" + assert result["reportIncludeUserId"] is True + assert result["reportIncludeCaseId"] is True + assert result["reportEmailBody"] == "email body" + + # Recipients are excluded + assert result["reportRecipients"] == [] + async def test_duplicate_applet_name_already_exists( self, client: TestClient, tom: User, applet_one: AppletFull, encryption: Encryption ): From 3b8cb7dc46a69114353051b5c8465b6f2dd2625c Mon Sep 17 00:00:00 2001 From: vshvechko Date: Fri, 11 Oct 2024 22:37:53 +0300 Subject: [PATCH 33/41] chore: Change report server contract (M2-7481,M2-7483) (#1584) --- src/apps/answers/crud/answers.py | 1 + src/apps/answers/domain/answers.py | 4 ++++ src/apps/answers/service.py | 33 ++++++++++++++++-------------- src/cli.py | 2 +- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/apps/answers/crud/answers.py b/src/apps/answers/crud/answers.py index 14b5b1bce76..b02ab11ed99 100644 --- a/src/apps/answers/crud/answers.py +++ b/src/apps/answers/crud/answers.py @@ -100,6 +100,7 @@ async def get_list(self, **filters) -> list[Answer]: """ @param filters: see supported filters: _AnswerListFilter """ + self.session.expire_all() query = select(AnswerSchema).join(AnswerSchema.answer_item).options(contains_eager(AnswerSchema.answer_item)) _filters = _AnswerListFilter().get_clauses(**filters) diff --git a/src/apps/answers/domain/answers.py b/src/apps/answers/domain/answers.py index 741add8c950..0139168d4bb 100644 --- a/src/apps/answers/domain/answers.py +++ b/src/apps/answers/domain/answers.py @@ -41,6 +41,10 @@ class Answer(InternalModel): activity_history_id: str respondent_id: uuid.UUID | None is_flow_completed: bool | None = False + target_subject_id: uuid.UUID | None = None + source_subject_id: uuid.UUID | None = None + input_subject_id: uuid.UUID | None = None + relation: str | None = None migrated_data: dict | None = None created_at: datetime.datetime diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 37b1a05bbc0..30207981a7e 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -60,6 +60,7 @@ SummaryActivityFlow, ) from apps.answers.domain.answers import ( + Answer, AnswersCopyCheckResult, AppletSubmission, FilesCopyCheckResult, @@ -1949,7 +1950,10 @@ async def is_flow_finished(self, submit_id: uuid.UUID, answer_id: uuid.UUID) -> async def create_report( self, submit_id: uuid.UUID, answer_id: uuid.UUID | None = None ) -> ReportServerResponse | None: - answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) + filters = dict(submit_id=submit_id) + if answer_id: + filters.update(answer_id=answer_id) + answers = await AnswersCRUD(self.answers_session).get_list(**filters) if not answers: return None applet_id_version: str = answers[0].applet_history_id @@ -1960,8 +1964,8 @@ async def create_report( # If answers only on performance tasks if not answers_for_report: return None - answer_map = dict((answer.id, answer) for answer in answers_for_report) initial_answer = answers_for_report[0] + assert initial_answer.target_subject_id applet = await AppletsCRUD(self.session).get_by_id(initial_answer.applet_id) user_info = await self._get_user_info(initial_answer.target_subject_id) @@ -1973,12 +1977,10 @@ async def create_report( ) encryption = ReportServerEncryption(applet.report_public_key) - responses, user_public_keys = await self._prepare_responses(answer_map) + responses = await self._prepare_responses(answers_for_report) data = dict( responses=responses, - userPublicKeys=user_public_keys, - userPublicKey=user_public_keys[0], now=datetime.datetime.utcnow().strftime("%x"), user=user_info, applet=applet_full, @@ -2044,17 +2046,18 @@ async def _get_user_info(self, subject_id: uuid.UUID): tag=subject.tag, ) - async def _prepare_responses(self, answers_map: dict[uuid.UUID, AnswerSchema]) -> tuple[list[dict], list[str]]: - answer_items = await AnswerItemsCRUD(self.answers_session).get_respondent_submits_by_answer_ids( - list(answers_map.keys()) - ) - + async def _prepare_responses(self, answers: list[Answer]) -> list[dict]: responses = list() - for answer_item in answer_items: - answer = answers_map[answer_item.answer_id] - activity_id, version = answer.activity_history_id.split("_") - responses.append(dict(activityId=activity_id, answer=answer_item.answer)) - return responses, [ai.user_public_key for ai in answer_items] + for answer in answers: + activity_id = HistoryAware().id_from_history_id(answer.activity_history_id) + responses.append( + dict( + activityId=activity_id, + answer=answer.answer_item.answer, + userPublicKey=answer.answer_item.user_public_key, + ) + ) + return responses class ReportServerEncryption: diff --git a/src/cli.py b/src/cli.py index 74d471d971a..64673e8510e 100755 --- a/src/cli.py +++ b/src/cli.py @@ -5,7 +5,7 @@ os.chdir(dname) -import typer # noqa: E402 +import typer # noqa: E402,I001 from apps.activities.commands import activities # noqa: E402 from apps.answers.commands import convert_assessments # noqa: E402 From 4f7d8da93bf4276c81c926a1e1040ecd86a0e5c3 Mon Sep 17 00:00:00 2001 From: Rodrigo Colao Merlo Date: Tue, 15 Oct 2024 17:21:05 -0300 Subject: [PATCH 34/41] fix: Downgrade boto3 to version 1.26.10 (M2-8020) (#1627) --- Pipfile | 2 +- Pipfile.lock | 2271 +++++++++++++++++++++++++++----------------------- 2 files changed, 1231 insertions(+), 1042 deletions(-) diff --git a/Pipfile b/Pipfile index 9438a2662f1..5c98dff1c1b 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,7 @@ name = "pypi" redis = "==5.0.8" alembic = "==1.13.2" asyncpg = "==0.29.0" -boto3 = "==1.35.16" +boto3 = "==1.26.10" fastapi = "==0.110.3" # The latest version of the fastapi is not taken because of the issue # with fastapi-mail that requires 0.21 < starlette < 0.22 diff --git a/Pipfile.lock b/Pipfile.lock index e41225c1e26..66c5726364e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4176cbc56761e9bb9ae86678e121b87dc4381f6d4f4791bb3c92e7d66e354a3b" + "sha256": "71c257124ef612ad083bfcfbc540232dd5733ef3896e065d3b5a983469e77993" }, "pipfile-spec": 6, "requires": { @@ -36,11 +36,11 @@ }, "aiohappyeyeballs": { "hashes": [ - "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2", - "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd" + "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", + "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572" ], "markers": "python_version >= '3.8'", - "version": "==2.4.0" + "version": "==2.4.3" }, "aiohttp": { "hashes": [ @@ -175,11 +175,11 @@ }, "anyio": { "hashes": [ - "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94", - "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7" + "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", + "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" ], - "markers": "python_version >= '3.8'", - "version": "==4.4.0" + "markers": "python_version >= '3.9'", + "version": "==4.6.2.post1" }, "asgiref": { "hashes": [ @@ -194,7 +194,7 @@ "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" ], - "markers": "python_full_version < '3.12.0'", + "markers": "python_full_version < '3.11.3'", "version": "==4.0.3" }, "asyncpg": { @@ -255,11 +255,11 @@ }, "azure-core": { "hashes": [ - "sha256:a14dc210efcd608821aa472d9fb8e8d035d29b68993819147bc290a8ac224472", - "sha256:cf019c1ca832e96274ae85abd3d9f752397194d9fea3b41487290562ac8abe4a" + "sha256:22954de3777e0250029360ef31d80448ef1be13b80a459bff80ba7073379e2cd", + "sha256:656a0dd61e1869b1506b7c6a3b31d62f15984b1a573d6326f6aa2f3e4123284b" ], "markers": "python_version >= '3.8'", - "version": "==1.30.2" + "version": "==1.31.0" }, "azure-storage-blob": { "hashes": [ @@ -314,20 +314,20 @@ }, "boto3": { "hashes": [ - "sha256:9b96c210678cf430b16b49dee87db30f46044602bb9a605a465e1900f468a43f", - "sha256:9c5b0ce4a25bb78d659478d1c552f1dbb7ff275aab3263bb41cdbef8bca28693" + "sha256:0e2444f1f653c2fa87e6e30b3ac983cba2961a98a27dd788753a34e198c9e450", + "sha256:48e579088ec320f84266bb26434a14ab3e375456feb0f3bf043f78c485a3cee2" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.35.16" + "markers": "python_version >= '3.7'", + "version": "==1.26.10" }, "botocore": { "hashes": [ - "sha256:0d35d03ea647b5d464c7f77bdab6fb23ae5d49752b13cf97ab84444518c7b1bd", - "sha256:a93f773ca93139529b5d36730b382dbee63ab4c7f26129aa5c84835255ca999d" + "sha256:6f35d59e230095aed7cd747604fe248fa384bebb7d09549077892f936a8ca3df", + "sha256:988b948be685006b43c4bbd8f5c0cb93e77c66deb70561994e0c5b31b5a67210" ], - "markers": "python_version >= '3.8'", - "version": "==1.35.17" + "markers": "python_version >= '3.7'", + "version": "==1.29.165" }, "cachecontrol": { "hashes": [ @@ -428,99 +428,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "click": { "hashes": [ @@ -573,11 +588,11 @@ }, "dnspython": { "hashes": [ - "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", - "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc" + "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", + "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1" ], - "markers": "python_version >= '3.8'", - "version": "==2.6.1" + "markers": "python_version >= '3.9'", + "version": "==2.7.0" }, "ecdsa": { "hashes": [ @@ -618,7 +633,7 @@ "sha256:e85783a5a05fa8f48677b965ed8f2056535d7c78b8de714359a01f45f1cd3e21" ], "index": "pypi", - "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", + "markers": "python_full_version >= '3.8.1' and python_version < '4.0'", "version": "==1.2.9" }, "firebase-admin": { @@ -724,27 +739,27 @@ "grpc" ], "hashes": [ - "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", - "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" + "sha256:4a152fd11a9f774ea606388d423b68aa7e6d6a0ffe4c8266f74979613ec09f81", + "sha256:6869eacb2a37720380ba5898312af79a4d30b8bca1548fb4093e0697dc4bdf5d" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==2.19.2" + "version": "==2.21.0" }, "google-api-python-client": { "hashes": [ - "sha256:8b84dde11aaccadc127e4846f5cd932331d804ea324e353131595e3f25376e97", - "sha256:d74da1358f3f2d63daf3c6f26bd96d89652051183bc87cf10a56ceb2a70beb50" + "sha256:1a5232e9cfed8c201799d9327e4d44dc7ea7daa3c6e1627fca41aa201539c0da", + "sha256:b9d68c6b14ec72580d66001bd33c5816b78e2134b93ccc5cf8f624516b561750" ], "markers": "python_version >= '3.7'", - "version": "==2.145.0" + "version": "==2.149.0" }, "google-auth": { "hashes": [ - "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", - "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc" + "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f", + "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a" ], "markers": "python_version >= '3.7'", - "version": "==2.34.0" + "version": "==2.35.0" }, "google-auth-httplib2": { "hashes": [ @@ -763,11 +778,11 @@ }, "google-cloud-firestore": { "hashes": [ - "sha256:3db5dd42334b9904d82b3786703a5a4b576810fb50f61b8fa83ecf4f17b7fdae", - "sha256:9a735860b692f39f93f900dd3390713ceb9b47ea82cda98360bb551f03d2b916" + "sha256:1b2ce6e0b791aee89a1e4f072beba1012247e89baca361eed721fb467fe054b0", + "sha256:b49f0019d7bd0d4ab5972a4cff13994b0aabe72d24242200d904db2fb49df7f7" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==2.18.0" + "version": "==2.19.0" }, "google-cloud-storage": { "hashes": [ @@ -828,127 +843,142 @@ }, "greenlet": { "hashes": [ - "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9", - "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17", - "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc", - "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637", - "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2", - "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3", - "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6", - "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b", - "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf", - "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27", - "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1", - "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc", - "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a", - "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b", - "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d", - "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28", - "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303", - "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99", - "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f", - "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7", - "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6", - "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a", - "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc", - "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0", - "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8", - "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a", - "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca", - "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b", - "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989", - "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19", - "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6", - "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484", - "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd", - "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25", - "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b", - "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910", - "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0", - "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5", - "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345", - "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6", - "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00", - "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df", - "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811", - "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca", - "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8", - "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33", - "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97", - "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0", - "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b", - "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682", - "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39", - "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64", - "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f", - "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665", - "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f", - "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc", - "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d", - "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a", - "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0", - "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09", - "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b", - "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491", - "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7", - "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954", - "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501", - "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.0" + "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", + "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", + "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", + "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", + "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", + "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", + "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", + "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", + "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", + "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", + "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", + "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", + "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", + "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", + "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", + "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", + "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", + "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", + "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", + "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", + "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", + "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", + "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", + "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", + "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", + "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", + "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", + "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", + "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", + "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", + "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", + "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", + "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", + "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", + "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", + "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", + "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", + "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", + "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", + "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", + "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", + "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", + "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", + "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", + "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", + "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", + "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", + "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", + "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", + "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", + "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", + "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", + "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", + "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", + "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", + "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", + "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", + "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", + "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", + "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", + "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", + "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", + "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", + "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", + "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", + "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", + "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", + "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", + "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", + "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", + "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", + "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", + "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" + ], + "version": "==3.1.1" }, "grpcio": { "hashes": [ - "sha256:0e6c9b42ded5d02b6b1fea3a25f036a2236eeb75d0579bfd43c0018c88bf0a3e", - "sha256:161d5c535c2bdf61b95080e7f0f017a1dfcb812bf54093e71e5562b16225b4ce", - "sha256:17663598aadbedc3cacd7bbde432f541c8e07d2496564e22b214b22c7523dac8", - "sha256:1c17ebcec157cfb8dd445890a03e20caf6209a5bd4ac5b040ae9dbc59eef091d", - "sha256:292a846b92cdcd40ecca46e694997dd6b9be6c4c01a94a0dfb3fcb75d20da858", - "sha256:2ca2559692d8e7e245d456877a85ee41525f3ed425aa97eb7a70fc9a79df91a0", - "sha256:307b1d538140f19ccbd3aed7a93d8f71103c5d525f3c96f8616111614b14bf2a", - "sha256:30a1c2cf9390c894c90bbc70147f2372130ad189cffef161f0432d0157973f45", - "sha256:31a049daa428f928f21090403e5d18ea02670e3d5d172581670be006100db9ef", - "sha256:35334f9c9745add3e357e3372756fd32d925bd52c41da97f4dfdafbde0bf0ee2", - "sha256:3750c5a00bd644c75f4507f77a804d0189d97a107eb1481945a0cf3af3e7a5ac", - "sha256:3885f037eb11f1cacc41f207b705f38a44b69478086f40608959bf5ad85826dd", - "sha256:4573608e23f7e091acfbe3e84ac2045680b69751d8d67685ffa193a4429fedb1", - "sha256:4825a3aa5648010842e1c9d35a082187746aa0cdbf1b7a2a930595a94fb10fce", - "sha256:4877ba180591acdf127afe21ec1c7ff8a5ecf0fe2600f0d3c50e8c4a1cbc6492", - "sha256:48b0d92d45ce3be2084b92fb5bae2f64c208fea8ceed7fccf6a7b524d3c4942e", - "sha256:4d813316d1a752be6f5c4360c49f55b06d4fe212d7df03253dfdae90c8a402bb", - "sha256:5dd67ed9da78e5121efc5c510f0122a972216808d6de70953a740560c572eb44", - "sha256:6f914386e52cbdeb5d2a7ce3bf1fdfacbe9d818dd81b6099a05b741aaf3848bb", - "sha256:7101db1bd4cd9b880294dec41a93fcdce465bdbb602cd8dc5bd2d6362b618759", - "sha256:7e06aa1f764ec8265b19d8f00140b8c4b6ca179a6dc67aa9413867c47e1fb04e", - "sha256:84ca1be089fb4446490dd1135828bd42a7c7f8421e74fa581611f7afdf7ab761", - "sha256:8a1e224ce6f740dbb6b24c58f885422deebd7eb724aff0671a847f8951857c26", - "sha256:97ae7edd3f3f91480e48ede5d3e7d431ad6005bfdbd65c1b56913799ec79e791", - "sha256:9c9bebc6627873ec27a70fc800f6083a13c70b23a5564788754b9ee52c5aef6c", - "sha256:a013c5fbb12bfb5f927444b477a26f1080755a931d5d362e6a9a720ca7dbae60", - "sha256:a66fe4dc35d2330c185cfbb42959f57ad36f257e0cc4557d11d9f0a3f14311df", - "sha256:a92c4f58c01c77205df6ff999faa008540475c39b835277fb8883b11cada127a", - "sha256:aa8ba945c96e73de29d25331b26f3e416e0c0f621e984a3ebdb2d0d0b596a3b3", - "sha256:b0aa03d240b5539648d996cc60438f128c7f46050989e35b25f5c18286c86734", - "sha256:b1b24c23d51a1e8790b25514157d43f0a4dce1ac12b3f0b8e9f66a5e2c4c132f", - "sha256:b7ffb8ea674d68de4cac6f57d2498fef477cef582f1fa849e9f844863af50083", - "sha256:b9feb4e5ec8dc2d15709f4d5fc367794d69277f5d680baf1910fc9915c633524", - "sha256:bff2096bdba686019fb32d2dde45b95981f0d1490e054400f70fc9a8af34b49d", - "sha256:c30aeceeaff11cd5ddbc348f37c58bcb96da8d5aa93fed78ab329de5f37a0d7a", - "sha256:c9f80f9fad93a8cf71c7f161778ba47fd730d13a343a46258065c4deb4b550c0", - "sha256:cfd349de4158d797db2bd82d2020554a121674e98fbe6b15328456b3bf2495bb", - "sha256:d0cd7050397b3609ea51727b1811e663ffda8bda39c6a5bb69525ef12414b503", - "sha256:d639c939ad7c440c7b2819a28d559179a4508783f7e5b991166f8d7a34b52815", - "sha256:e3ba04659e4fce609de2658fe4dbf7d6ed21987a94460f5f92df7579fd5d0e22", - "sha256:ecfe735e7a59e5a98208447293ff8580e9db1e890e232b8b292dc8bd15afc0d2", - "sha256:ef82d361ed5849d34cf09105d00b94b6728d289d6b9235513cb2fcc79f7c432c", - "sha256:f03a5884c56256e08fd9e262e11b5cfacf1af96e2ce78dc095d2c41ccae2c80d", - "sha256:f1fe60d0772831d96d263b53d83fb9a3d050a94b0e94b6d004a5ad111faa5b5b", - "sha256:f517fd7259fe823ef3bd21e508b653d5492e706e9f0ef82c16ce3347a8a5620c", - "sha256:fdb14bad0835914f325349ed34a51940bc2ad965142eb3090081593c6e347be9" - ], - "markers": "python_version >= '3.8'", - "version": "==1.66.1" + "sha256:02697eb4a5cbe5a9639f57323b4c37bcb3ab2d48cec5da3dc2f13334d72790dd", + "sha256:03b0b307ba26fae695e067b94cbb014e27390f8bc5ac7a3a39b7723fed085604", + "sha256:05bc2ceadc2529ab0b227b1310d249d95d9001cd106aa4d31e8871ad3c428d73", + "sha256:06de8ec0bd71be123eec15b0e0d457474931c2c407869b6c349bd9bed4adbac3", + "sha256:0be4e0490c28da5377283861bed2941d1d20ec017ca397a5df4394d1c31a9b50", + "sha256:12fda97ffae55e6526825daf25ad0fa37483685952b5d0f910d6405c87e3adb6", + "sha256:1caa38fb22a8578ab8393da99d4b8641e3a80abc8fd52646f1ecc92bcb8dee34", + "sha256:2018b053aa15782db2541ca01a7edb56a0bf18c77efed975392583725974b249", + "sha256:20657d6b8cfed7db5e11b62ff7dfe2e12064ea78e93f1434d61888834bc86d75", + "sha256:2335c58560a9e92ac58ff2bc5649952f9b37d0735608242973c7a8b94a6437d8", + "sha256:31fd163105464797a72d901a06472860845ac157389e10f12631025b3e4d0453", + "sha256:38b68498ff579a3b1ee8f93a05eb48dc2595795f2f62716e797dc24774c1aaa8", + "sha256:3b00efc473b20d8bf83e0e1ae661b98951ca56111feb9b9611df8efc4fe5d55d", + "sha256:3ed71e81782966ffead60268bbda31ea3f725ebf8aa73634d5dda44f2cf3fb9c", + "sha256:45a3d462826f4868b442a6b8fdbe8b87b45eb4f5b5308168c156b21eca43f61c", + "sha256:49f0ca7ae850f59f828a723a9064cadbed90f1ece179d375966546499b8a2c9c", + "sha256:4e504572433f4e72b12394977679161d495c4c9581ba34a88d843eaf0f2fbd39", + "sha256:4ea1d062c9230278793820146c95d038dc0f468cbdd172eec3363e42ff1c7d01", + "sha256:563588c587b75c34b928bc428548e5b00ea38c46972181a4d8b75ba7e3f24231", + "sha256:6001e575b8bbd89eee11960bb640b6da6ae110cf08113a075f1e2051cc596cae", + "sha256:66a0cd8ba6512b401d7ed46bb03f4ee455839957f28b8d61e7708056a806ba6a", + "sha256:6851de821249340bdb100df5eacfecfc4e6075fa85c6df7ee0eb213170ec8e5d", + "sha256:728bdf36a186e7f51da73be7f8d09457a03061be848718d0edf000e709418987", + "sha256:73e3b425c1e155730273f73e419de3074aa5c5e936771ee0e4af0814631fb30a", + "sha256:73fc8f8b9b5c4a03e802b3cd0c18b2b06b410d3c1dcbef989fdeb943bd44aff7", + "sha256:78fa51ebc2d9242c0fc5db0feecc57a9943303b46664ad89921f5079e2e4ada7", + "sha256:7b2c86457145ce14c38e5bf6bdc19ef88e66c5fee2c3d83285c5aef026ba93b3", + "sha256:7d69ce1f324dc2d71e40c9261d3fdbe7d4c9d60f332069ff9b2a4d8a257c7b2b", + "sha256:802d84fd3d50614170649853d121baaaa305de7b65b3e01759247e768d691ddf", + "sha256:80fd702ba7e432994df208f27514280b4b5c6843e12a48759c9255679ad38db8", + "sha256:8ac475e8da31484efa25abb774674d837b343afb78bb3bcdef10f81a93e3d6bf", + "sha256:950da58d7d80abd0ea68757769c9db0a95b31163e53e5bb60438d263f4bed7b7", + "sha256:99a641995a6bc4287a6315989ee591ff58507aa1cbe4c2e70d88411c4dcc0839", + "sha256:9c3a99c519f4638e700e9e3f83952e27e2ea10873eecd7935823dab0c1c9250e", + "sha256:9c509a4f78114cbc5f0740eb3d7a74985fd2eff022971bc9bc31f8bc93e66a3b", + "sha256:a18e20d8321c6400185b4263e27982488cb5cdd62da69147087a76a24ef4e7e3", + "sha256:a917d26e0fe980b0ac7bfcc1a3c4ad6a9a4612c911d33efb55ed7833c749b0ee", + "sha256:a9539f01cb04950fd4b5ab458e64a15f84c2acc273670072abe49a3f29bbad54", + "sha256:ad2efdbe90c73b0434cbe64ed372e12414ad03c06262279b104a029d1889d13e", + "sha256:b672abf90a964bfde2d0ecbce30f2329a47498ba75ce6f4da35a2f4532b7acbc", + "sha256:bbd27c24a4cc5e195a7f56cfd9312e366d5d61b86e36d46bbe538457ea6eb8dd", + "sha256:c400ba5675b67025c8a9f48aa846f12a39cf0c44df5cd060e23fda5b30e9359d", + "sha256:c408f5ef75cfffa113cacd8b0c0e3611cbfd47701ca3cdc090594109b9fcbaed", + "sha256:c806852deaedee9ce8280fe98955c9103f62912a5b2d5ee7e3eaa284a6d8d8e7", + "sha256:ce89f5876662f146d4c1f695dda29d4433a5d01c8681fbd2539afff535da14d4", + "sha256:d25a14af966438cddf498b2e338f88d1c9706f3493b1d73b93f695c99c5f0e2a", + "sha256:d8d4732cc5052e92cea2f78b233c2e2a52998ac40cd651f40e398893ad0d06ec", + "sha256:d9a9724a156c8ec6a379869b23ba3323b7ea3600851c91489b871e375f710bc8", + "sha256:e636ce23273683b00410f1971d209bf3689238cf5538d960adc3cdfe80dd0dbd", + "sha256:e88264caad6d8d00e7913996030bac8ad5f26b7411495848cc218bd3a9040b6c", + "sha256:f145cc21836c332c67baa6fc81099d1d27e266401565bf481948010d6ea32d46", + "sha256:fb57870449dfcfac428afbb5a877829fcb0d6db9d9baa1148705739e9083880e", + "sha256:fb70487c95786e345af5e854ffec8cb8cc781bcc5df7930c4fbb7feaa72e1cdf", + "sha256:fe96281713168a3270878255983d2cb1a97e034325c8c2c25169a69289d3ecfa", + "sha256:ff1f7882e56c40b0d33c4922c15dfa30612f05fb785074a012f7cda74d1c3679" + ], + "markers": "python_version >= '3.8'", + "version": "==1.66.2" }, "grpcio-status": { "hashes": [ @@ -967,11 +997,11 @@ }, "httpcore": { "hashes": [ - "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61", - "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5" + "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", + "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f" ], "markers": "python_version >= '3.8'", - "version": "==1.0.5" + "version": "==1.0.6" }, "httplib2": { "hashes": [ @@ -983,44 +1013,51 @@ }, "httptools": { "hashes": [ - "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563", - "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142", - "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d", - "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b", - "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4", - "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb", - "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658", - "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084", - "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2", - "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97", - "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837", - "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3", - "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58", - "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da", - "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d", - "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90", - "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0", - "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1", - "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2", - "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e", - "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0", - "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf", - "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc", - "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3", - "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503", - "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a", - "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3", - "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949", - "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84", - "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb", - "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a", - "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f", - "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e", - "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81", - "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185", - "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3" + "sha256:0238f07780782c018e9801d8f5f5aea3a4680a1af132034b444f677718c6fe88", + "sha256:052f7f50e4a38f069478143878371ed17937f268349bcd68f6f7a9de9fcfce21", + "sha256:054bdee08e4f7c15c186f6e7dbc8f0cf974b8dd1832b5f17f988faf8b12815c9", + "sha256:1099f73952e18c718ccaaf7a97ae58c94a91839c3d247c6184326f85a2eda7b4", + "sha256:10d28e5597d4349390c640232c9366ddc15568114f56724fe30a53de9686b6ab", + "sha256:1b7bc59362143dc2d02896dde94004ef54ff1989ceedf4b389ad3b530f312364", + "sha256:1bb67d47f045f56e9a5da4deccf710bdde21212e4b1f4776b7a542449f6a7682", + "sha256:2d5c33d98b2311ddbe06e92b12b14de334dcfbe64ebcbb2c7a34b5c6036db512", + "sha256:2e9d225b178a6cc700c23cf2f5daf85a10f93f1db7c34e9ee4ee0bbc29ad458a", + "sha256:352a496244360deb1c1d108391d76cd6f3dd9f53ccf975a082e74c6761af30c9", + "sha256:3f0246ca7f78fa8e3902ddb985b9f55509d417a862f4634a8fa63a7a496266c8", + "sha256:406f7dc5d9db68cd9ac638d14c74d077085f76b45f704d3ec38d43b842b3cb44", + "sha256:41965586b02715c3d83dd9153001f654e5b621de0c5255f5ef0635485212d0c0", + "sha256:4502620722b453c2c6306fad392c515dcb804dfa9c6d3b90d8926a07a7a01109", + "sha256:4d6e0ba155a1b3159551ac6b4551eb20028617e2e4bb71f2c61efed0756e6825", + "sha256:5141ccc9dbd8cdc59d1e93e318d405477a940dc6ebadcb8d9f8da17d2812d353", + "sha256:53cd2d776700bf0ed0e6fb203d716b041712ea4906479031cc5ac5421ecaa7d2", + "sha256:56bcd9ba0adf16edb4e3e45b8b9346f5b3b2372402e953d54c84b345d0f691e0", + "sha256:76dcb8f5c866f1537ccbaad01ebb3611890d281ef8d25e050d1cc3d90fba6b3d", + "sha256:77e22c33123ce11231ff2773d8905e20b45d77a69459def7481283b72a583955", + "sha256:78f920a75c1dbcb5a48a495f384d73ceb41e437a966c318eb7e56f1c1ad1df3e", + "sha256:7da016a0dab1fcced89dfff8537033c5dc200015e14023368f3f4a69e39b8716", + "sha256:8d80878cb40ebf88a48839ff7206ceb62e4b54327e0c2f9f15ee12edbd8b907e", + "sha256:8fdb4634040d1dbde7e0b373e19668cdb61c0ee8690d3b4064ac748d85365bca", + "sha256:93b1839d54b80a06a51a31b90d024a1770e250d00de57e7ae069bafba932f398", + "sha256:9ddaf99e362ae4169f6a8b3508f3487264e0a1b1e58c0b07b86407bc9ecee831", + "sha256:ad44569b0f508e046ffe85b4a547d5b68d1548fd90767df69449cc28021ee709", + "sha256:ae694efefcb61317c79b2fa1caebc122060992408e389bb00889567e463a47f1", + "sha256:b57cb8a4a8a8ffdaf0395326ef3b9c1aba36e58a421438fc04c002a1f511db63", + "sha256:b73cda1326738eab5d60640ca0b87ac4e4db09a099423c41b59a5681917e8d1d", + "sha256:c30902f9b9da0d74668b6f71d7b57081a4879d9a5ea93d5922dbe15b15b3b24a", + "sha256:c3e45d004531330030f7d07abe4865bc17963b9989bc1941cebbf7224010fb82", + "sha256:c7a5715b1f46e9852442f496c0df2f8c393cc8f293f5396d2c8d95cac852fb51", + "sha256:c92d2b7c1a914ab2f66454961eeaf904f4fe7529b93ff537619d22c18b82d070", + "sha256:cf61238811a75335751b4b17f8b221a35f93f2d57489296742adf98412d2a568", + "sha256:d25f8fdbc6cc6561353c7a384d76295e6a85a4945115b8bc347855db150e8c77", + "sha256:d49b14fcc9b12a52da8667587efa124a18e1a3eb63bbbcabf9882f4008d171d6", + "sha256:ddaf38943dbb32333a182c894b6092a68b56c5e36d0c54ba3761d28119b15447", + "sha256:ddc328c2a2daf2cf4bdc7bbc8a458dc4c840637223d4b8e01bce2168cc79fd23", + "sha256:e350a887adb38ac65c93c2f395b60cf482baca61fd396ed8d6fd313dbcce6fac", + "sha256:efc9d039b6b8a36b182bc60774bb5d456b8ff9ec44cf97719f2f38bb1dcdd546", + "sha256:f0481154c91725f7e7b729a535190388be6c7cbae3bbf0e793343ca386282312", + "sha256:f4f2fea370361a90cb9330610a95303587eda9d1e69930dbbee9978eac1d5946" ], - "version": "==0.6.1" + "version": "==0.6.2" }, "httpx": { "hashes": [ @@ -1033,11 +1070,11 @@ }, "idna": { "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], "markers": "python_version >= '3.6'", - "version": "==3.8" + "version": "==3.10" }, "importlib-metadata": { "hashes": [ @@ -1049,10 +1086,11 @@ }, "isodate": { "hashes": [ - "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", - "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9" + "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", + "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6" ], - "version": "==0.6.1" + "markers": "python_version >= '3.7'", + "version": "==0.7.2" }, "jinja2": { "hashes": [ @@ -1089,69 +1127,70 @@ }, "markupsafe": { "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396", + "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38", + "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a", + "sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8", + "sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b", + "sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad", + "sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a", + "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a", + "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da", + "sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6", + "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8", + "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344", + "sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a", + "sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8", + "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5", + "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7", + "sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170", + "sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132", + "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9", + "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd", + "sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9", + "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346", + "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc", + "sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589", + "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5", + "sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915", + "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295", + "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453", + "sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea", + "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b", + "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d", + "sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b", + "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4", + "sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b", + "sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7", + "sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf", + "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f", + "sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91", + "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd", + "sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50", + "sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b", + "sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583", + "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a", + "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984", + "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c", + "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c", + "sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25", + "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa", + "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4", + "sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3", + "sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97", + "sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1", + "sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd", + "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772", + "sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a", + "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729", + "sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca", + "sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6", + "sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635", + "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b", + "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" + "markers": "python_version >= '3.9'", + "version": "==3.0.1" }, "mdurl": { "hashes": [ @@ -1580,6 +1619,110 @@ ], "version": "==1.7.4" }, + "propcache": { + "hashes": [ + "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", + "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", + "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", + "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", + "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", + "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", + "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", + "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", + "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", + "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", + "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", + "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", + "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", + "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", + "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", + "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", + "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", + "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", + "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", + "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", + "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", + "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", + "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", + "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", + "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", + "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", + "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", + "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", + "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", + "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", + "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", + "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", + "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", + "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", + "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", + "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", + "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", + "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", + "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", + "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", + "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", + "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", + "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", + "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", + "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", + "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", + "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", + "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", + "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", + "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", + "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", + "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", + "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", + "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", + "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", + "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", + "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", + "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", + "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", + "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", + "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", + "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", + "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", + "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", + "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", + "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", + "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", + "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", + "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", + "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", + "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", + "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", + "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", + "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", + "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", + "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", + "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", + "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", + "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", + "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", + "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", + "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", + "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", + "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", + "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", + "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", + "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", + "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", + "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", + "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", + "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", + "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", + "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", + "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", + "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", + "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", + "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", + "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504" + ], + "markers": "python_version >= '3.8'", + "version": "==0.2.0" + }, "proto-plus": { "hashes": [ "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", @@ -1590,20 +1733,20 @@ }, "protobuf": { "hashes": [ - "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1", - "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d", - "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040", - "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835", - "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1", - "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca", - "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f", - "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d", - "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978", - "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4", - "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b" + "sha256:0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41", + "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea", + "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8", + "sha256:7a183f592dc80aa7c8da7ad9e55091c4ffc9497b3054452d629bb85fa27c2a45", + "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584", + "sha256:919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d", + "sha256:98d8d8aa50de6a2747efd9cceba361c9034050ecce3e09136f90de37ddba66e1", + "sha256:abe32aad8561aa7cc94fc7ba4fdef646e576983edb94a73381b03c53728a626f", + "sha256:b0234dd5a03049e4ddd94b93400b67803c823cfc405689688f59b34e0742381a", + "sha256:b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173", + "sha256:fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331" ], "markers": "python_version >= '3.8'", - "version": "==4.25.4" + "version": "==4.25.5" }, "pyasn1": { "hashes": [ @@ -1631,9 +1774,11 @@ }, "pycron": { "hashes": [ - "sha256:b916044e3e8253d5409c68df3ac64a3472c4e608dab92f40e8f595e5d3acb3de" + "sha256:6de8a555f721c74aa0bb681071a97da3d784ebf8ccea1d2bb9a3e66460b87da2", + "sha256:bf676a66b8dcf69edf1d3fd1d8f3f2b9468a2a007012c89a14e9d087d3f76198" ], - "version": "==3.0.0" + "markers": "python_version < '3.13' and python_version >= '3.9'", + "version": "==3.1.1" }, "pydantic": { "extras": [ @@ -1708,60 +1853,69 @@ }, "pymongo": { "hashes": [ - "sha256:0fc18b3a093f3db008c5fea0e980dbd3b743449eee29b5718bc2dc15ab5088bb", - "sha256:16e5019f75f6827bb5354b6fef8dfc9d6c7446894a27346e03134d290eb9e758", - "sha256:180d5eb1dc28b62853e2f88017775c4500b07548ed28c0bd9c005c3d7bc52526", - "sha256:18c9d8f975dd7194c37193583fd7d1eb9aea0c21ee58955ecf35362239ff31ac", - "sha256:236bbd7d0aef62e64caf4b24ca200f8c8670d1a6f5ea828c39eccdae423bc2b2", - "sha256:284d0717d1a7707744018b0b6ee7801b1b1ff044c42f7be7a01bb013de639470", - "sha256:2ecd71b9226bd1d49416dc9f999772038e56f415a713be51bf18d8676a0841c8", - "sha256:31e4d21201bdf15064cf47ce7b74722d3e1aea2597c6785882244a3bb58c7eab", - "sha256:3b5802151fc2b51cd45492c80ed22b441d20090fb76d1fd53cd7760b340ff554", - "sha256:3c68fe128a171493018ca5c8020fc08675be130d012b7ab3efe9e22698c612a1", - "sha256:3ed1c316718a2836f7efc3d75b4b0ffdd47894090bc697de8385acd13c513a70", - "sha256:408b2f8fdbeca3c19e4156f28fff1ab11c3efb0407b60687162d49f68075e63c", - "sha256:417369ce39af2b7c2a9c7152c1ed2393edfd1cbaf2a356ba31eb8bcbd5c98dd7", - "sha256:454f2295875744dc70f1881e4b2eb99cdad008a33574bc8aaf120530f66c0cde", - "sha256:47ec8c3f0a7b2212dbc9be08d3bf17bc89abd211901093e3ef3f2adea7de7a69", - "sha256:4bf58e6825b93da63e499d1a58de7de563c31e575908d4e24876234ccb910eba", - "sha256:519d1bab2b5e5218c64340b57d555d89c3f6c9d717cecbf826fb9d42415e7750", - "sha256:52b4108ac9469febba18cea50db972605cc43978bedaa9fea413378877560ef8", - "sha256:658d0170f27984e0d89c09fe5c42296613b711a3ffd847eb373b0dbb5b648d5f", - "sha256:6b50040d9767197b77ed420ada29b3bf18a638f9552d80f2da817b7c4a4c9c68", - "sha256:7148419eedfea9ecb940961cfe465efaba90595568a1fb97585fb535ea63fe2b", - "sha256:77f53429515d2b3e86dcc83dadecf7ff881e538c168d575f3688698a8707b80a", - "sha256:87075a1feb1e602e539bdb1ef8f4324a3427eb0d64208c3182e677d2c0718b6f", - "sha256:8b18c8324809539c79bd6544d00e0607e98ff833ca21953df001510ca25915d1", - "sha256:9097c331577cecf8034422956daaba7ec74c26f7b255d718c584faddd7fa2e3c", - "sha256:920d4f8f157a71b3cb3f39bc09ce070693d6e9648fb0e30d00e2657d1dca4e49", - "sha256:9365166aa801c63dff1a3cb96e650be270da06e3464ab106727223123405510f", - "sha256:940d456774b17814bac5ea7fc28188c7a1338d4a233efbb6ba01de957bded2e8", - "sha256:aec2b9088cdbceb87e6ca9c639d0ff9b9d083594dda5ca5d3c4f6774f4c81b33", - "sha256:af3e98dd9702b73e4e6fd780f6925352237f5dce8d99405ff1543f3771201704", - "sha256:b6564780cafd6abeea49759fe661792bd5a67e4f51bca62b88faab497ab5fe89", - "sha256:b747c0e257b9d3e6495a018309b9e0c93b7f0d65271d1d62e572747f4ffafc88", - "sha256:bf821bd3befb993a6db17229a2c60c1550e957de02a6ff4dd0af9476637b2e4d", - "sha256:c6b804bb4f2d9dc389cc9e827d579fa327272cdb0629a99bfe5b83cb3e269ebf", - "sha256:cc8b8582f4209c2459b04b049ac03c72c618e011d3caa5391ff86d1bda0cc486", - "sha256:cd39455b7ee70aabee46f7399b32ab38b86b236c069ae559e22be6b46b2bbfc4", - "sha256:d0cf61450feadca81deb1a1489cb1a3ae1e4266efd51adafecec0e503a8dcd84", - "sha256:d18d86bc9e103f4d3d4f18b85a0471c0e13ce5b79194e4a0389a224bb70edd53", - "sha256:d5428dbcd43d02f6306e1c3c95f692f68b284e6ee5390292242f509004c9e3a8", - "sha256:de3a860f037bb51f968de320baef85090ff0bbb42ec4f28ec6a5ddf88be61871", - "sha256:e0061af6e8c5e68b13f1ec9ad5251247726653c5af3c0bbdfbca6cf931e99216", - "sha256:e5df28f74002e37bcbdfdc5109799f670e4dfef0fb527c391ff84f078050e7b5", - "sha256:e6a720a3d22b54183352dc65f08cd1547204d263e0651b213a0a2e577e838526", - "sha256:e8400587d594761e5136a3423111f499574be5fd53cf0aefa0d0f05b180710b0", - "sha256:e84bc7707492f06fbc37a9f215374d2977d21b72e10a67f1b31893ec5a140ad8", - "sha256:ef7225755ed27bfdb18730c68f6cb023d06c28f2b734597480fb4c0e500feb6f", - "sha256:f2b7bec27e047e84947fbd41c782f07c54c30c76d14f3b8bf0c89f7413fac67a", - "sha256:f2fbdb87fe5075c8beb17a5c16348a1ea3c8b282a5cb72d173330be2fecf22f5", - "sha256:f5bf0eb8b6ef40fa22479f09375468c33bebb7fe49d14d9c96c8fd50355188b0", - "sha256:fdc20cd1e1141b04696ffcdb7c71e8a4a665db31fe72e51ec706b3bdd2d09f36" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.8.0" + "sha256:0783e0c8e95397c84e9cf8ab092ab1e5dd7c769aec0ef3a5838ae7173b98dea0", + "sha256:0f56707497323150bd2ed5d63067f4ffce940d0549d4ea2dfae180deec7f9363", + "sha256:11280809e5dacaef4971113f0b4ff4696ee94cfdb720019ff4fa4f9635138252", + "sha256:15a624d752dd3c89d10deb0ef6431559b6d074703cab90a70bb849ece02adc6b", + "sha256:15b1492cc5c7cd260229590be7218261e81684b8da6d6de2660cf743445500ce", + "sha256:1a970fd3117ab40a4001c3dad333bbf3c43687d90f35287a6237149b5ccae61d", + "sha256:1ec3fa88b541e0481aff3c35194c9fac96e4d57ec5d1c122376000eb28c01431", + "sha256:1ecc2455e3974a6c429687b395a0bc59636f2d6aedf5785098cf4e1f180f1c71", + "sha256:23e1d62df5592518204943b507be7b457fb8a4ad95a349440406fd42db5d0923", + "sha256:29e1c323c28a4584b7095378ff046815e39ff82cdb8dc4cc6dfe3acf6f9ad1f8", + "sha256:2e3a593333e20c87415420a4fb76c00b7aae49b6361d2e2205b6fece0563bf40", + "sha256:345f8d340802ebce509f49d5833cc913da40c82f2e0daf9f60149cacc9ca680f", + "sha256:3a70d5efdc0387ac8cd50f9a5f379648ecfc322d14ec9e1ba8ec957e5d08c372", + "sha256:409ab7d6c4223e5c85881697f365239dd3ed1b58f28e4124b846d9d488c86880", + "sha256:442ca247f53ad24870a01e80a71cd81b3f2318655fd9d66748ee2bd1b1569d9e", + "sha256:45ee87a4e12337353242bc758accc7fb47a2f2d9ecc0382a61e64c8f01e86708", + "sha256:4924355245a9c79f77b5cda2db36e0f75ece5faf9f84d16014c0a297f6d66786", + "sha256:544890085d9641f271d4f7a47684450ed4a7344d6b72d5968bfae32203b1bb7c", + "sha256:57ee6becae534e6d47848c97f6a6dff69e3cce7c70648d6049bd586764febe59", + "sha256:594dd721b81f301f33e843453638e02d92f63c198358e5a0fa8b8d0b1218dabc", + "sha256:5ded27a4a5374dae03a92e084a60cdbcecd595306555bda553b833baf3fc4868", + "sha256:6131bc6568b26e7495a9f3ef2b1700566b76bbecd919f4472bfe90038a61f425", + "sha256:6f437a612f4d4f7aca1812311b1e84477145e950fdafe3285b687ab8c52541f3", + "sha256:6fb6a72e88df46d1c1040fd32cd2d2c5e58722e5d3e31060a0393f04ad3283de", + "sha256:70645abc714f06b4ad6b72d5bf73792eaad14e3a2cfe29c62a9c81ada69d9e4b", + "sha256:72e2ace7456167c71cfeca7dcb47bd5dceda7db2231265b80fc625c5e8073186", + "sha256:778ac646ce6ac1e469664062dfe9ae1f5c9961f7790682809f5ec3b8fda29d65", + "sha256:7bd26b2aec8ceeb95a5d948d5cc0f62b0eb6d66f3f4230705c1e3d3d2c04ec76", + "sha256:7c4d0e7cd08ef9f8fbf2d15ba281ed55604368a32752e476250724c3ce36c72e", + "sha256:88dc4aa45f8744ccfb45164aedb9a4179c93567bbd98a33109d7dc400b00eb08", + "sha256:8ad05eb9c97e4f589ed9e74a00fcaac0d443ccd14f38d1258eb4c39a35dd722b", + "sha256:90bc6912948dfc8c363f4ead54d54a02a15a7fee6cfafb36dc450fc8962d2cb7", + "sha256:9235fa319993405ae5505bf1333366388add2e06848db7b3deee8f990b69808e", + "sha256:93a0833c10a967effcd823b4e7445ec491f0bf6da5de0ca33629c0528f42b748", + "sha256:95207503c41b97e7ecc7e596d84a61f441b4935f11aa8332828a754e7ada8c82", + "sha256:9df4ab5594fdd208dcba81be815fa8a8a5d8dedaf3b346cbf8b61c7296246a7a", + "sha256:a920fee41f7d0259f5f72c1f1eb331bc26ffbdc952846f9bd8c3b119013bb52c", + "sha256:a9de02be53b6bb98efe0b9eda84ffa1ec027fcb23a2de62c4f941d9a2f2f3330", + "sha256:ae2fd94c9fe048c94838badcc6e992d033cb9473eb31e5710b3707cba5e8aee2", + "sha256:b3337804ea0394a06e916add4e5fac1c89902f1b6f33936074a12505cab4ff05", + "sha256:ba164e73fdade9b4614a2497321c5b7512ddf749ed508950bdecc28d8d76a2d9", + "sha256:bb99f003c720c6d83be02c8f1a7787c22384a8ca9a4181e406174db47a048619", + "sha256:ca6f700cff6833de4872a4e738f43123db34400173558b558ae079b5535857a4", + "sha256:cec237c305fcbeef75c0bcbe9d223d1e22a6e3ba1b53b2f0b79d3d29c742b45b", + "sha256:dabe8bf1ad644e6b93f3acf90ff18536d94538ca4d27e583c6db49889e98e48f", + "sha256:dac78a650dc0637d610905fd06b5fa6419ae9028cf4d04d6a2657bc18a66bbce", + "sha256:dcc07b1277e8b4bf4d7382ca133850e323b7ab048b8353af496d050671c7ac52", + "sha256:e0a15665b2d6cf364f4cd114d62452ce01d71abfbd9c564ba8c74dcd7bbd6822", + "sha256:e0e961923a7b8a1c801c43552dcb8153e45afa41749d9efbd3a6d33f45489f7a", + "sha256:e4a65567bd17d19f03157c7ec992c6530eafd8191a4e5ede25566792c4fe3fa2", + "sha256:e5d55f2a82e5eb23795f724991cac2bffbb1c0f219c0ba3bf73a835f97f1bb2e", + "sha256:e699aa68c4a7dea2ab5a27067f7d3e08555f8d2c0dc6a0c8c60cfd9ff2e6a4b1", + "sha256:e974ab16a60be71a8dfad4e5afccf8dd05d41c758060f5d5bda9a758605d9a5d", + "sha256:ee4c86d8e6872a61f7888fc96577b0ea165eb3bdb0d841962b444fa36001e2bb", + "sha256:f1945d48fb9b8a87d515da07f37e5b2c35b364a435f534c122e92747881f4a7c", + "sha256:f2bc1ee4b1ca2c4e7e6b7a5e892126335ec8d9215bcd3ac2fe075870fefc3358", + "sha256:fb104c3c2a78d9d85571c8ac90ec4f95bca9b297c6eee5ada71fabf1129e1674", + "sha256:fbedc4617faa0edf423621bb0b3b8707836687161210d470e69a4184be9ca011", + "sha256:fdeba88c540c9ed0338c0b2062d9f81af42b18d6646b3e6dda05cf6edd46ada9" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.10.1" }, "pyopenssl": { "hashes": [ @@ -1774,18 +1928,18 @@ }, "pyparsing": { "hashes": [ - "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", - "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version >= '3.1'", - "version": "==3.1.4" + "markers": "python_version > '3.0'", + "version": "==3.2.0" }, "python-dateutil": { "hashes": [ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-dotenv": { @@ -1898,11 +2052,11 @@ }, "rich": { "hashes": [ - "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", - "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" + "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", + "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.8.1" + "markers": "python_full_version >= '3.8.0'", + "version": "==13.9.2" }, "rsa": { "hashes": [ @@ -1914,28 +2068,28 @@ }, "s3transfer": { "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + "sha256:b014be3a8a2aab98cfe1abc7229cc5a9a0cf05eb9c1f2b86b230fd8df3f78084", + "sha256:cab66d3380cca3e70939ef2255d01cd8aece6a4907a9528740f668c4b0611861" ], - "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "markers": "python_version >= '3.7'", + "version": "==0.6.2" }, "sentry-sdk": { "hashes": [ - "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d", - "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4" + "sha256:49139c31ebcd398f4f6396b18910610a0c1602f6e67083240c33019d1f6aa30c", + "sha256:90f733b32e15dfc1999e6b7aca67a38688a567329de4d6e184154a73f96c6892" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.14.0" + "version": "==2.16.0" }, "setuptools": { "hashes": [ - "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", - "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" + "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", + "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" ], "markers": "python_version >= '3.8'", - "version": "==74.1.2" + "version": "==75.1.0" }, "shellingham": { "hashes": [ @@ -1950,7 +2104,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sniffio": { @@ -2053,11 +2207,11 @@ }, "taskiq-dependencies": { "hashes": [ - "sha256:ee52d01e6683cafbeb0a0b0e4abf58b2d304d7db0769e024fe0a0e8bcedadc57", - "sha256:ffa81997b8d6f4be0b2e08280b241dc3cecd9b49ff1fd259688e244d977cadd4" + "sha256:04546a5786e0f8cb2e008af19cdf415dd27d63d6d29ccf15eda8fa6b8b6b8006", + "sha256:8b10d2635a8ada8774f1b555e0a6d72c4fb5e6089601858d38dd95ff6d214a4c" ], "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==1.5.3" + "version": "==1.5.4" }, "taskiq-fastapi": { "hashes": [ @@ -2107,11 +2261,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" ], - "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" }, "uvicorn": { "extras": [ @@ -2126,39 +2280,45 @@ }, "uvloop": { "hashes": [ - "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847", - "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2", - "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b", - "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315", - "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5", - "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469", - "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d", - "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf", - "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9", - "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab", - "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e", - "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e", - "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0", - "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756", - "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73", - "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006", - "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541", - "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae", - "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a", - "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996", - "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7", - "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00", - "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b", - "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10", - "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95", - "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9", - "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037", - "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6", - "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66", - "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba", - "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf" - ], - "version": "==0.20.0" + "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", + "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", + "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc", + "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414", + "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", + "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", + "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", + "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", + "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", + "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", + "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", + "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a", + "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", + "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", + "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", + "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", + "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", + "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", + "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", + "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", + "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", + "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", + "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", + "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", + "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", + "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", + "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206", + "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", + "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", + "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", + "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", + "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79", + "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", + "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe", + "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", + "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", + "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2" + ], + "version": "==0.21.0" }, "watchdog": { "hashes": [ @@ -2283,94 +2443,94 @@ }, "websockets": { "hashes": [ - "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026", - "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad", - "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99", - "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920", - "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448", - "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4", - "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c", - "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37", - "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3", - "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2", - "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4", - "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333", - "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543", - "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b", - "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8", - "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f", - "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c", - "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa", - "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9", - "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf", - "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc", - "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb", - "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb", - "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060", - "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f", - "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185", - "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc", - "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418", - "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab", - "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63", - "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e", - "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7", - "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36", - "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0", - "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2", - "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f", - "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f", - "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9", - "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e", - "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75", - "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b", - "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d", - "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075", - "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603", - "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d", - "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7", - "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491", - "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956", - "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376", - "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97", - "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f", - "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553", - "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0", - "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237", - "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f", - "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58", - "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980", - "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9", - "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4", - "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097", - "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df", - "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e", - "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32", - "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817", - "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501", - "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0", - "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d", - "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83", - "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c", - "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b", - "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4", - "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e", - "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870", - "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096", - "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231", - "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5", - "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae", - "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329", - "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a", - "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f", - "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2", - "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491", - "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af", - "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a", - "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462", - "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89" - ], - "version": "==13.0.1" + "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a", + "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54", + "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23", + "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7", + "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135", + "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700", + "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf", + "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5", + "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e", + "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c", + "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02", + "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a", + "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418", + "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f", + "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3", + "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68", + "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978", + "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20", + "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295", + "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b", + "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6", + "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb", + "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a", + "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa", + "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0", + "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a", + "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238", + "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c", + "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084", + "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19", + "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d", + "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7", + "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9", + "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79", + "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96", + "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6", + "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe", + "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842", + "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa", + "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3", + "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d", + "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51", + "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7", + "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09", + "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096", + "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9", + "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b", + "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5", + "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678", + "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea", + "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d", + "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49", + "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc", + "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5", + "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027", + "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0", + "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878", + "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c", + "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa", + "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f", + "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6", + "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2", + "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf", + "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708", + "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6", + "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f", + "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd", + "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2", + "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d", + "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7", + "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f", + "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5", + "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6", + "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557", + "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14", + "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7", + "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd", + "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c", + "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17", + "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23", + "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db", + "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6", + "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d", + "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9", + "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee", + "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6" + ], + "version": "==13.1" }, "wrapt": { "hashes": [ @@ -2450,109 +2610,115 @@ }, "yarl": { "hashes": [ - "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", - "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", - "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", - "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", - "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", - "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", - "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", - "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", - "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", - "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", - "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", - "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", - "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", - "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", - "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", - "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", - "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", - "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", - "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", - "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", - "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", - "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", - "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", - "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", - "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", - "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", - "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", - "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", - "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", - "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", - "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", - "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", - "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", - "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", - "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", - "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", - "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", - "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", - "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", - "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", - "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", - "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", - "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", - "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", - "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", - "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", - "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", - "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", - "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", - "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", - "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", - "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", - "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", - "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", - "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", - "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", - "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", - "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", - "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", - "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", - "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", - "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", - "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", - "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", - "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", - "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", - "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", - "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", - "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", - "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", - "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", - "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", - "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", - "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", - "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", - "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", - "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", - "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", - "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", - "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", - "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", - "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", - "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", - "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", - "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", - "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", - "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", - "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", - "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", - "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", - "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", - "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" - ], - "markers": "python_version >= '3.8'", - "version": "==1.11.1" + "sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e", + "sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c", + "sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747", + "sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179", + "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a", + "sha256:0e1af74a9529a1137c67c887ed9cde62cff53aa4d84a3adbec329f9ec47a3936", + "sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19", + "sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8", + "sha256:15c87339490100c63472a76d87fe7097a0835c705eb5ae79fd96e343473629ed", + "sha256:1695497bb2a02a6de60064c9f077a4ae9c25c73624e0d43e3aa9d16d983073c2", + "sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33", + "sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057", + "sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548", + "sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c", + "sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b", + "sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f", + "sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9", + "sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f", + "sha256:31561a5b4d8dbef1559b3600b045607cf804bae040f64b5f5bca77da38084a8a", + "sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04", + "sha256:3433da95b51a75692dcf6cc8117a31410447c75a9a8187888f02ad45c0a86c50", + "sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2", + "sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46", + "sha256:38d0124fa992dbacd0c48b1b755d3ee0a9f924f427f95b0ef376556a24debf01", + "sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5", + "sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf", + "sha256:3e52474256a7db9dcf3c5f4ca0b300fdea6c21cca0148c8891d03a025649d935", + "sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84", + "sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d", + "sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5", + "sha256:43ebdcc120e2ca679dba01a779333a8ea76b50547b55e812b8b92818d604662c", + "sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7", + "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9", + "sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367", + "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad", + "sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d", + "sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d", + "sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea", + "sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7", + "sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf", + "sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b", + "sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036", + "sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc", + "sha256:74abb8709ea54cc483c4fb57fb17bb66f8e0f04438cff6ded322074dbd17c7ec", + "sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b", + "sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627", + "sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368", + "sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810", + "sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94", + "sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50", + "sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6", + "sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb", + "sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b", + "sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7", + "sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931", + "sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178", + "sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d", + "sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f", + "sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2", + "sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5", + "sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc", + "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84", + "sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b", + "sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172", + "sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644", + "sha256:ace4cad790f3bf872c082366c9edd7f8f8f77afe3992b134cfc810332206884f", + "sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776", + "sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd", + "sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04", + "sha256:b7f227ca6db5a9fda0a2b935a2ea34a7267589ffc63c8045f0e4edb8d8dcf956", + "sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4", + "sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7", + "sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8", + "sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb", + "sha256:c77494a2f2282d9bbbbcab7c227a4d1b4bb829875c96251f66fb5f3bae4fb053", + "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe", + "sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a", + "sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b", + "sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb", + "sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417", + "sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c", + "sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980", + "sha256:ded1b1803151dd0f20a8945508786d57c2f97a50289b16f2629f85433e546d47", + "sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b", + "sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904", + "sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8", + "sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0", + "sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611", + "sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2", + "sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d", + "sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715", + "sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897", + "sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046", + "sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b", + "sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e", + "sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16", + "sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d", + "sha256:ffd591e22b22f9cb48e472529db6a47203c41c2c5911ff0a52e85723196c0d75" + ], + "markers": "python_version >= '3.8'", + "version": "==1.15.2" }, "zipp": { "hashes": [ - "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", - "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" + "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" ], "markers": "python_version >= '3.8'", - "version": "==3.20.1" + "version": "==3.20.2" } }, "develop": { @@ -2597,11 +2763,11 @@ }, "cattrs": { "hashes": [ - "sha256:16e94a13f9aaf6438bd5be5df521e072b1b00481b4cf807bcb1acbd49f814c08", - "sha256:ec8ce8fdc725de9d07547cd616f968670687c6fa7a2e263b088370c46d834d97" + "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0", + "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85" ], "markers": "python_version >= '3.8'", - "version": "==24.1.1" + "version": "==24.1.2" }, "certifi": { "hashes": [ @@ -2621,99 +2787,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "ci-info": { "hashes": [ @@ -2736,96 +2917,86 @@ "toml" ], "hashes": [ - "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", - "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", - "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", - "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", - "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", - "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", - "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", - "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", - "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", - "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", - "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", - "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", - "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", - "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", - "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", - "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", - "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", - "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", - "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", - "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", - "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", - "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", - "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", - "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", - "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", - "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", - "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", - "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", - "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", - "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", - "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", - "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", - "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", - "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", - "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", - "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", - "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", - "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", - "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", - "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", - "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", - "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", - "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", - "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", - "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", - "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", - "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", - "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", - "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", - "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", - "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", - "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", - "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", - "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", - "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", - "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", - "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", - "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", - "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", - "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", - "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", - "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", - "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", - "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", - "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", - "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", - "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", - "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", - "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", - "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", - "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", - "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" - ], - "markers": "python_version >= '3.8'", - "version": "==7.6.1" + "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6", + "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2", + "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba", + "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb", + "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6", + "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4", + "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0", + "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6", + "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990", + "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3", + "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43", + "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175", + "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a", + "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6", + "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97", + "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b", + "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e", + "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39", + "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd", + "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d", + "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f", + "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc", + "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976", + "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549", + "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c", + "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5", + "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4", + "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b", + "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e", + "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3", + "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6", + "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e", + "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929", + "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234", + "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13", + "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007", + "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3", + "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167", + "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d", + "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d", + "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40", + "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181", + "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054", + "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd", + "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2", + "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91", + "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3", + "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b", + "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38", + "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd", + "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f", + "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2", + "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba", + "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f", + "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83", + "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce", + "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38", + "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c", + "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f", + "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21", + "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4", + "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92" + ], + "markers": "python_version >= '3.9'", + "version": "==7.6.3" }, "decorator": { "hashes": [ "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" ], - "markers": "python_version < '3.11' and python_version >= '3.7'", + "markers": "python_version > '3.6' and python_version < '3.11'", "version": "==5.1.1" }, "distlib": { "hashes": [ - "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", - "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" + "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", + "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" ], - "version": "==0.3.8" + "version": "==0.3.9" }, "etelemetry": { "hashes": [ @@ -2852,58 +3023,64 @@ }, "faker": { "hashes": [ - "sha256:4294d169255a045990720d6f3fa4134b764a4cdf46ef0d3c7553d2506f1adaa1", - "sha256:e59c01d1e8b8e20a83255ab8232c143cb2af3b4f5ab6a3f5ce495f385ad8ab4c" + "sha256:8760fbb34564fbb2f394345eef24aec5b8f6506b6cfcefe8195ed66dd1032bdb", + "sha256:e8a15fd1b0f72992b008f5ea94c70d3baa0cb51b0d5a0e899c17b1d1b23d2771" ], "markers": "python_version >= '3.8'", - "version": "==28.4.1" + "version": "==30.3.0" }, "filelock": { "hashes": [ - "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec", - "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], "markers": "python_version >= '3.8'", - "version": "==3.16.0" + "version": "==3.16.1" }, "frozendict": { "hashes": [ - "sha256:07c3a5dee8bbb84cba770e273cdbf2c87c8e035903af8f781292d72583416801", - "sha256:12a342e439aef28ccec533f0253ea53d75fe9102bd6ea928ff530e76eac38906", - "sha256:1697793b5f62b416c0fc1d94638ec91ed3aa4ab277f6affa3a95216ecb3af170", - "sha256:199a4d32194f3afed6258de7e317054155bc9519252b568d9cfffde7e4d834e5", - "sha256:259528ba6b56fa051bc996f1c4d8b57e30d6dd3bc2f27441891b04babc4b5e73", - "sha256:2b70b431e3a72d410a2cdf1497b3aba2f553635e0c0f657ce311d841bf8273b6", - "sha256:2bd009cf4fc47972838a91e9b83654dc9a095dc4f2bb3a37c3f3124c8a364543", - "sha256:2d8536e068d6bf281f23fa835ac07747fb0f8851879dd189e9709f9567408b4d", - "sha256:3148062675536724502c6344d7c485dd4667fdf7980ca9bd05e338ccc0c4471e", - "sha256:3f7c031b26e4ee6a3f786ceb5e3abf1181c4ade92dce1f847da26ea2c96008c7", - "sha256:4297d694eb600efa429769125a6f910ec02b85606f22f178bafbee309e7d3ec7", - "sha256:4a59578d47b3949437519b5c39a016a6116b9e787bb19289e333faae81462e59", - "sha256:4ae8d05c8d0b6134bfb6bfb369d5fa0c4df21eabb5ca7f645af95fdc6689678e", - "sha256:5d58d9a8d9e49662c6dafbea5e641f97decdb3d6ccd76e55e79818415362ba25", - "sha256:63aa49f1919af7d45fb8fd5dec4c0859bc09f46880bd6297c79bb2db2969b63d", - "sha256:6874fec816b37b6eb5795b00e0574cba261bf59723e2de607a195d5edaff0786", - "sha256:6eb716e6a6d693c03b1d53280a1947716129f5ef9bcdd061db5c17dea44b80fe", - "sha256:705efca8d74d3facbb6ace80ab3afdd28eb8a237bfb4063ed89996b024bc443d", - "sha256:78c94991944dd33c5376f720228e5b252ee67faf3bac50ef381adc9e51e90d9d", - "sha256:7f79c26dff10ce11dad3b3627c89bb2e87b9dd5958c2b24325f16a23019b8b94", - "sha256:7fee9420475bb6ff357000092aa9990c2f6182b2bab15764330f4ad7de2eae49", - "sha256:812ab17522ba13637826e65454115a914c2da538356e85f43ecea069813e4b33", - "sha256:85375ec6e979e6373bffb4f54576a68bf7497c350861d20686ccae38aab69c0a", - "sha256:87ebcde21565a14fe039672c25550060d6f6d88cf1f339beac094c3b10004eb0", - "sha256:93a7b19afb429cbf99d56faf436b45ef2fa8fe9aca89c49eb1610c3bd85f1760", - "sha256:b3b967d5065872e27b06f785a80c0ed0a45d1f7c9b85223da05358e734d858ca", - "sha256:c6bf9260018d653f3cab9bd147bd8592bf98a5c6e338be0491ced3c196c034a3", - "sha256:c8f92425686323a950337da4b75b4c17a3327b831df8c881df24038d560640d4", - "sha256:d13b4310db337f4d2103867c5a05090b22bc4d50ca842093779ef541ea9c9eea", - "sha256:d9647563e76adb05b7cde2172403123380871360a114f546b4ae1704510801e5", - "sha256:dc2228874eacae390e63fd4f2bb513b3144066a977dc192163c9f6c7f6de6474", - "sha256:e1b941132d79ce72d562a13341d38fc217bc1ee24d8c35a20d754e79ff99e038", - "sha256:fefeb700bc7eb8b4c2dc48704e4221860d254c8989fb53488540bc44e44a1ac2" + "sha256:02331541611f3897f260900a1815b63389654951126e6e65545e529b63c08361", + "sha256:0aaa11e7c472150efe65adbcd6c17ac0f586896096ab3963775e1c5c58ac0098", + "sha256:18d50a2598350b89189da9150058191f55057581e40533e470db46c942373acf", + "sha256:1b4a3f8f6dd51bee74a50995c39b5a606b612847862203dd5483b9cd91b0d36a", + "sha256:1f42e6b75254ea2afe428ad6d095b62f95a7ae6d4f8272f0bd44a25dddd20f67", + "sha256:2d69418479bfb834ba75b0e764f058af46ceee3d655deb6a0dd0c0c1a5e82f09", + "sha256:323f1b674a2cc18f86ab81698e22aba8145d7a755e0ac2cccf142ee2db58620d", + "sha256:377a65be0a700188fc21e669c07de60f4f6d35fae8071c292b7df04776a1c27b", + "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", + "sha256:49ffaf09241bc1417daa19362a2241a4aa435f758fd4375c39ce9790443a39cd", + "sha256:622301b1c29c4f9bba633667d592a3a2b093cb408ba3ce578b8901ace3931ef3", + "sha256:665fad3f0f815aa41294e561d98dbedba4b483b3968e7e8cab7d728d64b96e33", + "sha256:669237c571856be575eca28a69e92a3d18f8490511eff184937283dc6093bd67", + "sha256:7088102345d1606450bd1801a61139bbaa2cb0d805b9b692f8d81918ea835da6", + "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", + "sha256:7291abacf51798d5ffe632771a69c14fb423ab98d63c4ccd1aa382619afe2f89", + "sha256:74b6b26c15dddfefddeb89813e455b00ebf78d0a3662b89506b4d55c6445a9f4", + "sha256:7730f8ebe791d147a1586cbf6a42629351d4597773317002181b66a2da0d509e", + "sha256:807862e14b0e9665042458fde692c4431d660c4219b9bb240817f5b918182222", + "sha256:94321e646cc39bebc66954a31edd1847d3a2a3483cf52ff051cd0996e7db07db", + "sha256:9647c74efe3d845faa666d4853cfeabbaee403b53270cabfc635b321f770e6b8", + "sha256:9a8a43036754a941601635ea9c788ebd7a7efbed2becba01b54a887b41b175b9", + "sha256:a4e3737cb99ed03200cd303bdcd5514c9f34b29ee48f405c1184141bd68611c9", + "sha256:a76cee5c4be2a5d1ff063188232fffcce05dde6fd5edd6afe7b75b247526490e", + "sha256:b8f2829048f29fe115da4a60409be2130e69402e29029339663fac39c90e6e2b", + "sha256:ba5ef7328706db857a2bdb2c2a17b4cd37c32a19c017cff1bb7eeebc86b0f411", + "sha256:c131f10c4d3906866454c4e89b87a7e0027d533cce8f4652aa5255112c4d6677", + "sha256:c3a05c0a50cab96b4bb0ea25aa752efbfceed5ccb24c007612bc63e51299336f", + "sha256:c9905dcf7aa659e6a11b8051114c9fa76dfde3a6e50e6dc129d5aece75b449a2", + "sha256:ce1e9217b85eec6ba9560d520d5089c82dbb15f977906eb345d81459723dd7e3", + "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", + "sha256:da6a10164c8a50b34b9ab508a9420df38f4edf286b9ca7b7df8a91767baecb34", + "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", + "sha256:e72fb86e48811957d66ffb3e95580af7b1af1e6fbd760ad63d7bd79b2c9a07f8", + "sha256:eabd21d8e5db0c58b60d26b4bb9839cac13132e88277e1376970172a85ee04b3", + "sha256:eddabeb769fab1e122d3a6872982c78179b5bcc909fdc769f3cf1964f55a6d20", + "sha256:f4c789fd70879ccb6289a603cdebdc4953e7e5dea047d30c1b180529b28257b5", + "sha256:f5b94d5b07c00986f9e37a38dd83c13f5fe3bf3f1ccc8e88edea8fe15d6cd88c", + "sha256:fc67cbb3c96af7a798fab53d52589752c1673027e516b702ab355510ddf6bdff" ], "markers": "python_version >= '3.6'", - "version": "==2.4.4" + "version": "==2.4.6" }, "gevent": { "hashes": [ @@ -2955,99 +3132,104 @@ }, "greenlet": { "hashes": [ - "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9", - "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17", - "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc", - "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637", - "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2", - "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3", - "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6", - "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b", - "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf", - "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27", - "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1", - "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc", - "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a", - "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b", - "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d", - "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28", - "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303", - "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99", - "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f", - "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7", - "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6", - "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a", - "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc", - "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0", - "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8", - "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a", - "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca", - "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b", - "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989", - "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19", - "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6", - "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484", - "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd", - "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25", - "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b", - "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910", - "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0", - "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5", - "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345", - "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6", - "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00", - "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df", - "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811", - "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca", - "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8", - "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33", - "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97", - "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0", - "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b", - "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682", - "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39", - "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64", - "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f", - "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665", - "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f", - "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc", - "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d", - "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a", - "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0", - "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09", - "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b", - "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491", - "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7", - "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954", - "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501", - "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.0" + "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", + "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7", + "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", + "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", + "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", + "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", + "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", + "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", + "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", + "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa", + "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", + "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", + "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", + "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22", + "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9", + "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", + "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba", + "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3", + "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", + "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", + "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291", + "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", + "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", + "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", + "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", + "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef", + "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c", + "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", + "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c", + "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", + "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", + "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8", + "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d", + "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", + "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145", + "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", + "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", + "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e", + "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", + "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1", + "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef", + "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", + "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", + "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", + "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437", + "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd", + "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981", + "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", + "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", + "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798", + "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", + "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", + "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", + "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", + "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af", + "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", + "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", + "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42", + "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e", + "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81", + "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", + "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", + "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc", + "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de", + "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111", + "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", + "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", + "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", + "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", + "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", + "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803", + "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", + "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f" + ], + "version": "==3.1.1" }, "html5lib": { "hashes": [ "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.1" }, "identify": { "hashes": [ - "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", - "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0" + "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", + "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" ], "markers": "python_version >= '3.8'", - "version": "==2.6.0" + "version": "==2.6.1" }, "idna": { "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], "markers": "python_version >= '3.6'", - "version": "==3.8" + "version": "==3.10" }, "importlib-metadata": { "hashes": [ @@ -3076,18 +3258,19 @@ }, "ipython": { "hashes": [ - "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e", - "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c" + "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a", + "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35" ], - "markers": "python_version < '3.11' and python_version >= '3.7'", - "version": "==8.27.0" + "markers": "python_version > '3.6' and python_version < '3.11'", + "version": "==8.28.0" }, "isodate": { "hashes": [ - "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96", - "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9" + "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", + "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6" ], - "version": "==0.6.1" + "markers": "python_version >= '3.7'", + "version": "==0.7.2" }, "jedi": { "hashes": [ @@ -3341,11 +3524,11 @@ }, "platformdirs": { "hashes": [ - "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c", - "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], "markers": "python_version >= '3.8'", - "version": "==4.3.2" + "version": "==4.3.6" }, "pluggy": { "hashes": [ @@ -3369,16 +3552,16 @@ "sha256:7e23ca1e68bbfd06ba8de98bf553bf3493264c96d5e8a615c0471025deeba722", "sha256:aa17083feb6c71da11a68b2c213b04675c4af4ce9c541762632ca3f2cb3546dd" ], - "markers": "python_version < '3.12' and python_version >= '3.8'", + "markers": "python_version >= '3.8' and python_version < '3.12'", "version": "==3.11.0" }, "prompt-toolkit": { "hashes": [ - "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10", - "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360" + "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", + "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.0.47" + "version": "==3.0.48" }, "ptyprocess": { "hashes": [ @@ -3482,19 +3665,19 @@ }, "pyparsing": { "hashes": [ - "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c", - "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032" + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" ], - "markers": "python_version >= '3.1'", - "version": "==3.1.4" + "markers": "python_version > '3.0'", + "version": "==3.2.0" }, "pyshacl": { "hashes": [ - "sha256:48d44f317cd9aad8e3fdb5df8aa5706fa92dc6b2746419698035e84a320fb89d", - "sha256:a4bef4296d56305a30e0a97509e541ebe4f2cc2d5da73536d0541233e28f2d22" + "sha256:4cdbaea15ec0b37a39e2a846898e79bfd32fe5d1f1b87c106e3e56ec473e3d63", + "sha256:875ccf8cd5d3c9f8ad3eec1837fb91b9ef5b876312a855f056f47d03b8bd1104" ], "markers": "python_full_version >= '3.8.1' and python_full_version < '4.0.0'", - "version": "==0.26.0" + "version": "==0.27.0" }, "pytest": { "hashes": [ @@ -3555,7 +3738,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pyyaml": { @@ -3617,6 +3800,9 @@ "version": "==6.0.2" }, "rdflib": { + "extras": [ + "html" + ], "hashes": [ "sha256:0438920912a642c866a513de6fe8a0001bd86ef975057d6962c79ce4771687cd", "sha256:9995eb8569428059b8c1affd26b25eac510d64f5043d9ce8c84e0d0036e995ae" @@ -3675,18 +3861,18 @@ }, "setuptools": { "hashes": [ - "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308", - "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6" + "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", + "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" ], "markers": "python_version >= '3.8'", - "version": "==74.1.2" + "version": "==75.1.0" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "stack-data": { @@ -3698,11 +3884,11 @@ }, "tomli": { "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" ], "markers": "python_version < '3.11'", - "version": "==2.0.1" + "version": "==2.0.2" }, "traitlets": { "hashes": [ @@ -3741,11 +3927,11 @@ }, "types-pyasn1": { "hashes": [ - "sha256:2cee8bfddf06d88e25ea122f7fb3b9d127a9f4a532e4d1415ef99ee0c2f902ea", - "sha256:40873dbd960e8ddb4bebda5195d3aa5e9f9c7c19d461af2f6c8540aa97e8055d" + "sha256:95f3cb1fbd63ff91cd0410945f8aeae6b0be359533c00f39d8e17124884157af", + "sha256:a1da054db13d3d4ccfa69c515678154014336ad3d9f9ade01845f9edb1a2bc71" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0.20240824" + "version": "==0.6.0.20240913" }, "types-python-dateutil": { "hashes": [ @@ -3801,33 +3987,33 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" ], - "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" }, "urwid": { "hashes": [ - "sha256:71b3171cabaa0092902f556768756bd2f2ebb24c0da287ee08f081d235340cb7", - "sha256:9ecc57330d88c8d9663ffd7092a681674c03ff794b6330ccfef479af7aa9671b" + "sha256:93ad239939e44c385e64aa00027878b9e5c486d59e855ec8ab5b1e1adcdb32a2", + "sha256:de14896c6df9eb759ed1fd93e0384a5279e51e0dde8f621e4083f7a8368c0797" ], "markers": "python_version >= '3.8'", - "version": "==2.6.15" + "version": "==2.6.16" }, "urwid-readline": { "hashes": [ - "sha256:8fabd2e501c124a30d38cfb10610b32f119a615ec0b310ae5591c583fb00bd09" + "sha256:9301444b86d58f7d26388506b704f142cefd193888488b4070d3a0fdfcfc0f84" ], - "version": "==0.14" + "version": "==0.15.1" }, "virtualenv": { "hashes": [ - "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55", - "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c" + "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", + "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2" ], "markers": "python_version >= '3.7'", - "version": "==20.26.4" + "version": "==20.26.6" }, "wcwidth": { "hashes": [ @@ -3845,11 +4031,11 @@ }, "zipp": { "hashes": [ - "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", - "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" + "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" ], "markers": "python_version >= '3.8'", - "version": "==3.20.1" + "version": "==3.20.2" }, "zope.event": { "hashes": [ @@ -3861,43 +4047,46 @@ }, "zope.interface": { "hashes": [ - "sha256:01e6e58078ad2799130c14a1d34ec89044ada0e1495329d72ee0407b9ae5100d", - "sha256:064ade95cb54c840647205987c7b557f75d2b2f7d1a84bfab4cf81822ef6e7d1", - "sha256:11fa1382c3efb34abf16becff8cb214b0b2e3144057c90611621f2d186b7e1b7", - "sha256:1bee1b722077d08721005e8da493ef3adf0b7908e0cd85cc7dc836ac117d6f32", - "sha256:1eeeb92cb7d95c45e726e3c1afe7707919370addae7ed14f614e22217a536958", - "sha256:21a207c6b2c58def5011768140861a73f5240f4f39800625072ba84e76c9da0b", - "sha256:2545d6d7aac425d528cd9bf0d9e55fcd47ab7fd15f41a64b1c4bf4c6b24946dc", - "sha256:2c4316a30e216f51acbd9fb318aa5af2e362b716596d82cbb92f9101c8f8d2e7", - "sha256:35062d93bc49bd9b191331c897a96155ffdad10744ab812485b6bad5b588d7e4", - "sha256:382d31d1e68877061daaa6499468e9eb38eb7625d4369b1615ac08d3860fe896", - "sha256:3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d", - "sha256:3d4b91821305c8d8f6e6207639abcbdaf186db682e521af7855d0bea3047c8ca", - "sha256:3de1d553ce72868b77a7e9d598c9bff6d3816ad2b4cc81c04f9d8914603814f3", - "sha256:3fcdc76d0cde1c09c37b7c6b0f8beba2d857d8417b055d4f47df9c34ec518bdd", - "sha256:5112c530fa8aa2108a3196b9c2f078f5738c1c37cfc716970edc0df0414acda8", - "sha256:53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386", - "sha256:6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58", - "sha256:6d04b11ea47c9c369d66340dbe51e9031df2a0de97d68f442305ed7625ad6493", - "sha256:6dd647fcd765030638577fe6984284e0ebba1a1008244c8a38824be096e37fe3", - "sha256:799ef7a444aebbad5a145c3b34bff012b54453cddbde3332d47ca07225792ea4", - "sha256:7d92920416f31786bc1b2f34cc4fc4263a35a407425319572cbf96b51e835cd3", - "sha256:7e0c151a6c204f3830237c59ee4770cc346868a7a1af6925e5e38650141a7f05", - "sha256:84f8794bd59ca7d09d8fce43ae1b571be22f52748169d01a13d3ece8394d8b5b", - "sha256:95e5913ec718010dc0e7c215d79a9683b4990e7026828eedfda5268e74e73e11", - "sha256:9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b", - "sha256:ab985c566a99cc5f73bc2741d93f1ed24a2cc9da3890144d37b9582965aff996", - "sha256:af94e429f9d57b36e71ef4e6865182090648aada0cb2d397ae2b3f7fc478493a", - "sha256:c96b3e6b0d4f6ddfec4e947130ec30bd2c7b19db6aa633777e46c8eecf1d6afd", - "sha256:cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1", - "sha256:d3b7ce6d46fb0e60897d62d1ff370790ce50a57d40a651db91a3dde74f73b738", - "sha256:d976fa7b5faf5396eb18ce6c132c98e05504b52b60784e3401f4ef0b2e66709b", - "sha256:db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca", - "sha256:ecd32f30f40bfd8511b17666895831a51b532e93fc106bfa97f366589d3e4e0e", - "sha256:f418c88f09c3ba159b95a9d1cfcdbe58f208443abb1f3109f4b9b12fd60b187c" - ], - "markers": "python_version >= '3.8'", - "version": "==7.0.3" + "sha256:07add15de0cc7e69917f7d286b64d54125c950aeb43efed7a5ea7172f000fbc1", + "sha256:0ac20581fc6cd7c754f6dff0ae06fedb060fa0e9ea6309d8be8b2701d9ea51c4", + "sha256:124149e2d42067b9c6597f4dafdc7a0983d0163868f897b7bb5dc850b14f9a87", + "sha256:27cfb5205d68b12682b6e55ab8424662d96e8ead19550aad0796b08dd2c9a45e", + "sha256:2a29ac607e970b5576547f0e3589ec156e04de17af42839eedcf478450687317", + "sha256:2b6a4924f5bad9fe21d99f66a07da60d75696a136162427951ec3cb223a5570d", + "sha256:2bd9e9f366a5df08ebbdc159f8224904c1c5ce63893984abb76954e6fbe4381a", + "sha256:3bcff5c09d0215f42ba64b49205a278e44413d9bf9fa688fd9e42bfe472b5f4f", + "sha256:3f005869a1a05e368965adb2075f97f8ee9a26c61898a9e52a9764d93774f237", + "sha256:4a00ead2e24c76436e1b457a5132d87f83858330f6c923640b7ef82d668525d1", + "sha256:4af4a12b459a273b0b34679a5c3dc5e34c1847c3dd14a628aa0668e19e638ea2", + "sha256:5501e772aff595e3c54266bc1bfc5858e8f38974ce413a8f1044aae0f32a83a3", + "sha256:5e28ea0bc4b084fc93a483877653a033062435317082cdc6388dec3438309faf", + "sha256:5e956b1fd7f3448dd5e00f273072e73e50dfafcb35e4227e6d5af208075593c9", + "sha256:5fcf379b875c610b5a41bc8a891841533f98de0520287d7f85e25386cd10d3e9", + "sha256:6159e767d224d8f18deff634a1d3722e68d27488c357f62ebeb5f3e2f5288b1f", + "sha256:661d5df403cd3c5b8699ac480fa7f58047a3253b029db690efa0c3cf209993ef", + "sha256:711eebc77f2092c6a8b304bad0b81a6ce3cf5490b25574e7309fbc07d881e3af", + "sha256:80a3c00b35f6170be5454b45abe2719ea65919a2f09e8a6e7b1362312a872cd3", + "sha256:848b6fa92d7c8143646e64124ed46818a0049a24ecc517958c520081fd147685", + "sha256:91b6c30689cfd87c8f264acb2fc16ad6b3c72caba2aec1bf189314cf1a84ca33", + "sha256:9733a9a0f94ef53d7aa64661811b20875b5bc6039034c6e42fb9732170130573", + "sha256:9940d5bc441f887c5f375ec62bcf7e7e495a2d5b1da97de1184a88fb567f06af", + "sha256:9e3e48f3dea21c147e1b10c132016cb79af1159facca9736d231694ef5a740a8", + "sha256:a14c9decf0eb61e0892631271d500c1e306c7b6901c998c7035e194d9150fdd1", + "sha256:a735f82d2e3ed47ca01a20dfc4c779b966b16352650a8036ab3955aad151ed8a", + "sha256:a99240b1d02dc469f6afbe7da1bf617645e60290c272968f4e53feec18d7dce8", + "sha256:b7b25db127db3e6b597c5f74af60309c4ad65acd826f89609662f0dc33a54728", + "sha256:b936d61dbe29572fd2cfe13e30b925e5383bed1aba867692670f5a2a2eb7b4e9", + "sha256:bec001798ab62c3fc5447162bf48496ae9fba02edc295a9e10a0b0c639a6452e", + "sha256:cc8a318162123eddbdf22fcc7b751288ce52e4ad096d3766ff1799244352449d", + "sha256:d0a45b5af9f72c805ee668d1479480ca85169312211bed6ed18c343e39307d5f", + "sha256:e53c291debef523b09e1fe3dffe5f35dde164f1c603d77f770b88a1da34b7ed6", + "sha256:ec1ef1fdb6f014d5886b97e52b16d0f852364f447d2ab0f0c6027765777b6667", + "sha256:ec59fe53db7d32abb96c6d4efeed84aab4a7c38c62d7a901a9b20c09dd936e7a", + "sha256:f245d039f72e6f802902375755846f5de1ee1e14c3e8736c078565599bcab621", + "sha256:ff115ef91c0eeac69cd92daeba36a9d8e14daee445b504eeea2b1c0b55821984" + ], + "markers": "python_version >= '3.8'", + "version": "==7.1.0" } } } From 1c5daeaeee049c48ba1e84093d2f99b58f2582ad Mon Sep 17 00:00:00 2001 From: Billie He Date: Tue, 15 Oct 2024 14:55:42 -0700 Subject: [PATCH 35/41] feat: add Greek email templates (M2-7665) (#1624) * chore: add language assertion to invitation tests * fix: tom's fixture function return type * chore: rename mailing service html template getter * fix: shell account re-invite unit test * chore: parameterize invitation unit tests * chore: fix html email template formatting * feat: implement fallback to English for localized email templates * feat: add placeholder templates for greek emails * fix: lints * fix: some typing errors in invitation unit tests * chore: replace placeholder with actual greek translations * chore: rename parameter names for email template function * chore: add tests for assignment email language --------- Co-authored-by: Billie He --- src/apps/activity_assignments/service.py | 11 +- .../tests/test_assignments.py | 15 +- src/apps/answers/service.py | 4 +- src/apps/invitations/domain.py | 5 +- src/apps/invitations/services.py | 30 +- src/apps/invitations/test_invite.py | 686 ++++++++++-------- src/apps/mailing/services.py | 15 +- .../static/templates/blocks/team_info_en.html | 2 +- .../static/templates/blocks/team_info_fr.html | 2 +- .../templates/footers/footer_info_en.html | 23 +- .../templates/footers/footer_info_fr.html | 72 +- src/apps/mailing/static/templates/header.html | 2 + .../templates/invitation_new_user_el.html | 28 + .../templates/invitation_new_user_en.html | 44 +- .../templates/invitation_new_user_fr.html | 44 +- .../invitation_registered_user_el.html | 28 + .../invitation_registered_user_en.html | 44 +- .../invitation_registered_user_fr.html | 43 +- .../new_activity_assignments_el.html | 38 + .../new_activity_assignments_en.html | 52 +- .../new_activity_assignments_fr.html | 51 +- .../static/templates/reset_password_en.html | 10 +- ...transfer_ownership_registered_user_en.html | 7 +- ...ansfer_ownership_unregistered_user_en.html | 3 +- src/apps/transfer_ownership/service.py | 9 +- src/apps/users/services/core.py | 5 +- src/apps/users/tests/fixtures/users.py | 3 +- 27 files changed, 728 insertions(+), 548 deletions(-) create mode 100644 src/apps/mailing/static/templates/invitation_new_user_el.html create mode 100644 src/apps/mailing/static/templates/invitation_registered_user_el.html create mode 100644 src/apps/mailing/static/templates/new_activity_assignments_el.html diff --git a/src/apps/activity_assignments/service.py b/src/apps/activity_assignments/service.py index 10136eda9b5..f1db62b9c3f 100644 --- a/src/apps/activity_assignments/service.py +++ b/src/apps/activity_assignments/service.py @@ -167,7 +167,6 @@ async def send_email_notification( respondent_subject: SubjectSchema = subjects[respondent_subject_id] language = respondent_subject.language or "en" - template_name = self._get_email_template_name(language) domain = settings.service.urls.frontend.web_base path = settings.service.urls.frontend.applet_home @@ -180,12 +179,12 @@ async def send_email_notification( message = MessageSchema( recipients=[respondent_subject.email], subject=self._get_email_assignment_subject(language), - body=service.get_template( - path=template_name, + body=service.get_localized_html_template( + template_name=self._get_email_template_name(), + language=language, first_name=respondent_subject.first_name, applet_name=applet.display_name, link=link, - language=language, activity_or_flows_names=activities, ), ) @@ -431,8 +430,8 @@ async def check_if_auto_assigned(self, activity_or_flow_id: uuid.UUID) -> bool | return await ActivityAssigmentCRUD(self.session).check_if_auto_assigned(activity_or_flow_id) @staticmethod - def _get_email_template_name(language: str) -> str: - return f"new_activity_assignments_{language}" + def _get_email_template_name() -> str: + return "new_activity_assignments" @staticmethod def _get_email_assignment_subject(language: str) -> str: diff --git a/src/apps/activity_assignments/tests/test_assignments.py b/src/apps/activity_assignments/tests/test_assignments.py index 24c50037b35..56f4a5f3e82 100644 --- a/src/apps/activity_assignments/tests/test_assignments.py +++ b/src/apps/activity_assignments/tests/test_assignments.py @@ -1,4 +1,5 @@ import http +import re import uuid import pytest @@ -22,6 +23,7 @@ from apps.shared.enums import Language from apps.shared.test import BaseTest from apps.shared.test.client import TestClient +from apps.subjects.crud import SubjectsCrud from apps.subjects.db.schemas import SubjectSchema from apps.subjects.domain import Subject, SubjectCreate, SubjectFull from apps.subjects.services import SubjectsService @@ -117,11 +119,19 @@ async def applet_one_shell_account(session: AsyncSession, applet_one: AppletFull ) +def message_language(message_body: str): + assert message_body + match_result = re.search(r"", message_body) + assert match_result + return match_result.group(1) + + class TestActivityAssignments(BaseTest): activities_assignments_applet = "/assignments/applet/{applet_id}" user_activities_assignments = "/users/me/assignments/{applet_id}" activities_assign_unassign_applet = "/assignments/applet/{applet_id}" + @pytest.mark.parametrize("invite_language", ["en", "fr", "el"]) async def test_create_one_assignment( self, client: TestClient, @@ -131,7 +141,10 @@ async def test_create_one_assignment( tom_applet_one_subject, session: AsyncSession, mailbox: TestMail, + invite_language: str, ): + await SubjectsCrud(session).update(SubjectSchema(id=tom_applet_one_subject.id, language=invite_language)) + client.login(tom) assignments_create = ActivitiesAssignmentsCreate( @@ -166,7 +179,7 @@ async def test_create_one_assignment( assert str(model.id) == assignment["id"] assert model.activity_id == applet_one.activities[0].id assert mailbox.mails[0].recipients == [tom_applet_one_subject.email] - assert mailbox.mails[0].subject == "Assignment Notification" + assert message_language(mailbox.mails[0].body) == invite_language async def test_create_assignment_fail_wrong_activity( self, diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 30207981a7e..a833b264f79 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1618,7 +1618,9 @@ async def send_alert_mail(users: List[UserSchema]): MessageSchema( recipients=email_list, subject="Response alert", - body=mail_service.get_template(path="response_alert_en", domain=domain), + body=mail_service.get_localized_html_template( + template_name="response_alert", language="en", domain=domain + ), ) ) diff --git a/src/apps/invitations/domain.py b/src/apps/invitations/domain.py index 9fe1b3830a6..f2b80c8ea6a 100644 --- a/src/apps/invitations/domain.py +++ b/src/apps/invitations/domain.py @@ -16,8 +16,9 @@ class Applet(PublicModel): class InvitationLanguage(str, Enum): - EN = "en" - FR = "fr" + EN = "en" # English + FR = "fr" # French + EL = "el" # Greek class InvitationRequest(InternalModel): diff --git a/src/apps/invitations/services.py b/src/apps/invitations/services.py index aaa4eb43190..f91068c355a 100644 --- a/src/apps/invitations/services.py +++ b/src/apps/invitations/services.py @@ -136,22 +136,21 @@ async def send_respondent_invitation( invitation_internal: InvitationRespondent = InvitationRespondent.from_orm(invitation_schema) applet = await AppletsCRUD(self.session).get_by_id(invitation_internal.applet_id) - template_name = self._get_email_template_name(invited_user_id, schema.language) # Send email to the user service = MailingService() message = MessageSchema( recipients=[schema.email], subject=self._get_invitation_subject(applet), - body=service.get_template( - path=template_name, + body=service.get_localized_html_template( + template_name=self._get_email_template_name(invited_user_id), + language=schema.language, first_name=subject.first_name, last_name=subject.last_name, applet_name=applet.display_name, role=invitation_internal.role, link=self._get_invitation_url_by_role(invitation_internal.role), key=invitation_internal.key, - language=schema.language, ), ) _ = asyncio.create_task(service.send(message)) @@ -212,8 +211,6 @@ async def send_reviewer_invitation( applet = await AppletsCRUD(self.session).get_by_id(invitation_internal.applet_id) - template_name = self._get_email_template_name(invited_user_id, schema.language) - await WorkspaceService(self.session, self._user.id).update_workspace_name(self._user, schema.workspace_prefix) # Send email to the user @@ -221,15 +218,15 @@ async def send_reviewer_invitation( message = MessageSchema( recipients=[schema.email], subject=self._get_invitation_subject(applet), - body=service.get_template( - path=template_name, + body=service.get_localized_html_template( + template_name=self._get_email_template_name(invited_user_id), + language=schema.language, first_name=schema.first_name, last_name=schema.last_name, applet_name=applet.display_name, role=invitation_internal.role, link=self._get_invitation_url_by_role(invitation_internal.role), key=invitation_internal.key, - language=schema.language, ), ) @@ -286,8 +283,9 @@ async def send_managers_invitation( else: invitation_schema = await self.invitations_crud.save(InvitationSchema(**payload)) invitation_internal = InvitationManagers.from_orm(invitation_schema) + applet = await AppletsCRUD(self.session).get_by_id(invitation_internal.applet_id) - template_name = self._get_email_template_name(invited_user_id, schema.language) + await WorkspaceService(self.session, self._user.id).update_workspace_name(self._user, schema.workspace_prefix) # Send email to the user @@ -295,15 +293,15 @@ async def send_managers_invitation( message = MessageSchema( recipients=[schema.email], subject=self._get_invitation_subject(applet), - body=service.get_template( - path=template_name, + body=service.get_localized_html_template( + template_name=self._get_email_template_name(invited_user_id), + language=schema.language, first_name=schema.first_name, last_name=schema.last_name, applet_name=applet.display_name, role=invitation_internal.role, link=self._get_invitation_url_by_role(invitation_internal.role), key=invitation_internal.key, - language=schema.language, ), ) @@ -427,10 +425,10 @@ async def delete_for_respondents(self, applet_ids: list[uuid.UUID]): await InvitationCRUD(self.session).delete_by_applet_ids(self._user.email, applet_ids, roles) @staticmethod - def _get_email_template_name(invited_user_id: uuid.UUID | None, language: str) -> str: + def _get_email_template_name(invited_user_id: uuid.UUID | None) -> str: if not invited_user_id: - return f"invitation_new_user_{language}" - return f"invitation_registered_user_{language}" + return "invitation_new_user" + return "invitation_registered_user" async def get_meta(self, key: uuid.UUID) -> dict | None: return await InvitationCRUD(self.session).get_meta(key) diff --git a/src/apps/invitations/test_invite.py b/src/apps/invitations/test_invite.py index 3749cf194c4..3a36de67c95 100644 --- a/src/apps/invitations/test_invite.py +++ b/src/apps/invitations/test_invite.py @@ -1,7 +1,8 @@ import http import json +import re import uuid -from typing import Literal +from typing import Any, Literal import pytest from pydantic import EmailStr @@ -14,6 +15,7 @@ from apps.applets.service.applet import AppletService from apps.invitations.crud import InvitationCRUD from apps.invitations.domain import ( + InvitationLanguage, InvitationManagersRequest, InvitationRespondentRequest, InvitationReviewerRequest, @@ -29,6 +31,7 @@ ) from apps.mailing.services import TestMail from apps.shared.test import BaseTest +from apps.shared.test.client import TestClient from apps.subjects.crud import SubjectsCrud from apps.subjects.domain import Subject, SubjectCreate from apps.subjects.services import SubjectsService @@ -40,7 +43,7 @@ @pytest.fixture -async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletFull, tom): +async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletFull, tom: User): srv = AppletService(session, tom.id) await srv.create_access_link(applet_one.id, CreateAccessLink(require_login=False)) applet = await srv.get_full_applet(applet_one.id) @@ -49,7 +52,7 @@ async def applet_one_with_public_link(session: AsyncSession, applet_one: AppletF @pytest.fixture -async def applet_one_with_link(session: AsyncSession, applet_one: AppletFull, tom): +async def applet_one_with_link(session: AsyncSession, applet_one: AppletFull, tom: User): srv = AppletService(session, tom.id) await srv.create_access_link(applet_one.id, CreateAccessLink(require_login=True)) applet = await srv.get_full_applet(applet_one.id) @@ -58,19 +61,23 @@ async def applet_one_with_link(session: AsyncSession, applet_one: AppletFull, to @pytest.fixture -async def applet_one_lucy_manager(session: AsyncSession, applet_one: AppletFull, tom, lucy) -> AppletFull: +async def applet_one_lucy_manager(session: AsyncSession, applet_one: AppletFull, tom: User, lucy: User) -> AppletFull: await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.MANAGER) return applet_one @pytest.fixture -async def applet_one_lucy_coordinator(session: AsyncSession, applet_one: AppletFull, tom, lucy) -> AppletFull: +async def applet_one_lucy_coordinator( + session: AsyncSession, applet_one: AppletFull, tom: User, lucy: User +) -> AppletFull: await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.COORDINATOR) return applet_one @pytest.fixture -async def applet_one_lucy_respondent(session: AsyncSession, applet_one: AppletFull, tom, lucy) -> AppletFull: +async def applet_one_lucy_respondent( + session: AsyncSession, applet_one: AppletFull, tom: User, lucy: User +) -> AppletFull: await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.RESPONDENT) return applet_one @@ -93,7 +100,7 @@ def user_create_data() -> UserCreateRequest: @pytest.fixture -def subject_ids(tom, tom_applet_one_subject) -> list[str]: +def subject_ids(tom: User, tom_applet_one_subject: Subject) -> list[uuid.UUID]: return [tom_applet_one_subject.id] @@ -176,6 +183,13 @@ async def applet_one_lucy_subject(session: AsyncSession, applet_one: AppletFull, ) +def message_language(message_body: str): + assert message_body + match_result = re.search(r"", message_body) + assert match_result + return match_result.group(1) + + class TestInvite(BaseTest): fixtures = [ "invitations/fixtures/invitations.json", @@ -196,7 +210,143 @@ class TestInvite(BaseTest): shell_acc_create_url = f"{invitation_list}/{{applet_id}}/shell-account" shell_acc_invite_url = f"{invitation_list}/{{applet_id}}/subject" - async def test_invitation_list(self, client, tom): + @pytest.mark.parametrize("invite_language", ["en", "fr", "el"]) + @pytest.mark.parametrize( + "inviter_type,invitee_type,invite_status", + [ + ("admin", "manager", http.HTTPStatus.OK), + ("admin", "coordinator", http.HTTPStatus.OK), + ("admin", "editor", http.HTTPStatus.OK), + ("admin", "reviewer", http.HTTPStatus.OK), + ("admin", "respondent", http.HTTPStatus.OK), + ("manager", "manager", http.HTTPStatus.OK), + ("manager", "coordinator", http.HTTPStatus.OK), + ("manager", "editor", http.HTTPStatus.OK), + ("manager", "reviewer", http.HTTPStatus.OK), + ("manager", "respondent", http.HTTPStatus.OK), + ("coordinator", "respondent", http.HTTPStatus.OK), + ("coordinator", "reviewer", http.HTTPStatus.OK), + ("coordinator", "manager", http.HTTPStatus.FORBIDDEN), + ("editor", "respondent", http.HTTPStatus.FORBIDDEN), + ], + ) + async def test_invite_existing_user( + self, + client: TestClient, + session: AsyncSession, + tom: User, + lucy: User, + user: User, + applet_one: AppletFull, + invitation_manager_data: InvitationManagersRequest, + invitation_coordinator_data: InvitationManagersRequest, + invitation_editor_data: InvitationManagersRequest, + invitation_reviewer_data: InvitationReviewerRequest, + invitation_respondent_data: InvitationRespondentRequest, + mailbox: TestMail, + invite_language: str, + inviter_type: str, + invitee_type: str, + invite_status: str, + ): + if inviter_type == "admin": + client.login(tom) + elif inviter_type == "manager": + client.login(lucy) + await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.MANAGER) + elif inviter_type == "coordinator": + client.login(lucy) + await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.COORDINATOR) + elif inviter_type == "editor": + client.login(lucy) + await UserAppletAccessService(session, tom.id, applet_one.id).add_role(lucy.id, Role.EDITOR) + else: + raise Exception(f"Invalid inviter_type: {inviter_type}") + + payload: Any + if invitee_type == "manager": + url = self.invite_manager_url + payload = invitation_manager_data + elif invitee_type == "coordinator": + url = self.invite_manager_url + payload = invitation_coordinator_data + elif invitee_type == "editor": + url = self.invite_manager_url + payload = invitation_editor_data + elif invitee_type == "reviewer": + url = self.invite_reviewer_url + payload = invitation_reviewer_data + elif invitee_type == "respondent": + url = self.invite_respondent_url + payload = invitation_respondent_data + else: + raise Exception(f"Invalid invitee_type: {invitee_type}") + payload.language = InvitationLanguage(invite_language) + + response = await client.post(url.format(applet_id=str(applet_one.id)), payload) + assert response.status_code == invite_status + + if invite_status == http.HTTPStatus.OK: + assert response.json()["result"]["userId"] == str(user.id) + assert len(mailbox.mails) == 1 + assert message_language(mailbox.mails[0].body) == invite_language + assert len(mailbox.mails[0].recipients) == 1 + assert mailbox.mails[0].recipients[0] == payload.email + + if invitee_type == "respondent": + assert response.json()["result"]["userId"] == str(user.id) + assert response.json()["result"]["tag"] is not None + assert response.json()["result"]["tag"] == payload.tag + + @pytest.mark.parametrize("invite_language", ["en", "fr"]) + @pytest.mark.parametrize("invitee_type", ["manager", "coordinator", "editor", "reviewer", "respondent"]) + async def test_invite_new_user( + self, + client: TestClient, + tom: User, + applet_one: AppletFull, + invitation_manager_data: InvitationManagersRequest, + invitation_coordinator_data: InvitationManagersRequest, + invitation_editor_data: InvitationManagersRequest, + invitation_reviewer_data: InvitationReviewerRequest, + invitation_respondent_data: InvitationRespondentRequest, + mailbox: TestMail, + invite_language: str, + invitee_type: str, + ): + client.login(tom) + + payload: Any + if invitee_type == "manager": + payload = invitation_manager_data + url = self.invite_manager_url + elif invitee_type == "coordinator": + payload = invitation_coordinator_data + url = self.invite_manager_url + elif invitee_type == "editor": + payload = invitation_editor_data + url = self.invite_manager_url + elif invitee_type == "reviewer": + payload = invitation_reviewer_data + url = self.invite_reviewer_url + elif invitee_type == "respondent": + payload = invitation_respondent_data + url = self.invite_respondent_url + else: + raise Exception(f"Invalid invitee_type: {invitee_type}") + payload.email = EmailStr(f"new{invitation_manager_data.email}") + payload.language = InvitationLanguage(invite_language) + + response = await client.post( + url.format(applet_id=str(applet_one.id)), + payload, + ) + assert response.status_code == http.HTTPStatus.OK + assert not response.json()["result"]["userId"] + assert len(mailbox.mails) == 1 + assert message_language(mailbox.mails[0].body) == invite_language + + async def test_invitation_list(self, client: TestClient, tom: User): client.login(tom) response = await client.get(self.invitation_list) @@ -204,7 +354,7 @@ async def test_invitation_list(self, client, tom): assert len(response.json()["result"]) == 3 - async def test_applets_invitation_list(self, client, tom, applet_one): + async def test_applets_invitation_list(self, client: TestClient, tom: User, applet_one: AppletFull): client.login(tom) response = await client.get( @@ -215,7 +365,9 @@ async def test_applets_invitation_list(self, client, tom, applet_one): assert len(response.json()["result"]) == 2 - async def test_invitation_retrieve(self, client, applet_one, lucy, applet_one_lucy_subject): + async def test_invitation_retrieve( + self, client: TestClient, applet_one: AppletFull, lucy: User, applet_one_lucy_subject: Subject + ): client.login(lucy) response = await client.get(self.invitation_detail.format(key="6a3ab8e6-f2fa-49ae-b2db-197136677da6")) assert response.status_code == http.HTTPStatus.OK @@ -227,7 +379,7 @@ async def test_invitation_retrieve(self, client, applet_one, lucy, applet_one_lu assert response.json()["result"]["tag"] is not None assert response.json()["result"]["title"] == "PHD" - async def test_private_invitation_retrieve(self, client, applet_one_with_link, lucy): + async def test_private_invitation_retrieve(self, client: TestClient, applet_one_with_link: AppletFull, lucy: User): client.login(lucy) response = await client.get(self.private_invitation_detail.format(key=applet_one_with_link.link)) @@ -236,80 +388,12 @@ async def test_private_invitation_retrieve(self, client, applet_one_with_link, l assert response.json()["result"]["appletId"] == str(applet_one_with_link.id) assert response.json()["result"]["role"] == Role.RESPONDENT - async def test_admin_invite_manager_success( - self, client, invitation_manager_data, tom, user, applet_one, mailbox: TestMail - ): - client.login(tom) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one.id)), - invitation_manager_data, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["userId"] == str(user.id) - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_manager_data.email] - assert mailbox.mails[0].subject == "Applet 1 invitation" - - async def test_admin_invite_coordinator_success( - self, client, invitation_coordinator_data, tom, user, applet_one, mailbox: TestMail - ): - client.login(tom) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one.id)), - invitation_coordinator_data, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["userId"] == str(user.id) - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_coordinator_data.email] - - async def test_admin_invite_editor_success( - self, client, invitation_editor_data, tom, user, applet_one, mailbox: TestMail - ): - client.login(tom) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one.id)), - invitation_editor_data, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["userId"] == str(user.id) - assert response.json()["result"]["tag"] == "Team" - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_editor_data.email] - - async def test_admin_invite_reviewer_success( - self, client, invitation_reviewer_data, tom, user, applet_one, mailbox: TestMail - ): - client.login(tom) - response = await client.post( - self.invite_reviewer_url.format(applet_id=str(applet_one.id)), - invitation_reviewer_data, - ) - assert response.status_code == http.HTTPStatus.OK, response.json() - assert response.json()["result"]["userId"] == str(user.id) - assert response.json()["result"]["tag"] == "Team" - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_reviewer_data.email] - assert mailbox.mails[0].subject == "Applet 1 invitation" - - async def test_admin_invite_respondent_success( - self, client, invitation_respondent_data, tom, user, applet_one, mailbox: TestMail - ): - client.login(tom) - response = await client.post( - self.invite_respondent_url.format(applet_id=str(applet_one.id)), - invitation_respondent_data, - ) - assert response.status_code == http.HTTPStatus.OK - assert response.json()["result"]["userId"] == str(user.id) - assert response.json()["result"]["tag"] == invitation_respondent_data.tag - assert response.json()["result"]["tag"] is not None - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_respondent_data.email] - assert mailbox.mails[0].subject == "Applet 1 invitation" - async def test_admin_invite_respondent_duplicate_pending_secret_id( - self, client, invitation_respondent_data, tom, applet_one + self, + client: TestClient, + invitation_respondent_data: InvitationRespondentRequest, + tom: User, + applet_one: AppletFull, ): client.login(tom) response = await client.post( @@ -318,7 +402,7 @@ async def test_admin_invite_respondent_duplicate_pending_secret_id( ) assert response.status_code == http.HTTPStatus.OK - invitation_respondent_data.email = "patric1@gmail.com" + invitation_respondent_data.email = EmailStr("patric1@gmail.com") response = await client.post( self.invite_respondent_url.format(applet_id=str(applet_one.id)), invitation_respondent_data, @@ -329,123 +413,15 @@ async def test_admin_invite_respondent_duplicate_pending_secret_id( assert result[0]["message"] == NonUniqueValue.message assert result[0]["path"] == ["body", "secretUserId"] - async def test_manager_invite_manager_success( - self, client, invitation_manager_data, applet_one_lucy_manager, lucy, mailbox: TestMail - ): - client.login(lucy) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one_lucy_manager.id)), - invitation_manager_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_manager_data.email] - assert mailbox.mails[0].subject == "Applet 1 invitation" - - async def test_manager_invite_coordinator_success( - self, client, invitation_coordinator_data, applet_one_lucy_manager, lucy, mailbox: TestMail - ): - client.login(lucy) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one_lucy_manager.id)), - invitation_coordinator_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_coordinator_data.email] - - async def test_manager_invite_editor_success( - self, client, invitation_editor_data, applet_one_lucy_manager, lucy, mailbox: TestMail - ): - client.login(lucy) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one_lucy_manager.id)), - invitation_editor_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_editor_data.email] - - async def test_manager_invite_reviewer_success( - self, client, invitation_reviewer_data, lucy, applet_one_lucy_manager, mailbox - ): - client.login(lucy) - response = await client.post( - self.invite_reviewer_url.format(applet_id=str(applet_one_lucy_manager.id)), - invitation_reviewer_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_reviewer_data.email] - - async def test_manager_invite_respondent_success( - self, client, invitation_respondent_data, applet_one_lucy_manager, lucy, mailbox: TestMail - ): - client.login(lucy) - response = await client.post( - self.invite_respondent_url.format(applet_id=str(applet_one_lucy_manager.id)), - invitation_respondent_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_respondent_data.email] - - async def test_coordinator_invite_respondent_success( - self, client, invitation_respondent_data, applet_one_lucy_coordinator, lucy, mailbox: TestMail - ): - client.login(lucy) - response = await client.post( - self.invite_respondent_url.format(applet_id=str(applet_one_lucy_coordinator.id)), - invitation_respondent_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_respondent_data.email] - - async def test_coordinator_invite_reviewer_success( - self, client, invitation_reviewer_data, applet_one_lucy_coordinator, lucy, mailbox: TestMail - ): - client.login(lucy) - response = await client.post( - self.invite_reviewer_url.format(applet_id=str(applet_one_lucy_coordinator.id)), - invitation_reviewer_data, - ) - assert response.status_code == http.HTTPStatus.OK - - assert len(mailbox.mails) == 1 - assert mailbox.mails[0].recipients == [invitation_reviewer_data.email] - - async def test_coordinator_invite_manager_fail( - self, client, invitation_manager_data, applet_one_lucy_coordinator, lucy - ): - client.login(lucy) - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one_lucy_coordinator.id)), - invitation_manager_data, - ) - - assert response.status_code == 403 - assert response.json()["result"][0]["message"] == "Access denied." - - async def test_editor_invite_respondent_fail( - self, client, invitation_respondent_data, lucy, applet_one_lucy_editor + async def test_invitation_accept_and_absorb_roles( + self, + session: AsyncSession, + client: TestClient, + lucy: User, + applet_one_lucy_roles: AppletFull, + applet_one: AppletFull, ): client.login(lucy) - response = await client.post( - self.invite_respondent_url.format(applet_id=str(applet_one_lucy_editor.id)), - invitation_respondent_data, - ) - assert response.status_code == 403 - assert response.json()["result"][0]["message"] == "Access denied to manipulate with invites of the applet." - - async def test_invitation_accept_and_absorb_roles(self, session, client, lucy, applet_one_lucy_roles, applet_one): - client.login(lucy) roles = await UserAppletAccessCRUD(session).get_user_roles_to_applet(lucy.id, applet_one.id) assert len(roles) == 3 @@ -460,7 +436,9 @@ async def test_invitation_accept_and_absorb_roles(self, session, client, lucy, a assert Role.MANAGER in roles assert Role.RESPONDENT in roles - async def test_private_invitation_accept(self, session, client, lucy, applet_one_with_link): + async def test_private_invitation_accept( + self, session: AsyncSession, client: TestClient, lucy: User, applet_one_with_link: AppletFull + ): client.login(lucy) response = await client.post(self.accept_private_url.format(key=applet_one_with_link.link)) @@ -470,21 +448,22 @@ async def test_private_invitation_accept(self, session, client, lucy, applet_one applet_id=applet_one_with_link.id, ordered_roles=[Role.RESPONDENT], ) - assert access.role == Role.RESPONDENT + assert access + assert access.role == Role.RESPONDENT.value - async def test_invitation_accept_invitation_does_not_exists(self, client, tom, uuid_zero): + async def test_invitation_accept_invitation_does_not_exists(self, client: TestClient, tom: User, uuid_zero): client.login(tom) response = await client.post(self.accept_url.format(key=uuid_zero)) assert response.status_code == http.HTTPStatus.NOT_FOUND - async def test_invitation_decline(self, client, lucy): + async def test_invitation_decline(self, client: TestClient, lucy: User): client.login(lucy) response = await client.delete(self.decline_url.format(key="6a3ab8e6-f2fa-49ae-b2db-197136677da6")) assert response.status_code == http.HTTPStatus.OK - async def test_invitation_decline_wrong_invitation_does_not_exists(self, client, tom): + async def test_invitation_decline_wrong_invitation_does_not_exists(self, client: TestClient, tom: User): client.login(tom) response = await client.delete(self.decline_url.format(key="6a3ab8e6-f2fa-49ae-b2db-197136677da9")) @@ -493,10 +472,16 @@ async def test_invitation_decline_wrong_invitation_does_not_exists(self, client, @pytest.mark.parametrize("role", (Role.MANAGER, Role.COORDINATOR, Role.EDITOR)) async def test_manager_invite_if_duplicate_email_and_role_not_accepted( - self, client, role, invitation_manager_data, applet_one_lucy_manager, lucy, mailbox: TestMail + self, + client: TestClient, + role: Role, + invitation_manager_data: InvitationManagersRequest, + applet_one_lucy_manager: AppletFull, + lucy: User, + mailbox: TestMail, ): client.login(lucy) - invitation_manager_data.role = role + invitation_manager_data.role = ManagersRole(role) response = await client.post( self.invite_manager_url.format(applet_id=str(applet_one_lucy_manager.id)), invitation_manager_data, @@ -509,12 +494,20 @@ async def test_manager_invite_if_duplicate_email_and_role_not_accepted( ) assert response.status_code == http.HTTPStatus.OK assert len(mailbox.mails) == 2 + assert message_language(mailbox.mails[0].body) == "en" + assert message_language(mailbox.mails[1].body) == "en" async def test_admin_invite_respondent_fail_if_duplicate_email( - self, client, invitation_respondent_data, tom, applet_one_lucy_respondent, lucy, mailbox: TestMail + self, + client: TestClient, + invitation_respondent_data: InvitationRespondentRequest, + tom: User, + applet_one_lucy_respondent: AppletFull, + lucy: User, + mailbox: TestMail, ): client.login(tom) - invitation_respondent_data.email = lucy.email_encrypted + invitation_respondent_data.email = EmailStr(lucy.email_encrypted) response = await client.post( self.invite_respondent_url.format(applet_id=str(applet_one_lucy_respondent.id)), invitation_respondent_data, @@ -526,10 +519,16 @@ async def test_admin_invite_respondent_fail_if_duplicate_email( assert len(mailbox.mails) == 0 async def test_fail_if_invite_manager_on_editor_role( - self, client, invitation_editor_data, tom, applet_one_lucy_manager, lucy, mailbox: TestMail + self, + client: TestClient, + invitation_editor_data: InvitationManagersRequest, + tom: User, + applet_one_lucy_manager: AppletFull, + lucy: User, + mailbox: TestMail, ): client.login(tom) - invitation_editor_data.email = lucy.email_encrypted + invitation_editor_data.email = EmailStr(lucy.email_encrypted) response = await client.post( self.invite_manager_url.format(applet_id=applet_one_lucy_manager.id), invitation_editor_data, @@ -540,45 +539,6 @@ async def test_fail_if_invite_manager_on_editor_role( assert res["message"] == ManagerInvitationExist.message assert len(mailbox.mails) == 0 - async def test_invite_not_registered_user_manager( - self, client, invitation_manager_data, tom, applet_one, mailbox: TestMail - ): - client.login(tom) - invitation_manager_data.email = f"new{invitation_manager_data.email}" - response = await client.post( - self.invite_manager_url.format(applet_id=str(applet_one.id)), - invitation_manager_data, - ) - assert response.status_code == http.HTTPStatus.OK - assert not response.json()["result"]["userId"] - assert len(mailbox.mails) == 1 - - async def test_invite_not_registered_user_reviewer( - self, client, invitation_reviewer_data, tom, applet_one, mailbox: TestMail - ): - client.login(tom) - invitation_reviewer_data.email = f"new{invitation_reviewer_data.email}" - response = await client.post( - self.invite_reviewer_url.format(applet_id=str(applet_one.id)), - invitation_reviewer_data.dict(), - ) - assert response.status_code == http.HTTPStatus.OK, response.json() - assert not response.json()["result"]["userId"] - assert len(mailbox.mails) == 1 - - async def test_invite_not_registered_user_respondent( - self, client, invitation_respondent_data, tom, applet_one, mailbox: TestMail - ): - client.login(tom) - invitation_respondent_data.email = f"new{invitation_respondent_data.email}" - response = await client.post( - self.invite_respondent_url.format(applet_id=str(applet_one.id)), - invitation_respondent_data.dict(), - ) - assert response.status_code == http.HTTPStatus.OK - assert not response.json()["result"]["userId"] - assert len(mailbox.mails) == 1 - @pytest.mark.parametrize( "status,url,method", ( @@ -587,11 +547,20 @@ async def test_invite_not_registered_user_respondent( ), ) async def test_new_user_accept_decline_invitation( - self, session, client, user_create_data, status, url, method, invitation_manager_data, tom, applet_one + self, + session: AsyncSession, + client: TestClient, + user_create_data: UserCreateRequest, + status: InvitationStatus, + url: str, + method: str, + invitation_manager_data: InvitationManagersRequest, + tom: User, + applet_one: AppletFull, ) -> None: client.login(tom) new_email = f"new{invitation_manager_data.email}" - invitation_manager_data.email = new_email + invitation_manager_data.email = EmailStr(new_email) # Send an invite response = await client.post( self.invite_manager_url.format(applet_id=str(applet_one.id)), @@ -601,7 +570,7 @@ async def test_new_user_accept_decline_invitation( assert not response.json()["result"]["userId"] invitation_key = response.json()["result"]["key"] - user_create_data.email = new_email + user_create_data.email = EmailStr(new_email) # An invited user creates an account resp = await client.post("/users", data=user_create_data.dict()) assert resp.status_code == http.HTTPStatus.CREATED @@ -618,11 +587,16 @@ async def test_new_user_accept_decline_invitation( assert inv.status == status # type: ignore[union-attr] async def test_update_invitation_for_new_user_who_registered_after_first_invitation( - self, client, user_create_data, invitation_manager_data, tom, applet_one + self, + client: TestClient, + user_create_data, + invitation_manager_data: InvitationManagersRequest, + tom: User, + applet_one: AppletFull, ) -> None: client.login(tom) new_email = f"new{invitation_manager_data.email}" - invitation_manager_data.email = new_email + invitation_manager_data.email = EmailStr(new_email) # Send an invite response = await client.post( self.invite_manager_url.format(applet_id=str(applet_one.id)), @@ -648,7 +622,12 @@ async def test_update_invitation_for_new_user_who_registered_after_first_invitat assert response.json()["result"]["userId"] == exp_user_id async def test_resend_invitation_with_updates_for_respondent_with_pending_invitation( - self, session, client, invitation_respondent_data, tom: User, applet_one: AppletFull + self, + session: AsyncSession, + client: TestClient, + invitation_respondent_data: InvitationRespondentRequest, + tom: User, + applet_one: AppletFull, ): client.login(tom) response = await client.post( @@ -669,7 +648,12 @@ async def test_resend_invitation_with_updates_for_respondent_with_pending_invita assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY async def test_resend_invitation_for_respondent_with_pending_invitation_only_last_key_valid( - self, client, invitation_respondent_data, tom, applet_one, user + self, + client: TestClient, + invitation_respondent_data: InvitationRespondentRequest, + tom: User, + applet_one: AppletFull, + user: User, ): client.login(tom) response = await client.post( @@ -698,16 +682,16 @@ async def test_resend_invitation_for_respondent_with_pending_invitation_only_las async def test_send_many_pending_invitations_for_one_email_valid_only_last( self, - client, - session, - invitation_coordinator_data, - invitation_editor_data, - invitation_manager_data, - invitation_respondent_data, - invitation_reviewer_data, - tom, - applet_one, - user, + client: TestClient, + session: AsyncSession, + invitation_coordinator_data: InvitationManagersRequest, + invitation_editor_data: InvitationManagersRequest, + invitation_manager_data: InvitationManagersRequest, + invitation_respondent_data: InvitationRespondentRequest, + invitation_reviewer_data: InvitationReviewerRequest, + tom: User, + applet_one: AppletFull, + user: User, ): client.login(tom) invitations_urls = [ @@ -742,7 +726,9 @@ async def test_send_many_pending_invitations_for_one_email_valid_only_last( response = await client.get(self.invitation_detail.format(key=keys[-1])) assert response.status_code == http.HTTPStatus.OK - async def test_get_invitation_by_key_invitation_does_not_exist(self, client, tom, uuid_zero): + async def test_get_invitation_by_key_invitation_does_not_exist( + self, client: TestClient, tom: User, uuid_zero: uuid.UUID + ): client.login(tom) response = await client.get(self.invitation_detail.format(key=uuid_zero)) @@ -754,7 +740,11 @@ async def test_get_invitation_by_key_invitation_does_not_exist(self, client, tom (("decline_url", "delete"), ("accept_url", "post")), ) async def test_get_invitation_by_key_already_accpted_declined( - self, client, url: Literal["decline_url", "accept_url"], method: Literal["delete", "post"], lucy + self, + client: TestClient, + url: Literal["decline_url", "accept_url"], + method: Literal["delete", "post"], + lucy: User, ): client.login(lucy) key = "6a3ab8e6-f2fa-49ae-b2db-197136677da6" @@ -767,14 +757,18 @@ async def test_get_invitation_by_key_already_accpted_declined( assert response.status_code == http.HTTPStatus.BAD_REQUEST assert response.json()["result"][0]["message"] == InvitationAlreadyProcessed.message - async def test_get_private_invitation_by_link_does_not_exist(self, client, tom, uuid_zero): + async def test_get_private_invitation_by_link_does_not_exist( + self, client: TestClient, tom: User, uuid_zero: uuid.UUID + ): client.login(tom) response = await client.get(self.private_invitation_detail.format(key=uuid_zero)) assert response.status_code == http.HTTPStatus.NOT_FOUND assert response.json()["result"][0]["message"] == InvitationDoesNotExist.message - async def test_private_invitation_accept_invitation_does_not_exist(self, client, tom, uuid_zero): + async def test_private_invitation_accept_invitation_does_not_exist( + self, client: TestClient, tom: User, uuid_zero: uuid.UUID + ): client.login(tom) response = await client.post(self.accept_private_url.format(key=uuid_zero)) @@ -782,7 +776,12 @@ async def test_private_invitation_accept_invitation_does_not_exist(self, client, assert response.json()["result"][0]["message"] == InvitationDoesNotExist.message async def test_send_invitation_to_reviewer_invitation_already_approved( - self, client, invitation_reviewer_data, tom, applet_one, user + self, + client: TestClient, + invitation_reviewer_data: InvitationReviewerRequest, + tom: User, + applet_one: AppletFull, + user: User, ): client.login(tom) # send an invite @@ -806,7 +805,9 @@ async def test_send_invitation_to_reviewer_invitation_already_approved( assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY assert response.json()["result"][0]["message"] == ManagerInvitationExist.message - async def test_send_incorrect_role_to_invite_managers(self, client, invitation_manager_data, tom, applet_one): + async def test_send_incorrect_role_to_invite_managers( + self, client: TestClient, invitation_manager_data: InvitationManagersRequest, tom: User, applet_one: AppletFull + ): client.login(tom) data = invitation_manager_data.dict() data["role"] = "notvalid" @@ -821,7 +822,12 @@ async def test_send_incorrect_role_to_invite_managers(self, client, invitation_m assert result[0]["message"] == emsg async def test_invite_reviewer_with_respondent_does_not_exist( - self, client, invitation_reviewer_data, tom, applet_one, uuid_zero + self, + client: TestClient, + invitation_reviewer_data: InvitationReviewerRequest, + tom: User, + applet_one: AppletFull, + uuid_zero: uuid.UUID, ): client.login(tom) invitation_reviewer_data.subjects = [uuid_zero] @@ -839,7 +845,14 @@ async def test_invite_reviewer_with_respondent_does_not_exist( (("accept_url", "post"), ("decline_url", "delete")), ) async def test_accept_or_decline_already_processed_invitation( - self, client, url, method, invitation_manager_data, tom, applet_one, user + self, + client: TestClient, + url: str, + method: str, + invitation_manager_data: InvitationManagersRequest, + tom: User, + applet_one: AppletFull, + user: User, ) -> None: client.login(tom) # Send an invite @@ -860,7 +873,9 @@ async def test_accept_or_decline_already_processed_invitation( resp = await client_method(getattr(self, url).format(key=invitation_key)) assert resp.status_code == http.HTTPStatus.BAD_REQUEST - async def test_shell_create_account(self, client, shell_create_data, bob: User, applet_four: AppletFull): + async def test_shell_create_account( + self, client: TestClient, shell_create_data: dict, bob: User, applet_four: AppletFull + ): client.login(bob) applet_id = str(applet_four.id) creator_id = str(bob.id) @@ -892,7 +907,9 @@ async def test_shell_create_account(self, client, shell_create_data, bob: User, ), ), ) - async def test_shell_invite(self, client, session, bob: User, applet_four: AppletFull, shell_create): + async def test_shell_invite( + self, client: TestClient, session: AsyncSession, bob: User, applet_four: AppletFull, shell_create: dict + ): client.login(bob) email = "mm@mail.com" applet_id = str(applet_four.id) @@ -910,7 +927,7 @@ async def test_shell_invite(self, client, session, bob: User, applet_four: Apple assert subject_model.language == "fr" async def test_shell_invite_no_language( - self, client, session, shell_create_data, bob: User, applet_four: AppletFull + self, client: TestClient, session: AsyncSession, shell_create_data: dict, bob: User, applet_four: AppletFull ): client.login(bob) email = "mm_english@mail.com" @@ -929,7 +946,13 @@ async def test_shell_invite_no_language( assert subject_model.language == shell_create_data["language"] async def test_invite_and_accept_invitation_as_respondent( - self, client, session, invitation_respondent_data, tom: User, applet_one: AppletFull, bill_bronson: User + self, + client: TestClient, + session: AsyncSession, + invitation_respondent_data: InvitationRespondentRequest, + tom: User, + applet_one: AppletFull, + bill_bronson: User, ): subject_crud = SubjectsCrud(session) applet_id = applet_one.id @@ -937,7 +960,7 @@ async def test_invite_and_accept_invitation_as_respondent( user_id = tom.id # Create invitation to Mike client.login(tom) - invitation_respondent_data.email = user_email + invitation_respondent_data.email = EmailStr(user_email) subjects_on_applet0 = await subject_crud.count(applet_id=applet_id) response = await client.post( self.invite_respondent_url.format(applet_id=applet_id), @@ -958,7 +981,13 @@ async def test_invite_and_accept_invitation_as_respondent( assert subjects_on_applet2 == subjects_on_applet1 async def test_invite_and_accept_invitation_as_manager( - self, client, session, invitation_manager_data, tom: User, user: UserSchema, applet_one: AppletFull + self, + client: TestClient, + session: AsyncSession, + invitation_manager_data: InvitationManagersRequest, + tom: User, + user: UserSchema, + applet_one: AppletFull, ): subject_crud = SubjectsCrud(session) applet_id = applet_one.id @@ -985,7 +1014,7 @@ async def test_invite_and_accept_invitation_as_manager( assert subjects_on_applet2 == (subjects_on_applet1 + 1) async def test_private_invitation_accept_create_subject( - self, client, session, user: User, applet_one_with_link: AppletFull + self, client: TestClient, session: AsyncSession, user: User, applet_one_with_link: AppletFull ): assert applet_one_with_link.link subject_crud = SubjectsCrud(session) @@ -1000,7 +1029,13 @@ async def test_private_invitation_accept_create_subject( assert subject async def test_move_pins_from_subject_to_user( - self, client, session, tom: User, bob: User, shell_create_data, applet_one: AppletFull + self, + client: TestClient, + session: AsyncSession, + tom: User, + bob: User, + shell_create_data: dict, + applet_one: AppletFull, ): client.login(tom) applet_id = str(applet_one.id) @@ -1026,38 +1061,14 @@ async def test_move_pins_from_subject_to_user( pins = await UserAppletAccessCRUD(session).get_workspace_pins(tom.id) assert pins[0].pinned_user_id == bob.id - @pytest.mark.skip("Not actual") - async def test_shell_invite_cant_twice(self, client, session, shell_create_data, tom: User, applet_one: AppletFull): - client.login(self.login_url, tom.email_encrypted, "Test1234!") - email = "mm@mail.com" - applet_id = str(applet_one.id) - url = self.shell_acc_create_url.format(applet_id=applet_id) - - subjects = [] - for i in range(2): - body = {**shell_create_data, "secretUserId": f"{uuid.uuid4()}"} - response = await client.post(url, body) - subject = response.json()["result"] - subjects.append(subject) - - url = self.shell_acc_invite_url.format(applet_id=applet_id) - # Invite first subject - response = await client.post(url, dict(subjectId=subjects[0]["id"], email=email)) - assert response.status_code == http.HTTPStatus.OK - # Try to invite next subject on same email - response = await client.post(url, dict(subjectId=subjects[1]["id"], email=email)) - assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY - message = response.json()["result"][0]["message"] - assert message == RespondentInvitationExist.message - async def test_cant_create_invitation_with_same_secret_id_as_shell_account( self, - client, - session, + client: TestClient, + session: AsyncSession, applet_one: AppletFull, applet_one_shell_account: Subject, tom: User, - invitation_respondent_data, + invitation_respondent_data: InvitationRespondentRequest, ): client.login(tom) invitation_respondent_data.secret_user_id = applet_one_shell_account.secret_user_id @@ -1071,7 +1082,13 @@ async def test_cant_create_invitation_with_same_secret_id_as_shell_account( assert payload["result"][0]["message"] == NonUniqueValue().error async def test_shell_update_email_on_accept( - self, client, session, bob: User, lucy: User, applet_four: AppletFull, shell_create_data + self, + client: TestClient, + session: AsyncSession, + bob: User, + lucy: User, + applet_four: AppletFull, + shell_create_data: dict, ): client.login(bob) applet_id = str(applet_four.id) @@ -1103,3 +1120,50 @@ async def test_shell_update_email_on_accept( subject_model = await crud.get_by_id(subject["id"]) assert subject_model assert subject_model.email == lucy.email_encrypted + + async def test_shell_reinvite( + self, + client: TestClient, + session: AsyncSession, + bob: User, + lucy: User, + applet_four: AppletFull, + shell_create_data: dict, + mailbox: TestMail, + ): + client.login(bob) + response = await client.post(self.shell_acc_create_url.format(applet_id=str(applet_four.id)), shell_create_data) + assert response.status_code == http.HTTPStatus.OK + subject = response.json()["result"] + assert subject + + response = await client.post( + self.shell_acc_invite_url.format(applet_id=str(applet_four.id)), + dict(subjectId=subject["id"], email=lucy.email_encrypted), + ) + assert response.status_code == http.HTTPStatus.OK + assert len(mailbox.mails) == 1 + assert message_language(mailbox.mails[0].body) == "en" + invitation_key = response.json()["result"]["key"] + assert invitation_key + + client.login(lucy) + response = await client.post(self.accept_url.format(key=invitation_key)) + assert response.status_code == http.HTTPStatus.OK + + client.login(bob) + response = await client.post( + self.shell_acc_create_url.format(applet_id=str(applet_four.id)), + {**shell_create_data, "secretUserId": f"{uuid.uuid4()}"}, + ) + assert response.status_code == http.HTTPStatus.OK + subject = response.json()["result"] + assert subject + + response = await client.post( + self.shell_acc_invite_url.format(applet_id=str(applet_four.id)), + dict(subjectId=subject["id"], email=lucy.email_encrypted), + ) + assert response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY + message = response.json()["result"][0]["message"] + assert message == RespondentInvitationExist.message diff --git a/src/apps/mailing/services.py b/src/apps/mailing/services.py index 9d2cc966444..7876dd09058 100644 --- a/src/apps/mailing/services.py +++ b/src/apps/mailing/services.py @@ -1,5 +1,5 @@ from fastapi_mail import ConnectionConfig, FastMail -from jinja2 import Environment, PackageLoader, select_autoescape +from jinja2 import Environment, PackageLoader, TemplateNotFound, select_autoescape from apps.mailing.domain import MessageSchema from config import settings @@ -62,8 +62,11 @@ async def send(self, message: MessageSchema) -> None: fm = mailing_class(self._connection) await fm.send_message(message) - def get_template(self, path: str, **kwargs): - template = self.env.get_template(f"{path}.html") - html = template.render(**kwargs) - - return html + def get_localized_html_template(self, template_name: str, language: str, **kwargs) -> str: + kwargs["language"] = language + try: + return self.env.get_template(f"{template_name}_{language}.html").render(**kwargs) + except TemplateNotFound: + if language != "en": + return self.get_localized_html_template(template_name, "en", **kwargs) + raise diff --git a/src/apps/mailing/static/templates/blocks/team_info_en.html b/src/apps/mailing/static/templates/blocks/team_info_en.html index dc1ccd5f00a..8f6cbff029c 100644 --- a/src/apps/mailing/static/templates/blocks/team_info_en.html +++ b/src/apps/mailing/static/templates/blocks/team_info_en.html @@ -1,3 +1,3 @@ - – The MindLogger Team + – The MindLogger Team diff --git a/src/apps/mailing/static/templates/blocks/team_info_fr.html b/src/apps/mailing/static/templates/blocks/team_info_fr.html index a605af3c467..084b3ec23ca 100644 --- a/src/apps/mailing/static/templates/blocks/team_info_fr.html +++ b/src/apps/mailing/static/templates/blocks/team_info_fr.html @@ -1,3 +1,3 @@ - – L'équipe MindLogger + – L'équipe MindLogger diff --git a/src/apps/mailing/static/templates/footers/footer_info_en.html b/src/apps/mailing/static/templates/footers/footer_info_en.html index e7b5930b73d..8b3e65bdcf5 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_en.html +++ b/src/apps/mailing/static/templates/footers/footer_info_en.html @@ -25,31 +25,30 @@ - + Get iOS app + /> + - + Get Android app + /> + Need help? - Visit our Help Center. + + Visit our Help Center + . diff --git a/src/apps/mailing/static/templates/footers/footer_info_fr.html b/src/apps/mailing/static/templates/footers/footer_info_fr.html index 099ebb214e5..2c407584025 100644 --- a/src/apps/mailing/static/templates/footers/footer_info_fr.html +++ b/src/apps/mailing/static/templates/footers/footer_info_fr.html @@ -1,36 +1,40 @@ - - - - - - - - - + + + + + + + + +
- - - - - - - - - - - -
- Obtenez l'application mobile MindLogger: -
- Obtenez l'application iOS - - Obtenez l'application Android -
- Besoin d'aide? Visitez notre centre d'aide. -
-
- Child Mind Institute -
- Le Child Mind Institute est le créateur de MindLogger mais n'est pas responsable du contenu créé par des tiers. -
+ + + + + + + + + + + +
+ Obtenez l'application mobile MindLogger: +
+ + Obtenez l'application iOS + + + + Obtenez l'application Android + +
+ Besoin d'aide? Visitez notre centre d'aide. +
+
+ Child Mind Institute +
+ Le Child Mind Institute est le créateur de MindLogger mais n'est pas responsable du contenu créé par des tiers. +
diff --git a/src/apps/mailing/static/templates/header.html b/src/apps/mailing/static/templates/header.html index 1fe9ce31c9c..7b06ddc1f11 100644 --- a/src/apps/mailing/static/templates/header.html +++ b/src/apps/mailing/static/templates/header.html @@ -1,3 +1,5 @@ + +