From 72178ca520d6edd85d11c9e546fc32433da80e36 Mon Sep 17 00:00:00 2001 From: fbelginetw <167361860+fbelginetw@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:25:19 -0500 Subject: [PATCH 01/31] fix: Opentrons ai client create protocol fixes (#16802) # Overview This PR fixes many defects opened by the Opentrons team and refactors some code to remove duplication defects [AUTH-1031](https://opentrons.atlassian.net/browse/AUTH-1031), [AUTH-1032](https://opentrons.atlassian.net/browse/AUTH-1032), [AUTH-1033](https://opentrons.atlassian.net/browse/AUTH-1033), [AUTH-1034](https://opentrons.atlassian.net/browse/AUTH-1034), [AUTH-1035](https://opentrons.atlassian.net/browse/AUTH-1035), [AUTH-1040](https://opentrons.atlassian.net/browse/AUTH-1040), [AUTH-1042](https://opentrons.atlassian.net/browse/AUTH-1042) ## Test Plan and Hands on Testing Retested manually ## Risk assessment low risk [AUTH-1031]: https://opentrons.atlassian.net/browse/AUTH-1031?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [AUTH-1032]: https://opentrons.atlassian.net/browse/AUTH-1032?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [AUTH-1033]: https://opentrons.atlassian.net/browse/AUTH-1033?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [AUTH-1034]: https://opentrons.atlassian.net/browse/AUTH-1034?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [AUTH-1035]: https://opentrons.atlassian.net/browse/AUTH-1035?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [AUTH-1040]: https://opentrons.atlassian.net/browse/AUTH-1040?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [AUTH-1042]: https://opentrons.atlassian.net/browse/AUTH-1042?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- .../localization/en/create_protocol.json | 2 +- .../ControlledAddTextAreaFields/index.tsx | 8 ++- .../src/molecules/ExitConfirmModal/index.tsx | 5 +- .../__tests__/ApplicationSection.test.tsx | 14 ++-- .../organisms/ApplicationSection/index.tsx | 42 +---------- .../__tests__/InstrumentsSection.test.tsx | 28 +++----- .../organisms/InstrumentsSection/index.tsx | 38 +--------- .../__tests__/LabwareLiquidsSection.test.tsx | 36 ++++++---- .../organisms/LabwareLiquidsSection/index.tsx | 39 +---------- .../src/organisms/LabwareModal/index.tsx | 16 +++-- .../__tests__/ModulesSection.test.tsx | 20 ++---- .../src/organisms/ModulesSection/index.tsx | 35 +--------- .../ProtocolSectionsContainer/index.tsx | 70 ++++++++++++++----- .../__tests__/StepsSection.test.tsx | 8 ++- .../src/organisms/StepsSection/index.tsx | 35 +--------- .../src/organisms/UpdateProtocol/index.tsx | 11 ++- .../src/pages/CreateProtocol/index.tsx | 14 ++-- opentrons-ai-client/src/resources/atoms.ts | 8 +-- opentrons-ai-client/src/resources/types.ts | 6 +- .../resources/utils/createProtocolUtils.tsx | 6 +- 20 files changed, 157 insertions(+), 284 deletions(-) diff --git a/opentrons-ai-client/src/assets/localization/en/create_protocol.json b/opentrons-ai-client/src/assets/localization/en/create_protocol.json index ebf38133718..a91099593f4 100644 --- a/opentrons-ai-client/src/assets/localization/en/create_protocol.json +++ b/opentrons-ai-client/src/assets/localization/en/create_protocol.json @@ -17,7 +17,7 @@ "opentrons_ot2_label": "Opentrons OT-2", "opentrons_ot2": "Opentrons OT-2", "instruments_pipettes_title": "What pipettes would you like to use?", - "two_pipettes_label": "Two pipettes", + "two_pipettes_label": "1-Channel or 8-Channel pipettes", "right_pipette_label": "Right mount", "left_pipette_label": "Left mount", "choose_pipette_placeholder": "Choose pipette", diff --git a/opentrons-ai-client/src/molecules/ControlledAddTextAreaFields/index.tsx b/opentrons-ai-client/src/molecules/ControlledAddTextAreaFields/index.tsx index bfa248be1a8..c7599068b3c 100644 --- a/opentrons-ai-client/src/molecules/ControlledAddTextAreaFields/index.tsx +++ b/opentrons-ai-client/src/molecules/ControlledAddTextAreaFields/index.tsx @@ -63,7 +63,13 @@ export function ControlledAddTextAreaFields({ { - field.onChange(values.filter((_, i) => i !== index)) + const newValues = values + .filter((_, i) => i !== index) + .map( + (value, i) => + `${t(name)} ${i + 1}: ${value.split(': ')[1]}` + ) + field.onChange(newValues) }} css={css` justify-content: flex-end; diff --git a/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx b/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx index b975ddec40c..fb4b82afb75 100644 --- a/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx +++ b/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx @@ -37,7 +37,10 @@ export function ExitConfirmModal(): JSX.Element { return ( - + {t('exit_confirmation_body')} diff --git a/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx b/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx index 50e01fcb66f..477c871b7c5 100644 --- a/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx +++ b/opentrons-ai-client/src/organisms/ApplicationSection/__tests__/ApplicationSection.test.tsx @@ -13,6 +13,8 @@ const TestFormProviderComponent = () => { return ( + +

{`form is ${methods.formState.isValid ? 'valid' : 'invalid'}`}

) } @@ -33,7 +35,6 @@ describe('ApplicationSection', () => { expect( screen.getByText('Describe what you are trying to do') ).toBeInTheDocument() - expect(screen.getByText('Confirm')).toBeInTheDocument() }) it('should not render other application dropdown if Other option is not selected', () => { @@ -54,10 +55,10 @@ describe('ApplicationSection', () => { expect(screen.getByText('Other application')).toBeInTheDocument() }) - it('should enable confirm button when all fields are filled', async () => { + it('should update the form state to valid when all fields are filled', async () => { render() - expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() const applicationDropdown = screen.getByText('Select an option') fireEvent.click(applicationDropdown) @@ -69,14 +70,13 @@ describe('ApplicationSection', () => { fireEvent.change(describeInput, { target: { value: 'Test description' } }) await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled() + expect(screen.getByText('form is valid')).toBeInTheDocument() }) }) - it('should disable confirm button when all fields are not filled', () => { + it('should update the form state to invalid when not all fields are filled', () => { render() - const confirmButton = screen.getByRole('button') - expect(confirmButton).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() }) }) diff --git a/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx b/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx index 5e3cc523f68..54819e99a3a 100644 --- a/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx +++ b/opentrons-ai-client/src/organisms/ApplicationSection/index.tsx @@ -1,19 +1,8 @@ -import { - DIRECTION_COLUMN, - DISPLAY_FLEX, - Flex, - JUSTIFY_FLEX_END, - LargeButton, - SPACING, -} from '@opentrons/components' +import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' import { ControlledDropdownMenu } from '../../atoms/ControlledDropdownMenu' import { ControlledInputField } from '../../atoms/ControlledInputField' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { APPLICATION_STEP } from '../ProtocolSectionsContainer' export const BASIC_ALIQUOTING = 'basic_aliquoting' export const PCR = 'pcr' @@ -25,11 +14,7 @@ export const APPLICATION_DESCRIBE = 'application.description' export function ApplicationSection(): JSX.Element | null { const { t } = useTranslation('create_protocol') - const { - watch, - formState: { isValid }, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + const { watch } = useFormContext() const options = [ { name: t(BASIC_ALIQUOTING), value: BASIC_ALIQUOTING }, @@ -39,16 +24,6 @@ export function ApplicationSection(): JSX.Element | null { const isOtherSelected = watch(APPLICATION_SCIENTIFIC_APPLICATION) === OTHER - function handleConfirmButtonClick(): void { - const step = - currentStep > APPLICATION_STEP ? currentStep : APPLICATION_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - return ( - - - - ) } - -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` diff --git a/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx b/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx index 11e6bceaf45..0712ede4757 100644 --- a/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx +++ b/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx @@ -13,6 +13,8 @@ const TestFormProviderComponent = () => { return ( + +

{`form is ${methods.formState.isValid ? 'valid' : 'invalid'}`}

) } @@ -24,7 +26,7 @@ const render = (): ReturnType => { } describe('ApplicationSection', () => { - it('should render robot, pipette, flex gripper radios, mounts dropdowns, and confirm button', async () => { + it('should render robot, pipette, flex gripper radios and mounts dropdowns', async () => { render() expect( @@ -40,7 +42,6 @@ describe('ApplicationSection', () => { expect( screen.getByText('Do you want to use the Flex Gripper?') ).toBeInTheDocument() - expect(screen.getByText('Confirm')).toBeInTheDocument() }) it('should not render left and right mount dropdowns if 96-Channel 1000µL pipette radio is selected', () => { @@ -80,7 +81,7 @@ describe('ApplicationSection', () => { render() await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() }) const leftMount = screen.getAllByText('Choose pipette')[0] @@ -96,14 +97,14 @@ describe('ApplicationSection', () => { }) expect(screen.getByText('None')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled() + expect(screen.getByText('form is valid')).toBeInTheDocument() }) it('should not be able to select two pipettes with none value', async () => { render() await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() }) const leftMount = screen.getAllByText('Choose pipette')[0] @@ -115,15 +116,15 @@ describe('ApplicationSection', () => { fireEvent.click(screen.getAllByText('None')[1]) await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() }) }) - it('should enable confirm button when all fields are filled', async () => { + it('should update the form state to valid when all fields are filled', async () => { render() await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() }) const leftMount = screen.getAllByText('Choose pipette')[0] @@ -135,16 +136,7 @@ describe('ApplicationSection', () => { fireEvent.click(screen.getByText('Flex 8-Channel 50 μL')) await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled() - }) - }) - - it('should disable confirm button when all fields are not filled', async () => { - render() - - const confirmButton = screen.getByRole('button') - await waitFor(() => { - expect(confirmButton).not.toBeEnabled() + expect(screen.getByText('form is valid')).toBeInTheDocument() }) }) }) diff --git a/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx b/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx index c56276e1dfa..6f815b45a5d 100644 --- a/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx +++ b/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx @@ -1,19 +1,13 @@ import { COLORS, DIRECTION_COLUMN, - DISPLAY_FLEX, Flex, - JUSTIFY_FLEX_END, - LargeButton, SPACING, StyledText, } from '@opentrons/components' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { INSTRUMENTS_STEP } from '../ProtocolSectionsContainer' import { ControlledDropdownMenu } from '../../atoms/ControlledDropdownMenu' import { ControlledRadioButtonGroup } from '../../molecules/ControlledRadioButtonGroup' import { useMemo } from 'react' @@ -21,7 +15,6 @@ import { getAllPipetteNames, getPipetteSpecsV2, OT2_PIPETTES, - OT2_ROBOT_TYPE, OT3_PIPETTES, } from '@opentrons/shared-data' @@ -40,11 +33,7 @@ export const NO_PIPETTES = 'none' export function InstrumentsSection(): JSX.Element | null { const { t } = useTranslation('create_protocol') - const { - formState: { isValid }, - watch, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + const { watch } = useFormContext() const robotType = watch(ROBOT_FIELD_NAME) const isOtherPipettesSelected = watch(PIPETTES_FIELD_NAME) === TWO_PIPETTES const isOpentronsOT2Selected = robotType === OPENTRONS_OT2 @@ -91,7 +80,7 @@ export function InstrumentsSection(): JSX.Element | null { const pipetteOptions = useMemo(() => { const allPipetteOptions = getAllPipetteNames('maxVolume', 'channels') .filter(name => - (robotType === OT2_ROBOT_TYPE ? OT2_PIPETTES : OT3_PIPETTES).includes( + (robotType === OPENTRONS_OT2 ? OT2_PIPETTES : OT3_PIPETTES).includes( name ) ) @@ -103,16 +92,6 @@ export function InstrumentsSection(): JSX.Element | null { return [{ name: t('none'), value: NO_PIPETTES }, ...allPipetteOptions] }, [robotType]) - function handleConfirmButtonClick(): void { - const step = - currentStep > INSTRUMENTS_STEP ? currentStep : INSTRUMENTS_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - return ( )} - - - - ) } -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` - const PipettesDropdown = styled.div<{ isOpentronsOT2Selected?: boolean }>` display: flex; flex-direction: column; diff --git a/opentrons-ai-client/src/organisms/LabwareLiquidsSection/__tests__/LabwareLiquidsSection.test.tsx b/opentrons-ai-client/src/organisms/LabwareLiquidsSection/__tests__/LabwareLiquidsSection.test.tsx index ce5f40e8e52..9e4d76391f0 100644 --- a/opentrons-ai-client/src/organisms/LabwareLiquidsSection/__tests__/LabwareLiquidsSection.test.tsx +++ b/opentrons-ai-client/src/organisms/LabwareLiquidsSection/__tests__/LabwareLiquidsSection.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, screen } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import { describe, it, expect } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' @@ -15,6 +15,8 @@ const TestFormProviderComponent = () => { return ( + +

{`form is ${methods.formState.isValid ? 'valid' : 'invalid'}`}

) } @@ -31,7 +33,6 @@ describe('LabwareLiquidsSection', () => { expect(screen.getByText('Add Opentrons labware')).toBeInTheDocument() expect(screen.getByText('No labware added yet')).toBeInTheDocument() - expect(screen.getByText('Confirm')).toBeInTheDocument() }) it('should not display the no labware added message if labwares have been added', async () => { @@ -51,20 +52,27 @@ describe('LabwareLiquidsSection', () => { expect(screen.queryByText('No labware added yet')).not.toBeInTheDocument() }) - // it('should enable the confirm button when labwares have been added', async () => { - // render() + it('should update form state to valid when labwares and liquids have been added', async () => { + render() - // expect(screen.getByText('Confirm')).toBeDisabled() + await waitFor(() => { + expect(screen.getByText('form is invalid')).toBeInTheDocument() + }) + const addButton = screen.getByText('Add Opentrons labware') + fireEvent.click(addButton) - // const addButton = screen.getByText('Add Opentrons labware') - // fireEvent.click(addButton) + fireEvent.click(screen.getByText('Tip rack')) + fireEvent.click( + await screen.findByText('Opentrons Flex 96 Tip Rack 1000 µL') + ) + fireEvent.click(screen.getByText('Save')) - // fireEvent.click(screen.getByText('Tip rack')) - // fireEvent.click( - // await screen.findByText('Opentrons Flex 96 Tip Rack 1000 µL') - // ) - // fireEvent.click(screen.getByText('Save')) + fireEvent.change(screen.getByRole('textbox'), { + target: { value: 'test liquid' }, + }) - // expect(screen.getByText('Confirm')).toBeEnabled() - // }) + await waitFor(() => { + expect(screen.getByText('form is valid')).toBeInTheDocument() + }) + }) }) diff --git a/opentrons-ai-client/src/organisms/LabwareLiquidsSection/index.tsx b/opentrons-ai-client/src/organisms/LabwareLiquidsSection/index.tsx index 627b3a5ea76..24d50e32f21 100644 --- a/opentrons-ai-client/src/organisms/LabwareLiquidsSection/index.tsx +++ b/opentrons-ai-client/src/organisms/LabwareLiquidsSection/index.tsx @@ -1,21 +1,14 @@ import { COLORS, DIRECTION_COLUMN, - DISPLAY_FLEX, EmptySelectorButton, Flex, InfoScreen, - JUSTIFY_FLEX_END, - LargeButton, SPACING, StyledText, } from '@opentrons/components' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { LABWARE_LIQUIDS_STEP } from '../ProtocolSectionsContainer' import { useState } from 'react' import { LabwareModal } from '../LabwareModal' import { ControlledLabwareListItems } from '../../molecules/ControlledLabwareListItems' @@ -31,29 +24,12 @@ export const LIQUIDS_FIELD_NAME = 'liquids' export function LabwareLiquidsSection(): JSX.Element | null { const { t } = useTranslation('create_protocol') - const { - formState: { isValid }, - setValue, - watch, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + const { setValue, watch } = useFormContext() const [displayLabwareModal, setDisplayLabwareModal] = useState(false) const labwares: DisplayLabware[] = watch(LABWARES_FIELD_NAME) ?? [] const liquids: string[] = watch(LIQUIDS_FIELD_NAME) ?? [] - function handleConfirmButtonClick(): void { - const step = - currentStep > LABWARE_LIQUIDS_STEP - ? currentStep - : LABWARE_LIQUIDS_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - return ( - - - - ) } - -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` diff --git a/opentrons-ai-client/src/organisms/LabwareModal/index.tsx b/opentrons-ai-client/src/organisms/LabwareModal/index.tsx index baf83087877..28324b18dee 100644 --- a/opentrons-ai-client/src/organisms/LabwareModal/index.tsx +++ b/opentrons-ai-client/src/organisms/LabwareModal/index.tsx @@ -229,10 +229,18 @@ export function LabwareModal({ setValue( LABWARES_FIELD_NAME, [ - ...selectedLabwares.map(labwareURI => ({ - labwareURI, - count: 1, - })), + ...selectedLabwares.map(labwareURI => { + const existingLabware = labwares.find( + lw => lw.labwareURI === labwareURI + ) + return { + labwareURI, + count: + existingLabware != null + ? existingLabware.count + : 1, + } + }), ], { shouldValidate: true } ) diff --git a/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx b/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx index 16c7046f152..652df2a1b95 100644 --- a/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx +++ b/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx @@ -15,6 +15,8 @@ const TestFormProviderComponent = () => { return ( + +

{`form is ${methods.formState.isValid ? 'valid' : 'invalid'}`}

) } @@ -26,12 +28,11 @@ const render = (): ReturnType => { } describe('ModulesSection', () => { - it('should render modules buttons, no modules added yet, and confirm button', async () => { + it('should render modules buttons and no modules added yet', async () => { render() - expect(screen.getAllByRole('button').length).toBe(5) + expect(screen.getAllByRole('button').length).toBe(4) expect(screen.getByText('No modules added yet')).toBeInTheDocument() - expect(screen.getByText('Confirm')).toBeInTheDocument() }) it('should render a list item with the selected module if user clicks the module button', () => { @@ -71,20 +72,11 @@ describe('ModulesSection', () => { expect(screen.getAllByText('Heater-Shaker Module GEN1').length).toBe(1) }) - it('should disable confirm button when all fields are not filled', async () => { - render() - - const confirmButton = screen.getByRole('button', { name: 'Confirm' }) - await waitFor(() => { - expect(confirmButton).not.toBeEnabled() - }) - }) - - it('should render with Confirm button enabled, modules are not required', async () => { + it('should render with form state valid, modules are not required', async () => { render() await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled() + expect(screen.getByText('form is valid')).toBeInTheDocument() }) }) }) diff --git a/opentrons-ai-client/src/organisms/ModulesSection/index.tsx b/opentrons-ai-client/src/organisms/ModulesSection/index.tsx index 85f068bc226..2531d9aab37 100644 --- a/opentrons-ai-client/src/organisms/ModulesSection/index.tsx +++ b/opentrons-ai-client/src/organisms/ModulesSection/index.tsx @@ -1,18 +1,11 @@ import { DIRECTION_COLUMN, - DISPLAY_FLEX, Flex, InfoScreen, - JUSTIFY_FLEX_END, - LargeButton, SPACING, } from '@opentrons/components' import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { MODULES_STEP } from '../ProtocolSectionsContainer' import { ControlledEmptySelectorButtonGroup } from '../../molecules/ControlledEmptySelectorButtonGroup' import { ModuleListItemGroup } from '../../molecules/ModuleListItemGroup' import type { ModuleType, ModuleModel } from '@opentrons/shared-data' @@ -31,11 +24,7 @@ export const MODULES_FIELD_NAME = 'modules' export function ModulesSection(): JSX.Element | null { const { t } = useTranslation('create_protocol') - const { - formState: { isValid }, - watch, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + const { watch } = useFormContext() const modules: DisplayModules[] = [ { @@ -60,15 +49,6 @@ export function ModulesSection(): JSX.Element | null { }, ] - function handleConfirmButtonClick(): void { - const step = currentStep > MODULES_STEP ? currentStep : MODULES_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - const modulesWatch: DisplayModules[] = watch(MODULES_FIELD_NAME) ?? [] return ( @@ -84,19 +64,6 @@ export function ModulesSection(): JSX.Element | null { )} - - - -
) } - -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` diff --git a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx index 7240a83ae4f..3946d6a5cf5 100644 --- a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx +++ b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx @@ -1,15 +1,23 @@ -import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components' +import { + DIRECTION_COLUMN, + DISPLAY_FLEX, + Flex, + JUSTIFY_FLEX_END, + LargeButton, + SPACING, +} from '@opentrons/components' import { useTranslation } from 'react-i18next' import { Accordion } from '../../molecules/Accordion' import styled from 'styled-components' import { ApplicationSection } from '../../organisms/ApplicationSection' import { createProtocolAtom } from '../../resources/atoms' import { useAtom } from 'jotai' -import { useFormContext } from 'react-hook-form' import { InstrumentsSection } from '../InstrumentsSection' import { ModulesSection } from '../ModulesSection' import { LabwareLiquidsSection } from '../LabwareLiquidsSection' import { StepsSection } from '../StepsSection' +import { useFormContext } from 'react-hook-form' +import { COLUMN } from '@opentrons/shared-data' export const APPLICATION_STEP = 0 export const INSTRUMENTS_STEP = 1 @@ -22,62 +30,83 @@ export function ProtocolSectionsContainer(): JSX.Element | null { const { formState: { isValid }, } = useFormContext() - const [{ currentStep, focusStep }, setCreateProtocolAtom] = useAtom( + const [{ currentSection, focusSection }, setCreateProtocolAtom] = useAtom( createProtocolAtom ) function handleSectionClick(stepNumber: number): void { - currentStep >= stepNumber && + currentSection >= stepNumber && isValid && setCreateProtocolAtom({ - currentStep, - focusStep: stepNumber, + currentSection, + focusSection: stepNumber, }) } function displayCheckmark(stepNumber: number): boolean { - return currentStep > stepNumber && focusStep !== stepNumber + return currentSection > stepNumber && focusSection !== stepNumber + } + + function handleConfirmButtonClick(): void { + const step = + currentSection > focusSection ? currentSection : focusSection + 1 + + setCreateProtocolAtom({ + currentSection: step, + focusSection: step, + }) } return ( {[ { - stepNumber: APPLICATION_STEP, + sectionNumber: APPLICATION_STEP, title: 'application_title', Component: ApplicationSection, }, { - stepNumber: INSTRUMENTS_STEP, + sectionNumber: INSTRUMENTS_STEP, title: 'instruments_title', Component: InstrumentsSection, }, { - stepNumber: MODULES_STEP, + sectionNumber: MODULES_STEP, title: 'modules_title', Component: ModulesSection, }, { - stepNumber: LABWARE_LIQUIDS_STEP, + sectionNumber: LABWARE_LIQUIDS_STEP, title: 'labware_liquids_title', Component: LabwareLiquidsSection, }, { - stepNumber: STEPS_STEP, + sectionNumber: STEPS_STEP, title: 'steps_title', Component: StepsSection, }, - ].map(({ stepNumber, title, Component }) => ( + ].map(({ sectionNumber, title, Component }) => ( { - handleSectionClick(stepNumber) + handleSectionClick(sectionNumber) }} - isCompleted={displayCheckmark(stepNumber)} + isCompleted={displayCheckmark(sectionNumber)} > - {focusStep === stepNumber && } + {focusSection === sectionNumber && ( + + + + + + + )} ))} @@ -89,3 +118,8 @@ const ProtocolSections = styled(Flex)` width: 100%; gap: ${SPACING.spacing16}; ` + +const ButtonContainer = styled.div` + display: ${DISPLAY_FLEX}; + justify-content: ${JUSTIFY_FLEX_END}; +` diff --git a/opentrons-ai-client/src/organisms/StepsSection/__tests__/StepsSection.test.tsx b/opentrons-ai-client/src/organisms/StepsSection/__tests__/StepsSection.test.tsx index 8ef93100bc0..06fc8b30741 100644 --- a/opentrons-ai-client/src/organisms/StepsSection/__tests__/StepsSection.test.tsx +++ b/opentrons-ai-client/src/organisms/StepsSection/__tests__/StepsSection.test.tsx @@ -23,6 +23,8 @@ const TestFormProviderComponent = () => { ) : (

{steps}

)} + +

{`form is ${methods.formState.isValid ? 'valid' : 'invalid'}`}

) } @@ -113,17 +115,17 @@ describe('StepsSection', () => { expect(screen.getAllByText('description test')[1]).toBeInTheDocument() }) - it('should enable the confirm button when steps have been added', async () => { + it('should update form state to valid when steps have been added', async () => { render() - expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled() + expect(screen.getByText('form is invalid')).toBeInTheDocument() fireEvent.change(screen.getByRole('textbox'), { target: { value: 'description test' }, }) await waitFor(() => { - expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled() + expect(screen.getByText('form is valid')).toBeInTheDocument() }) }) }) diff --git a/opentrons-ai-client/src/organisms/StepsSection/index.tsx b/opentrons-ai-client/src/organisms/StepsSection/index.tsx index b44d0985039..f2fe7dd8024 100644 --- a/opentrons-ai-client/src/organisms/StepsSection/index.tsx +++ b/opentrons-ai-client/src/organisms/StepsSection/index.tsx @@ -1,11 +1,8 @@ import { COLORS, DIRECTION_COLUMN, - DISPLAY_FLEX, EmptySelectorButton, Flex, - JUSTIFY_FLEX_END, - LargeButton, SPACING, StyledText, Tabs, @@ -13,9 +10,6 @@ import { import { useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { STEPS_STEP } from '../ProtocolSectionsContainer' import { useState } from 'react' import { COLUMN } from '@opentrons/shared-data' import { ControlledAddTextAreaFields } from '../../molecules/ControlledAddTextAreaFields' @@ -25,25 +19,11 @@ export const STEPS_FIELD_NAME = 'steps' export function StepsSection(): JSX.Element | null { const { t } = useTranslation('create_protocol') - const { - formState: { isValid }, - setValue, - watch, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + const { setValue, watch } = useFormContext() const [isIndividualStep, setIsIndividualStep] = useState(true) const steps = watch(STEPS_FIELD_NAME) ?? [] - function handleConfirmButtonClick(): void { - const step = currentStep > STEPS_STEP ? currentStep : STEPS_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - return ( )} - - - -
) } -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` - const ExampleOrderedList = styled.ol` margin-left: ${SPACING.spacing20}; font-size: 14px; diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx index 9b47ce4c251..4e13e5dfc98 100644 --- a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx +++ b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx @@ -4,7 +4,6 @@ import { DIRECTION_COLUMN, DIRECTION_ROW, Flex, - InputField, JUSTIFY_CENTER, JUSTIFY_END, LargeButton, @@ -28,6 +27,7 @@ import { import { CSSTransition } from 'react-transition-group' import { useAtom } from 'jotai' import { useTrackEvent } from '../../resources/hooks/useTrackEvent' +import { TextAreaField } from '../../atoms/TextAreaField' interface UpdateOptionsDropdown extends DropdownOption { value: UpdateOptions @@ -156,7 +156,7 @@ export function UpdateProtocol(): JSX.Element { setHeaderWithMeterAtom, ]) - const handleInputChange = (event: ChangeEvent): void => { + const handleInputChange = (event: ChangeEvent): void => { setDetailsValue(event.target.value) } @@ -171,7 +171,6 @@ export function UpdateProtocol(): JSX.Element { if (typeof text === 'string' && text !== '') { setErrorText(null) - console.log('File read successfully:\n', text) setPythonTextValue(text) } else { setErrorText(t('file_length_error')) @@ -194,8 +193,6 @@ export function UpdateProtocol(): JSX.Element { const chatPrompt = `${introText}${originalCodeText}${updateTypeText}${detailsText}` - console.log(chatPrompt) - setUpdatePromptAtom({ prompt: chatPrompt, protocol_text: pythonText, @@ -310,10 +307,10 @@ export function UpdateProtocol(): JSX.Element { /> {t('provide_details_of_changes')} - { return () => { @@ -106,8 +108,8 @@ export function CreateProtocol(): JSX.Element | null { methods.reset() setCreateProtocolAtom({ - currentStep: 0, - focusStep: 0, + currentSection: 0, + focusSection: 0, }) } }, []) @@ -138,7 +140,7 @@ export function CreateProtocol(): JSX.Element | null { }, [isResizing]) function calculateProgress(): number { - return currentStep > 0 ? currentStep / TOTAL_STEPS : 0 + return currentSection > 0 ? currentSection / TOTAL_STEPS : 0 } function handleMouseDown( @@ -209,7 +211,7 @@ export function CreateProtocol(): JSX.Element | null {
diff --git a/opentrons-ai-client/src/resources/atoms.ts b/opentrons-ai-client/src/resources/atoms.ts index 01018abfe08..40ddce7fc53 100644 --- a/opentrons-ai-client/src/resources/atoms.ts +++ b/opentrons-ai-client/src/resources/atoms.ts @@ -4,7 +4,7 @@ import type { Chat, ChatData, CreatePrompt, - createProtocolAtomProps, + CreateProtocolAtomProps, HeaderWithMeterAtomProps, Mixpanel, UpdatePrompt, @@ -59,9 +59,9 @@ export const headerWithMeterAtom = atom({ progress: 0, }) -export const createProtocolAtom = atom({ - currentStep: 0, - focusStep: 0, +export const createProtocolAtom = atom({ + currentSection: 0, + focusSection: 0, }) export const displayExitConfirmModalAtom = atom(false) diff --git a/opentrons-ai-client/src/resources/types.ts b/opentrons-ai-client/src/resources/types.ts index 7a84eba1054..516f87e9354 100644 --- a/opentrons-ai-client/src/resources/types.ts +++ b/opentrons-ai-client/src/resources/types.ts @@ -84,9 +84,9 @@ export interface HeaderWithMeterAtomProps { progress: number } -export interface createProtocolAtomProps { - currentStep: number - focusStep: number +export interface CreateProtocolAtomProps { + currentSection: number + focusSection: number } export interface PromptData { diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index 1c5899a7d0e..3b574e11f10 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -95,7 +95,11 @@ export function generatePromptPreviewLabwareLiquidsItems( const defs = getAllDefinitions() labwares?.forEach(labware => { - items.push(getLabwareDisplayName(defs[labware.labwareURI]) as string) + items.push( + `${labware.count} x ${ + getLabwareDisplayName(defs[labware.labwareURI]) as string + }` + ) }) liquids?.forEach(liquid => { From 5f9d2a3d1795a95f60b812bdec34e51d80f88375 Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 14 Nov 2024 09:07:48 -0500 Subject: [PATCH 02/31] feat(api): add monadic error passing to protocol engine (#16788) Let's make an intermediate layer of command execution, inside the command domain so we don't have to move a bunch of types, that calls execution layer functions and returns a new Maybe class that has nice monadic chaining calls like a JS promise or some other burrito-style implementations. As an example, let's use it for overpressure errors in PrepareToAspirate. ## reviews - does this seem worth it? in your mind's eye, imagine doing this for a new `gantry_common.move_to()` that wraps `gantry_mover.move_to()` and returns a stall defined error. ## testing - none, this is a refactor that passes tests with few changes Closes EXEC-830 --- .../protocol_engine/commands/command.py | 246 +++++++++++++++++- .../commands/pipetting_common.py | 51 +++- .../commands/prepare_to_aspirate.py | 68 ++--- .../commands/test_prepare_to_aspirate.py | 7 +- 4 files changed, 326 insertions(+), 46 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index 1fefcbf7315..fe47c9dbbcc 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -6,8 +6,9 @@ import dataclasses from abc import ABC, abstractmethod from datetime import datetime -from enum import Enum +import enum from typing import ( + cast, TYPE_CHECKING, Generic, Optional, @@ -15,6 +16,11 @@ List, Type, Union, + Callable, + Awaitable, + Literal, + Final, + TypeAlias, ) from pydantic import BaseModel, Field @@ -41,7 +47,7 @@ _ErrorT_co = TypeVar("_ErrorT_co", bound=ErrorOccurrence, covariant=True) -class CommandStatus(str, Enum): +class CommandStatus(str, enum.Enum): """Command execution status.""" QUEUED = "queued" @@ -50,7 +56,7 @@ class CommandStatus(str, Enum): FAILED = "failed" -class CommandIntent(str, Enum): +class CommandIntent(str, enum.Enum): """Run intent for a given command. Props: @@ -242,6 +248,240 @@ class BaseCommand( ] +class IsErrorValue(Exception): + """Panic exception if a Maybe contains an Error.""" + + pass + + +class _NothingEnum(enum.Enum): + _NOTHING = enum.auto() + + +NOTHING: Final = _NothingEnum._NOTHING +NothingT: TypeAlias = Literal[_NothingEnum._NOTHING] + + +class _UnknownEnum(enum.Enum): + _UNKNOWN = enum.auto() + + +UNKNOWN: Final = _UnknownEnum._UNKNOWN +UnknownT: TypeAlias = Literal[_UnknownEnum._UNKNOWN] + +_ResultT_co_general = TypeVar("_ResultT_co_general", covariant=True) +_ErrorT_co_general = TypeVar("_ErrorT_co_general", covariant=True) + + +_SecondResultT_co_general = TypeVar("_SecondResultT_co_general", covariant=True) +_SecondErrorT_co_general = TypeVar("_SecondErrorT_co_general", covariant=True) + + +@dataclasses.dataclass +class Maybe(Generic[_ResultT_co_general, _ErrorT_co_general]): + """Represents an possibly completed, possibly errored result. + + By using this class's chaining methods like and_then or or_else, you can build + functions that preserve previous defined errors and augment them or transform them + and transform the results. + + Build objects of this type using from_result or from_error on fully type-qualified + aliases. For instance, + + MyFunctionReturn = Maybe[SuccessData[SomeSuccessModel], DefinedErrorData[SomeErrorKind]] + + def my_function(args...) -> MyFunctionReturn: + try: + do_thing(args...) + except SomeException as e: + return MyFunctionReturn.from_error(ErrorOccurrence.from_error(e)) + else: + return MyFunctionReturn.from_result(SuccessData(SomeSuccessModel(args...))) + + Then, in the calling function, you can react to the results and unwrap to a union: + + OuterMaybe = Maybe[SuccessData[SomeOtherModel], DefinedErrorData[SomeErrors]] + OuterReturn = Union[SuccessData[SomeOtherModel], DefinedErrorData[SomeErrors]] + + def my_calling_function(args...) -> OuterReturn: + def handle_result(result: SuccessData[SomeSuccessModel]) -> OuterMaybe: + return OuterMaybe.from_result(result=some_result_transformer(result)) + return do_thing.and_then(handle_result).unwrap() + """ + + _contents: tuple[_ResultT_co_general, NothingT] | tuple[ + NothingT, _ErrorT_co_general + ] + + _CtorErrorT = TypeVar("_CtorErrorT") + _CtorResultT = TypeVar("_CtorResultT") + + @classmethod + def from_result( + cls: Type[Maybe[_CtorResultT, _CtorErrorT]], result: _CtorResultT + ) -> Maybe[_CtorResultT, _CtorErrorT]: + """Build a Maybe from a valid result.""" + return cls(_contents=(result, NOTHING)) + + @classmethod + def from_error( + cls: Type[Maybe[_CtorResultT, _CtorErrorT]], error: _CtorErrorT + ) -> Maybe[_CtorResultT, _CtorErrorT]: + """Build a Maybe from a known error.""" + return cls(_contents=(NOTHING, error)) + + def result_or_panic(self) -> _ResultT_co_general: + """Unwrap to a result or throw if the Maybe is an error.""" + contents = self._contents + if contents[1] is NOTHING: + # https://github.com/python/mypy/issues/12364 + return cast(_ResultT_co_general, contents[0]) + else: + raise IsErrorValue() + + def unwrap(self) -> _ResultT_co_general | _ErrorT_co_general: + """Unwrap to a union, which is useful for command returns.""" + # https://github.com/python/mypy/issues/12364 + if self._contents[1] is NOTHING: + return cast(_ResultT_co_general, self._contents[0]) + else: + return self._contents[1] + + # note: casts in these methods are because of https://github.com/python/mypy/issues/11730 + def and_then( + self, + functor: Callable[ + [_ResultT_co_general], + Maybe[_SecondResultT_co_general, _SecondErrorT_co_general], + ], + ) -> Maybe[ + _SecondResultT_co_general, _ErrorT_co_general | _SecondErrorT_co_general + ]: + """Conditionally execute functor if the Maybe contains a result. + + Functor should take the result type and return a new Maybe. Since this function returns + a Maybe, it can be chained. The result type will have only the Result type of the Maybe + returned by the functor, but the error type is the union of the error type in the Maybe + returned by the functor and the error type in this Maybe, since the functor may not have + actually been called. + """ + match self._contents: + case (result, _NothingEnum._NOTHING): + return cast( + Maybe[ + _SecondResultT_co_general, + _ErrorT_co_general | _SecondErrorT_co_general, + ], + functor(cast(_ResultT_co_general, result)), + ) + case _: + return cast( + Maybe[ + _SecondResultT_co_general, + _ErrorT_co_general | _SecondErrorT_co_general, + ], + self, + ) + + def or_else( + self, + functor: Callable[ + [_ErrorT_co_general], + Maybe[_SecondResultT_co_general, _SecondErrorT_co_general], + ], + ) -> Maybe[ + _SecondResultT_co_general | _ResultT_co_general, _SecondErrorT_co_general + ]: + """Conditionally execute functor if the Maybe contains an error. + + The functor should take the error type and return a new Maybe. Since this function returns + a Maybe, it can be chained. The result type will have only the Error type of the Maybe + returned by the functor, but the result type is the union of the Result of the Maybe returned + by the functor and the Result of this Maybe, since the functor may not have been called. + """ + match self._contents: + case (_NothingEnum._NOTHING, error): + return cast( + Maybe[ + _ResultT_co_general | _SecondResultT_co_general, + _SecondErrorT_co_general, + ], + functor(cast(_ErrorT_co_general, error)), + ) + case _: + return cast( + Maybe[ + _ResultT_co_general | _SecondResultT_co_general, + _SecondErrorT_co_general, + ], + self, + ) + + async def and_then_async( + self, + functor: Callable[ + [_ResultT_co_general], + Awaitable[Maybe[_SecondResultT_co_general, _SecondErrorT_co_general]], + ], + ) -> Awaitable[ + Maybe[_SecondResultT_co_general, _ErrorT_co_general | _SecondErrorT_co_general] + ]: + """As and_then, but for an async functor.""" + match self._contents: + case (result, _NothingEnum._NOTHING): + return cast( + Awaitable[ + Maybe[ + _SecondResultT_co_general, + _ErrorT_co_general | _SecondErrorT_co_general, + ] + ], + await functor(cast(_ResultT_co_general, result)), + ) + case _: + return cast( + Awaitable[ + Maybe[ + _SecondResultT_co_general, + _ErrorT_co_general | _SecondErrorT_co_general, + ] + ], + self, + ) + + async def or_else_async( + self, + functor: Callable[ + [_ErrorT_co_general], + Awaitable[Maybe[_SecondResultT_co_general, _SecondErrorT_co_general]], + ], + ) -> Awaitable[ + Maybe[_SecondResultT_co_general | _ResultT_co_general, _SecondErrorT_co_general] + ]: + """As or_else, but for an async functor.""" + match self._contents: + case (_NothingEnum._NOTHING, error): + return cast( + Awaitable[ + Maybe[ + _ResultT_co_general | _SecondResultT_co_general, + _SecondErrorT_co_general, + ] + ], + await functor(cast(_ErrorT_co_general, error)), + ) + case _: + return cast( + Awaitable[ + Maybe[ + _ResultT_co_general | _SecondResultT_co_general, + _SecondErrorT_co_general, + ] + ], + self, + ) + + _ExecuteReturnT_co = TypeVar( "_ExecuteReturnT_co", bound=Union[ diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index 2dafb4c81b2..6e0064211fa 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -1,12 +1,20 @@ """Common pipetting command base models.""" +from __future__ import annotations from opentrons_shared_data.errors import ErrorCodes from pydantic import BaseModel, Field -from typing import Literal, Optional, Tuple, TypedDict +from typing import Literal, Optional, Tuple, TypedDict, TYPE_CHECKING from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence +from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +from .command import Maybe, DefinedErrorData, SuccessData +from opentrons.protocol_engine.state.update_types import StateUpdate from ..types import WellLocation, LiquidHandlingWellLocation, DeckPoint +if TYPE_CHECKING: + from ..execution.pipetting import PipettingHandler + from ..resources import ModelUtils + class PipetteIdMixin(BaseModel): """Mixin for command requests that take a pipette ID.""" @@ -201,3 +209,44 @@ class TipPhysicallyAttachedError(ErrorOccurrence): errorCode: str = ErrorCodes.TIP_DROP_FAILED.value.code detail: str = ErrorCodes.TIP_DROP_FAILED.value.detail + + +PrepareForAspirateReturn = Maybe[ + SuccessData[BaseModel], DefinedErrorData[OverpressureError] +] + + +async def prepare_for_aspirate( + pipette_id: str, + pipetting: PipettingHandler, + model_utils: ModelUtils, + location_if_error: ErrorLocationInfo, +) -> PrepareForAspirateReturn: + """Execute pipetting.prepare_for_aspirate, handle errors, and marshal success.""" + state_update = StateUpdate() + try: + await pipetting.prepare_for_aspirate(pipette_id) + except PipetteOverpressureError as e: + state_update.set_fluid_unknown(pipette_id=pipette_id) + return PrepareForAspirateReturn.from_error( + DefinedErrorData( + public=OverpressureError( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + wrappedErrors=[ + ErrorOccurrence.from_failed( + id=model_utils.generate_id(), + createdAt=model_utils.get_timestamp(), + error=e, + ) + ], + errorInfo=location_if_error, + ), + state_update=state_update, + ) + ) + else: + state_update.set_fluid_empty(pipette_id=pipette_id) + return PrepareForAspirateReturn.from_result( + SuccessData(public=BaseModel(), state_update=state_update) + ) diff --git a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py index f5525b3c90e..38f3a60516a 100644 --- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py @@ -1,24 +1,20 @@ """Prepare to aspirate command request, result, and implementation models.""" from __future__ import annotations -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError from pydantic import BaseModel from typing import TYPE_CHECKING, Optional, Type, Union from typing_extensions import Literal -from .pipetting_common import ( - OverpressureError, - PipetteIdMixin, -) +from .pipetting_common import OverpressureError, PipetteIdMixin, prepare_for_aspirate from .command import ( AbstractCommandImpl, BaseCommand, BaseCommandCreate, DefinedErrorData, SuccessData, + Maybe, ) from ..errors.error_occurrence import ErrorOccurrence -from ..state import update_types if TYPE_CHECKING: from ..execution import PipettingHandler, GantryMover @@ -46,6 +42,11 @@ class PrepareToAspirateResult(BaseModel): ] +_ExecuteMaybe = Maybe[ + SuccessData[PrepareToAspirateResult], DefinedErrorData[OverpressureError] +] + + class PrepareToAspirateImplementation( AbstractCommandImpl[PrepareToAspirateParams, _ExecuteReturn] ): @@ -62,44 +63,29 @@ def __init__( self._model_utils = model_utils self._gantry_mover = gantry_mover + def _transform_result(self, result: SuccessData[BaseModel]) -> _ExecuteMaybe: + return _ExecuteMaybe.from_result( + SuccessData( + public=PrepareToAspirateResult(), state_update=result.state_update + ) + ) + async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn: """Prepare the pipette to aspirate.""" current_position = await self._gantry_mover.get_position(params.pipetteId) - state_update = update_types.StateUpdate() - try: - await self._pipetting_handler.prepare_for_aspirate( - pipette_id=params.pipetteId, - ) - except PipetteOverpressureError as e: - state_update.set_fluid_unknown(pipette_id=params.pipetteId) - return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo=( - { - "retryLocation": ( - current_position.x, - current_position.y, - current_position.z, - ) - } - ), - ), - state_update=state_update, - ) - else: - state_update.set_fluid_empty(pipette_id=params.pipetteId) - return SuccessData( - public=PrepareToAspirateResult(), state_update=state_update - ) + prepare_result = await prepare_for_aspirate( + pipette_id=params.pipetteId, + pipetting=self._pipetting_handler, + model_utils=self._model_utils, + location_if_error={ + "retryLocation": ( + current_position.x, + current_position.y, + current_position.z, + ) + }, + ) + return prepare_result.and_then(self._transform_result).unwrap() class PrepareToAspirate( diff --git a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py index 2de35e38332..f9eded1ffa0 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py @@ -34,14 +34,19 @@ def subject( async def test_prepare_to_aspirate_implementation( - decoy: Decoy, subject: PrepareToAspirateImplementation, pipetting: PipettingHandler + decoy: Decoy, + gantry_mover: GantryMover, + subject: PrepareToAspirateImplementation, + pipetting: PipettingHandler, ) -> None: """A PrepareToAspirate command should have an executing implementation.""" data = PrepareToAspirateParams(pipetteId="some id") + position = Point(x=1, y=2, z=3) decoy.when(await pipetting.prepare_for_aspirate(pipette_id="some id")).then_return( None ) + decoy.when(await gantry_mover.get_position("some id")).then_return(position) result = await subject.execute(data) assert result == SuccessData( From 2f016e09eed4ff9db605e518d37b554e024be980 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Thu, 14 Nov 2024 09:09:00 -0500 Subject: [PATCH 03/31] feat(ai-server): integrate google sheets and update deployment (#16808) --- opentrons-ai-server/Makefile | 13 +- opentrons-ai-server/Pipfile | 3 + opentrons-ai-server/Pipfile.lock | 2329 +++++++++-------- opentrons-ai-server/README.md | 54 + opentrons-ai-server/api/handler/fast.py | 69 +- opentrons-ai-server/api/integration/auth.py | 12 +- .../api/integration/google_sheets.py | 75 + .../api/models/error_response.py | 5 + .../api/models/feedback_request.py | 15 + opentrons-ai-server/api/models/user.py | 16 + opentrons-ai-server/api/settings.py | 4 + opentrons-ai-server/deploy.py | 120 +- opentrons-ai-server/tests/helpers/client.py | 15 +- .../tests/test_google_sheets_sanatize.py | 23 + opentrons-ai-server/tests/test_live.py | 31 +- 15 files changed, 1566 insertions(+), 1218 deletions(-) create mode 100644 opentrons-ai-server/api/integration/google_sheets.py create mode 100644 opentrons-ai-server/api/models/error_response.py create mode 100644 opentrons-ai-server/api/models/feedback_request.py create mode 100644 opentrons-ai-server/api/models/user.py create mode 100644 opentrons-ai-server/tests/test_google_sheets_sanatize.py diff --git a/opentrons-ai-server/Makefile b/opentrons-ai-server/Makefile index ecc643d9cd0..2bc7170e931 100644 --- a/opentrons-ai-server/Makefile +++ b/opentrons-ai-server/Makefile @@ -75,6 +75,12 @@ deploy: gen-requirements @echo "Deploying to environment: $(ENV)" python -m pipenv run python deploy.py --env $(ENV) $(if $(TAG),--tag $(TAG),) +.PHONY: dry-deploy +dry-deploy: gen-requirements + @echo "Dry run deploying to environment: $(ENV)" + @echo "Data is retrieved from AWS but no changes are made" + python -m pipenv run python deploy.py --dry --env $(ENV) $(if $(TAG),--tag $(TAG),) + .PHONY: prompted-deploy prompted-deploy: gen-requirements python -m pipenv run python deploy.py @@ -132,4 +138,9 @@ run-shell: .PHONY: shell shell: - docker exec -it $(CONTAINER_NAME) /bin/bash] + docker exec -it $(CONTAINER_NAME) /bin/bash + +.PHONY: test-googlesheet +test-googlesheet: + @echo "Loading environment variables from .env and running test-googlesheet" + pipenv run python -m api.integration.google_sheets diff --git a/opentrons-ai-server/Pipfile b/opentrons-ai-server/Pipfile index 34b0b8d32dd..4586798349a 100644 --- a/opentrons-ai-server/Pipfile +++ b/opentrons-ai-server/Pipfile @@ -17,6 +17,9 @@ beautifulsoup4 = "==4.12.3" markdownify = "==0.13.1" structlog = "==24.4.0" asgi-correlation-id = "==4.3.3" +gspread = "==6.1.4" +google-auth = "==2.36.0" +google-auth-oauthlib = "==1.2.1" [dev-packages] docker = "==7.1.0" diff --git a/opentrons-ai-server/Pipfile.lock b/opentrons-ai-server/Pipfile.lock index 55811db04cf..a4b9ba0dca5 100644 --- a/opentrons-ai-server/Pipfile.lock +++ b/opentrons-ai-server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "20b9e324d809f68cb0465d5e3d98467ceb5860f583fddc347ade1e5ad6a3b6ab" + "sha256": "56aef120fbddf42f146e054b7d59ee0f59be75aa6e43f332f86b7ba8fa2499e0" }, "pipfile-spec": 6, "requires": { @@ -26,100 +26,85 @@ }, "aiohttp": { "hashes": [ - "sha256:02d1d6610588bcd743fae827bd6f2e47e0d09b346f230824b4c6fb85c6065f9c", - "sha256:03690541e4cc866eef79626cfa1ef4dd729c5c1408600c8cb9e12e1137eed6ab", - "sha256:0bc059ecbce835630e635879f5f480a742e130d9821fbe3d2f76610a6698ee25", - "sha256:0c21c82df33b264216abffff9f8370f303dab65d8eee3767efbbd2734363f677", - "sha256:1298b854fd31d0567cbb916091be9d3278168064fca88e70b8468875ef9ff7e7", - "sha256:1321658f12b6caffafdc35cfba6c882cb014af86bef4e78c125e7e794dfb927b", - "sha256:143b0026a9dab07a05ad2dd9e46aa859bffdd6348ddc5967b42161168c24f857", - "sha256:16e6a51d8bc96b77f04a6764b4ad03eeef43baa32014fce71e882bd71302c7e4", - "sha256:172ad884bb61ad31ed7beed8be776eb17e7fb423f1c1be836d5cb357a096bf12", - "sha256:17c272cfe7b07a5bb0c6ad3f234e0c336fb53f3bf17840f66bd77b5815ab3d16", - "sha256:1a0ee6c0d590c917f1b9629371fce5f3d3f22c317aa96fbdcce3260754d7ea21", - "sha256:2746d8994ebca1bdc55a1e998feff4e94222da709623bb18f6e5cfec8ec01baf", - "sha256:2914caa46054f3b5ff910468d686742ff8cff54b8a67319d75f5d5945fd0a13d", - "sha256:2bbf94d4a0447705b7775417ca8bb8086cc5482023a6e17cdc8f96d0b1b5aba6", - "sha256:2bd9f3eac515c16c4360a6a00c38119333901b8590fe93c3257a9b536026594d", - "sha256:2c33fa6e10bb7ed262e3ff03cc69d52869514f16558db0626a7c5c61dde3c29f", - "sha256:2d37f4718002863b82c6f391c8efd4d3a817da37030a29e2682a94d2716209de", - "sha256:3668d0c2a4d23fb136a753eba42caa2c0abbd3d9c5c87ee150a716a16c6deec1", - "sha256:36d4fba838be5f083f5490ddd281813b44d69685db910907636bc5dca6322316", - "sha256:40ff5b7660f903dc587ed36ef08a88d46840182d9d4b5694e7607877ced698a1", - "sha256:42775de0ca04f90c10c5c46291535ec08e9bcc4756f1b48f02a0657febe89b10", - "sha256:482c85cf3d429844396d939b22bc2a03849cb9ad33344689ad1c85697bcba33a", - "sha256:4e6cb75f8ddd9c2132d00bc03c9716add57f4beff1263463724f6398b813e7eb", - "sha256:4edc3fd701e2b9a0d605a7b23d3de4ad23137d23fc0dbab726aa71d92f11aaaf", - "sha256:4fd16b30567c5b8e167923be6e027eeae0f20cf2b8a26b98a25115f28ad48ee0", - "sha256:5002a02c17fcfd796d20bac719981d2fca9c006aac0797eb8f430a58e9d12431", - "sha256:51d0a4901b27272ae54e42067bc4b9a90e619a690b4dc43ea5950eb3070afc32", - "sha256:558b3d223fd631ad134d89adea876e7fdb4c93c849ef195049c063ada82b7d08", - "sha256:5c070430fda1a550a1c3a4c2d7281d3b8cfc0c6715f616e40e3332201a253067", - "sha256:5f392ef50e22c31fa49b5a46af7f983fa3f118f3eccb8522063bee8bfa6755f8", - "sha256:60555211a006d26e1a389222e3fab8cd379f28e0fbf7472ee55b16c6c529e3a6", - "sha256:608cecd8d58d285bfd52dbca5b6251ca8d6ea567022c8a0eaae03c2589cd9af9", - "sha256:60ad5b8a7452c0f5645c73d4dad7490afd6119d453d302cd5b72b678a85d6044", - "sha256:63649309da83277f06a15bbdc2a54fbe75efb92caa2c25bb57ca37762789c746", - "sha256:6ebdc3b3714afe1b134b3bbeb5f745eed3ecbcff92ab25d80e4ef299e83a5465", - "sha256:6f3c6648aa123bcd73d6f26607d59967b607b0da8ffcc27d418a4b59f4c98c7c", - "sha256:7003f33f5f7da1eb02f0446b0f8d2ccf57d253ca6c2e7a5732d25889da82b517", - "sha256:776e9f3c9b377fcf097c4a04b241b15691e6662d850168642ff976780609303c", - "sha256:85711eec2d875cd88c7eb40e734c4ca6d9ae477d6f26bd2b5bb4f7f60e41b156", - "sha256:87d1e4185c5d7187684d41ebb50c9aeaaaa06ca1875f4c57593071b0409d2444", - "sha256:8a3f063b41cc06e8d0b3fcbbfc9c05b7420f41287e0cd4f75ce0a1f3d80729e6", - "sha256:8b3fb28a9ac8f2558760d8e637dbf27aef1e8b7f1d221e8669a1074d1a266bb2", - "sha256:8bd9125dd0cc8ebd84bff2be64b10fdba7dc6fd7be431b5eaf67723557de3a31", - "sha256:8be1a65487bdfc285bd5e9baf3208c2132ca92a9b4020e9f27df1b16fab998a9", - "sha256:8cc0d13b4e3b1362d424ce3f4e8c79e1f7247a00d792823ffd640878abf28e56", - "sha256:8d9d10d10ec27c0d46ddaecc3c5598c4db9ce4e6398ca872cdde0525765caa2f", - "sha256:8debb45545ad95b58cc16c3c1cc19ad82cffcb106db12b437885dbee265f0ab5", - "sha256:91aa966858593f64c8a65cdefa3d6dc8fe3c2768b159da84c1ddbbb2c01ab4ef", - "sha256:9331dd34145ff105177855017920dde140b447049cd62bb589de320fd6ddd582", - "sha256:99f9678bf0e2b1b695e8028fedac24ab6770937932eda695815d5a6618c37e04", - "sha256:9fdf5c839bf95fc67be5794c780419edb0dbef776edcfc6c2e5e2ffd5ee755fa", - "sha256:a14e4b672c257a6b94fe934ee62666bacbc8e45b7876f9dd9502d0f0fe69db16", - "sha256:a19caae0d670771ea7854ca30df76f676eb47e0fd9b2ee4392d44708f272122d", - "sha256:a35ed3d03910785f7d9d6f5381f0c24002b2b888b298e6f941b2fc94c5055fcd", - "sha256:a61df62966ce6507aafab24e124e0c3a1cfbe23c59732987fc0fd0d71daa0b88", - "sha256:a6e00c8a92e7663ed2be6fcc08a2997ff06ce73c8080cd0df10cc0321a3168d7", - "sha256:ac3196952c673822ebed8871cf8802e17254fff2a2ed4835d9c045d9b88c5ec7", - "sha256:ac74e794e3aee92ae8f571bfeaa103a141e409863a100ab63a253b1c53b707eb", - "sha256:ad3675c126f2a95bde637d162f8231cff6bc0bc9fbe31bd78075f9ff7921e322", - "sha256:aeebd3061f6f1747c011e1d0b0b5f04f9f54ad1a2ca183e687e7277bef2e0da2", - "sha256:ba1a599255ad6a41022e261e31bc2f6f9355a419575b391f9655c4d9e5df5ff5", - "sha256:bbdb8def5268f3f9cd753a265756f49228a20ed14a480d151df727808b4531dd", - "sha256:c2555e4949c8d8782f18ef20e9d39730d2656e218a6f1a21a4c4c0b56546a02e", - "sha256:c2695c61cf53a5d4345a43d689f37fc0f6d3a2dc520660aec27ec0f06288d1f9", - "sha256:c2b627d3c8982691b06d89d31093cee158c30629fdfebe705a91814d49b554f8", - "sha256:c46131c6112b534b178d4e002abe450a0a29840b61413ac25243f1291613806a", - "sha256:c54dc329cd44f7f7883a9f4baaefe686e8b9662e2c6c184ea15cceee587d8d69", - "sha256:c7d7cafc11d70fdd8801abfc2ff276744ae4cb39d8060b6b542c7e44e5f2cfc2", - "sha256:cb0b2d5d51f96b6cc19e6ab46a7b684be23240426ae951dcdac9639ab111b45e", - "sha256:d15a29424e96fad56dc2f3abed10a89c50c099f97d2416520c7a543e8fddf066", - "sha256:d1f5c9169e26db6a61276008582d945405b8316aae2bb198220466e68114a0f5", - "sha256:d271f770b52e32236d945911b2082f9318e90ff835d45224fa9e28374303f729", - "sha256:d646fdd74c25bbdd4a055414f0fe32896c400f38ffbdfc78c68e62812a9e0257", - "sha256:d6e395c3d1f773cf0651cd3559e25182eb0c03a2777b53b4575d8adc1149c6e9", - "sha256:d7c071235a47d407b0e93aa6262b49422dbe48d7d8566e1158fecc91043dd948", - "sha256:d97273a52d7f89a75b11ec386f786d3da7723d7efae3034b4dda79f6f093edc1", - "sha256:dcf354661f54e6a49193d0b5653a1b011ba856e0b7a76bda2c33e4c6892f34ea", - "sha256:e3e7fabedb3fe06933f47f1538df7b3a8d78e13d7167195f51ca47ee12690373", - "sha256:e525b69ee8a92c146ae5b4da9ecd15e518df4d40003b01b454ad694a27f498b5", - "sha256:e709d6ac598c5416f879bb1bae3fd751366120ac3fa235a01de763537385d036", - "sha256:e83dfefb4f7d285c2d6a07a22268344a97d61579b3e0dce482a5be0251d672ab", - "sha256:e86260b76786c28acf0b5fe31c8dca4c2add95098c709b11e8c35b424ebd4f5b", - "sha256:e883b61b75ca6efc2541fcd52a5c8ccfe288b24d97e20ac08fdf343b8ac672ea", - "sha256:f0a44bb40b6aaa4fb9a5c1ee07880570ecda2065433a96ccff409c9c20c1624a", - "sha256:f82ace0ec57c94aaf5b0e118d4366cff5889097412c75aa14b4fd5fc0c44ee3e", - "sha256:f9ca09414003c0e96a735daa1f071f7d7ed06962ef4fa29ceb6c80d06696d900", - "sha256:fa430b871220dc62572cef9c69b41e0d70fcb9d486a4a207a5de4c1f25d82593", - "sha256:fc262c3df78c8ff6020c782d9ce02e4bcffe4900ad71c0ecdad59943cba54442", - "sha256:fcd546782d03181b0b1d20b43d612429a90a68779659ba8045114b867971ab71", - "sha256:fd4ceeae2fb8cabdd1b71c82bfdd39662473d3433ec95b962200e9e752fb70d0", - "sha256:fec5fac7aea6c060f317f07494961236434928e6f4374e170ef50b3001e14581" - ], - "markers": "python_version >= '3.8'", - "version": "==3.10.9" + "sha256:024409c1b1d6076d0ed933dcebd7e4fc6f3320a227bfa0c1b6b93a8b5a146f04", + "sha256:04b24497b3baf15035730de5f207ade88a67d4483a5f16ced7ece348933a5b47", + "sha256:08474e71772a516ba2e2167b4707af8361d2c452b3d8a5364c984f4867869499", + "sha256:0e7a0762cc29cd3acd01a4d2b547b3af7956ad230ebb80b529a8e4f3e4740fe8", + "sha256:104deb7873681273c5daa13c41924693df394043a118dae90387d35bc5531788", + "sha256:104ea21994b1403e4c1b398866f1187c1694fa291314ad7216ec1d8ec6b49f38", + "sha256:113bf06b029143e94a47c4f36e11a8b7e396e9d1f1fc8cea58e6b7e370cfed38", + "sha256:12071dd2cc95ba81e0f2737bebcb98b2a8656015e87772e84e8fb9e635b5da6e", + "sha256:170fb2324826bb9f08055a8291f42192ae5ee2f25b2966c8f0f4537c61d73a7b", + "sha256:21b4545e8d96870da9652930c5198366605ff8f982757030e2148cf341e5746b", + "sha256:229ae13959a5f499d90ffbb4b9eac2255d8599315027d6f7c22fa9803a94d5b1", + "sha256:2ec5efbc872b00ddd85e3904059d274f284cff314e13f48776050ca2c58f451d", + "sha256:31b91ff3a1fcb206a1fa76e0de1f08c9ffb1dc0deb7296fa2618adfe380fc676", + "sha256:329f5059e0bf6983dceebac8e6ed20e75eaff6163b3414f4a4cb59e0d7037672", + "sha256:37f8cf3c43f292d9bb3e6760476c2b55b9663a581fad682a586a410c43a7683e", + "sha256:3e1ed8d152cccceffb1ee7a2ac227c16372e453fb11b3aeaa56783049b85d3f6", + "sha256:3ed360d6672a9423aad39902a4e9fe305464d20ed7931dbdba30a4625782d875", + "sha256:40dc9446cff326672fcbf93efdb8ef7e949824de1097624efe4f61ac7f0d2c43", + "sha256:4d218d3eca40196384ad3b481309c56fd60e664128885d1734da0a8aa530d433", + "sha256:4e4e155968040e32c124a89852a1a5426d0e920a35f4331e1b3949037bfe93a3", + "sha256:4f698aa61879df64425191d41213dfd99efdc1627e6398e6d7aa5c312fac9702", + "sha256:508cfcc99534b1282595357592d8367b44392b21f6eb5d4dc021f8d0d809e94d", + "sha256:577c7429f8869fa30186fc2c9eee64d75a30b51b61f26aac9725866ae5985cfd", + "sha256:57e17c6d71f2dc857a8a1d09be1be7802e35d90fb4ba4b06cf1aab6414a57894", + "sha256:5ecc2fb1a0a9d48cf773add34196cddf7e488e48e9596e090849751bf43098f4", + "sha256:600b1d9f86a130131915e2f2127664311b33902c486b21a747d626f5144b4471", + "sha256:62502b8ffee8c6a4b5c6bf99d1de277d42bf51b2fb713975d9b63b560150b7ac", + "sha256:62a2f5268b672087c45b33479ba1bb1d5a48c6d76c133cfce3a4f77410c200d1", + "sha256:6362f50a6f0e5482c4330d2151cb682779230683da0e155c15ec9fc58cb50b6a", + "sha256:6533dd06df3d17d1756829b68b365b1583929b54082db8f65083a4184bf68322", + "sha256:6c5a6958f4366496004cf503d847093d464814543f157ef3b738bbf604232415", + "sha256:72cd984f7f14e8c01b3e38f18f39ea85dba84e52ea05e37116ba5e2a72eef396", + "sha256:76d6ee8bb132f8ee0fcb0e205b4708ddb6fba524eb515ee168113063d825131b", + "sha256:7867d0808614f04e78e0a8d5a2c1f8ac6bc626a0c0e2f62be48be6b749e2f8b2", + "sha256:7d664e5f937c08adb7908ea9f391fbf2928a9b09cb412ac0aba602bde9e499e4", + "sha256:85ae6f182be72c3531915e90625cc65afce4df8a0fc4988bd52d8a5d5faaeb68", + "sha256:89a96a0696dc67d548f69cb518c581a7a33cc1f26ab42229dea1709217c9d926", + "sha256:8b323b5d3aef7dd811424c269322eec58a977c0c8152e650159e47210d900504", + "sha256:8c47a0ba6c2b3d3e5715f8338d657badd21f778c6be16701922c65521c5ecfc9", + "sha256:8fef105113d56e817cb9bcc609667ee461321413a7b972b03f5b4939f40f307c", + "sha256:900ff74d78eb580ae4aa5883242893b123a0c442a46570902500f08d6a7e6696", + "sha256:9095580806d9ed07c0c29b23364a0b1fb78258ef9f4bddf7e55bac0e475d4edf", + "sha256:91d3991fad8b65e5dbc13cd95669ea689fe0a96ff63e4e64ac24ed724e4f8103", + "sha256:9231d610754724273a6ac05a1f177979490bfa6f84d49646df3928af2e88cfd5", + "sha256:97056d3422594e0787733ac4c45bef58722d452f4dc6615fee42f59fe51707dd", + "sha256:a896059b6937d1a22d8ee8377cdcd097bd26cd8c653b8f972051488b9baadee9", + "sha256:aabc4e92cb153636d6be54e84dad1b252ddb9aebe077942b6dcffe5e468d476a", + "sha256:ad14cdc0fba4df31c0f6e06c21928c5b924725cbf60d0ccc5f6e7132636250e9", + "sha256:ae36ae52b0c22fb69fb8b744eff82a20db512a29eafc6e3a4ab43b17215b219d", + "sha256:b3e4fb7f5354d39490d8209aefdf5830b208d01c7293a2164e404312c3d8bc55", + "sha256:b40c304ab01e89ad0aeeecf91bbaa6ae3b00e27b796c9e8d50b71a4a7e885cc8", + "sha256:b7349205bb163318dcc102329d30be59a647a3d24c82c3d91ed35b7e7301ea7e", + "sha256:b8b95a63a8e8b5f0464bd8b1b0d59d2bec98a59b6aacc71e9be23df6989b3dfb", + "sha256:bb2e82e515e268b965424ecabebd91834a41b36260b6ef5db015ee12ddb28ef3", + "sha256:c0315978b2a4569e03fb59100f6a7e7d23f718a4521491f5c13d946d37549f3d", + "sha256:c1828e10c3a49e2b234b87600ecb68a92b8a8dcf8b99bca9447f16c4baaa1630", + "sha256:c1c49bc393d854d4421ebc174a0a41f9261f50d3694d8ca277146cbbcfd24ee7", + "sha256:c415b9601ff50709d6050c8a9281733a9b042b9e589265ac40305b875cf9c463", + "sha256:c54c635d1f52490cde7ef3a423645167a8284e452a35405d5c7dc1242a8e75c9", + "sha256:c5e6a1f8b0268ffa1c84d7c3558724956002ba8361176e76406233e704bbcffb", + "sha256:c98a596ac20e8980cc6f34c0c92a113e98eb08f3997c150064d26d2aeb043e5a", + "sha256:cd0834e4260eab78671b81d34f110fbaac449563e48d419cec0030d9a8e58693", + "sha256:cdad66685fcf2ad14ce522cf849d4a025f4fd206d6cfc3f403d9873e4c243b03", + "sha256:d1ea006426edf7e1299c52a58b0443158012f7a56fed3515164b60bfcb1503a9", + "sha256:d33b4490026968bdc7f0729b9d87a3a6b1e09043557d2fc1c605c6072deb2f11", + "sha256:d5cae4cd271e20b7ab757e966cc919186b9f02535418ab36c471a5377ef4deaa", + "sha256:dd505a1121ad5b666191840b7bd1d8cb917df2647deeca6f3474331b72452362", + "sha256:e1668ef2f3a7ec9881f4b6a917e5f97c87a343fa6b0d5fc826b7b0297ddd0887", + "sha256:e7bcfcede95531589295f56e924702cef7f9685c9e4e5407592e04ded6a65bf3", + "sha256:ebf610c37df4f09c71c9bbf8309b4b459107e6fe889ac0d7e16f6e4ebd975f86", + "sha256:f3bf5c132eb48002bcc3825702d241d35b4e9585009e65e9dcf9c4635d0b7424", + "sha256:f40380c96dd407dfa84eb2d264e68aa47717b53bdbe210a59cc3c35a4635f195", + "sha256:f57a0de48dda792629e7952d34a0c7b81ea336bb9b721391c7c58145b237fe55", + "sha256:f6b925c7775ab857bdc1e52e1f5abcae7d18751c09b751aeb641a5276d9b990e", + "sha256:f8f0d79b923070f25674e4ea8f3d61c9d89d24d9598d50ff32c5b9b23c79a25b", + "sha256:feca9fafa4385aea6759c171cd25ea82f7375312fca04178dae35331be45e538" + ], + "markers": "python_version >= '3.9'", + "version": "==3.11.0" }, "aiosignal": { "hashes": [ @@ -139,11 +124,11 @@ }, "anyio": { "hashes": [ - "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", - "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a" + "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", + "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d" ], "markers": "python_version >= '3.9'", - "version": "==4.6.0" + "version": "==4.6.2.post1" }, "asgi-correlation-id": { "hashes": [ @@ -173,11 +158,19 @@ }, "bytecode": { "hashes": [ - "sha256:0a1dc340cac823cff605609b8b214f7f9bf80418c6b9e0fc8c6db1793c27137d", - "sha256:7263239a8d3f70fc7c303862b20cd2c6788052e37ce0a26e67309d280e985984" + "sha256:06676a3c3bccc9d3dc73ee625650ea57df2bc117358826f4f290f0e1faa42292", + "sha256:76080b7c0eb9e7e17f961d61fd06e933aa47f3b753770a3249537439d8203a25" ], "markers": "python_version >= '3.12'", - "version": "==0.15.1" + "version": "==0.16.0" + }, + "cachetools": { + "hashes": [ + "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", + "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a" + ], + "markers": "python_version >= '3.7'", + "version": "==5.5.0" }, "cattrs": { "hashes": [ @@ -270,99 +263,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": [ @@ -374,35 +382,35 @@ }, "cryptography": { "hashes": [ - "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" - ], - "version": "==43.0.1" + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" + ], + "version": "==43.0.3" }, "dataclasses-json": { "hashes": [ @@ -528,11 +536,11 @@ }, "envier": { "hashes": [ - "sha256:4e7e398cb09a8dd360508ef7e12511a152355426d2544b8487a34dad27cc20ad", - "sha256:65099cf3aa9b3b3b4b92db2f7d29e2910672e085b76f7e587d2167561a834add" + "sha256:3309a01bb3d8850c9e7a31a5166d5a836846db2faecb79b9cb32654dd50ca9f9", + "sha256:73609040a76be48bbcb97074d9969666484aa0de706183a6e9ef773156a8a6a9" ], "markers": "python_version >= '3.7'", - "version": "==0.5.2" + "version": "==0.6.1" }, "fastapi": { "hashes": [ @@ -553,94 +561,127 @@ }, "frozenlist": { "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.1" + "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", + "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", + "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", + "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", + "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", + "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", + "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", + "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", + "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", + "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", + "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", + "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", + "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c", + "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", + "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", + "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", + "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", + "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", + "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10", + "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", + "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", + "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", + "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", + "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10", + "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", + "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", + "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", + "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", + "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", + "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923", + "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", + "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", + "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", + "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", + "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", + "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", + "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", + "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", + "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", + "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", + "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", + "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", + "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", + "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", + "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", + "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604", + "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", + "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", + "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", + "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", + "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", + "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", + "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", + "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", + "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3", + "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", + "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", + "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", + "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf", + "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", + "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", + "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171", + "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", + "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", + "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", + "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", + "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", + "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", + "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9", + "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", + "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723", + "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", + "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", + "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99", + "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e", + "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", + "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", + "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", + "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", + "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", + "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca", + "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", + "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", + "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", + "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", + "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307", + "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e", + "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", + "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", + "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", + "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", + "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" }, "fsspec": { "hashes": [ - "sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8", - "sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b" + "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", + "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493" ], "markers": "python_version >= '3.8'", - "version": "==2024.9.0" + "version": "==2024.10.0" + }, + "google-auth": { + "hashes": [ + "sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb", + "sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.36.0" + }, + "google-auth-oauthlib": { + "hashes": [ + "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f", + "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==1.2.1" }, "greenlet": { "hashes": [ @@ -721,6 +762,15 @@ "markers": "python_version < '3.13' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==3.1.1" }, + "gspread": { + "hashes": [ + "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de", + "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.1.4" + }, "h11": { "hashes": [ "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", @@ -739,44 +789,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" - ], - "version": "==0.6.1" + "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", + "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd", + "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", + "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", + "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", + "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", + "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", + "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", + "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", + "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", + "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", + "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff", + "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", + "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", + "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", + "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", + "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", + "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9", + "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", + "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", + "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003", + "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", + "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc", + "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076", + "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490", + "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", + "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6", + "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", + "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", + "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547", + "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba", + "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440", + "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", + "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab", + "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", + "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", + "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", + "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f", + "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", + "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", + "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", + "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", + "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43" + ], + "version": "==0.6.4" }, "httpx": { "hashes": [ @@ -797,11 +854,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", - "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5" + "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", + "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7" ], "markers": "python_version >= '3.8'", - "version": "==8.4.0" + "version": "==8.5.0" }, "jinja2": { "hashes": [ @@ -878,11 +935,11 @@ }, "llama-index-legacy": { "hashes": [ - "sha256:04221320d84d96ba9ee3e21e5055bd8527cbd769e8f1c60cf0368ed907e012a2", - "sha256:f6969f1085efb0abebd6367e46f3512020f3f6b9c086f458a519830dd61e8206" + "sha256:4b817d7c343fb5f7f00c4410eff519f320013b8d5f24c4fedcf270c471f92038", + "sha256:f8a9764e7e134a52bfef5e53d2d62561bfc01fc09874c51cc001df6f5302ae30" ], "markers": "python_version < '4.0' and python_full_version >= '3.8.1'", - "version": "==0.9.48.post3" + "version": "==0.9.48.post4" }, "llama-index-llms-openai": { "hashes": [ @@ -958,78 +1015,78 @@ }, "markupsafe": { "hashes": [ - "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" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], "markers": "python_version >= '3.9'", - "version": "==3.0.1" + "version": "==3.0.2" }, "marshmallow": { "hashes": [ - "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e", - "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9" + "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468", + "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491" ], - "markers": "python_version >= '3.8'", - "version": "==3.22.0" + "markers": "python_version >= '3.9'", + "version": "==3.23.1" }, "mdurl": { "hashes": [ @@ -1155,11 +1212,11 @@ }, "networkx": { "hashes": [ - "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9", - "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2" + "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", + "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f" ], "markers": "python_version >= '3.10'", - "version": "==3.3" + "version": "==3.4.2" }, "nltk": { "hashes": [ @@ -1211,6 +1268,14 @@ "markers": "python_version >= '3.9'", "version": "==1.26.4" }, + "oauthlib": { + "hashes": [ + "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", + "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918" + ], + "markers": "python_version >= '3.6'", + "version": "==3.2.2" + }, "openai": { "hashes": [ "sha256:aa2f381f476f5fa4df8728a34a3e454c321caa064b7b68ab6e9daa1ed082dbf9", @@ -1222,82 +1287,83 @@ }, "opentelemetry-api": { "hashes": [ - "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7", - "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342" + "sha256:6fa7295a12c707f5aebef82da3d9ec5afe6992f3e42bfe7bec0339a44b3518e7", + "sha256:bfe86c95576cf19a914497f439fd79c9553a38de0adbdc26f7cfc46b0c00b16c" ], "markers": "python_version >= '3.8'", - "version": "==1.27.0" + "version": "==1.28.1" }, "orjson": { "hashes": [ - "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23", - "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9", - "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5", - "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad", - "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98", - "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412", - "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1", - "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864", - "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6", - "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91", - "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac", - "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c", - "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1", - "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f", - "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250", - "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09", - "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0", - "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225", - "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354", - "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f", - "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e", - "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469", - "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c", - "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12", - "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3", - "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3", - "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149", - "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb", - "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2", - "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2", - "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f", - "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0", - "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a", - "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58", - "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe", - "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09", - "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e", - "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2", - "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c", - "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313", - "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6", - "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93", - "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7", - "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866", - "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c", - "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b", - "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5", - "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175", - "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9", - "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0", - "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff", - "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20", - "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5", - "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960", - "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024", - "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd", - "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84" - ], - "markers": "python_version >= '3.8'", - "version": "==3.10.7" + "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", + "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", + "sha256:10f416b2a017c8bd17f325fb9dee1fb5cdd7a54e814284896b7c3f2763faa017", + "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", + "sha256:1789d9db7968d805f3d94aae2c25d04014aae3a2fa65b1443117cd462c6da647", + "sha256:19b3763e8bbf8ad797df6b6b5e0fc7c843ec2e2fc0621398534e0c6400098f87", + "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", + "sha256:1be83a13312e5e58d633580c5eb8d0495ae61f180da2722f20562974188af205", + "sha256:1f39728c7f7d766f1f5a769ce4d54b5aaa4c3f92d5b84817053cc9995b977acc", + "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", + "sha256:461311b693d3d0a060439aa669c74f3603264d4e7a08faa68c47ae5a863f352d", + "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", + "sha256:4bfb30c891b530f3f80e801e3ad82ef150b964e5c38e1fb8482441c69c35c61c", + "sha256:4d83f87582d223e54efb2242a79547611ba4ebae3af8bae1e80fa9a0af83bb7f", + "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", + "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", + "sha256:52ca832f17d86a78cbab86cdc25f8c13756ebe182b6fc1a97d534051c18a08de", + "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", + "sha256:5576b1e5a53a5ba8f8df81872bb0878a112b3ebb1d392155f00f54dd86c83ff6", + "sha256:63fc9d5fe1d4e8868f6aae547a7b8ba0a2e592929245fff61d633f4caccdcdd6", + "sha256:655a493bac606655db9a47fe94d3d84fc7f3ad766d894197c94ccf0c5408e7d3", + "sha256:65cd3e3bb4fbb4eddc3c1e8dce10dc0b73e808fcb875f9fab40c81903dd9323e", + "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", + "sha256:6dade64687f2bd7c090281652fe18f1151292d567a9302b34c2dbb92a3872f1f", + "sha256:6f67c570602300c4befbda12d153113b8974a3340fdcf3d6de095ede86c06d92", + "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", + "sha256:77b0fed6f209d76c1c39f032a70df2d7acf24b1812ca3e6078fd04e8972685a3", + "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", + "sha256:80c00d4acded0c51c98754fe8218cb49cb854f0f7eb39ea4641b7f71732d2cb7", + "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", + "sha256:82f07c550a6ccd2b9290849b22316a609023ed851a87ea888c0456485a7d196a", + "sha256:86b9dd983857970c29e4c71bb3e95ff085c07d3e83e7c46ebe959bac07ebd80b", + "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", + "sha256:96ed1de70fcb15d5fed529a656df29f768187628727ee2788344e8a51e1c1350", + "sha256:9fd0ad1c129bc9beb1154c2655f177620b5beaf9a11e0d10bac63ef3fce96950", + "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", + "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", + "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", + "sha256:afacfd1ab81f46dedd7f6001b6d4e8de23396e4884cd3c3436bd05defb1a6446", + "sha256:b592597fe551d518f42c5a2eb07422eb475aa8cfdc8c51e6da7054b836b26782", + "sha256:b7fcfc6f7ca046383fb954ba528587e0f9336828b568282b27579c49f8e16aad", + "sha256:b9546b278c9fb5d45380f4809e11b4dd9844ca7aaf1134024503e134ed226161", + "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", + "sha256:bd9a187742d3ead9df2e49240234d728c67c356516cf4db018833a86f20ec18c", + "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", + "sha256:c95f2ecafe709b4e5c733b5e2768ac569bed308623c85806c395d9cca00e08af", + "sha256:cb4d0bea56bba596723d73f074c420aec3b2e5d7d30698bc56e6048066bd560c", + "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", + "sha256:d496c74fc2b61341e3cefda7eec21b7854c5f672ee350bc55d9a4997a8a95204", + "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", + "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", + "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", + "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", + "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", + "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", + "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", + "sha256:f4c57ea78a753812f528178aa2f1c57da633754c91d2124cb28991dab4c79a54", + "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd" + ], + "markers": "python_version >= '3.8'", + "version": "==3.10.11" }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pandas": { "hashes": [ @@ -1349,89 +1415,84 @@ }, "pillow": { "hashes": [ - "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", - "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", - "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", - "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", - "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", - "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", - "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", - "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", - "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", - "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", - "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", - "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", - "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", - "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", - "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", - "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", - "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", - "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", - "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", - "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", - "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", - "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", - "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", - "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", - "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", - "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", - "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", - "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", - "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", - "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", - "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", - "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", - "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", - "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", - "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", - "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", - "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", - "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", - "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", - "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", - "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", - "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", - "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", - "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", - "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", - "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", - "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", - "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", - "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", - "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", - "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", - "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", - "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", - "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", - "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", - "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", - "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", - "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", - "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", - "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", - "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", - "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", - "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", - "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", - "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", - "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", - "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", - "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", - "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", - "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", - "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", - "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", - "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", - "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", - "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", - "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", - "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", - "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", - "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", - "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" - ], - "markers": "python_version >= '3.8'", - "version": "==10.4.0" + "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", + "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", + "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", + "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", + "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", + "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", + "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", + "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", + "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", + "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", + "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", + "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", + "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", + "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", + "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", + "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", + "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", + "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", + "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", + "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", + "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", + "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", + "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", + "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", + "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", + "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", + "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", + "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", + "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", + "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", + "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", + "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", + "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", + "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", + "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", + "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", + "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", + "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", + "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", + "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", + "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", + "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", + "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", + "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", + "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", + "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", + "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", + "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", + "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", + "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", + "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", + "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", + "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", + "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", + "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", + "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", + "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", + "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", + "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", + "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", + "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", + "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", + "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", + "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", + "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", + "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", + "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", + "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", + "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", + "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", + "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", + "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", + "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", + "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", + "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" + ], + "markers": "python_version >= '3.9'", + "version": "==11.0.0" }, "propcache": { "hashes": [ @@ -1539,20 +1600,36 @@ }, "protobuf": { "hashes": [ - "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132", - "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f", - "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece", - "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0", - "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f", - "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0", - "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276", - "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7", - "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3", - "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36", - "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d" + "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24", + "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535", + "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b", + "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548", + "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584", + "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b", + "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36", + "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135", + "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868", + "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687", + "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed" + ], + "markers": "python_version >= '3.8'", + "version": "==5.28.3" + }, + "pyasn1": { + "hashes": [ + "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", + "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.1" + }, + "pyasn1-modules": { + "hashes": [ + "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", + "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", - "version": "==5.28.2" + "version": "==0.4.1" }, "pycparser": { "hashes": [ @@ -1711,11 +1788,11 @@ }, "python-multipart": { "hashes": [ - "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb", - "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf" + "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d", + "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538" ], "markers": "python_version >= '3.8'", - "version": "==0.0.12" + "version": "==0.0.17" }, "pytz": { "hashes": [ @@ -1785,103 +1862,103 @@ }, "regex": { "hashes": [ - "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623", - "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199", - "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664", - "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f", - "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca", - "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066", - "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca", - "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39", - "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d", - "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6", - "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35", - "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408", - "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5", - "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a", - "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9", - "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92", - "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766", - "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168", - "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca", - "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508", - "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df", - "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf", - "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b", - "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4", - "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268", - "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6", - "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c", - "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62", - "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231", - "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36", - "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba", - "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4", - "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e", - "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822", - "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4", - "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d", - "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71", - "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50", - "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d", - "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad", - "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8", - "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8", - "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8", - "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd", - "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16", - "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664", - "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a", - "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f", - "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd", - "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a", - "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9", - "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199", - "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d", - "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963", - "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009", - "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a", - "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679", - "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96", - "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42", - "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8", - "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e", - "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7", - "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8", - "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802", - "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366", - "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137", - "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784", - "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29", - "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3", - "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771", - "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60", - "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a", - "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4", - "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0", - "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84", - "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd", - "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1", - "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776", - "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142", - "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89", - "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c", - "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8", - "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35", - "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a", - "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86", - "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9", - "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64", - "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554", - "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85", - "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb", - "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0", - "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8", - "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb", - "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919" - ], - "markers": "python_version >= '3.8'", - "version": "==2024.9.11" + "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", + "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", + "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", + "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", + "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", + "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773", + "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", + "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", + "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", + "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", + "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", + "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", + "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", + "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", + "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", + "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", + "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", + "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", + "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", + "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", + "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", + "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", + "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", + "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", + "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b", + "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", + "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd", + "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", + "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", + "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", + "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f", + "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", + "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", + "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", + "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", + "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", + "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", + "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", + "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", + "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", + "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", + "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", + "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", + "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", + "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4", + "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", + "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", + "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", + "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", + "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", + "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", + "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc", + "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", + "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", + "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", + "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", + "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", + "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", + "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd", + "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", + "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", + "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", + "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", + "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", + "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3", + "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", + "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", + "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", + "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", + "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", + "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467", + "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", + "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001", + "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", + "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", + "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", + "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf", + "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6", + "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", + "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", + "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", + "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df", + "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", + "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5", + "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", + "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", + "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", + "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", + "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c", + "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f", + "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", + "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", + "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", + "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91" + ], + "markers": "python_version >= '3.8'", + "version": "==2024.11.6" }, "requests": { "hashes": [ @@ -1891,21 +1968,37 @@ "markers": "python_version >= '3.8'", "version": "==2.32.3" }, + "requests-oauthlib": { + "hashes": [ + "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", + "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9" + ], + "markers": "python_version >= '3.4'", + "version": "==2.0.0" + }, "rich": { "hashes": [ - "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", - "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1" + "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", + "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90" ], "markers": "python_full_version >= '3.8.0'", - "version": "==13.9.2" + "version": "==13.9.4" + }, + "rsa": { + "hashes": [ + "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", + "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21" + ], + "markers": "python_version >= '3.6' and python_version < '4'", + "version": "==4.9" }, "setuptools": { "hashes": [ - "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2", - "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538" + "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef", + "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829" ], "markers": "python_version >= '3.12'", - "version": "==75.1.0" + "version": "==75.5.0" }, "shellingham": { "hashes": [ @@ -1944,58 +2037,66 @@ "asyncio" ], "hashes": [ - "sha256:016b2e665f778f13d3c438651dd4de244214b527a275e0acf1d44c05bc6026a9", - "sha256:032d979ce77a6c2432653322ba4cbeabf5a6837f704d16fa38b5a05d8e21fa00", - "sha256:0375a141e1c0878103eb3d719eb6d5aa444b490c96f3fedab8471c7f6ffe70ee", - "sha256:042622a5306c23b972192283f4e22372da3b8ddf5f7aac1cc5d9c9b222ab3ff6", - "sha256:05c3f58cf91683102f2f0265c0db3bd3892e9eedabe059720492dbaa4f922da1", - "sha256:0630774b0977804fba4b6bbea6852ab56c14965a2b0c7fc7282c5f7d90a1ae72", - "sha256:0f9f3f9a3763b9c4deb8c5d09c4cc52ffe49f9876af41cc1b2ad0138878453cf", - "sha256:1b56961e2d31389aaadf4906d453859f35302b4eb818d34a26fab72596076bb8", - "sha256:22b83aed390e3099584b839b93f80a0f4a95ee7f48270c97c90acd40ee646f0b", - "sha256:25b0f63e7fcc2a6290cb5f7f5b4fc4047843504983a28856ce9b35d8f7de03cc", - "sha256:2a275a806f73e849e1c309ac11108ea1a14cd7058577aba962cd7190e27c9e3c", - "sha256:2ab3f0336c0387662ce6221ad30ab3a5e6499aab01b9790879b6578fd9b8faa1", - "sha256:2e795c2f7d7249b75bb5f479b432a51b59041580d20599d4e112b5f2046437a3", - "sha256:3655af10ebcc0f1e4e06c5900bb33e080d6a1fa4228f502121f28a3b1753cde5", - "sha256:4668bd8faf7e5b71c0319407b608f278f279668f358857dbfd10ef1954ac9f90", - "sha256:4c31943b61ed8fdd63dfd12ccc919f2bf95eefca133767db6fbbd15da62078ec", - "sha256:4fdcd72a789c1c31ed242fd8c1bcd9ea186a98ee8e5408a50e610edfef980d71", - "sha256:627dee0c280eea91aed87b20a1f849e9ae2fe719d52cbf847c0e0ea34464b3f7", - "sha256:67219632be22f14750f0d1c70e62f204ba69d28f62fd6432ba05ab295853de9b", - "sha256:6921ee01caf375363be5e9ae70d08ce7ca9d7e0e8983183080211a062d299468", - "sha256:69683e02e8a9de37f17985905a5eca18ad651bf592314b4d3d799029797d0eb3", - "sha256:6a93c5a0dfe8d34951e8a6f499a9479ffb9258123551fa007fc708ae2ac2bc5e", - "sha256:732e026240cdd1c1b2e3ac515c7a23820430ed94292ce33806a95869c46bd139", - "sha256:7befc148de64b6060937231cbff8d01ccf0bfd75aa26383ffdf8d82b12ec04ff", - "sha256:890da8cd1941fa3dab28c5bac3b9da8502e7e366f895b3b8e500896f12f94d11", - "sha256:89b64cd8898a3a6f642db4eb7b26d1b28a497d4022eccd7717ca066823e9fb01", - "sha256:8a6219108a15fc6d24de499d0d515c7235c617b2540d97116b663dade1a54d62", - "sha256:8cdf1a0dbe5ced887a9b127da4ffd7354e9c1a3b9bb330dce84df6b70ccb3a8d", - "sha256:8d625eddf7efeba2abfd9c014a22c0f6b3796e0ffb48f5d5ab106568ef01ff5a", - "sha256:93a71c8601e823236ac0e5d087e4f397874a421017b3318fd92c0b14acf2b6db", - "sha256:9509c4123491d0e63fb5e16199e09f8e262066e58903e84615c301dde8fa2e87", - "sha256:a29762cd3d116585278ffb2e5b8cc311fb095ea278b96feef28d0b423154858e", - "sha256:a62dd5d7cc8626a3634208df458c5fe4f21200d96a74d122c83bc2015b333bc1", - "sha256:ada603db10bb865bbe591939de854faf2c60f43c9b763e90f653224138f910d9", - "sha256:aee110e4ef3c528f3abbc3c2018c121e708938adeeff9006428dd7c8555e9b3f", - "sha256:b76d63495b0508ab9fc23f8152bac63205d2a704cd009a2b0722f4c8e0cba8e0", - "sha256:c0d8326269dbf944b9201911b0d9f3dc524d64779a07518199a58384c3d37a44", - "sha256:c41411e192f8d3ea39ea70e0fae48762cd11a2244e03751a98bd3c0ca9a4e936", - "sha256:c68fe3fcde03920c46697585620135b4ecfdfc1ed23e75cc2c2ae9f8502c10b8", - "sha256:cb8bea573863762bbf45d1e13f87c2d2fd32cee2dbd50d050f83f87429c9e1ea", - "sha256:cc32b2990fc34380ec2f6195f33a76b6cdaa9eecf09f0c9404b74fc120aef36f", - "sha256:ccae5de2a0140d8be6838c331604f91d6fafd0735dbdcee1ac78fc8fbaba76b4", - "sha256:d299797d75cd747e7797b1b41817111406b8b10a4f88b6e8fe5b5e59598b43b0", - "sha256:e04b622bb8a88f10e439084486f2f6349bf4d50605ac3e445869c7ea5cf0fa8c", - "sha256:e11d7ea4d24f0a262bccf9a7cd6284c976c5369dac21db237cff59586045ab9f", - "sha256:e21f66748ab725ade40fa7af8ec8b5019c68ab00b929f6643e1b1af461eddb60", - "sha256:eb60b026d8ad0c97917cb81d3662d0b39b8ff1335e3fabb24984c6acd0c900a2", - "sha256:f021d334f2ca692523aaf7bbf7592ceff70c8594fad853416a81d66b35e3abf9", - "sha256:f552023710d4b93d8fb29a91fadf97de89c5926c6bd758897875435f2a939f33" + "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763", + "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436", + "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2", + "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588", + "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e", + "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959", + "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d", + "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575", + "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908", + "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8", + "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8", + "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545", + "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7", + "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971", + "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855", + "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c", + "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71", + "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d", + "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb", + "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72", + "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f", + "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5", + "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346", + "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24", + "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e", + "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5", + "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08", + "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793", + "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88", + "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686", + "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b", + "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2", + "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28", + "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d", + "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5", + "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a", + "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a", + "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3", + "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf", + "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5", + "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef", + "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689", + "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c", + "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b", + "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07", + "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa", + "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06", + "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1", + "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff", + "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa", + "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687", + "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4", + "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb", + "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44", + "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c", + "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", + "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53" ], "markers": "python_version >= '3.7'", - "version": "==2.0.35" + "version": "==2.0.36" }, "starlette": { "hashes": [ @@ -2068,19 +2169,19 @@ }, "tqdm": { "hashes": [ - "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", - "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad" + "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be", + "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a" ], "markers": "python_version >= '3.7'", - "version": "==4.66.5" + "version": "==4.67.0" }, "typer": { "hashes": [ - "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", - "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722" + "sha256:d85fe0b777b2517cc99c8055ed735452f2659cd45e451507c76f48ce5c1d00e2", + "sha256:f1c7198347939361eec90139ffa0fd8b3df3a2259d5852a0f7400e476d95985c" ], "markers": "python_version >= '3.7'", - "version": "==0.12.5" + "version": "==0.13.0" }, "typing-extensions": { "hashes": [ @@ -2202,47 +2303,53 @@ "standard" ], "hashes": [ - "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906", - "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced" + "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", + "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e" ], "markers": "python_version >= '3.8'", - "version": "==0.31.0" + "version": "==0.32.0" }, "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" }, "watchfiles": { "hashes": [ @@ -2334,94 +2441,77 @@ }, "websockets": { "hashes": [ - "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" + "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", + "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a", + "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", + "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", + "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", + "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", + "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4", + "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", + "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", + "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7", + "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", + "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", + "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5", + "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", + "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", + "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", + "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735", + "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", + "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", + "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", + "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc", + "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6", + "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", + "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", + "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", + "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", + "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", + "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0", + "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", + "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", + "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", + "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", + "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", + "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", + "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", + "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", + "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", + "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", + "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56", + "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", + "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", + "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", + "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", + "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", + "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b", + "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", + "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", + "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", + "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a", + "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", + "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", + "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", + "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78", + "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", + "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", + "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", + "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c", + "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a", + "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", + "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979", + "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370", + "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", + "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", + "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", + "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1", + "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", + "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", + "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", + "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89" + ], + "version": "==14.1" }, "wrapt": { "hashes": [ @@ -2501,117 +2591,107 @@ }, "xmltodict": { "hashes": [ - "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", - "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852" + "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553", + "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac" ], - "markers": "python_version >= '3.4'", - "version": "==0.13.0" + "markers": "python_version >= '3.6'", + "version": "==0.14.2" }, "yarl": { "hashes": [ - "sha256:047b258e00b99091b6f90355521f026238c63bd76dcf996d93527bb13320eefd", - "sha256:06ff23462398333c78b6f4f8d3d70410d657a471c2c5bbe6086133be43fc8f1a", - "sha256:07f9eaf57719d6721ab15805d85f4b01a5b509a0868d7320134371bcb652152d", - "sha256:0aa92e3e30a04f9462a25077db689c4ac5ea9ab6cc68a2e563881b987d42f16d", - "sha256:0cf21f46a15d445417de8fc89f2568852cf57fe8ca1ab3d19ddb24d45c0383ae", - "sha256:0fd7b941dd1b00b5f0acb97455fea2c4b7aac2dd31ea43fb9d155e9bc7b78664", - "sha256:147e36331f6f63e08a14640acf12369e041e0751bb70d9362df68c2d9dcf0c87", - "sha256:16a682a127930f3fc4e42583becca6049e1d7214bcad23520c590edd741d2114", - "sha256:176110bff341b6730f64a1eb3a7070e12b373cf1c910a9337e7c3240497db76f", - "sha256:19268b4fec1d7760134f2de46ef2608c2920134fb1fa61e451f679e41356dc55", - "sha256:1b16f6c75cffc2dc0616ea295abb0e1967601bd1fb1e0af6a1de1c6c887f3439", - "sha256:1bfc25aa6a7c99cf86564210f79a0b7d4484159c67e01232b116e445b3036547", - "sha256:1ca3894e9e9f72da93544f64988d9c052254a338a9f855165f37f51edb6591de", - "sha256:1dda53508df0de87b6e6b0a52d6718ff6c62a5aca8f5552748404963df639269", - "sha256:217a782020b875538eebf3948fac3a7f9bbbd0fd9bf8538f7c2ad7489e80f4e8", - "sha256:2192f718db4a8509f63dd6d950f143279211fa7e6a2c612edc17d85bf043d36e", - "sha256:29a84a46ec3ebae7a1c024c055612b11e9363a8a23238b3e905552d77a2bc51b", - "sha256:3007a5b75cb50140708420fe688c393e71139324df599434633019314ceb8b59", - "sha256:30600ba5db60f7c0820ef38a2568bb7379e1418ecc947a0f76fd8b2ff4257a97", - "sha256:337912bcdcf193ade64b9aae5a4017a0a1950caf8ca140362e361543c6773f21", - "sha256:37001e5d4621cef710c8dc1429ca04e189e572f128ab12312eab4e04cf007132", - "sha256:3d569f877ed9a708e4c71a2d13d2940cb0791da309f70bd970ac1a5c088a0a92", - "sha256:4009def9be3a7e5175db20aa2d7307ecd00bbf50f7f0f989300710eee1d0b0b9", - "sha256:46a9772a1efa93f9cd170ad33101c1817c77e0e9914d4fe33e2da299d7cf0f9b", - "sha256:47eede5d11d669ab3759b63afb70d28d5328c14744b8edba3323e27dc52d298d", - "sha256:498b3c55087b9d762636bca9b45f60d37e51d24341786dc01b81253f9552a607", - "sha256:4e0d45ebf975634468682c8bec021618b3ad52c37619e5c938f8f831fa1ac5c0", - "sha256:4f24f08b6c9b9818fd80612c97857d28f9779f0d1211653ece9844fc7b414df2", - "sha256:55c144d363ad4626ca744556c049c94e2b95096041ac87098bb363dcc8635e8d", - "sha256:582cedde49603f139be572252a318b30dc41039bc0b8165f070f279e5d12187f", - "sha256:587c3cc59bc148a9b1c07a019346eda2549bc9f468acd2f9824d185749acf0a6", - "sha256:5cd5dad8366e0168e0fd23d10705a603790484a6dbb9eb272b33673b8f2cce72", - "sha256:5d02d700705d67e09e1f57681f758f0b9d4412eeb70b2eb8d96ca6200b486db3", - "sha256:625f207b1799e95e7c823f42f473c1e9dbfb6192bd56bba8695656d92be4535f", - "sha256:659603d26d40dd4463200df9bfbc339fbfaed3fe32e5c432fe1dc2b5d4aa94b4", - "sha256:689a99a42ee4583fcb0d3a67a0204664aa1539684aed72bdafcbd505197a91c4", - "sha256:68ac1a09392ed6e3fd14be880d39b951d7b981fd135416db7d18a6208c536561", - "sha256:6a615cad11ec3428020fb3c5a88d85ce1b5c69fd66e9fcb91a7daa5e855325dd", - "sha256:73bedd2be05f48af19f0f2e9e1353921ce0c83f4a1c9e8556ecdcf1f1eae4892", - "sha256:742aef0a99844faaac200564ea6f5e08facb285d37ea18bd1a5acf2771f3255a", - "sha256:75ff4c819757f9bdb35de049a509814d6ce851fe26f06eb95a392a5640052482", - "sha256:781e2495e408a81e4eaeedeb41ba32b63b1980dddf8b60dbbeff6036bcd35049", - "sha256:7a9f917966d27f7ce30039fe8d900f913c5304134096554fd9bea0774bcda6d1", - "sha256:7e2637d75e92763d1322cb5041573279ec43a80c0f7fbbd2d64f5aee98447b17", - "sha256:8089d4634d8fa2b1806ce44fefa4979b1ab2c12c0bc7ef3dfa45c8a374811348", - "sha256:816d24f584edefcc5ca63428f0b38fee00b39fe64e3c5e558f895a18983efe96", - "sha256:8385ab36bf812e9d37cf7613999a87715f27ef67a53f0687d28c44b819df7cb0", - "sha256:85cb3e40eaa98489f1e2e8b29f5ad02ee1ee40d6ce6b88d50cf0f205de1d9d2c", - "sha256:8648180b34faaea4aa5b5ca7e871d9eb1277033fa439693855cf0ea9195f85f1", - "sha256:8892fa575ac9b1b25fae7b221bc4792a273877b9b56a99ee2d8d03eeb3dbb1d2", - "sha256:88c7d9d58aab0724b979ab5617330acb1c7030b79379c8138c1c8c94e121d1b3", - "sha256:8a2f8fb7f944bcdfecd4e8d855f84c703804a594da5123dd206f75036e536d4d", - "sha256:8f4e475f29a9122f908d0f1f706e1f2fc3656536ffd21014ff8a6f2e1b14d1d8", - "sha256:8f50eb3837012a937a2b649ec872b66ba9541ad9d6f103ddcafb8231cfcafd22", - "sha256:91d875f75fabf76b3018c5f196bf3d308ed2b49ddcb46c1576d6b075754a1393", - "sha256:94b2bb9bcfd5be9d27004ea4398fb640373dd0c1a9e219084f42c08f77a720ab", - "sha256:9557c9322aaa33174d285b0c1961fb32499d65ad1866155b7845edc876c3c835", - "sha256:95e16e9eaa2d7f5d87421b8fe694dd71606aa61d74b824c8d17fc85cc51983d1", - "sha256:96952f642ac69075e44c7d0284528938fdff39422a1d90d3e45ce40b72e5e2d9", - "sha256:985623575e5c4ea763056ffe0e2d63836f771a8c294b3de06d09480538316b13", - "sha256:99ff3744f5fe48288be6bc402533b38e89749623a43208e1d57091fc96b783b9", - "sha256:9abe80ae2c9d37c17599557b712e6515f4100a80efb2cda15f5f070306477cd2", - "sha256:a152751af7ef7b5d5fa6d215756e508dd05eb07d0cf2ba51f3e740076aa74373", - "sha256:a2e4725a08cb2b4794db09e350c86dee18202bb8286527210e13a1514dc9a59a", - "sha256:a56fbe3d7f3bce1d060ea18d2413a2ca9ca814eea7cedc4d247b5f338d54844e", - "sha256:ab3abc0b78a5dfaa4795a6afbe7b282b6aa88d81cf8c1bb5e394993d7cae3457", - "sha256:b03384eed107dbeb5f625a99dc3a7de8be04fc8480c9ad42fccbc73434170b20", - "sha256:b0547ab1e9345dc468cac8368d88ea4c5bd473ebc1d8d755347d7401982b5dd8", - "sha256:b4c1ecba93e7826dc71ddba75fb7740cdb52e7bd0be9f03136b83f54e6a1f511", - "sha256:b693c63e7e64b524f54aa4888403c680342d1ad0d97be1707c531584d6aeeb4f", - "sha256:b6d0147574ce2e7b812c989e50fa72bbc5338045411a836bd066ce5fc8ac0bce", - "sha256:b9cfef3f14f75bf6aba73a76caf61f9d00865912a04a4393c468a7ce0981b519", - "sha256:b9f805e37ed16cc212fdc538a608422d7517e7faf539bedea4fe69425bc55d76", - "sha256:bab03192091681d54e8225c53f270b0517637915d9297028409a2a5114ff4634", - "sha256:bc24f968b82455f336b79bf37dbb243b7d76cd40897489888d663d4e028f5069", - "sha256:c14b504a74e58e2deb0378b3eca10f3d076635c100f45b113c18c770b4a47a50", - "sha256:c2089a9afef887664115f7fa6d3c0edd6454adaca5488dba836ca91f60401075", - "sha256:c8ed4034f0765f8861620c1f2f2364d2e58520ea288497084dae880424fc0d9f", - "sha256:cd2660c01367eb3ef081b8fa0a5da7fe767f9427aa82023a961a5f28f0d4af6c", - "sha256:d8361c7d04e6a264481f0b802e395f647cd3f8bbe27acfa7c12049efea675bd1", - "sha256:d9baec588f015d0ee564057aa7574313c53a530662ffad930b7886becc85abdf", - "sha256:dbd9ff43a04f8ffe8a959a944c2dca10d22f5f99fc6a459f49c3ebfb409309d9", - "sha256:e3f8bfc1db82589ef965ed234b87de30d140db8b6dc50ada9e33951ccd8ec07a", - "sha256:e6a2c5c5bb2556dfbfffffc2bcfb9c235fd2b566d5006dfb2a37afc7e3278a07", - "sha256:e749af6c912a7bb441d105c50c1a3da720474e8acb91c89350080dd600228f0e", - "sha256:e85d86527baebb41a214cc3b45c17177177d900a2ad5783dbe6f291642d4906f", - "sha256:ee2c68e4f2dd1b1c15b849ba1c96fac105fca6ffdb7c1e8be51da6fabbdeafb9", - "sha256:f3ab950f8814f3b7b5e3eebc117986f817ec933676f68f0a6c5b2137dd7c9c69", - "sha256:f4f4547944d4f5cfcdc03f3f097d6f05bbbc915eaaf80a2ee120d0e756de377d", - "sha256:f72a0d746d38cb299b79ce3d4d60ba0892c84bbc905d0d49c13df5bace1b65f8", - "sha256:fc2c80bc87fba076e6cbb926216c27fba274dae7100a7b9a0983b53132dd99f2", - "sha256:fe4d2536c827f508348d7b40c08767e8c7071614250927233bf0c92170451c0a" - ], - "markers": "python_version >= '3.8'", - "version": "==1.14.0" + "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", + "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", + "sha256:0b1794853124e2f663f0ea54efb0340b457f08d40a1cef78edfa086576179c91", + "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", + "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", + "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3", + "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", + "sha256:16bca6678a83657dd48df84b51bd56a6c6bd401853aef6d09dc2506a78484c7b", + "sha256:1a3b91c44efa29e6c8ef8a9a2b583347998e2ba52c5d8280dbd5919c02dfc3b5", + "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74", + "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", + "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3", + "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4", + "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", + "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", + "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", + "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", + "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", + "sha256:380e6c38ef692b8fd5a0f6d1fa8774d81ebc08cfbd624b1bca62a4d4af2f9931", + "sha256:3b74ff4767d3ef47ffe0cd1d89379dc4d828d4873e5528976ced3b44fe5b0a21", + "sha256:3e844be8d536afa129366d9af76ed7cb8dfefec99f5f1c9e4f8ae542279a6dc3", + "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", + "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", + "sha256:482c122b72e3c5ec98f11457aeb436ae4aecca75de19b3d1de7cf88bc40db82f", + "sha256:561c87fea99545ef7d692403c110b2f99dced6dff93056d6e04384ad3bc46243", + "sha256:578d00c9b7fccfa1745a44f4eddfdc99d723d157dad26764538fbdda37209857", + "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f", + "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca", + "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", + "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", + "sha256:62a91aefff3d11bf60e5956d340eb507a983a7ec802b19072bb989ce120cd948", + "sha256:64cc6e97f14cf8a275d79c5002281f3040c12e2e4220623b5759ea7f9868d6a5", + "sha256:6f4c9156c4d1eb490fe374fb294deeb7bc7eaccda50e23775b2354b6a6739934", + "sha256:7294e38f9aa2e9f05f765b28ffdc5d81378508ce6dadbe93f6d464a8c9594473", + "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7", + "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", + "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", + "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", + "sha256:7fac95714b09da9278a0b52e492466f773cfe37651cf467a83a1b659be24bf71", + "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", + "sha256:846dd2e1243407133d3195d2d7e4ceefcaa5f5bf7278f0a9bda00967e6326b04", + "sha256:84c063af19ef5130084db70ada40ce63a84f6c1ef4d3dbc34e5e8c4febb20822", + "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11", + "sha256:8994b29c462de9a8fce2d591028b986dbbe1b32f3ad600b2d3e1c482c93abad6", + "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0", + "sha256:8ee427208c675f1b6e344a1f89376a9613fc30b52646a04ac0c1f6587c7e46ec", + "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", + "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", + "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4", + "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", + "sha256:a7ac5b4984c468ce4f4a553df281450df0a34aefae02e58d77a0847be8d1e11f", + "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", + "sha256:ae3476e934b9d714aa8000d2e4c01eb2590eee10b9d8cd03e7983ad65dfbfcba", + "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", + "sha256:b40d1bf6e6f74f7c0a567a9e5e778bbd4699d1d3d2c0fe46f4b717eef9e96b95", + "sha256:b5c4804e4039f487e942c13381e6c27b4b4e66066d94ef1fae3f6ba8b953f383", + "sha256:b5d6a6c9602fd4598fa07e0389e19fe199ae96449008d8304bf5d47cb745462e", + "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", + "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", + "sha256:c180ac742a083e109c1a18151f4dd8675f32679985a1c750d2ff806796165b55", + "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", + "sha256:c7e177c619342e407415d4f35dec63d2d134d951e24b5166afcdfd1362828e17", + "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217", + "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", + "sha256:cc7c92c1baa629cb03ecb0c3d12564f172218fb1739f54bf5f3881844daadc6d", + "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe", + "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", + "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d", + "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", + "sha256:d6324274b4e0e2fa1b3eccb25997b1c9ed134ff61d296448ab8269f5ac068c4c", + "sha256:d8a8b74d843c2638f3864a17d97a4acda58e40d3e44b6303b8cc3d3c44ae2d29", + "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", + "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860", + "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", + "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", + "sha256:eb6dce402734575e1a8cc0bb1509afca508a400a57ce13d306ea2c663bad1138", + "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", + "sha256:f5efe0661b9fcd6246f27957f6ae1c0eb29bc60552820f01e970b4996e016004", + "sha256:f9cbfbc5faca235fbdf531b93aa0f9f005ec7d267d9d738761a4d42b744ea159", + "sha256:fbea1751729afe607d84acfd01efd95e3b31db148a181a441984ce9b3d3469da", + "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988", + "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75" + ], + "markers": "python_version >= '3.9'", + "version": "==1.17.1" }, "zipp": { "hashes": [ - "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", - "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29" + "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", + "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931" ], - "markers": "python_version >= '3.8'", - "version": "==3.20.2" + "markers": "python_version >= '3.9'", + "version": "==3.21.0" } }, "develop": { @@ -2672,11 +2752,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:b1aebecdfa4f4fc02b0a68a5e438877034b195168809a7202ee32b42245d3ece", - "sha256:d79a408dfc503a1a0389d10cd29ad22a01450d0d53902ea216815e2ba98913ba" + "sha256:1456af3358be1a0e49dd8428bfb81863406659d9fad871362bf18a098eeac90a", + "sha256:dd83003963ca957a6e4835d192d7f163fb55312ce3d3f798f625ac9438616e4f" ], "markers": "python_version >= '3.8'", - "version": "==1.35.35" + "version": "==1.35.59" }, "certifi": { "hashes": [ @@ -2761,99 +2841,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": [ @@ -2865,35 +2960,35 @@ }, "cryptography": { "hashes": [ - "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" - ], - "version": "==43.0.1" + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" + ], + "version": "==43.0.3" }, "docker": { "hashes": [ @@ -2988,11 +3083,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pathspec": { "hashes": [ @@ -3061,11 +3156,11 @@ }, "rich": { "hashes": [ - "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", - "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1" + "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", + "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90" ], "markers": "python_full_version >= '3.8.0'", - "version": "==13.9.2" + "version": "==13.9.4" }, "ruff": { "hashes": [ @@ -3093,11 +3188,11 @@ }, "s3transfer": { "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "six": { "hashes": [ @@ -3109,20 +3204,20 @@ }, "types-awscrt": { "hashes": [ - "sha256:67a660c90bad360c339f6a79310cc17094d12472042c7ca5a41450aaf5fc9a54", - "sha256:b2c196bbd3226bab42d80fae13c34548de9ddc195f5a366d79c15d18e5897aa9" + "sha256:3fd1edeac923d1956c0e907c973fb83bda465beae7f054716b371b293f9b5fdc", + "sha256:517d9d06f19cf58d778ca90ad01e52e0489466bf70dcf78c7f47f74fdf151a60" ], "markers": "python_version >= '3.8'", - "version": "==0.22.0" + "version": "==0.23.0" }, "types-beautifulsoup4": { "hashes": [ - "sha256:32f5ac48514b488f15241afdd7d2f73f0baf3c54e874e23b66708503dd288489", - "sha256:8d023b86530922070417a1d4c4d91678ab0ff2439b3b2b2cffa3b628b49ebab1" + "sha256:158370d08d0cd448bd11b132a50ff5279237a5d4b5837beba074de152a513059", + "sha256:c95e66ce15a4f5f0835f7fbc5cd886321ae8294f977c495424eaf4225307fd30" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.12.0.20240907" + "version": "==4.12.0.20241020" }, "types-docker": { "hashes": [ @@ -3135,28 +3230,28 @@ }, "types-html5lib": { "hashes": [ - "sha256:575c4fd84ba8eeeaa8520c7e4c7042b7791f5ec3e9c0a5d5c418124c42d9e7e4", - "sha256:8060dc98baf63d6796a765bbbc809fff9f7a383f6e3a9add526f814c086545ef" + "sha256:3f1e064d9ed2c289001ae6392c84c93833abb0816165c6ff0abfc304a779f403", + "sha256:98042555ff78d9e3a51c77c918b1041acbb7eb6c405408d8a9e150ff5beccafa" ], "markers": "python_version >= '3.8'", - "version": "==1.1.11.20240806" + "version": "==1.1.11.20241018" }, "types-requests": { "hashes": [ - "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405", - "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310" + "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", + "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.32.0.20240914" + "version": "==2.32.0.20241016" }, "types-s3transfer": { "hashes": [ - "sha256:60167a3bfb5c536ec6cdb5818f7f9a28edca9dc3e0b5ff85ae374526fc5e576e", - "sha256:7a3fec8cd632e2b5efb665a355ef93c2a87fdd5a45b74a949f95a9e628a86356" + "sha256:d34c5a82f531af95bb550927136ff5b737a1ed3087f90a59d545591dfde5b4cc", + "sha256:f761b2876ac4c208e6c6b75cdf5f6939009768be9950c545b11b0225e7703ee7" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "typing-extensions": { "hashes": [ diff --git a/opentrons-ai-server/README.md b/opentrons-ai-server/README.md index b072429c41c..041328a5c99 100644 --- a/opentrons-ai-server/README.md +++ b/opentrons-ai-server/README.md @@ -60,6 +60,15 @@ In the deployed environments the FastAPI server is run in a docker container. To Now the API is running at View the API docs in a browser at +##### Docker shell + +1. make clean +1. make build +1. make run-shell +1. make shell + +Now you are in the docker container and can inspect the environment and such. + #### Direct API Interaction and Authentication > There is only 1 endpoint with the potential to call the OpenAI API. This is the `/api/chat/completion` endpoint. This endpoint requires authentication and the steps are outlined below. In the POST request body setting `"fake": true` will short circuit the handling of the call. The OpenAI API will not be hit. Instead, a hard coded response is returned. We plan to extend this capability to allow for live local testing of the UI without calling the OpenAI API. @@ -117,3 +126,48 @@ The live-test target will run tests against any environment. The default is loca 1. alter the `Pipfile` to the new pinned version 1. run `make setup` to update the `Pipfile.lock` + +## Google Sheets Integration + +1. Create a Google Cloud Platform project +1. Enable the Google Sheets and Drive API +1. Go to APIs & Services > Library and enable the Google Sheets API. +1. Go to APIs & Services > Credentials and create a Service Account. This account will be used by your application to access the Google Sheets API. +1. After creating the Service Account, click on it in the Credentials section, go to the Keys tab, and create a JSON key. This will download a JSON file with credentials for your Service Account. +1. Open the JSON file and store its content securely. You’ll set this JSON content as an environment variable. +1. Configure Access to the Google Sheet +1. Open the Google Sheet you want to access. +1. Click Share and add the Service Account email (found in the JSON file under "client_email") as a collaborator, typically with Editor access. This allows the Service Account to interact with the sheet. + +### Test that the credentials work with a direct call to the Integration + +```shell +make test-googlesheet +``` + +## Add Secrets or Environment Variables + +1. Define the new secret or environment variable in the `api/settings.py` file. +1. Add the new secret or environment variable to your local `.env` file. +1. Test locally. +1. Log into the AWS console and navigate to the Secrets Manager. +1. Environment variables are added into the json secret named ENV_VARIABLES_SECRET_NAME in deploy.py for a given environment. +1. Environment variables MUST be named the same as the property in the Settings class. +1. Secret names MUST be the same as the property in the Settings class but with \_ replaced with - and prefixed with the environment name-. +1. The deploy script will load the environment variables from the secret and set them in the container definition. +1. The deploy script will map the secrets from Settings and match them to the container secrets. +1. If any secrets are missing, the deploy script with retrieve the secret ARN and set the secret in the container definition. + +## AWS Deployment + +Locally test the deployment script like so: + +```shell +AWS_PROFILE=robotics_ai_staging make dry-deploy ENV=staging +``` + +Locally deploy to the staging environment like so: + +```shell +AWS_PROFILE=robotics_ai_staging make deploy ENV=staging +``` diff --git a/opentrons-ai-server/api/handler/fast.py b/opentrons-ai-server/api/handler/fast.py index 9534906adbe..9182f827a9a 100644 --- a/opentrons-ai-server/api/handler/fast.py +++ b/opentrons-ai-server/api/handler/fast.py @@ -1,14 +1,14 @@ import asyncio import os import time -from typing import Any, Awaitable, Callable, List, Literal, Union +from typing import Annotated, Any, Awaitable, Callable, List, Literal, Union import structlog from asgi_correlation_id import CorrelationIdMiddleware from asgi_correlation_id.context import correlation_id from ddtrace import tracer from ddtrace.contrib.asgi.middleware import TraceMiddleware -from fastapi import FastAPI, HTTPException, Query, Request, Response, Security, status +from fastapi import BackgroundTasks, FastAPI, HTTPException, Query, Request, Response, Security, status from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html @@ -21,13 +21,17 @@ from api.domain.openai_predict import OpenAIPredict from api.handler.custom_logging import setup_logging from api.integration.auth import VerifyToken +from api.integration.google_sheets import GoogleSheetsClient from api.models.chat_request import ChatRequest from api.models.chat_response import ChatResponse from api.models.create_protocol import CreateProtocol from api.models.empty_request_error import EmptyRequestError +from api.models.error_response import ErrorResponse +from api.models.feedback_request import FeedbackRequest from api.models.feedback_response import FeedbackResponse from api.models.internal_server_error import InternalServerError from api.models.update_protocol import UpdateProtocol +from api.models.user import User from api.settings import Settings settings: Settings = Settings() @@ -38,6 +42,7 @@ auth: VerifyToken = VerifyToken() openai: OpenAIPredict = OpenAIPredict(settings) +google_sheets_client = GoogleSheetsClient(settings) # Initialize FastAPI app with metadata @@ -147,10 +152,6 @@ class Status(BaseModel): version: str -class ErrorResponse(BaseModel): - message: str - - class HealthResponse(BaseModel): status: Status @@ -175,7 +176,7 @@ class CorsHeadersResponse(BaseModel): description="Generate a chat response based on the provided prompt.", ) async def create_chat_completion( - body: ChatRequest, auth_result: Any = Security(auth.verify) # noqa: B008 + body: ChatRequest, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ Generate a chat completion response using OpenAI. @@ -183,7 +184,7 @@ async def create_chat_completion( - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. """ - logger.info("POST /api/chat/completion", extra={"body": body.model_dump(), "auth_result": auth_result}) + logger.info("POST /api/chat/completion", extra={"body": body.model_dump(), "user": user}) try: if not body.message or body.message == "": raise HTTPException( @@ -198,9 +199,9 @@ async def create_chat_completion( response: Union[str, None] = openai.predict(prompt=body.message, chat_completion_message_params=body.history) if response is None or response == "": - return ChatResponse(reply="No response was generated", fake=body.fake) + return ChatResponse(reply="No response was generated", fake=bool(body.fake)) - return ChatResponse(reply=response, fake=body.fake) + return ChatResponse(reply=response, fake=bool(body.fake)) except Exception as e: logger.exception("Error processing chat completion") @@ -217,15 +218,15 @@ async def create_chat_completion( description="Generate a chat response based on the provided prompt that will update an existing protocol with the required changes.", ) async def update_protocol( - body: UpdateProtocol, auth_result: Any = Security(auth.verify) # noqa: B008 + body: UpdateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate an updated protocolusing OpenAI. + Generate an updated protocol using OpenAI. - **request**: The HTTP request containing the existing protocol and other relevant parameters. - **returns**: A chat response or an error message. """ - logger.info("POST /api/chat/updateProtocol", extra={"body": body.model_dump(), "auth_result": auth_result}) + logger.info("POST /api/chat/updateProtocol", extra={"body": body.model_dump(), "user": user}) try: if not body.protocol_text or body.protocol_text == "": raise HTTPException( @@ -233,14 +234,14 @@ async def update_protocol( ) if body.fake: - return ChatResponse(reply="Fake response", fake=body.fake) + return ChatResponse(reply="Fake response", fake=bool(body.fake)) response: Union[str, None] = openai.predict(prompt=body.prompt, chat_completion_message_params=None) if response is None or response == "": - return ChatResponse(reply="No response was generated", fake=body.fake) + return ChatResponse(reply="No response was generated", fake=bool(body.fake)) - return ChatResponse(reply=response, fake=body.fake) + return ChatResponse(reply=response, fake=bool(body.fake)) except Exception as e: logger.exception("Error processing protocol update") @@ -257,15 +258,15 @@ async def update_protocol( description="Generate a chat response based on the provided prompt that will create a new protocol with the required changes.", ) async def create_protocol( - body: CreateProtocol, auth_result: Any = Security(auth.verify) # noqa: B008 + body: CreateProtocol, user: Annotated[User, Security(auth.verify)] ) -> Union[ChatResponse, ErrorResponse]: # noqa: B008 """ - Generate an updated protocolusing OpenAI. + Generate an updated protocol using OpenAI. - **request**: The HTTP request containing the chat message. - **returns**: A chat response or an error message. """ - logger.info("POST /api/chat/createProtocol", extra={"body": body.model_dump(), "auth_result": auth_result}) + logger.info("POST /api/chat/createProtocol", extra={"body": body.model_dump(), "user": user}) try: if not body.prompt or body.prompt == "": @@ -279,9 +280,9 @@ async def create_protocol( response: Union[str, None] = openai.predict(prompt=str(body.model_dump()), chat_completion_message_params=None) if response is None or response == "": - return ChatResponse(reply="No response was generated", fake=body.fake) + return ChatResponse(reply="No response was generated", fake=bool(body.fake)) - return ChatResponse(reply=response, fake=body.fake) + return ChatResponse(reply=response, fake=bool(body.fake)) except Exception as e: logger.exception("Error processing protocol creation") @@ -339,23 +340,19 @@ async def redoc_html() -> HTMLResponse: summary="Feedback", description="Send feedback to the team.", ) -async def feedback(request: Request, auth_result: Any = Security(auth.verify)) -> FeedbackResponse: # noqa: B008 - """ - Send feedback to the team. - - - **request**: The HTTP request containing the feedback message. - - **returns**: A feedback response or an error message. - """ +async def feedback( + body: FeedbackRequest, user: Annotated[User, Security(auth.verify)], background_tasks: BackgroundTasks +) -> FeedbackResponse: logger.info("POST /api/feedback") try: - body = await request.json() - if "feedbackText" not in body.keys() or body["feedbackText"] == "": - logger.info("Feedback empty") - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=EmptyRequestError(message="Request body is empty")) - logger.info(f"Feedback received: {body}") - feedbackText = body["feedbackText"] - # todo: Store feedback text in a database - return FeedbackResponse(reply=f"Feedback Received: {feedbackText}", fake=False) + if body.fake: + return FeedbackResponse(reply="Fake response", fake=bool(body.fake)) + feedback_text = body.feedbackText + logger.info("Feedback received", user_id=user.sub, feedback=feedback_text) + background_tasks.add_task(google_sheets_client.append_feedback_to_sheet, user_id=str(user.sub), feedback=feedback_text) + return FeedbackResponse( + reply=f"Feedback Received and sanitized: {google_sheets_client.sanitize_for_google_sheets(feedback_text)}", fake=False + ) except Exception as e: logger.exception("Error processing feedback") diff --git a/opentrons-ai-server/api/integration/auth.py b/opentrons-ai-server/api/integration/auth.py index 12e8b2a4a9e..addc0abafb8 100644 --- a/opentrons-ai-server/api/integration/auth.py +++ b/opentrons-ai-server/api/integration/auth.py @@ -1,10 +1,9 @@ -from typing import Any, Optional - import jwt import structlog from fastapi import HTTPException, Security, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, SecurityScopes +from api.models.user import User from api.settings import Settings settings: Settings = Settings() @@ -28,8 +27,8 @@ def __init__(self) -> None: self.jwks_client = jwt.PyJWKClient(jwks_url) async def verify( - self, security_scopes: SecurityScopes, credentials: Optional[HTTPAuthorizationCredentials] = Security(HTTPBearer()) # noqa: B008 - ) -> Any: + self, security_scopes: SecurityScopes, credentials: HTTPAuthorizationCredentials = Security(HTTPBearer()) # noqa: B008 + ) -> User: if credentials is None: raise UnauthenticatedException() @@ -50,8 +49,9 @@ async def verify( audience=self.config.auth0_api_audience, issuer=self.config.auth0_issuer, ) - logger.info("Decoded token", extra={"token": payload}) - return payload + user = User(**payload) + logger.info("User object", extra={"user": user}) + return user except jwt.ExpiredSignatureError: logger.error("Expired Signature", extra={"credentials": credentials}, exc_info=True) # Handle token expiration, e.g., refresh token, re-authenticate, etc. diff --git a/opentrons-ai-server/api/integration/google_sheets.py b/opentrons-ai-server/api/integration/google_sheets.py new file mode 100644 index 00000000000..e86d4103097 --- /dev/null +++ b/opentrons-ai-server/api/integration/google_sheets.py @@ -0,0 +1,75 @@ +import json +import random +import re + +import gspread +import structlog +from google.oauth2.service_account import Credentials +from gspread import SpreadsheetNotFound # type: ignore +from gspread.client import Client as GspreadClient + +from api.settings import Settings + + +class GoogleSheetsClient: + SCOPES = ["https://www.googleapis.com/auth/spreadsheets"] + + def __init__(self, settings: Settings) -> None: + self.settings = settings + self.logger = structlog.stdlib.get_logger(settings.logger_name) + self.client: GspreadClient = self._initialize_client() + + def _initialize_client(self) -> GspreadClient: + """Initialize the gspread client with Service Account credentials loaded from the environment.""" + creds: Credentials = self._get_credentials() + return gspread.authorize(creds) # type: ignore + + def _get_credentials(self) -> Credentials: + """Load Service Account credentials from an environment variable.""" + google_credentials_json = self.settings.google_credentials_json.get_secret_value() + if not google_credentials_json: + raise EnvironmentError("Missing GOOGLE_SHEETS_CREDENTIALS environment variable.") + + creds_info = json.loads(google_credentials_json) + creds: Credentials = Credentials.from_service_account_info(info=creds_info, scopes=self.SCOPES) # type: ignore + return creds + + @staticmethod + def sanitize_for_google_sheets(input_text: str) -> str: + """Sanitize input to remove JavaScript and HTML tags, and prevent formulas.""" + script_pattern = re.compile(r'(javascript:[^"]*|.*?|on\w+=".*?"|on\w+=\'.*?\')', re.IGNORECASE) + sanitized_text = re.sub(script_pattern, "", input_text) + sanitized_text = re.sub(r"(<.*?>|<.*?>)", "", sanitized_text) + sanitized_text = re.sub(r"^\s*=\s*", "", sanitized_text) + return sanitized_text.strip() + + def append_feedback_to_sheet(self, user_id: str, feedback: str) -> None: + """Append a row of feedback to the Google Sheet.""" + try: + sheet_id = self.settings.google_sheet_id + worksheet_name = self.settings.google_sheet_worksheet + spreadsheet = self.client.open_by_key(sheet_id) + worksheet = spreadsheet.worksheet(worksheet_name) + + feedback = self.sanitize_for_google_sheets(feedback) + + worksheet.append_row([user_id, feedback]) + self.logger.info("Feedback successfully appended to Google Sheet.") + except SpreadsheetNotFound: + self.logger.error("Spreadsheet not found or not accessible.") + except Exception: + self.logger.error("Error appending feedback to Google Sheet.", exc_info=True) + + +# Example usage +def main() -> None: + """Run an example appending feedback to Google Sheets.""" + settings = Settings() + google_sheets_client = GoogleSheetsClient(settings) + user_id = str(random.randint(100000, 999999)) + feedback = f"This is a test feedback for user {user_id}." + google_sheets_client.append_feedback_to_sheet(user_id, feedback) + + +if __name__ == "__main__": + main() diff --git a/opentrons-ai-server/api/models/error_response.py b/opentrons-ai-server/api/models/error_response.py new file mode 100644 index 00000000000..ba52d60547e --- /dev/null +++ b/opentrons-ai-server/api/models/error_response.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ErrorResponse(BaseModel): + message: str diff --git a/opentrons-ai-server/api/models/feedback_request.py b/opentrons-ai-server/api/models/feedback_request.py new file mode 100644 index 00000000000..89a098b0f92 --- /dev/null +++ b/opentrons-ai-server/api/models/feedback_request.py @@ -0,0 +1,15 @@ +from typing import Optional + +from pydantic import BaseModel, Field, field_validator + + +class FeedbackRequest(BaseModel): + feedbackText: str = Field(..., description="The feedback message content") + fake: Optional[bool] = Field(False, description="Indicates if this is a fake feedback entry") + + # Validation to ensure feedback_text is populated and not empty + @field_validator("feedbackText") + def feedback_text_must_not_be_empty(cls, value: str) -> str: + if not value or value.strip() == "": + raise ValueError("feedback_text must be populated and not empty") + return value diff --git a/opentrons-ai-server/api/models/user.py b/opentrons-ai-server/api/models/user.py new file mode 100644 index 00000000000..d1d79c2b6d1 --- /dev/null +++ b/opentrons-ai-server/api/models/user.py @@ -0,0 +1,16 @@ +from typing import List, Optional, Union + +from pydantic import BaseModel, Field + + +class User(BaseModel): + aud: Union[str, List[str]] = Field(..., description="Audience URL(s)") + azp: str = Field(..., description="Authorized party ID") + exp: int = Field(..., description="Expiration timestamp") + iat: int = Field(..., description="Issued-at timestamp") + iss: str = Field(..., description="Issuer URL") + scope: Optional[str] = Field(None, description="Space-separated scopes") + sub: str = Field(..., description="Subject identifier for the token") + + class Config: + extra = "allow" # Allows additional fields not specified in the model diff --git a/opentrons-ai-server/api/settings.py b/opentrons-ai-server/api/settings.py index c59a25c33de..9557b51614b 100644 --- a/opentrons-ai-server/api/settings.py +++ b/opentrons-ai-server/api/settings.py @@ -34,11 +34,15 @@ class Settings(BaseSettings): dd_trace_enabled: str = "false" cpu: str = "1028" memory: str = "2048" + google_sheet_id: str = "harcoded_default_from_settings" + google_sheet_worksheet: str = "Sheet1" # Secrets # These come from environment variables in the local and deployed execution environments openai_api_key: SecretStr = SecretStr("default_openai_api_key") huggingface_api_key: SecretStr = SecretStr("default_huggingface_api_key") + google_credentials_json: SecretStr = SecretStr("default_google_credentials_json") + datadog_api_key: SecretStr = SecretStr("default_datadog_api_key") @property def json_logging(self) -> bool: diff --git a/opentrons-ai-server/deploy.py b/opentrons-ai-server/deploy.py index 61cbc64b9a1..813dc3ccca1 100644 --- a/opentrons-ai-server/deploy.py +++ b/opentrons-ai-server/deploy.py @@ -3,7 +3,7 @@ import datetime import subprocess from dataclasses import dataclass -from typing import Dict, List +from typing import Any, Dict, List import boto3 import docker @@ -12,7 +12,7 @@ from rich import print from rich.prompt import Prompt -ENVIRONMENTS = ["crt", "dev", "sandbox", "staging", "prod"] +ENVIRONMENTS = ["staging", "prod"] def get_aws_account_id() -> str: @@ -28,6 +28,7 @@ def get_aws_region() -> str: @dataclass(frozen=True) class BaseDeploymentConfig: + ENV: str IMAGE_NAME: str # local image name ECR_URL: str ECR_REPOSITORY: str @@ -40,42 +41,9 @@ class BaseDeploymentConfig: DEPLOYMENT_POLL_INTERVAL_S: int = 20 -@dataclass(frozen=True) -class CrtDeploymentConfig(BaseDeploymentConfig): - ECR_REPOSITORY: str = "crt-ecr-repo" - ECR_URL: str = f"{get_aws_account_id()}.dkr.ecr.{get_aws_region()}.amazonaws.com" - IMAGE_NAME: str = "crt-ai-server" - CLUSTER_NAME: str = "crt-ai-cluster" - SERVICE_NAME: str = "crt-ai-service" - CONTAINER_NAME: str = "crt-ai-api" - ENV_VARIABLES_SECRET_NAME: str = "crt-environment-variables" - - -@dataclass(frozen=True) -class SandboxDeploymentConfig(BaseDeploymentConfig): - ECR_REPOSITORY: str = "sandbox-ecr-repo" - ECR_URL: str = f"{get_aws_account_id()}.dkr.ecr.{get_aws_region()}.amazonaws.com" - IMAGE_NAME: str = "sandbox-ai-server" - CLUSTER_NAME: str = "sandbox-ai-cluster" - SERVICE_NAME: str = "sandbox-ai-service" - CONTAINER_NAME: str = "sandbox-ai-api" - ENV_VARIABLES_SECRET_NAME: str = "sandbox-environment-variables" - - -@dataclass(frozen=True) -class DevDeploymentConfig(BaseDeploymentConfig): - ECR_REPOSITORY: str = "dev-ecr-repo" - ECR_URL: str = f"{get_aws_account_id()}.dkr.ecr.{get_aws_region()}.amazonaws.com" - FUNCTION_NAME: str = "dev-api-function" - IMAGE_NAME: str = "dev-ai-server" - CLUSTER_NAME: str = "dev-ai-cluster" - SERVICE_NAME: str = "dev-ai-service" - CONTAINER_NAME: str = "dev-ai-api" - ENV_VARIABLES_SECRET_NAME: str = "dev-environment-variables" - - @dataclass(frozen=True) class StagingDeploymentConfig(BaseDeploymentConfig): + ENV: str = "staging" ECR_REPOSITORY: str = "staging-ecr-repo" ECR_URL: str = f"{get_aws_account_id()}.dkr.ecr.{get_aws_region()}.amazonaws.com" IMAGE_NAME: str = "staging-ai-server" @@ -87,6 +55,7 @@ class StagingDeploymentConfig(BaseDeploymentConfig): @dataclass(frozen=True) class ProdDeploymentConfig(BaseDeploymentConfig): + ENV: str = "prod" ECR_REPOSITORY: str = "prod-ecr-repo" ECR_URL: str = f"{get_aws_account_id()}.dkr.ecr.{get_aws_region()}.amazonaws.com" IMAGE_NAME: str = "prod-ai-server" @@ -156,7 +125,43 @@ def update_environment_variables(self, environment_variables: List[Dict[str, str return updated_environment_variables - def update_ecs_task(self) -> None: + def get_secret_arn(self, secret_name: str) -> str: + response = self.secret_manager_client.describe_secret(SecretId=secret_name) + return str(response["ARN"]) + + def update_secrets_in_container_definition(self, container_definition: dict[str, Any]) -> None: + expected_secrets = {field.upper() for field, field_type in self.env_variables.__annotations__.items() if field_type == SecretStr} + print(f"Expected secrets: {expected_secrets}") + + task_secrets = {secret["name"].upper() for secret in container_definition.get("secrets", [])} + print(f"Existing secrets: {task_secrets}") + + if not task_secrets: + raise ValueError("No secrets found in the api container definition ...") + + unexpected_secrets = [secret.upper() for secret in task_secrets if secret not in expected_secrets] + if unexpected_secrets: + raise ValueError(f"Secrets found in the api container definition that are NOT in Settings: {', '.join(unexpected_secrets)}") + + missing_secrets = [secret.upper() for secret in expected_secrets if secret.upper() not in task_secrets] + + if missing_secrets: + print(f"Missing secrets: {missing_secrets}") + for secret in missing_secrets: + print(f"Adding missing secret: {secret}") + # secret name is the same as the property name + # of the secret in the Settings class + # but with _ replaced with - + secret_name = f"{self.config.ENV}-{secret.lower().replace("_", "-")}" + value_from = self.get_secret_arn(secret_name) + # name is the all caps version of the secret name + # valueFrom is the ARN of the secret + new_secret = {"name": secret, "valueFrom": value_from} + container_definition["secrets"].append(new_secret) + else: + print("No secrets need to be added.") + + def update_ecs_task(self, dry: bool) -> None: print(f"Updating ECS task with new image: {self.full_image_name}") response = self.ecs_client.describe_services(cluster=self.config.CLUSTER_NAME, services=[self.config.SERVICE_NAME]) task_definition_arn = response["services"][0]["taskDefinition"] @@ -164,6 +169,9 @@ def update_ecs_task(self) -> None: task_definition = self.ecs_client.describe_task_definition(taskDefinition=task_definition_arn)["taskDefinition"] container_definitions = task_definition["containerDefinitions"] for container_definition in container_definitions: + # ENV--datadog-agent container has one secret and 2 environment variables + # ENV--log-router container has no secrets or environment variables + # These are managed in the infra repo, NOT here if container_definition["name"] == self.config.CONTAINER_NAME: container_definition["image"] = self.full_image_name environment_variables = container_definition.get("environment", []) @@ -171,13 +179,15 @@ def update_ecs_task(self) -> None: for key, value in self.env_variables.model_dump().items(): if not isinstance(value, SecretStr): # Secrets are not set here - # They are set in the secrets key of ECS task definition + # They are set in the secrets key of the containerDefinition environment_variables = self.update_environment_variables(environment_variables, key, value) # Overwrite the DD_VERSION environment variable # with the current deployment tag # this is what we are using for version currently environment_variables = self.update_environment_variables(environment_variables, "DD_VERSION", self.config.TAG) container_definition["environment"] = environment_variables + # Update the secrets in the container definition + self.update_secrets_in_container_definition(container_definition) print("Updated container definition:") print(container_definition) break @@ -195,6 +205,11 @@ def update_ecs_task(self) -> None: } print("New task definition:") print(new_task_definition) + + if dry: + print("Dry run, not updating the ECS task.") + return + register_response = self.ecs_client.register_task_definition(**new_task_definition) new_task_definition_arn = register_response["taskDefinition"]["taskDefinitionArn"] @@ -204,14 +219,21 @@ def update_ecs_task(self) -> None: taskDefinition=new_task_definition_arn, forceNewDeployment=True, ) + print(f"Deployment to {self.config.ENV} started.") + print("The API container definition was updated.") + print("A new Task definition was defined and registered.") + print("Then we told the ECS service to deploy the new definition.") + print("Monitor the deployment in the ECS console.") def main() -> None: parser = argparse.ArgumentParser(description="Manage ECS Fargate deployment.") parser.add_argument("--env", type=str, help=f"Deployment environment {ENVIRONMENTS}") parser.add_argument("--tag", type=str, help="The tag and therefore version of the container to use") + # action="store_true" sets args.dry to True only if --dry is provided on the command line + parser.add_argument("--dry", action="store_true", help="Dry run, do not make any changes") args = parser.parse_args() - # Determine if the script was called with command-line arguments + if args.env: if args.env.lower() not in ENVIRONMENTS: print(f"[red]Invalid environment specified: {args.env}[/red]") @@ -221,7 +243,6 @@ def main() -> None: tag = args.tag else: if args.env: - # Passing --env alone generates a tag and does not prompt! tag = str(int(datetime.datetime.now().timestamp())) else: # Interactive prompts if env not set @@ -237,24 +258,17 @@ def main() -> None: config = ProdDeploymentConfig(TAG=tag) elif env == "staging": config = StagingDeploymentConfig(TAG=tag) - elif env == "crt": - config = CrtDeploymentConfig(TAG=tag) - elif env == "dev": - config = DevDeploymentConfig(TAG=tag) - elif env == "sandbox": - config = SandboxDeploymentConfig(TAG=tag) else: print(f"[red]Invalid environment specified: {env}[/red]") exit(1) aws = Deploy(config) aws.build_docker_image() - aws.push_docker_image_to_ecr() - aws.update_ecs_task() - print(f"Deployment to {env} started.") - print(f"A new image was built and pushed to ECR with tag: {tag}") - print("A new Task definition was defined and registered.") - print("Then we told the ECS service to deploy the new definition.") - print("Monitor the deployment in the ECS console.") + if args.dry: + print("Dry run, not pushing image to ECR.") + else: + aws.push_docker_image_to_ecr() + print(f"A new image was built and pushed to ECR with tag: {tag}") + aws.update_ecs_task(dry=args.dry) if __name__ == "__main__": diff --git a/opentrons-ai-server/tests/helpers/client.py b/opentrons-ai-server/tests/helpers/client.py index 7c0d2383ffd..bf5a7febb3c 100644 --- a/opentrons-ai-server/tests/helpers/client.py +++ b/opentrons-ai-server/tests/helpers/client.py @@ -3,6 +3,7 @@ from typing import Any, Callable, Optional, TypeVar from api.models.chat_request import ChatRequest, FakeKeys +from api.models.feedback_request import FeedbackRequest from httpx import Client as HttpxClient from httpx import Response, Timeout from rich.console import Console, Group @@ -68,10 +69,13 @@ def get_chat_completion(self, message: str, fake: bool = True, fake_key: Optiona headers = self.standard_headers if not bad_auth else self.invalid_auth_headers return self.httpx.post("/chat/completion", headers=headers, json=request.model_dump()) - def get_feedback(self, message: str, fake: bool = True) -> Response: + def post_feedback(self, message: str, fake: bool = True, bad_auth: bool = False) -> Response: """Call the /chat/feedback endpoint and return the response.""" - request = f'{"feedbackText": "{message}"}' - return self.httpx.post("/chat/feedback", headers=self.standard_headers, json=request) + request: dict[str, Any] = {"message": message, "fake": fake} + if message != "": + request = FeedbackRequest(feedbackText=message, fake=fake).model_dump() + headers = self.standard_headers if not bad_auth else self.invalid_auth_headers + return self.httpx.post("/chat/feedback", headers=headers, json=request) def get_bad_endpoint(self, bad_auth: bool = False) -> Response: """Call nonexistent endpoint and return the response.""" @@ -113,6 +117,11 @@ def main() -> None: response = client.get_health() print_response(response) + console.print(Rule("Submit feedback", style="bold")) + feedback_message = Prompt.ask("Enter feedback message") + response = client.post_feedback(feedback_message, fake=False) + print_response(response) + console.print(Rule("Getting chat completion with fake=True and good auth (won't call OpenAI)", style="bold")) response = client.get_chat_completion("How do I load a pipette?") print_response(response) diff --git a/opentrons-ai-server/tests/test_google_sheets_sanatize.py b/opentrons-ai-server/tests/test_google_sheets_sanatize.py new file mode 100644 index 00000000000..b7b8e3778f0 --- /dev/null +++ b/opentrons-ai-server/tests/test_google_sheets_sanatize.py @@ -0,0 +1,23 @@ +import pytest +from api.integration.google_sheets import GoogleSheetsClient + + +@pytest.mark.unit +@pytest.mark.parametrize( + "input_text, expected_output", + [ + ('Click here!', "Click here!"), + ('javascript:alert("Malicious code")', '"Malicious code")'), + ("Important message", "Important message"), + ("onload=\"alert('Attack')\" Hello!", "Hello!"), + ('=IMPORTRANGE("https://example.com/sheet", "Sheet1!A1")', 'IMPORTRANGE("https://example.com/sheet", "Sheet1!A1")'), + ("Hello, world!", "Hello, world!"), + ("link", "link"), + ("=SUM(A1:A10)", "SUM(A1:A10)"), + ('', ""), + ('<script>alert("test")</script>', 'alert("test")'), + ], +) +def test_sanitize_for_google_sheets(input_text: str, expected_output: str) -> None: + sanitized_text = GoogleSheetsClient.sanitize_for_google_sheets(input_text) + assert sanitized_text == expected_output, f"Expected '{expected_output}' but got '{sanitized_text}'" diff --git a/opentrons-ai-server/tests/test_live.py b/opentrons-ai-server/tests/test_live.py index ce22f4ff405..797d21fe7b6 100644 --- a/opentrons-ai-server/tests/test_live.py +++ b/opentrons-ai-server/tests/test_live.py @@ -1,5 +1,6 @@ import pytest from api.models.chat_response import ChatResponse +from api.models.error_response import ErrorResponse from api.models.feedback_response import FeedbackResponse from tests.helpers.client import Client @@ -28,13 +29,39 @@ def test_get_chat_completion_bad_auth(client: Client) -> None: @pytest.mark.live -def test_get_feedback_good_auth(client: Client) -> None: +def test_post_feedback_good_auth(client: Client) -> None: """Test the feedback endpoint with good authentication.""" - response = client.get_feedback("How do I load tipracks for my 8 channel pipette on an OT2?", fake=True) + response = client.post_feedback("Would be nice if it were faster", fake=False) assert response.status_code == 200, "Feedback with good auth should return HTTP 200" + assert response.json()["reply"] == "Feedback Received and sanitized: Would be nice if it were faster", "Response should contain input" FeedbackResponse.model_validate(response.json()) +@pytest.mark.live +def test_post_empty_feedback_good_auth(client: Client) -> None: + """Test the feedback endpoint with good authentication.""" + response = client.post_feedback("", fake=False) + assert response.status_code == 422, "Feedback with feebackText = '' should return HTTP 422" + ErrorResponse.model_validate(response.json()) + + +@pytest.mark.live +def test_post_feedback_good_auth_fake(client: Client) -> None: + """Test the feedback endpoint with good authentication.""" + response = client.post_feedback("More LLM", fake=True) + assert response.status_code == 200, "Fake response" + assert response.json()["fake"] is True, "Fake indicator should be True" + assert response.json()["reply"] == "Fake response", "Response should be 'Fake response'" + FeedbackResponse.model_validate(response.json()) + + +@pytest.mark.live +def test_post_feedback_bad_auth(client: Client) -> None: + """Test the feedback endpoint with bad authentication.""" + response = client.post_feedback("How do I load tipracks for my 8 channel pipette on an OT2?", fake=False, bad_auth=True) + assert response.status_code == 401, "Feedback with bad auth should return HTTP 401" + + @pytest.mark.live def test_get_bad_endpoint_with_good_auth(client: Client) -> None: """Test a nonexistent endpoint with good authentication.""" From 15de12be4a800cce313a56e67431e7dc8f281c6d Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:28:56 -0500 Subject: [PATCH 04/31] fix(opentrons-ai-client): Fixed protocol regenerate button (#16807) # Overview This functionality was broken as the app didn't know to use the new update/createnew endpoints when regenerating vs completion endpoint ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- opentrons-ai-client/src/OpentronsAIRoutes.tsx | 2 +- .../src/molecules/ChatDisplay/index.tsx | 33 +++++++++++++++-- .../src/molecules/InputPrompt/index.tsx | 37 +++++++++++++++---- .../src/pages/CreateProtocol/index.tsx | 23 +++++++++++- .../__tests__/UpdateProtocol.test.tsx | 0 .../UpdateProtocol/index.tsx | 24 ++++++++++-- opentrons-ai-client/src/resources/atoms.ts | 10 +++++ opentrons-ai-client/src/resources/types.ts | 1 + .../resources/utils/createProtocolUtils.tsx | 1 + .../api/models/create_protocol.py | 1 + 10 files changed, 117 insertions(+), 15 deletions(-) rename opentrons-ai-client/src/{organisms => pages}/UpdateProtocol/__tests__/UpdateProtocol.test.tsx (100%) rename opentrons-ai-client/src/{organisms => pages}/UpdateProtocol/index.tsx (93%) diff --git a/opentrons-ai-client/src/OpentronsAIRoutes.tsx b/opentrons-ai-client/src/OpentronsAIRoutes.tsx index 32d09f351cf..1b435ac4138 100644 --- a/opentrons-ai-client/src/OpentronsAIRoutes.tsx +++ b/opentrons-ai-client/src/OpentronsAIRoutes.tsx @@ -1,6 +1,6 @@ import { Route, Navigate, Routes } from 'react-router-dom' import { Landing } from './pages/Landing' -import { UpdateProtocol } from './organisms/UpdateProtocol' +import { UpdateProtocol } from './pages/UpdateProtocol' import type { RouteProps } from './resources/types' import { Chat } from './pages/Chat' diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 22dbee37f1a..7ebdf795ab8 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -26,7 +26,10 @@ import { useAtom } from 'jotai' import { chatDataAtom, feedbackModalAtom, + regenerateProtocolAtom, scrollToBottomAtom, + createProtocolChatAtom, + updateProtocolChatAtom, } from '../../resources/atoms' import { delay } from 'lodash' import { useFormContext } from 'react-hook-form' @@ -56,6 +59,9 @@ const StyledIcon = styled(Icon)` export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const { t } = useTranslation('protocol_generator') const [isCopied, setIsCopied] = useState(false) + const [, setRegenerateProtocol] = useAtom(regenerateProtocolAtom) + const [createProtocolChat] = useAtom(createProtocolChatAtom) + const [updateProtocolChat] = useAtom(updateProtocolChatAtom) const [, setShowFeedbackModal] = useAtom(feedbackModalAtom) const { setValue } = useFormContext() const [chatdata] = useAtom(chatDataAtom) @@ -64,9 +70,30 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const isUser = role === 'user' const setInputFieldToCorrespondingRequest = (): void => { - const prompt = chatdata.find( - chat => chat.role === 'user' && chat.requestId === requestId - )?.reply + let prompt = '' + if ( + requestId.includes('NewProtocol') || + requestId.includes('UpdateProtocol') + ) { + setRegenerateProtocol({ + isCreateOrUpdateProtocol: true, + regenerate: true, + }) + if (createProtocolChat.prompt !== '') { + prompt = createProtocolChat.prompt + } else { + prompt = updateProtocolChat.prompt + } + } else { + setRegenerateProtocol({ + isCreateOrUpdateProtocol: false, + regenerate: true, + }) + prompt = + chatdata.find( + chat => chat.role === 'user' && chat.requestId === requestId + )?.reply ?? '' + } setScrollToBottom(!scrollToBottom) setValue('userPrompt', prompt) } diff --git a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx index 56114535733..cc0ccd0f0d3 100644 --- a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx +++ b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx @@ -20,6 +20,7 @@ import { chatDataAtom, chatHistoryAtom, createProtocolChatAtom, + regenerateProtocolAtom, tokenAtom, updateProtocolChatAtom, } from '../../resources/atoms' @@ -38,7 +39,11 @@ import { } from '../../resources/constants' import type { AxiosRequestConfig } from 'axios' -import type { ChatData } from '../../resources/types' +import type { + ChatData, + CreatePrompt, + UpdatePrompt, +} from '../../resources/types' export function InputPrompt(): JSX.Element { const { t } = useTranslation('protocol_generator') @@ -50,6 +55,9 @@ export function InputPrompt(): JSX.Element { const [sendAutoFilledPrompt, setSendAutoFilledPrompt] = useState( false ) + const [regenerateProtocol, setRegenerateProtocol] = useAtom( + regenerateProtocolAtom + ) const [, setChatData] = useAtom(chatDataAtom) const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom) @@ -78,13 +86,24 @@ export function InputPrompt(): JSX.Element { } }, [watchUserPrompt]) + useEffect(() => { + if (regenerateProtocol.regenerate) { + handleClick(regenerateProtocol.isCreateOrUpdateProtocol, true) + setRegenerateProtocol({ + isCreateOrUpdateProtocol: false, + regenerate: false, + }) + } + }, [regenerateProtocol]) + const handleClick = async ( - isUpdateOrCreateRequest: boolean = false + isUpdateOrCreateRequest: boolean = false, + isRegenerateRequest: boolean = false ): Promise => { - setRequestId(uuidv4() + getPreFixText(isUpdateOrCreateRequest)) - + const newRequestId = uuidv4() + getPreFixText(isUpdateOrCreateRequest) + setRequestId(newRequestId) const userInput: ChatData = { - requestId, + requestId: newRequestId, role: 'user', reply: watchUserPrompt, } @@ -106,7 +125,7 @@ export function InputPrompt(): JSX.Element { method: 'POST', headers, data: isUpdateOrCreateRequest - ? getUpdateOrCreatePrompt() + ? getUpdateOrCreatePrompt(isRegenerateRequest) : { message: watchUserPrompt, history: chatHistory, @@ -126,7 +145,11 @@ export function InputPrompt(): JSX.Element { } } - const getUpdateOrCreatePrompt = (): any => { + const getUpdateOrCreatePrompt = ( + isRegenerateRequest: boolean + ): CreatePrompt | UpdatePrompt => { + createProtocol.regenerate = isRegenerateRequest + updateProtocol.regenerate = isRegenerateRequest return isNewProtocol ? createProtocol : updateProtocol } diff --git a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx index 050f4eca8e1..5adb9ac07d1 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx @@ -9,6 +9,8 @@ import { useEffect, useRef, useState } from 'react' import { PromptPreview } from '../../molecules/PromptPreview' import { useForm, FormProvider } from 'react-hook-form' import { + chatDataAtom, + chatHistoryAtom, createProtocolAtom, createProtocolChatAtom, headerWithMeterAtom, @@ -55,6 +57,8 @@ export function CreateProtocol(): JSX.Element | null { ) const [, setCreateProtocolChatAtom] = useAtom(createProtocolChatAtom) const [, setUpdateProtocolChatAtom] = useAtom(updateProtocolChatAtom) + const [, setChatHistoryAtom] = useAtom(chatHistoryAtom) + const [, setChatData] = useAtom(chatDataAtom) const navigate = useNavigate() const trackEvent = useTrackEvent() const [leftWidth, setLeftWidth] = useState(50) @@ -79,8 +83,23 @@ export function CreateProtocol(): JSX.Element | null { }, }) - // Reset the update protocol chat atom when navigating to the create protocol page + // Reset the chat data atom and protocol atoms when navigating to the update protocol page useEffect(() => { + setCreateProtocolChatAtom({ + prompt: '', + regenerate: false, + scientific_application_type: '', + description: '', + robots: 'opentrons_flex', + mounts: [], + flexGripper: false, + modules: [], + labware: [], + liquids: [], + steps: [], + fake: false, + fake_id: 0, + }) setUpdateProtocolChatAtom({ prompt: '', protocol_text: '', @@ -90,6 +109,8 @@ export function CreateProtocol(): JSX.Element | null { fake: false, fake_id: 0, }) + setChatHistoryAtom([]) + setChatData([]) }, []) useEffect(() => { diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx b/opentrons-ai-client/src/pages/UpdateProtocol/__tests__/UpdateProtocol.test.tsx similarity index 100% rename from opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx rename to opentrons-ai-client/src/pages/UpdateProtocol/__tests__/UpdateProtocol.test.tsx diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx b/opentrons-ai-client/src/pages/UpdateProtocol/index.tsx similarity index 93% rename from opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx rename to opentrons-ai-client/src/pages/UpdateProtocol/index.tsx index 4e13e5dfc98..e1a260113a7 100644 --- a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx +++ b/opentrons-ai-client/src/pages/UpdateProtocol/index.tsx @@ -20,9 +20,11 @@ import { Trans, useTranslation } from 'react-i18next' import { FileUpload } from '../../molecules/FileUpload' import { useNavigate } from 'react-router-dom' import { + chatHistoryAtom, createProtocolChatAtom, headerWithMeterAtom, updateProtocolChatAtom, + chatDataAtom, } from '../../resources/atoms' import { CSSTransition } from 'react-transition-group' import { useAtom } from 'jotai' @@ -105,16 +107,19 @@ export function UpdateProtocol(): JSX.Element { const [headerState, setHeaderWithMeterAtom] = useAtom(headerWithMeterAtom) const [updateType, setUpdateType] = useState(null) const [detailsValue, setDetailsValue] = useState('') - const [, setUpdatePromptAtom] = useAtom(updateProtocolChatAtom) + const [, setUpdateProtocolChatAtom] = useAtom(updateProtocolChatAtom) const [, setCreateProtocolChatAtom] = useAtom(createProtocolChatAtom) + const [, setChatHistoryAtom] = useAtom(chatHistoryAtom) + const [, setChatData] = useAtom(chatDataAtom) const [fileValue, setFile] = useState(null) const [pythonText, setPythonTextValue] = useState('') const [errorText, setErrorText] = useState(null) - // Reset the create protocol chat atom when navigating to the update protocol page + // Reset the chat data atom and protocol atoms when navigating to the update protocol page useEffect(() => { setCreateProtocolChatAtom({ prompt: '', + regenerate: false, scientific_application_type: '', description: '', robots: 'opentrons_flex', @@ -127,6 +132,17 @@ export function UpdateProtocol(): JSX.Element { fake: false, fake_id: 0, }) + setUpdateProtocolChatAtom({ + prompt: '', + protocol_text: '', + regenerate: false, + update_type: 'adapt_python_protocol', + update_details: '', + fake: false, + fake_id: 0, + }) + setChatHistoryAtom([]) + setChatData([]) }, []) useEffect(() => { @@ -193,7 +209,9 @@ export function UpdateProtocol(): JSX.Element { const chatPrompt = `${introText}${originalCodeText}${updateTypeText}${detailsText}` - setUpdatePromptAtom({ + console.log(chatPrompt) + + setUpdateProtocolChatAtom({ prompt: chatPrompt, protocol_text: pythonText, regenerate: false, diff --git a/opentrons-ai-client/src/resources/atoms.ts b/opentrons-ai-client/src/resources/atoms.ts index 40ddce7fc53..adbd81d010f 100644 --- a/opentrons-ai-client/src/resources/atoms.ts +++ b/opentrons-ai-client/src/resources/atoms.ts @@ -16,6 +16,7 @@ export const chatDataAtom = atom([]) /** CreateProtocolChatAtom is for the prefilled userprompt when navigating to the chat page from Create New protocol page */ export const createProtocolChatAtom = atom({ prompt: '', + regenerate: false, scientific_application_type: '', description: '', robots: 'opentrons_flex', @@ -40,6 +41,15 @@ export const updateProtocolChatAtom = atom({ fake_id: 0, }) +/** Regenerate protocol atom */ +export const regenerateProtocolAtom = atom<{ + isCreateOrUpdateProtocol: boolean + regenerate: boolean +}>({ + isCreateOrUpdateProtocol: false, + regenerate: false, +}) + /** Scroll to bottom of chat atom */ export const scrollToBottomAtom = atom(false) diff --git a/opentrons-ai-client/src/resources/types.ts b/opentrons-ai-client/src/resources/types.ts index 516f87e9354..7e16e1a8642 100644 --- a/opentrons-ai-client/src/resources/types.ts +++ b/opentrons-ai-client/src/resources/types.ts @@ -15,6 +15,7 @@ export interface ChatData { export interface CreatePrompt { /** the prompt that is generated by the create protocol page */ prompt: string + regenerate: boolean scientific_application_type: string description: string robots: 'opentrons_flex' | 'opentrons_ot2' | string diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index 3b574e11f10..b7483c40610 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -230,6 +230,7 @@ export function generateChatPrompt( setCreateProtocolChatAtom({ prompt, + regenerate: false, scientific_application_type: values.application.scientificApplication, description, robots: values.instruments.robot, diff --git a/opentrons-ai-server/api/models/create_protocol.py b/opentrons-ai-server/api/models/create_protocol.py index 5b011284848..f94d87d2bf8 100644 --- a/opentrons-ai-server/api/models/create_protocol.py +++ b/opentrons-ai-server/api/models/create_protocol.py @@ -5,6 +5,7 @@ class CreateProtocol(BaseModel): prompt: str = Field(..., description="Prompt") + regenerate: bool = Field(..., description="Flag to indicate if regeneration is needed") scientific_application_type: str = Field(..., description="Scientific application type") description: str = Field(..., description="Description of the protocol") robots: Literal["opentrons_flex", "opentrons_ot2"] = Field(..., description="List of required robots") From c918991fef0269395718d5ca35e74d57b4b2a251 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:29:13 -0500 Subject: [PATCH 05/31] fix(protocol-designer): fix WellOrderModal image and reset bugs (#16795) This PR fixes 2 bugs in the `WellOrderModal` component. 1. The first bug arose when attempting to set both primary and secondary order to the same axis. This is fixed by filtering secondary order based on the selected primary order. 2. The second bug stemmed from using a stale state when applying changes and closing the modal after reset-- our helper function to reset values to default set state and applied changes in the same render, resulting in old values being applied. Here, I fix this by directly applying the new values on reset. Closes RQA-3531, Closes RQA-3532 --- .../src/organisms/WellOrderModal/index.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/protocol-designer/src/organisms/WellOrderModal/index.tsx b/protocol-designer/src/organisms/WellOrderModal/index.tsx index 91a7406104e..cb0d7d64ee4 100644 --- a/protocol-designer/src/organisms/WellOrderModal/index.tsx +++ b/protocol-designer/src/organisms/WellOrderModal/index.tsx @@ -92,8 +92,7 @@ export function WellOrderModal(props: WellOrderModalProps): JSX.Element | null { } const handleReset = (): void => { - setWellOrder({ firstValue: DEFAULT_FIRST, secondValue: DEFAULT_SECOND }) - applyChanges() + updateValues(DEFAULT_FIRST, DEFAULT_SECOND) closeModal() } @@ -144,6 +143,13 @@ export function WellOrderModal(props: WellOrderModalProps): JSX.Element | null { if (!isOpen) return null + let secondaryOptions = WELL_ORDER_VALUES + if (VERTICAL_VALUES.includes(wellOrder.firstValue)) { + secondaryOptions = HORIZONTAL_VALUES + } else if (HORIZONTAL_VALUES.includes(wellOrder.firstValue)) { + secondaryOptions = VERTICAL_VALUES + } + return createPortal( ({ + filterOptions={secondaryOptions.map(value => ({ value, name: t(`step_edit_form.field.well_order.option.${value}`), disabled: isSecondOptionDisabled(value), From 22940d1eacea36bcadfef1a9b4109bc4b65662f0 Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:42:21 -0500 Subject: [PATCH 06/31] fix(opentrons-ai-client): Stop the spinner from looping over the texts (#16809) # Overview The spinner was initially designed to loop over the progress texts. Now it stops at finalize and takes 10 seconds to iterate over each progress text. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- opentrons-ai-client/src/atoms/SendButton/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/opentrons-ai-client/src/atoms/SendButton/index.tsx b/opentrons-ai-client/src/atoms/SendButton/index.tsx index ed4128e56ca..0bf4ff959ed 100644 --- a/opentrons-ai-client/src/atoms/SendButton/index.tsx +++ b/opentrons-ai-client/src/atoms/SendButton/index.tsx @@ -67,11 +67,14 @@ export function SendButton({ if (isLoading) { const interval = setInterval(() => { setProgressIndex(prevIndex => { - const newIndex = (prevIndex + 1) % progressTexts.length + let newIndex = prevIndex + 1 + if (newIndex > progressTexts.length - 1) { + newIndex = progressTexts.length - 1 + } setButtonText(progressTexts[newIndex]) return newIndex }) - }, 5000) + }, 10000) return () => { setProgressIndex(0) From fb62ffc3f0851ce3f76ee471cc84c79040492441 Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:16:11 -0500 Subject: [PATCH 07/31] fix(opentrons-ai-client): Navigate to landing page when refreshing chat page (#16810) # Overview Before the app would just show an empty chat screen when refreshed. Now it will navigate to the landing page ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../src/pages/Chat/__tests__/Chat.test.tsx | 17 ++++++++++++++++- opentrons-ai-client/src/pages/Chat/index.tsx | 8 ++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx b/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx index 77874086534..ad17acd26fd 100644 --- a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx +++ b/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx @@ -1,15 +1,25 @@ import { screen } from '@testing-library/react' -import { describe, it, vi, beforeEach } from 'vitest' +import { describe, it, vi, beforeEach, expect } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { PromptGuide } from '../../../molecules/PromptGuide' import { ChatFooter } from '../../../molecules/ChatFooter' import { Chat } from '../index' +import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../molecules/PromptGuide') vi.mock('../../../molecules/ChatFooter') // Note (kk:05/20/2024) to avoid TypeError: scrollRef.current.scrollIntoView is not a function window.HTMLElement.prototype.scrollIntoView = vi.fn() +const mockNavigate = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const reactRouterDom = await importOriginal() + return { + ...reactRouterDom, + useNavigate: () => mockNavigate, + } +}) const render = (): ReturnType => { return renderWithProviders(, { @@ -28,6 +38,11 @@ describe('Chat', () => { screen.getByText('mock ChatFooter') }) + it('should navigate to home if chatData is empty', () => { + render() + expect(mockNavigate).toHaveBeenCalledWith('/') + }) + it.skip('should not show the feedback modal when loading the page', () => { render() screen.getByText('Send feedback to Opentrons') diff --git a/opentrons-ai-client/src/pages/Chat/index.tsx b/opentrons-ai-client/src/pages/Chat/index.tsx index 7bedeb8dffe..82322996b34 100644 --- a/opentrons-ai-client/src/pages/Chat/index.tsx +++ b/opentrons-ai-client/src/pages/Chat/index.tsx @@ -12,6 +12,7 @@ import { ChatDisplay } from '../../molecules/ChatDisplay' import { ChatFooter } from '../../molecules/ChatFooter' import styled from 'styled-components' import { FeedbackModal } from '../../molecules/FeedbackModal' +import { useNavigate } from 'react-router-dom' export interface InputType { userPrompt: string @@ -28,6 +29,13 @@ export function Chat(): JSX.Element | null { const scrollRef = useRef(null) const [showFeedbackModal] = useAtom(feedbackModalAtom) const [scrollToBottom] = useAtom(scrollToBottomAtom) + const navigate = useNavigate() + + useEffect(() => { + if (chatData.length === 0) { + navigate('/') + } + }, []) useEffect(() => { if (scrollRef.current != null) From b73476b4effd6ac7222e8181c401063c252dca73 Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Thu, 14 Nov 2024 10:25:01 -0500 Subject: [PATCH 08/31] feat(opentrons-ai-client): only load latest labware defs (#16811) --- .../ControlledLabwareListItems/index.tsx | 4 +- .../molecules/ModuleListItemGroup/index.tsx | 4 +- .../src/organisms/LabwareModal/index.tsx | 4 +- .../resources/utils/createProtocolUtils.tsx | 6 +-- .../src/resources/utils/labware.ts | 46 +++++++++++++++++-- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/opentrons-ai-client/src/molecules/ControlledLabwareListItems/index.tsx b/opentrons-ai-client/src/molecules/ControlledLabwareListItems/index.tsx index f79f89ca858..507aa0b9392 100644 --- a/opentrons-ai-client/src/molecules/ControlledLabwareListItems/index.tsx +++ b/opentrons-ai-client/src/molecules/ControlledLabwareListItems/index.tsx @@ -14,7 +14,7 @@ import { getLabwareDisplayName } from '@opentrons/shared-data' import { LabwareDiagram } from '../../molecules/LabwareDiagram' import type { DisplayLabware } from '../../organisms/LabwareLiquidsSection' import { LABWARES_FIELD_NAME } from '../../organisms/LabwareLiquidsSection' -import { getAllDefinitions } from '../../resources/utils' +import { getOnlyLatestDefs } from '../../resources/utils' export function ControlledLabwareListItems(): JSX.Element | null { const { t } = useTranslation('create_protocol') @@ -22,7 +22,7 @@ export function ControlledLabwareListItems(): JSX.Element | null { const labwares: DisplayLabware[] = watch(LABWARES_FIELD_NAME) ?? [] - const defs = getAllDefinitions() + const defs = getOnlyLatestDefs() return ( Object.values(getAllDefinitions()), + () => Object.values(getOnlyLatestDefs()), [] ) diff --git a/opentrons-ai-client/src/organisms/LabwareModal/index.tsx b/opentrons-ai-client/src/organisms/LabwareModal/index.tsx index 28324b18dee..0fcb6b17de1 100644 --- a/opentrons-ai-client/src/organisms/LabwareModal/index.tsx +++ b/opentrons-ai-client/src/organisms/LabwareModal/index.tsx @@ -19,7 +19,7 @@ import { createPortal } from 'react-dom' import { reduce } from 'lodash' import { ListButtonCheckbox } from '../../atoms/ListButtonCheckbox/ListButtonCheckbox' import { LABWARES_FIELD_NAME } from '../LabwareLiquidsSection' -import { getAllDefinitions } from '../../resources/utils' +import { getOnlyLatestDefs } from '../../resources/utils' import type { DisplayLabware } from '../LabwareLiquidsSection' import type { LabwareDefByDefURI, @@ -56,7 +56,7 @@ export function LabwareModal({ const searchFilter = (termToCheck: string): boolean => termToCheck.toLowerCase().includes(searchTerm.toLowerCase()) - const defs = getAllDefinitions() + const defs = getOnlyLatestDefs() const labwareByCategory: Record< string, diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index b7483c40610..cb59f6e9694 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -13,7 +13,7 @@ import { } from '../../organisms/InstrumentsSection' import type { UseFormWatch } from 'react-hook-form' import type { CreateProtocolFormData } from '../../pages/CreateProtocol' -import { getAllDefinitions } from './labware' +import { getOnlyLatestDefs } from './labware' import type { CreatePrompt } from '../types' export function generatePromptPreviewApplicationItems( @@ -92,7 +92,7 @@ export function generatePromptPreviewLabwareLiquidsItems( const { labwares, liquids } = watch() const items: string[] = [] - const defs = getAllDefinitions() + const defs = getOnlyLatestDefs() labwares?.forEach(labware => { items.push( @@ -159,7 +159,7 @@ export function generateChatPrompt( args_0: CreatePrompt | ((prev: CreatePrompt) => CreatePrompt) ) => void ): string { - const defs = getAllDefinitions() + const defs = getOnlyLatestDefs() const robotType = t(values.instruments.robot) const scientificApplication = t(values.application.scientificApplication) diff --git a/opentrons-ai-client/src/resources/utils/labware.ts b/opentrons-ai-client/src/resources/utils/labware.ts index b0844c57a70..f7bf0ddf427 100644 --- a/opentrons-ai-client/src/resources/utils/labware.ts +++ b/opentrons-ai-client/src/resources/utils/labware.ts @@ -2,16 +2,52 @@ import { LABWAREV2_DO_NOT_LIST, RETIRED_LABWARE, getAllDefinitions as _getAllDefinitions, + getLabwareDefURI, +} from '@opentrons/shared-data' +import { groupBy } from 'lodash' +import type { + LabwareDefByDefURI, + LabwareDefinition2, } from '@opentrons/shared-data' -import type { LabwareDefByDefURI } from '@opentrons/shared-data' let _definitions: LabwareDefByDefURI | null = null + +const BLOCK_LIST = [...RETIRED_LABWARE, ...LABWAREV2_DO_NOT_LIST] + export function getAllDefinitions(): LabwareDefByDefURI { if (_definitions == null) { - _definitions = _getAllDefinitions([ - ...RETIRED_LABWARE, - ...LABWAREV2_DO_NOT_LIST, - ]) + _definitions = _getAllDefinitions(BLOCK_LIST) } return _definitions } + +// filter out all but the latest version of each labware +// NOTE: this is similar to labware-library's getOnlyLatestDefs, but this one +// has the {labwareDefURI: def} shape, instead of an array of labware defs +let _latestDefs: LabwareDefByDefURI | null = null +export function getOnlyLatestDefs(): LabwareDefByDefURI { + if (!_latestDefs) { + const allDefs = getAllDefinitions() + const allURIs = Object.keys(allDefs) + const labwareDefGroups: Record = groupBy( + allURIs.map((uri: string) => allDefs[uri]), + d => `${d.namespace}/${d.parameters.loadName}` + ) + _latestDefs = Object.keys(labwareDefGroups).reduce( + (acc, groupKey: string) => { + const group = labwareDefGroups[groupKey] + const allVersions = group.map(d => d.version) + const highestVersionNum = Math.max(...allVersions) + const resultIdx = group.findIndex(d => d.version === highestVersionNum) + const latestDefInGroup = group[resultIdx] + return { + ...acc, + [getLabwareDefURI(latestDefInGroup)]: latestDefInGroup, + } + }, + {} + ) + } + + return _latestDefs +} From e6524c932bf0f10a1e5314d60199c88836d95ddc Mon Sep 17 00:00:00 2001 From: fbelginetw <167361860+fbelginetw@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:56:04 -0500 Subject: [PATCH 09/31] fix: send other application correctly in case the option other is selected (#16812) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … # Overview ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- opentrons-ai-client/src/pages/CreateProtocol/index.tsx | 2 +- .../src/resources/utils/createProtocolUtils.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx index 5adb9ac07d1..674df6419bf 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx @@ -31,7 +31,7 @@ import { ResizeBar } from '../../atoms/ResizeBar' export interface CreateProtocolFormData { application: { scientificApplication: string - otherApplication?: string + otherApplication: string description: string } instruments: { diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index cb59f6e9694..a2ccffd988b 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -231,7 +231,10 @@ export function generateChatPrompt( setCreateProtocolChatAtom({ prompt, regenerate: false, - scientific_application_type: values.application.scientificApplication, + scientific_application_type: + values.application.scientificApplication === OTHER + ? values.application.otherApplication + : values.application.scientificApplication, description, robots: values.instruments.robot, mounts, From e569ab48ba4d0064312d9a8785d2c43a796de2bc Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:04:00 -0500 Subject: [PATCH 10/31] Revert "fix(opentrons-ai-client): Navigate to landing page when refreshing chat page" (#16817) Reverts Opentrons/opentrons#16810 --- .../src/pages/Chat/__tests__/Chat.test.tsx | 17 +---------------- opentrons-ai-client/src/pages/Chat/index.tsx | 8 -------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx b/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx index ad17acd26fd..77874086534 100644 --- a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx +++ b/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx @@ -1,25 +1,15 @@ import { screen } from '@testing-library/react' -import { describe, it, vi, beforeEach, expect } from 'vitest' +import { describe, it, vi, beforeEach } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { PromptGuide } from '../../../molecules/PromptGuide' import { ChatFooter } from '../../../molecules/ChatFooter' import { Chat } from '../index' -import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../molecules/PromptGuide') vi.mock('../../../molecules/ChatFooter') // Note (kk:05/20/2024) to avoid TypeError: scrollRef.current.scrollIntoView is not a function window.HTMLElement.prototype.scrollIntoView = vi.fn() -const mockNavigate = vi.fn() - -vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() - return { - ...reactRouterDom, - useNavigate: () => mockNavigate, - } -}) const render = (): ReturnType => { return renderWithProviders(, { @@ -38,11 +28,6 @@ describe('Chat', () => { screen.getByText('mock ChatFooter') }) - it('should navigate to home if chatData is empty', () => { - render() - expect(mockNavigate).toHaveBeenCalledWith('/') - }) - it.skip('should not show the feedback modal when loading the page', () => { render() screen.getByText('Send feedback to Opentrons') diff --git a/opentrons-ai-client/src/pages/Chat/index.tsx b/opentrons-ai-client/src/pages/Chat/index.tsx index 82322996b34..7bedeb8dffe 100644 --- a/opentrons-ai-client/src/pages/Chat/index.tsx +++ b/opentrons-ai-client/src/pages/Chat/index.tsx @@ -12,7 +12,6 @@ import { ChatDisplay } from '../../molecules/ChatDisplay' import { ChatFooter } from '../../molecules/ChatFooter' import styled from 'styled-components' import { FeedbackModal } from '../../molecules/FeedbackModal' -import { useNavigate } from 'react-router-dom' export interface InputType { userPrompt: string @@ -29,13 +28,6 @@ export function Chat(): JSX.Element | null { const scrollRef = useRef(null) const [showFeedbackModal] = useAtom(feedbackModalAtom) const [scrollToBottom] = useAtom(scrollToBottomAtom) - const navigate = useNavigate() - - useEffect(() => { - if (chatData.length === 0) { - navigate('/') - } - }, []) useEffect(() => { if (scrollRef.current != null) From 3dfbca055586fd2c893a62de2502fb49ff4752e0 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 14 Nov 2024 11:08:47 -0500 Subject: [PATCH 11/31] fix(protocol-designer): fix protocol description text wrap issue in overview page (#16806) * fix(protocol-designer): fix protocol description text wrap issue in overview page --- .../src/pages/ProtocolOverview/ProtocolMetadata.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/pages/ProtocolOverview/ProtocolMetadata.tsx b/protocol-designer/src/pages/ProtocolOverview/ProtocolMetadata.tsx index b29cdc1fbc6..d750edaaad5 100644 --- a/protocol-designer/src/pages/ProtocolOverview/ProtocolMetadata.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/ProtocolMetadata.tsx @@ -12,7 +12,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { BUTTON_LINK_STYLE } from '../../atoms' +import { BUTTON_LINK_STYLE, LINE_CLAMP_TEXT_STYLE } from '../../atoms' const REQUIRED_APP_VERSION = '8.2.0' @@ -74,7 +74,10 @@ export function ProtocolMetadata({
} content={ - + {value ?? t('na')} } From 3db8a402bee2dbf3c528380bad9767990f633e80 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 14 Nov 2024 11:32:16 -0500 Subject: [PATCH 12/31] fix(protocol-designer): eppendorf tip names cut off issue (#16820) * fix(protocol-designer): eppendorf tip names cut off issue --- components/src/atoms/Checkbox/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/src/atoms/Checkbox/index.tsx b/components/src/atoms/Checkbox/index.tsx index 02fa36da6d4..8ace61cb0bf 100644 --- a/components/src/atoms/Checkbox/index.tsx +++ b/components/src/atoms/Checkbox/index.tsx @@ -13,7 +13,6 @@ import { } from '../../styles' import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { StyledText } from '../StyledText' -import { truncateString } from '../../utils' export interface CheckboxProps { /** checkbox is checked if value is true */ @@ -41,7 +40,6 @@ export function Checkbox(props: CheckboxProps): JSX.Element { width = FLEX_MAX_CONTENT, type = 'round', } = props - const truncatedLabel = truncateString(labelText, 25) const CHECKBOX_STYLE = css` width: ${width}; @@ -89,7 +87,7 @@ export function Checkbox(props: CheckboxProps): JSX.Element { css={CHECKBOX_STYLE} > - {truncatedLabel} + {labelText} From cb147fd9bb6532c101bd66cff82d0348c922e6f9 Mon Sep 17 00:00:00 2001 From: koji Date: Thu, 14 Nov 2024 11:53:50 -0500 Subject: [PATCH 13/31] fix(protocol-designer): fix error msg display in LiquidToolbox (#16816) * fix(protocol-designer): fix error msg display in LiquidToolbox --- .../src/organisms/AssignLiquidsModal/LiquidToolbox.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index ef5650a6baf..1b2067bf534 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -53,7 +53,7 @@ interface LiquidToolboxProps { } export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { const { onClose } = props - const { t } = useTranslation(['liquids', 'shared']) + const { t } = useTranslation(['liquids', 'form', 'shared']) const dispatch = useDispatch() const [showDefineLiquidModal, setDefineLiquidModal] = useState(false) const liquids = useSelector(labwareIngredSelectors.allIngredientNamesIds) @@ -186,7 +186,7 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { if (volume == null || volume === '0') { volumeErrors = t('generic.error.more_than_zero') } else if (parseInt(volume) > selectedWellsMaxVolume) { - volumeErrors = t('liquid_placement.volume_exceeded', { + volumeErrors = t('form:liquid_placement.volume_exceeded', { volume: selectedWellsMaxVolume, }) } From 3c4c33c4dd5d710d10879d37e26648ccc73212f3 Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:22:20 -0500 Subject: [PATCH 14/31] fix(opentrons-ai-client): Text fixes (#16821) # Overview ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../src/assets/localization/en/protocol_generator.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index 94b1afae702..a4d89e97303 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -30,10 +30,10 @@ "login": "Login", "logout": "Logout", "make_sure_your_prompt": "Write a prompt in a natural language for OpentronsAI to generate a protocol using the Opentrons Python Protocol API v2. The better the prompt, the better the quality of the protocol produced by OpentronsAI.", - "modify_intro": "Modify the following Python code using the Opentrons Python Protocol API v2. Ensure that the new labware and pipettes are compatible with the Flex robot. Ensure that you perform the correct Type of Update use the Details of Changes.\n\n", + "modify_intro": "Modify the following Python code using the Opentrons Python Protocol API v2. Ensure that the new labware and pipettes are compatible with the Flex robot.\n\n", "modify_python_code": "Original Python Code:\n", "modify_type_of_update": "Type of update:\n- ", - "modify_details_of_change": "Details of Changes:\n- ", + "modify_details_of_change": "Detail of changes:\n- ", "modules_and_adapters": "Modules and adapters: Specify the modules and labware adapters required by your protocol.", "notes": "A few important things to note:", "opentrons": "Opentrons", From 2daabbff21e133f5f095e73dd3a9d671fb304f8d Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:26:18 -0500 Subject: [PATCH 15/31] =?UTF-8?q?fix(protocol-designer):=20properly=20sele?= =?UTF-8?q?ct=20and=20disable=20column=20dropdown=20o=E2=80=A6=20(#16787)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ption closes RQA-3528 --- components/src/molecules/DropdownMenu/index.tsx | 2 +- .../StepForm/PipetteFields/PartialTipField.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index 8f0265449e8..851e759abca 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -285,7 +285,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { {filterOptions.map((option, index) => ( { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx index afb321e8628..1410bbfda40 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx @@ -32,6 +32,10 @@ export function PartialTipField(props: FieldProps): JSX.Element { name: t('column'), value: COLUMN, disabled: tipracksNotOnAdapter.length === 0, + tooltipText: + tipracksNotOnAdapter.length === 0 + ? t('form:step_edit_form.field.nozzles.option_tooltip.COLUMN') + : undefined, }, ] @@ -50,7 +54,9 @@ export function PartialTipField(props: FieldProps): JSX.Element { dropdownType="neutral" filterOptions={options} title={t('select_nozzles')} - currentOption={options[0]} + currentOption={ + options.find(option => option.value === selectedValue) ?? options[0] + } onClick={value => { updateValue(value) setSelectedValue(value) From d56f5c757100773fe357fdff216c845bc9728de2 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:28:48 -0500 Subject: [PATCH 16/31] fix(components, protocol-designer): fix labware thermocycler render issue (#16826) Because protocol designer is unaware of actual thermocycler lid state until a thermocycler step explicitly sets its state, we default to a gray box. This PR resets that logic to show the lid as open if the lid state is indeterminate to avoid confusion; however, error messages prompting the user to open the lid before moving labware/liquid to or from the thermocycler will persist, as the thermocycler lid is neither open nor closed. Also, this fixes a rendering issue where labware was shown on top of the closed thermocycler lid. Closes RQA-3556 --- .../Designer/DeckSetup/DeckSetupDetails.tsx | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx index ed6150223c0..9669bf8ef14 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx @@ -31,7 +31,11 @@ import { SelectedHoveredItems } from './SelectedHoveredItems' import { getAdjacentLabware } from './utils' import type { ComponentProps, Dispatch, SetStateAction } from 'react' -import type { ModuleTemporalProperties } from '@opentrons/step-generation' +import type { ThermocyclerVizProps } from '@opentrons/components' +import type { + ModuleTemporalProperties, + ThermocyclerModuleState, +} from '@opentrons/step-generation' import type { AddressableArea, AddressableAreaName, @@ -194,6 +198,24 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { yDimension: labwareLoadedOnModule?.def.dimensions.yDimension ?? 0, zDimension: labwareLoadedOnModule?.def.dimensions.zDimension ?? 0, } + const isLabwareOccludedByThermocyclerLid = + moduleOnDeck.type === THERMOCYCLER_MODULE_TYPE && + (moduleOnDeck.moduleState as ThermocyclerModuleState).lidOpen === + false + + const tempInnerProps = getModuleInnerProps(moduleOnDeck.moduleState) + const innerProps = + moduleOnDeck.type === THERMOCYCLER_MODULE_TYPE + ? { + ...tempInnerProps, + lidMotorState: + (tempInnerProps as ThermocyclerVizProps).lidMotorState !== + 'closed' + ? 'open' + : 'closed', + } + : tempInnerProps + return moduleOnDeck.slot !== selectedSlot.slot ? ( - {labwareLoadedOnModule != null ? ( + {labwareLoadedOnModule != null && + !isLabwareOccludedByThermocyclerLid ? ( <> Date: Thu, 14 Nov 2024 14:41:56 -0500 Subject: [PATCH 17/31] fix(opentrons-ai-client): Fixed footer text size and spinner not resetting back to initializing (#16827) # Overview Fixed the size of the Opentrons may make mistake disclaimer to be smaller and reset the spinner to Initializing after it is done being called. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- opentrons-ai-client/src/atoms/SendButton/index.tsx | 2 +- opentrons-ai-client/src/molecules/ChatFooter/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opentrons-ai-client/src/atoms/SendButton/index.tsx b/opentrons-ai-client/src/atoms/SendButton/index.tsx index 0bf4ff959ed..eba4eeeae58 100644 --- a/opentrons-ai-client/src/atoms/SendButton/index.tsx +++ b/opentrons-ai-client/src/atoms/SendButton/index.tsx @@ -71,13 +71,13 @@ export function SendButton({ if (newIndex > progressTexts.length - 1) { newIndex = progressTexts.length - 1 } - setButtonText(progressTexts[newIndex]) return newIndex }) }, 10000) return () => { setProgressIndex(0) + setButtonText(progressTexts[0]) clearInterval(interval) } } diff --git a/opentrons-ai-client/src/molecules/ChatFooter/index.tsx b/opentrons-ai-client/src/molecules/ChatFooter/index.tsx index fef7596f6f4..817b97c3e0f 100644 --- a/opentrons-ai-client/src/molecules/ChatFooter/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatFooter/index.tsx @@ -29,7 +29,7 @@ export function ChatFooter(): JSX.Element { const DISCLAIMER_TEXT_STYLE = css` color: ${COLORS.grey55}; - font-size: ${TYPOGRAPHY.fontSize20}; + font-size: ${TYPOGRAPHY.fontSizeH3}; line-height: ${TYPOGRAPHY.lineHeight24}; text-align: ${TYPOGRAPHY.textAlignCenter}; ` From a5b9716c83bd619db5b5e25d6e633d4ddbd252f3 Mon Sep 17 00:00:00 2001 From: fbelginetw <167361860+fbelginetw@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:58:39 -0500 Subject: [PATCH 18/31] feat: add remaining analytics events (#16831) # Overview This PR adds and adjusts the AI Client analytics events tracking. ![image](https://github.com/user-attachments/assets/03b90a9a-3b18-436f-a7c8-034c42d23c91) ## Test Plan and Hands on Testing Unit tests for every event and manually tested. ## Changelog - Add analytics ## Review requests ## Risk assessment - low --- .../__tests__/ChatDisplay.test.tsx | 73 ++++++++++++++++++- .../src/molecules/ChatDisplay/index.tsx | 15 ++++ .../__tests__/FeedbackModal.test.tsx | 36 ++++++++- .../src/molecules/FeedbackModal/index.tsx | 8 ++ .../__tests__/InputPrompt.test.tsx | 31 +++++++- .../src/molecules/InputPrompt/index.tsx | 15 ++++ .../__tests__/CreateProtocol.test.tsx | 2 +- .../src/pages/CreateProtocol/index.tsx | 1 + .../__tests__/UpdateProtocol.test.tsx | 49 +++++++++++++ .../src/pages/UpdateProtocol/index.tsx | 1 + 10 files changed, 224 insertions(+), 7 deletions(-) diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx index 7836d18f90f..7226aa6a1a9 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx @@ -1,12 +1,22 @@ import type * as React from 'react' -import { screen } from '@testing-library/react' -import { describe, it, beforeEach } from 'vitest' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { ChatDisplay } from '../index' import { useForm, FormProvider } from 'react-hook-form' +const mockUseTrackEvent = vi.fn() + +vi.mock('../../../resources/hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + +vi.mock('../../../hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + const RenderChatDisplay = (props: React.ComponentProps) => { const methods = useForm({ defaultValues: {}, @@ -38,6 +48,11 @@ describe('ChatDisplay', () => { chatId: 'mockId', } }) + + afterEach(() => { + vi.clearAllMocks() + }) + it('should display response from the backend and label', () => { render(props) screen.getByText('OpentronsAI') @@ -62,4 +77,58 @@ describe('ChatDisplay', () => { // const display = screen.getByTextId('ChatDisplay_from_user') // expect(display).toHaveStyle(`background-color: ${COLORS.blue}`) }) + + it('should call trackEvent when regenerate button is clicked', () => { + render(props) + // eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style + const regeneratePath = document.querySelector( + '[aria-roledescription="reload"]' + ) as Element + fireEvent.click(regeneratePath) + + expect(mockUseTrackEvent).toHaveBeenCalledWith({ + name: 'regenerate-protocol', + properties: {}, + }) + }) + + it('should call trackEvent when download button is clicked', () => { + URL.createObjectURL = vi.fn() + window.URL.revokeObjectURL = vi.fn() + HTMLAnchorElement.prototype.click = vi.fn() + + render(props) + // eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style + const downloadPath = document.querySelector( + '[aria-roledescription="download"]' + ) as Element + fireEvent.click(downloadPath) + + expect(mockUseTrackEvent).toHaveBeenCalledWith({ + name: 'download-protocol', + properties: {}, + }) + }) + + it('should call trackEvent when copy button is clicked', async () => { + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: async () => {}, + }, + }) + + render(props) + // eslint-disable-next-line testing-library/no-node-access, @typescript-eslint/non-nullable-type-assertion-style + const copyPath = document.querySelector( + '[aria-roledescription="content-copy"]' + ) as Element + fireEvent.click(copyPath) + + await waitFor(() => { + expect(mockUseTrackEvent).toHaveBeenCalledWith({ + name: 'copy-protocol', + properties: {}, + }) + }) + }) }) diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 7ebdf795ab8..7d01d282903 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -33,6 +33,7 @@ import { } from '../../resources/atoms' import { delay } from 'lodash' import { useFormContext } from 'react-hook-form' +import { useTrackEvent } from '../../resources/hooks/useTrackEvent' interface ChatDisplayProps { chat: ChatData @@ -58,6 +59,7 @@ const StyledIcon = styled(Icon)` export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const { t } = useTranslation('protocol_generator') + const trackEvent = useTrackEvent() const [isCopied, setIsCopied] = useState(false) const [, setRegenerateProtocol] = useAtom(regenerateProtocolAtom) const [createProtocolChat] = useAtom(createProtocolChatAtom) @@ -96,6 +98,10 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { } setScrollToBottom(!scrollToBottom) setValue('userPrompt', prompt) + trackEvent({ + name: 'regenerate-protocol', + properties: {}, + }) } const handleFileDownload = (): void => { @@ -112,6 +118,11 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { a.download = 'OpentronsAI.py' a.click() window.URL.revokeObjectURL(url) + + trackEvent({ + name: 'download-protocol', + properties: {}, + }) } const handleClickCopy = async (): Promise => { @@ -119,6 +130,10 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const code = lastCodeBlock?.textContent ?? '' await navigator.clipboard.writeText(code) setIsCopied(true) + trackEvent({ + name: 'copy-protocol', + properties: {}, + }) } useEffect(() => { diff --git a/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx b/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx index 15d17938e93..2d881633822 100644 --- a/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx +++ b/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx @@ -1,10 +1,20 @@ import { FeedbackModal } from '..' import { renderWithProviders } from '../../../__testing-utils__' -import { screen } from '@testing-library/react' -import { describe, it, expect } from 'vitest' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { describe, it, expect, vi } from 'vitest' import { i18n } from '../../../i18n' import { feedbackModalAtom } from '../../../resources/atoms' +const mockUseTrackEvent = vi.fn() + +vi.mock('../../../resources/hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + +vi.mock('../../../hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + const initialValues: Array<[any, any]> = [[feedbackModalAtom, true]] const render = (): ReturnType => { @@ -33,4 +43,26 @@ describe('FeedbackModal', () => { // check if the feedbackModalAtom is set to false expect(feedbackModalAtom.read).toBe(false) }) + + it('should track event when feedback is sent', async () => { + render() + const feedbackInput = screen.getByRole('textbox') + fireEvent.change(feedbackInput, { + target: { value: 'This is a test feedback' }, + }) + const sendFeedbackButton = screen.getByRole('button', { + name: 'Send feedback', + }) + + fireEvent.click(sendFeedbackButton) + + await waitFor(() => { + expect(mockUseTrackEvent).toHaveBeenCalledWith({ + name: 'feedback-sent', + properties: { + feedback: 'This is a test feedback', + }, + }) + }) + }) }) diff --git a/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx b/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx index fbb006bdbd4..017048910b8 100644 --- a/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx +++ b/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx @@ -19,9 +19,11 @@ import { LOCAL_FEEDBACK_END_POINT, } from '../../resources/constants' import { useApiCall } from '../../resources/hooks' +import { useTrackEvent } from '../../resources/hooks/useTrackEvent' export function FeedbackModal(): JSX.Element { const { t } = useTranslation('protocol_generator') + const trackEvent = useTrackEvent() const [feedbackValue, setFeedbackValue] = useState('') const [, setShowFeedbackModal] = useAtom(feedbackModalAtom) @@ -58,6 +60,12 @@ export function FeedbackModal(): JSX.Element { }, } await callApi(config as AxiosRequestConfig) + trackEvent({ + name: 'feedback-sent', + properties: { + feedback: feedbackValue, + }, + }) setShowFeedbackModal(false) } catch (err: any) { console.error(`error: ${err.message}`) diff --git a/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx b/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx index f6f588aeab7..3f704a39dda 100644 --- a/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx +++ b/opentrons-ai-client/src/molecules/InputPrompt/__tests__/InputPrompt.test.tsx @@ -1,11 +1,21 @@ import type * as React from 'react' -import { describe, it, expect } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { FormProvider, useForm } from 'react-hook-form' -import { fireEvent, screen } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { InputPrompt } from '../index' +const mockUseTrackEvent = vi.fn() + +vi.mock('../../../resources/hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + +vi.mock('../../../hooks/useTrackEvent', () => ({ + useTrackEvent: () => mockUseTrackEvent, +})) + const WrappingForm = (wrappedComponent: { children: React.ReactNode }): JSX.Element => { @@ -44,4 +54,21 @@ describe('InputPrompt', () => { }) // ToDo (kk:04/19/2024) add more test cases + + it('should track event when send button is clicked', async () => { + render() + const textbox = screen.getByRole('textbox') + fireEvent.change(textbox, { target: { value: ['test'] } }) + const sendButton = screen.getByRole('button') + fireEvent.click(sendButton) + + await waitFor(() => { + expect(mockUseTrackEvent).toHaveBeenCalledWith({ + name: 'chat-submitted', + properties: { + chat: 'test', + }, + }) + }) + }) }) diff --git a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx index cc0ccd0f0d3..e90ba453ab9 100644 --- a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx +++ b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx @@ -44,10 +44,12 @@ import type { CreatePrompt, UpdatePrompt, } from '../../resources/types' +import { useTrackEvent } from '../../resources/hooks/useTrackEvent' export function InputPrompt(): JSX.Element { const { t } = useTranslation('protocol_generator') const { register, watch, reset, setValue } = useFormContext() + const trackEvent = useTrackEvent() const [updateProtocol] = useAtom(updateProtocolChatAtom) const [createProtocol] = useAtom(createProtocolChatAtom) @@ -138,6 +140,12 @@ export function InputPrompt(): JSX.Element { { role: 'user', content: watchUserPrompt }, ]) await callApi(config as AxiosRequestConfig) + trackEvent({ + name: 'chat-submitted', + properties: { + chat: watchUserPrompt, + }, + }) setSubmitted(true) } catch (err: any) { console.error(`error: ${err.message}`) @@ -182,6 +190,13 @@ export function InputPrompt(): JSX.Element { { role: 'assistant', content: reply }, ]) setChatData(chatData => [...chatData, assistantResponse]) + trackEvent({ + name: 'generated-protocol', + properties: { + createOrUpdate: isNewProtocol ? 'create' : 'update', + protocol: reply, + }, + }) setSubmitted(false) } }, [data, isLoading, submitted]) diff --git a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx index 4951299f2c6..919e5f735e8 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx @@ -260,7 +260,7 @@ describe('CreateProtocol', () => { expect(mockNavigate).toHaveBeenCalledWith('/chat') expect(mockUseTrackEvent).toHaveBeenCalledWith({ name: 'submit-prompt', - properties: { prompt: expect.any(String) }, + properties: { isCreateOrUpdate: 'create', prompt: expect.any(String) }, }) }) }) diff --git a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx index 674df6419bf..525c79154b9 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx @@ -207,6 +207,7 @@ export function CreateProtocol(): JSX.Element | null { trackEvent({ name: 'submit-prompt', properties: { + isCreateOrUpdate: 'create', prompt: chatPromptData, }, }) diff --git a/opentrons-ai-client/src/pages/UpdateProtocol/__tests__/UpdateProtocol.test.tsx b/opentrons-ai-client/src/pages/UpdateProtocol/__tests__/UpdateProtocol.test.tsx index 04c3ad3b167..f69d4b88bf8 100644 --- a/opentrons-ai-client/src/pages/UpdateProtocol/__tests__/UpdateProtocol.test.tsx +++ b/opentrons-ai-client/src/pages/UpdateProtocol/__tests__/UpdateProtocol.test.tsx @@ -122,4 +122,53 @@ describe('Update Protocol', () => { }) expect(mockNavigate).toHaveBeenCalledWith('/chat') }) + + it('should call trackEvent when submit prompt button is clicked', async () => { + render() + + // upload file + const blobParts: BlobPart[] = [ + 'x = 1\n', + 'x = 2\n', + 'x = 3\n', + 'x = 4\n', + 'print("x is 1.")\n', + ] + const file = new File(blobParts, 'test-file.py', { type: 'text/python' }) + fireEvent.drop(screen.getByTestId('file_drop_zone'), { + dataTransfer: { + files: [file], + }, + }) + + // input description + const describeInput = screen.getByRole('textbox') + fireEvent.change(describeInput, { target: { value: 'Test description' } }) + + expect(screen.getByDisplayValue('Test description')).toBeInTheDocument() + + // select update type + const applicationDropdown = screen.getByText('Select an option') + fireEvent.click(applicationDropdown) + + const basicOtherOption = screen.getByText('Other') + fireEvent.click(basicOtherOption) + + const submitPromptButton = screen.getByText('Submit prompt') + await waitFor(() => { + expect(submitPromptButton).toBeEnabled() + }) + + fireEvent.click(submitPromptButton) + + await waitFor(() => { + expect(mockUseTrackEvent).toHaveBeenCalledWith({ + name: 'submit-prompt', + properties: { + isCreateOrUpdate: 'update', + prompt: expect.any(String), + }, + }) + }) + }) }) diff --git a/opentrons-ai-client/src/pages/UpdateProtocol/index.tsx b/opentrons-ai-client/src/pages/UpdateProtocol/index.tsx index e1a260113a7..6d12c3f700a 100644 --- a/opentrons-ai-client/src/pages/UpdateProtocol/index.tsx +++ b/opentrons-ai-client/src/pages/UpdateProtocol/index.tsx @@ -224,6 +224,7 @@ export function UpdateProtocol(): JSX.Element { trackEvent({ name: 'submit-prompt', properties: { + isCreateOrUpdate: 'update', prompt: chatPrompt, }, }) From 3cd7c2f9582b280901a929f6c7bededc6fcf87a9 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:15:13 -0500 Subject: [PATCH 19/31] fix(protocol-designer): fix padding and footer position in onboard wizard (#16832) This PR fixes two bugs in the onboarding wizard. Here, I set a minimum grid gap between the left side content and footer buttons so that the buttons don't reposition on steps of different heights. I also fix the padding for the left side content to be a constant 5rem. Closes RQA-3512 --- .../CreateNewProtocolWizard/SelectPipettes.tsx | 1 - .../pages/CreateNewProtocolWizard/WizardBody.tsx | 15 +++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx index 33aa24787fb..fc811b2665a 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectPipettes.tsx @@ -182,7 +182,6 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { {page === 'add' ? ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx index 140878c9994..b5d69253435 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/WizardBody.tsx @@ -2,8 +2,8 @@ import type * as React from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { - ALIGN_END, ALIGN_CENTER, + ALIGN_END, BORDERS, Btn, COLORS, @@ -11,10 +11,11 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, LargeButton, + OVERFLOW_SCROLL, SPACING, StyledText, - TYPOGRAPHY, Tooltip, + TYPOGRAPHY, useHoverTooltip, } from '@opentrons/components' import temporaryImg from '../../assets/images/placeholder_image_delete.png' @@ -56,13 +57,19 @@ export function WizardBody(props: WizardBodyProps): JSX.Element { > - + Date: Thu, 14 Nov 2024 15:20:26 -0500 Subject: [PATCH 20/31] Abr update liquid setups (#16825) # Overview Update to liquid setup protocols for abr use --------- Co-authored-by: rclarke0 --- ...DQ DNA Bacteria Extraction Liquid Setup.py | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py index 410e46fd9bb..4addbd5c7e8 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/7_HDQ DNA Bacteria Extraction Liquid Setup.py @@ -35,15 +35,23 @@ def run(protocol: protocol_api.ProtocolContext) -> None: res1 = protocol.load_labware("nest_12_reservoir_15ml", "D2", "reagent reservoir 1") # Label Reservoirs well1 = res1["A1"].top() + well2 = res1["A2"].top() well3 = res1["A3"].top() well4 = res1["A4"].top() + well5 = res1["A5"].top() + well6 = res1["A6"].top() well7 = res1["A7"].top() + well8 = res1["A8"].top() + well9 = res1["A9"].top() well10 = res1["A10"].top() - + well11 = res1["A11"].top() + well12 = res1["A12"].top() # Volumes wash = 600 - al_and_pk = 468 - beads_and_binding = 552 + binding = 320 + beads = 230 + pk = 230 + lysis = 230 # Sample Plate p1000.transfer( @@ -65,9 +73,41 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Res 1 p1000.transfer( - volume=[beads_and_binding, al_and_pk, wash, wash, wash], + volume=[ + binding, + beads, + binding, + beads, + lysis, + pk, + wash, + wash, + wash, + wash, + wash, + wash, + wash, + wash, + wash, + ], source=source_reservoir["A1"].bottom(z=0.5), - dest=[well1, well3, well4, well7, well10], + dest=[ + well1, + well1, + well2, + well2, + well3, + well3, + well4, + well5, + well6, + well7, + well8, + well9, + well10, + well11, + well12, + ], blowout=True, blowout_location="source well", trash=False, From feeb999f68dd9dd50cf786d16b4ae056be8870b3 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Thu, 14 Nov 2024 15:42:08 -0500 Subject: [PATCH 21/31] fix(api): refactor protocol api integration tests to prevent thread leakage (#16834) Fixes an issue found where after a recent mergeback commit into edge, API unit tests were hanging and failing due to thread leakage in protocol api integration tests. --- .../protocol_api_integration/conftest.py | 28 ++++ .../test_liquid_classes.py | 32 +++-- .../protocol_api_integration/test_modules.py | 46 ++++--- .../test_pipette_movement_deck_conflicts.py | 129 ++++++++++++------ .../protocol_api_integration/test_trashes.py | 94 ++++++------- 5 files changed, 211 insertions(+), 118 deletions(-) create mode 100644 api/tests/opentrons/protocol_api_integration/conftest.py diff --git a/api/tests/opentrons/protocol_api_integration/conftest.py b/api/tests/opentrons/protocol_api_integration/conftest.py new file mode 100644 index 00000000000..fa98ccbb039 --- /dev/null +++ b/api/tests/opentrons/protocol_api_integration/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for protocol api integration tests.""" + +import pytest +from _pytest.fixtures import SubRequest +from typing import Generator + +from opentrons import simulate, protocol_api +from opentrons.protocol_api.core.engine import ENGINE_CORE_API_VERSION + + +@pytest.fixture +def simulated_protocol_context( + request: SubRequest, +) -> Generator[protocol_api.ProtocolContext, None, None]: + """Return a protocol context with requested version and robot.""" + version, robot_type = request.param + context = simulate.get_protocol_api(version=version, robot_type=robot_type) + try: + yield context + finally: + if context.api_version >= ENGINE_CORE_API_VERSION: + # TODO(jbl, 2024-11-14) this is a hack of a hack to close the hardware and the PE thread when a test is + # complete. At some point this should be replaced with a more holistic way of safely cleaning up these + # threads so they don't leak and cause tests to fail when `get_protocol_api` is called too many times. + simulate._LIVE_PROTOCOL_ENGINE_CONTEXTS.close() + else: + # If this is a non-PE context we need to clean up the hardware thread manually + context._hw_manager.hardware.clean_up() diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index 6621a790801..1a6e19f85be 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -3,22 +3,30 @@ from decoy import Decoy from opentrons_shared_data.robot.types import RobotTypeEnum -from opentrons import simulate +from opentrons.protocol_api import ProtocolContext from opentrons.config import feature_flags as ff @pytest.mark.ot2_only +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "OT-2")], indirect=True +) def test_liquid_class_creation_and_property_fetching( - decoy: Decoy, mock_feature_flags: None + decoy: Decoy, + mock_feature_flags: None, + simulated_protocol_context: ProtocolContext, ) -> None: """It should create the liquid class and provide access to its properties.""" decoy.when(ff.allow_liquid_classes(RobotTypeEnum.OT2)).then_return(True) - protocol_context = simulate.get_protocol_api(version="2.20", robot_type="OT-2") - pipette_left = protocol_context.load_instrument("p20_single_gen2", mount="left") - pipette_right = protocol_context.load_instrument("p300_multi", mount="right") - tiprack = protocol_context.load_labware("opentrons_96_tiprack_20ul", "1") + pipette_left = simulated_protocol_context.load_instrument( + "p20_single_gen2", mount="left" + ) + pipette_right = simulated_protocol_context.load_instrument( + "p300_multi", mount="right" + ) + tiprack = simulated_protocol_context.load_labware("opentrons_96_tiprack_20ul", "1") - glycerol_50 = protocol_context.define_liquid_class("fixture_glycerol50") + glycerol_50 = simulated_protocol_context.define_liquid_class("fixture_glycerol50") assert glycerol_50.name == "fixture_glycerol50" assert glycerol_50.display_name == "Glycerol 50%" @@ -50,11 +58,13 @@ def test_liquid_class_creation_and_property_fetching( glycerol_50.display_name = "bar" # type: ignore with pytest.raises(ValueError, match="Liquid class definition not found"): - protocol_context.define_liquid_class("non-existent-liquid") + simulated_protocol_context.define_liquid_class("non-existent-liquid") -def test_liquid_class_feature_flag() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "OT-2")], indirect=True +) +def test_liquid_class_feature_flag(simulated_protocol_context: ProtocolContext) -> None: """It should raise a not implemented error without the allowLiquidClass flag set.""" - protocol_context = simulate.get_protocol_api(version="2.20", robot_type="OT-2") with pytest.raises(NotImplementedError): - protocol_context.define_liquid_class("fixture_glycerol50") + simulated_protocol_context.define_liquid_class("fixture_glycerol50") diff --git a/api/tests/opentrons/protocol_api_integration/test_modules.py b/api/tests/opentrons/protocol_api_integration/test_modules.py index e8a26112d88..72ee8ed8c52 100644 --- a/api/tests/opentrons/protocol_api_integration/test_modules.py +++ b/api/tests/opentrons/protocol_api_integration/test_modules.py @@ -3,13 +3,17 @@ import typing import pytest -from opentrons import simulate, protocol_api +from opentrons import protocol_api -def test_absorbance_reader_labware_load_conflict() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.21", "Flex")], indirect=True +) +def test_absorbance_reader_labware_load_conflict( + simulated_protocol_context: protocol_api.ProtocolContext, +) -> None: """It should prevent loading a labware onto a closed absorbance reader.""" - protocol = simulate.get_protocol_api(version="2.21", robot_type="Flex") - module = protocol.load_module("absorbanceReaderV1", "A3") + module = simulated_protocol_context.load_module("absorbanceReaderV1", "A3") # The lid should be treated as initially closed. with pytest.raises(Exception): @@ -19,7 +23,7 @@ def test_absorbance_reader_labware_load_conflict() -> None: # Should not raise after opening the lid. labware_1 = module.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt") - protocol.move_labware(labware_1, protocol_api.OFF_DECK) + simulated_protocol_context.move_labware(labware_1, protocol_api.OFF_DECK) # Should raise after closing the lid again. module.close_lid() # type: ignore[union-attr] @@ -27,34 +31,44 @@ def test_absorbance_reader_labware_load_conflict() -> None: module.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt") -def test_absorbance_reader_labware_move_conflict() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.21", "Flex")], indirect=True +) +def test_absorbance_reader_labware_move_conflict( + simulated_protocol_context: protocol_api.ProtocolContext, +) -> None: """It should prevent moving a labware onto a closed absorbance reader.""" - protocol = simulate.get_protocol_api(version="2.21", robot_type="Flex") - module = protocol.load_module("absorbanceReaderV1", "A3") - labware = protocol.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt", "A1") + module = simulated_protocol_context.load_module("absorbanceReaderV1", "A3") + labware = simulated_protocol_context.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", "A1" + ) with pytest.raises(Exception): # The lid should be treated as initially closed. - protocol.move_labware(labware, module, use_gripper=True) + simulated_protocol_context.move_labware(labware, module, use_gripper=True) module.open_lid() # type: ignore[union-attr] # Should not raise after opening the lid. - protocol.move_labware(labware, module, use_gripper=True) + simulated_protocol_context.move_labware(labware, module, use_gripper=True) - protocol.move_labware(labware, "A1", use_gripper=True) + simulated_protocol_context.move_labware(labware, "A1", use_gripper=True) # Should raise after closing the lid again. module.close_lid() # type: ignore[union-attr] with pytest.raises(Exception): - protocol.move_labware(labware, module, use_gripper=True) + simulated_protocol_context.move_labware(labware, module, use_gripper=True) -def test_absorbance_reader_read_preconditions() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.21", "Flex")], indirect=True +) +def test_absorbance_reader_read_preconditions( + simulated_protocol_context: protocol_api.ProtocolContext, +) -> None: """Test the preconditions for triggering an absorbance reader read.""" - protocol = simulate.get_protocol_api(version="2.21", robot_type="Flex") module = typing.cast( protocol_api.AbsorbanceReaderContext, - protocol.load_module("absorbanceReaderV1", "A3"), + simulated_protocol_context.load_module("absorbanceReaderV1", "A3"), ) with pytest.raises(Exception, match="initialize"): diff --git a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py index cad2bffddf9..2b7fc11ca91 100644 --- a/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py +++ b/api/tests/opentrons/protocol_api_integration/test_pipette_movement_deck_conflicts.py @@ -2,54 +2,59 @@ import pytest -from opentrons import simulate -from opentrons.protocol_api import COLUMN, ALL, SINGLE, ROW +from opentrons.protocol_api import COLUMN, ALL, SINGLE, ROW, ProtocolContext from opentrons.protocol_api.core.engine.pipette_movement_conflict import ( PartialTipMovementNotAllowedError, ) @pytest.mark.ot3_only -def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.16", "Flex")], indirect=True +) +def test_deck_conflicts_for_96_ch_a12_column_configuration( + simulated_protocol_context: ProtocolContext, +) -> None: """It should raise errors for the expected deck conflicts.""" - protocol_context = simulate.get_protocol_api(version="2.16", robot_type="Flex") - trash_labware = protocol_context.load_labware( + trash_labware = simulated_protocol_context.load_labware( "opentrons_1_trash_3200ml_fixed", "A3" ) - badly_placed_tiprack = protocol_context.load_labware( + badly_placed_tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C2" ) - well_placed_tiprack = protocol_context.load_labware( + well_placed_tiprack = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C1" ) - tiprack_on_adapter = protocol_context.load_labware( + tiprack_on_adapter = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C3", adapter="opentrons_flex_96_tiprack_adapter", ) - thermocycler = protocol_context.load_module("thermocyclerModuleV2") - tc_adjacent_plate = protocol_context.load_labware( + thermocycler = simulated_protocol_context.load_module("thermocyclerModuleV2") + tc_adjacent_plate = simulated_protocol_context.load_labware( "opentrons_96_wellplate_200ul_pcr_full_skirt", "A2" ) accessible_plate = thermocycler.load_labware( "opentrons_96_wellplate_200ul_pcr_full_skirt" ) - instrument = protocol_context.load_instrument("flex_96channel_1000", mount="left") + instrument = simulated_protocol_context.load_instrument( + "flex_96channel_1000", mount="left" + ) instrument.trash_container = trash_labware # ############ SHORT LABWARE ################ # These labware should be to the west of tall labware to avoid any partial tip deck conflicts - badly_placed_labware = protocol_context.load_labware( + badly_placed_labware = simulated_protocol_context.load_labware( "nest_96_wellplate_200ul_flat", "D2" ) - well_placed_labware = protocol_context.load_labware( + well_placed_labware = simulated_protocol_context.load_labware( "nest_96_wellplate_200ul_flat", "D3" ) # ############ TALL LABWARE ############## - protocol_context.load_labware( + simulated_protocol_context.load_labware( "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "D1" ) @@ -104,24 +109,30 @@ def test_deck_conflicts_for_96_ch_a12_column_configuration() -> None: @pytest.mark.ot3_only -def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration( + simulated_protocol_context: ProtocolContext, +) -> None: """Shouldn't raise errors for "almost collision"s.""" - protocol_context = simulate.get_protocol_api(version="2.20", robot_type="Flex") - res12 = protocol_context.load_labware("nest_12_reservoir_15ml", "C3") + res12 = simulated_protocol_context.load_labware("nest_12_reservoir_15ml", "C3") # Mag block and tiprack adapter are very close to the destination reservoir labware - protocol_context.load_module("magneticBlockV1", "D2") - protocol_context.load_labware( + simulated_protocol_context.load_module("magneticBlockV1", "D2") + simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_200ul", "B3", adapter="opentrons_flex_96_tiprack_adapter", ) - tiprack_8 = protocol_context.load_labware("opentrons_flex_96_tiprack_200ul", "B2") - hs = protocol_context.load_module("heaterShakerModuleV1", "C1") + tiprack_8 = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_200ul", "B2" + ) + hs = simulated_protocol_context.load_module("heaterShakerModuleV1", "C1") hs_adapter = hs.load_adapter("opentrons_96_deep_well_adapter") deepwell = hs_adapter.load_labware("nest_96_wellplate_2ml_deep") - protocol_context.load_trash_bin("A3") - p1000_96 = protocol_context.load_instrument("flex_96channel_1000") + simulated_protocol_context.load_trash_bin("A3") + p1000_96 = simulated_protocol_context.load_instrument("flex_96channel_1000") p1000_96.configure_nozzle_layout(style=SINGLE, start="A12", tip_racks=[tiprack_8]) hs.close_labware_latch() # type: ignore[union-attr] @@ -135,16 +146,28 @@ def test_close_shave_deck_conflicts_for_96_ch_a12_column_configuration() -> None @pytest.mark.ot3_only -def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.16", "Flex")], indirect=True +) +def test_deck_conflicts_for_96_ch_a1_column_configuration( + simulated_protocol_context: ProtocolContext, +) -> None: """It should raise errors for expected deck conflicts.""" - protocol = simulate.get_protocol_api(version="2.16", robot_type="Flex") - instrument = protocol.load_instrument("flex_96channel_1000", mount="left") - trash_labware = protocol.load_labware("opentrons_1_trash_3200ml_fixed", "A3") + instrument = simulated_protocol_context.load_instrument( + "flex_96channel_1000", mount="left" + ) + trash_labware = simulated_protocol_context.load_labware( + "opentrons_1_trash_3200ml_fixed", "A3" + ) instrument.trash_container = trash_labware - badly_placed_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "C2") - well_placed_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "A1") - tiprack_on_adapter = protocol.load_labware( + badly_placed_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "C2" + ) + well_placed_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "A1" + ) + tiprack_on_adapter = simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "C3", adapter="opentrons_flex_96_tiprack_adapter", @@ -152,11 +175,15 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: # ############ SHORT LABWARE ################ # These labware should be to the east of tall labware to avoid any partial tip deck conflicts - badly_placed_plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "B1") - well_placed_plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "B3") + badly_placed_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "B1" + ) + well_placed_plate = simulated_protocol_context.load_labware( + "nest_96_wellplate_200ul_flat", "B3" + ) # ############ TALL LABWARE ############### - my_tuberack = protocol.load_labware( + my_tuberack = simulated_protocol_context.load_labware( "opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical", "B2" ) @@ -208,7 +235,7 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: instrument.drop_tip() instrument.trash_container = None # type: ignore - protocol.load_trash_bin("C1") + simulated_protocol_context.load_trash_bin("C1") # This doesn't raise an error because it now treats the trash bin as an addressable area # and the bounds check doesn't yet check moves to addressable areas. @@ -229,28 +256,38 @@ def test_deck_conflicts_for_96_ch_a1_column_configuration() -> None: @pytest.mark.ot3_only -def test_deck_conflicts_for_96_ch_and_reservoirs() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.20", "Flex")], indirect=True +) +def test_deck_conflicts_for_96_ch_and_reservoirs( + simulated_protocol_context: ProtocolContext, +) -> None: """It should raise errors for expected deck conflicts when moving to reservoirs. This test checks that the critical point of the pipette is taken into account, specifically when it differs from the primary nozzle. """ - protocol = simulate.get_protocol_api(version="2.20", robot_type="Flex") - instrument = protocol.load_instrument("flex_96channel_1000", mount="left") + instrument = simulated_protocol_context.load_instrument( + "flex_96channel_1000", mount="left" + ) # trash_labware = protocol.load_labware("opentrons_1_trash_3200ml_fixed", "A3") # instrument.trash_container = trash_labware - protocol.load_trash_bin("A3") - right_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "C3") - front_tiprack = protocol.load_labware("opentrons_flex_96_tiprack_50ul", "D2") + simulated_protocol_context.load_trash_bin("A3") + right_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "C3" + ) + front_tiprack = simulated_protocol_context.load_labware( + "opentrons_flex_96_tiprack_50ul", "D2" + ) # Tall deck item in B3 - protocol.load_labware( + simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "B3", adapter="opentrons_flex_96_tiprack_adapter", ) # Tall deck item in B1 - protocol.load_labware( + simulated_protocol_context.load_labware( "opentrons_flex_96_tiprack_50ul", "B1", adapter="opentrons_flex_96_tiprack_adapter", @@ -258,8 +295,12 @@ def test_deck_conflicts_for_96_ch_and_reservoirs() -> None: # ############ RESERVOIRS ################ # These labware should be to the east of tall labware to avoid any partial tip deck conflicts - reservoir_1_well = protocol.load_labware("nest_1_reservoir_195ml", "C2") - reservoir_12_well = protocol.load_labware("nest_12_reservoir_15ml", "B2") + reservoir_1_well = simulated_protocol_context.load_labware( + "nest_1_reservoir_195ml", "C2" + ) + reservoir_12_well = simulated_protocol_context.load_labware( + "nest_12_reservoir_15ml", "B2" + ) # ########### Use COLUMN A1 Config ############# instrument.configure_nozzle_layout(style=COLUMN, start="A1") diff --git a/api/tests/opentrons/protocol_api_integration/test_trashes.py b/api/tests/opentrons/protocol_api_integration/test_trashes.py index 18dfa62170d..1166ba01c70 100644 --- a/api/tests/opentrons/protocol_api_integration/test_trashes.py +++ b/api/tests/opentrons/protocol_api_integration/test_trashes.py @@ -1,46 +1,42 @@ """Tests for the APIs around waste chutes and trash bins.""" -from opentrons import protocol_api, simulate +from opentrons import protocol_api from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import UnsupportedAPIError import contextlib from typing import ContextManager, Optional, Type -from typing_extensions import Literal import re import pytest @pytest.mark.parametrize( - ("version", "robot_type", "expected_trash_class"), + ("simulated_protocol_context", "expected_trash_class"), [ - ("2.13", "OT-2", protocol_api.Labware), - ("2.14", "OT-2", protocol_api.Labware), - ("2.15", "OT-2", protocol_api.Labware), + (("2.13", "OT-2"), protocol_api.Labware), + (("2.14", "OT-2"), protocol_api.Labware), + (("2.15", "OT-2"), protocol_api.Labware), pytest.param( - "2.15", - "Flex", + ("2.15", "Flex"), protocol_api.Labware, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), pytest.param( - "2.16", - "OT-2", + ("2.16", "OT-2"), protocol_api.TrashBin, ), pytest.param( - "2.16", - "Flex", + ("2.16", "Flex"), None, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), ], + indirect=["simulated_protocol_context"], ) def test_fixed_trash_presence( - robot_type: Literal["OT-2", "Flex"], - version: str, + simulated_protocol_context: protocol_api.ProtocolContext, expected_trash_class: Optional[Type[object]], ) -> None: """Test the presence of the fixed trash. @@ -49,9 +45,10 @@ def test_fixed_trash_presence( For those that do, ProtocolContext.fixed_trash and InstrumentContext.trash_container should point to it. The type of the object depends on the API version. """ - protocol = simulate.get_protocol_api(version=version, robot_type=robot_type) - instrument = protocol.load_instrument( - "p300_single_gen2" if robot_type == "OT-2" else "flex_1channel_50", + instrument = simulated_protocol_context.load_instrument( + "p300_single_gen2" + if simulated_protocol_context._core.robot_type == "OT-2 Standard" + else "flex_1channel_50", mount="left", ) @@ -59,46 +56,53 @@ def test_fixed_trash_presence( with pytest.raises( UnsupportedAPIError, match=re.escape( - "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16." + " You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." ), ): - protocol.fixed_trash + simulated_protocol_context.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): instrument.trash_container else: - assert isinstance(protocol.fixed_trash, expected_trash_class) - assert instrument.trash_container is protocol.fixed_trash + assert isinstance(simulated_protocol_context.fixed_trash, expected_trash_class) + assert instrument.trash_container is simulated_protocol_context.fixed_trash @pytest.mark.ot3_only # Simulating a Flex protocol requires a Flex hardware API. -def test_trash_search() -> None: +@pytest.mark.parametrize( + "simulated_protocol_context", [("2.16", "Flex")], indirect=True +) +def test_trash_search(simulated_protocol_context: protocol_api.ProtocolContext) -> None: """Test the automatic trash search for protocols without a fixed trash.""" - protocol = simulate.get_protocol_api(version="2.16", robot_type="Flex") - instrument = protocol.load_instrument("flex_1channel_50", mount="left") + instrument = simulated_protocol_context.load_instrument( + "flex_1channel_50", mount="left" + ) # By default, there should be no trash. with pytest.raises( UnsupportedAPIError, match=re.escape( - "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16." + " You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." ), ): - protocol.fixed_trash + simulated_protocol_context.fixed_trash with pytest.raises(Exception, match="No trash container has been defined"): instrument.trash_container - loaded_first = protocol.load_trash_bin("A1") - loaded_second = protocol.load_trash_bin("B1") + loaded_first = simulated_protocol_context.load_trash_bin("A1") + loaded_second = simulated_protocol_context.load_trash_bin("B1") # After loading some trashes, there should still be no protocol.fixed_trash... with pytest.raises( UnsupportedAPIError, match=re.escape( - "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16. You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." + "Error 4002 API_REMOVED (UnsupportedAPIError): Fixed Trash is not available after API version 2.16." + " You are currently using API version 2.16. Fixed trash is no longer supported on Flex protocols." ), ): - protocol.fixed_trash + simulated_protocol_context.fixed_trash # ...but instrument.trash_container should automatically update to point to # the first trash that we loaded. assert instrument.trash_container is loaded_first @@ -109,40 +113,36 @@ def test_trash_search() -> None: @pytest.mark.parametrize( - ("version", "robot_type", "expect_load_to_succeed"), + ("simulated_protocol_context", "expect_load_to_succeed"), [ pytest.param( - "2.13", - "OT-2", + ("2.13", "OT-2"), False, # This xfail (the system does let you load a labware onto slot 12, and does not raise) # is surprising to me. It may be be a bug in old PAPI versions. marks=pytest.mark.xfail(strict=True, raises=pytest.fail.Exception), ), - ("2.14", "OT-2", False), - ("2.15", "OT-2", False), + (("2.14", "OT-2"), False), + (("2.15", "OT-2"), False), pytest.param( - "2.15", - "Flex", + ("2.15", "Flex"), False, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), pytest.param( - "2.16", - "OT-2", + ("2.16", "OT-2"), False, ), pytest.param( - "2.16", - "Flex", + ("2.16", "Flex"), True, marks=pytest.mark.ot3_only, # Simulating a Flex protocol requires a Flex hardware API. ), ], + indirect=["simulated_protocol_context"], ) def test_fixed_trash_load_conflicts( - robot_type: Literal["Flex", "OT-2"], - version: str, + simulated_protocol_context: protocol_api.ProtocolContext, expect_load_to_succeed: bool, ) -> None: """Test loading something onto the location historically used for the fixed trash. @@ -150,14 +150,12 @@ def test_fixed_trash_load_conflicts( In configurations where there is a fixed trash, this should be disallowed. In configurations without a fixed trash, this should be allowed. """ - protocol = simulate.get_protocol_api(version=version, robot_type=robot_type) - if expect_load_to_succeed: expected_error: ContextManager[object] = contextlib.nullcontext() else: # If we're expecting an error, it'll be a LocationIsOccupied for 2.15 and below, otherwise # it will fail with an IncompatibleAddressableAreaError, since slot 12 will not be in the deck config - if APIVersion.from_string(version) < APIVersion(2, 16): + if simulated_protocol_context.api_version < APIVersion(2, 16): error_name = "LocationIsOccupiedError" else: error_name = "IncompatibleAddressableAreaError" @@ -169,4 +167,6 @@ def test_fixed_trash_load_conflicts( ) with expected_error: - protocol.load_labware("opentrons_96_wellplate_200ul_pcr_full_skirt", 12) + simulated_protocol_context.load_labware( + "opentrons_96_wellplate_200ul_pcr_full_skirt", 12 + ) From 11cad09dd8659d50864f7fa54f3fd51b41f19d7f Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:10:55 -0500 Subject: [PATCH 22/31] fix(protocol-designer): step overflow menu positioning and prevent multiple opened (#16829) closes RQA-3408 RQA-3358, partially closes RQA-3402 (item 4 i think) --- .../Timeline/ConnectedStepInfo.tsx | 14 ++++- .../ProtocolSteps/Timeline/DraggableSteps.tsx | 22 ++++++- .../ProtocolSteps/Timeline/StepContainer.tsx | 59 +++++++++++-------- .../Timeline/StepOverflowMenu.tsx | 16 ++--- .../__tests__/StepOverflowMenu.test.tsx | 2 +- 5 files changed, 77 insertions(+), 36 deletions(-) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx index 778159b6d31..c198359ab52 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx @@ -1,4 +1,5 @@ import { useDispatch, useSelector } from 'react-redux' +import type { Dispatch, SetStateAction } from 'react' import { useTranslation } from 'react-i18next' import { useConditionalConfirm } from '@opentrons/components' import * as timelineWarningSelectors from '../../../../top-selectors/timelineWarnings' @@ -33,7 +34,6 @@ import { nonePressed, } from './utils' -import type * as React from 'react' import type { ThunkDispatch } from 'redux-thunk' import type { HoverOnStepAction, @@ -47,10 +47,18 @@ export interface ConnectedStepInfoProps { stepId: StepIdType stepNumber: number dragHovered?: boolean + openedOverflowMenuId?: string | null + setOpenedOverflowMenuId?: Dispatch> } export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { - const { stepId, stepNumber, dragHovered = false } = props + const { + stepId, + stepNumber, + dragHovered = false, + openedOverflowMenuId, + setOpenedOverflowMenuId, + } = props const { t } = useTranslation('application') const dispatch = useDispatch>() const stepIds = useSelector(getOrderedStepIds) @@ -203,6 +211,8 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { /> )} void findStepIndex: (stepId: StepIdType) => number orderedStepIds: string[] + openedOverflowMenuId?: string | null + setOpenedOverflowMenuId?: Dispatch> } interface DropType { @@ -30,7 +33,15 @@ interface DropType { } function DragDropStep(props: DragDropStepProps): JSX.Element { - const { stepId, moveStep, findStepIndex, orderedStepIds, stepNumber } = props + const { + stepId, + moveStep, + findStepIndex, + orderedStepIds, + stepNumber, + openedOverflowMenuId, + setOpenedOverflowMenuId, + } = props const stepRef = useRef(null) const [{ isDragging }, drag] = useDrag( @@ -73,6 +84,8 @@ function DragDropStep(props: DragDropStepProps): JSX.Element { data-handler-id={handlerId} > (null) const findStepIndex = (stepId: StepIdType): number => orderedStepIds.findIndex(id => stepId === id) @@ -123,6 +139,8 @@ export function DraggableSteps(props: DraggableStepsProps): JSX.Element | null { moveStep={moveStep} findStepIndex={findStepIndex} orderedStepIds={orderedStepIds} + openedOverflowMenuId={openedOverflowMenuId} + setOpenedOverflowMenuId={setOpenedOverflowMenuId} /> ))} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx index ce5860b2cbf..4ed55987f08 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx @@ -35,6 +35,11 @@ import { LINE_CLAMP_TEXT_STYLE } from '../../../../atoms' import { StepOverflowMenu } from './StepOverflowMenu' import { capitalizeFirstLetterAfterNumber } from './utils' +import type { + SetStateAction, + Dispatch, + MouseEvent as ReactMouseEvent, +} from 'react' import type { ThunkDispatch } from 'redux-thunk' import type { IconName } from '@opentrons/components' import type { StepIdType } from '../../../../form-types' @@ -42,16 +47,18 @@ import type { BaseState } from '../../../../types' const STARTING_DECK_STATE = 'Starting deck state' const FINAL_DECK_STATE = 'Final deck state' - +const PX_HEIGHT_TO_TOP_OF_CONTAINER = 32 export interface StepContainerProps { title: string iconName: IconName + openedOverflowMenuId?: string | null + setOpenedOverflowMenuId?: Dispatch> stepId?: string iconColor?: string - onClick?: (event: React.MouseEvent) => void - onDoubleClick?: (event: React.MouseEvent) => void - onMouseEnter?: (event: React.MouseEvent) => void - onMouseLeave?: (event: React.MouseEvent) => void + onClick?: (event: ReactMouseEvent) => void + onDoubleClick?: (event: ReactMouseEvent) => void + onMouseEnter?: (event: ReactMouseEvent) => void + onMouseLeave?: (event: ReactMouseEvent) => void selected?: boolean hovered?: boolean hasError?: boolean @@ -74,10 +81,11 @@ export function StepContainer(props: StepContainerProps): JSX.Element { hasError = false, isStepAfterError = false, dragHovered = false, + setOpenedOverflowMenuId, + openedOverflowMenuId, } = props const [top, setTop] = useState(0) const menuRootRef = useRef(null) - const [stepOverflowMenu, setStepOverflowMenu] = useState(false) const isStartingOrEndingState = title === STARTING_DECK_STATE || title === FINAL_DECK_STATE const dispatch = useDispatch>() @@ -104,22 +112,21 @@ export function StepContainer(props: StepContainerProps): JSX.Element { menuRootRef.current?.contains(event.target) ) - if (wasOutside && stepOverflowMenu) { - setStepOverflowMenu(false) + if (wasOutside) { + setOpenedOverflowMenuId?.(null) } } - const handleOverflowClick = (event: React.MouseEvent): void => { - const { clientY } = event - + const handleOverflowClick = (event: ReactMouseEvent): void => { + const buttonRect = event.currentTarget.getBoundingClientRect() const screenHeight = window.innerHeight - const rootHeight = menuRootRef.current - ? menuRootRef.current.offsetHeight - : 0 + const rootHeight = menuRootRef.current?.offsetHeight || 0 + + const spaceBelow = screenHeight - buttonRect.bottom const top = - screenHeight - clientY > rootHeight - ? clientY + 5 - : clientY - rootHeight - 5 + spaceBelow > rootHeight + ? buttonRect.bottom - PX_HEIGHT_TO_TOP_OF_CONTAINER + : buttonRect.top - rootHeight + PX_HEIGHT_TO_TOP_OF_CONTAINER setTop(top) } @@ -135,7 +142,7 @@ export function StepContainer(props: StepContainerProps): JSX.Element { if (stepId != null) { dispatch(populateForm(stepId)) } - setStepOverflowMenu(false) + setOpenedOverflowMenuId?.(null) } const onDeleteClickAction = (): void => { @@ -168,7 +175,6 @@ export function StepContainer(props: StepContainerProps): JSX.Element { ) } } - const { confirm: confirmDelete, showConfirmation: showDeleteConfirmation, @@ -242,10 +248,15 @@ export function StepContainer(props: StepContainerProps): JSX.Element { { + onClick={(e: ReactMouseEvent) => { e.preventDefault() e.stopPropagation() - setStepOverflowMenu(prev => !prev) + if (openedOverflowMenuId === stepId) { + setOpenedOverflowMenuId?.(null) + } else { + setOpenedOverflowMenuId?.(stepId ?? null) + } + handleOverflowClick(e) }} /> @@ -262,10 +273,12 @@ export function StepContainer(props: StepContainerProps): JSX.Element { /> ) : null} - {stepOverflowMenu && stepId != null + {stepId != null && + openedOverflowMenuId === stepId && + setOpenedOverflowMenuId != null ? createPortal( top: number - setStepOverflowMenu: React.Dispatch> + setOpenedOverflowMenuId: React.Dispatch> handleEdit: () => void confirmDelete: () => void confirmMultiDelete: () => void @@ -44,7 +44,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { stepId, menuRootRef, top, - setStepOverflowMenu, + setOpenedOverflowMenuId, handleEdit, confirmDelete, confirmMultiDelete, @@ -91,7 +91,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { ref={menuRootRef} zIndex={12} top={top} - left="19.5rem" + left="18.75rem" position={POSITION_ABSOLUTE} whiteSpace={NO_WRAP} borderRadius={BORDERS.borderRadius8} @@ -109,7 +109,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { disabled={batchEditFormHasUnstagedChanges} onClick={() => { duplicateMultipleSteps() - setStepOverflowMenu(false) + setOpenedOverflowMenuId(null) }} > {t('duplicate_steps')} @@ -118,7 +118,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { { confirmMultiDelete() - setStepOverflowMenu(false) + setOpenedOverflowMenuId(null) }} > {t('delete_steps')} @@ -133,7 +133,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { { - setStepOverflowMenu(false) + setOpenedOverflowMenuId(null) dispatch(hoverOnStep(stepId)) dispatch(toggleViewSubstep(stepId)) dispatch(analyticsEvent(selectViewDetailsEvent)) @@ -146,7 +146,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { disabled={singleEditFormHasUnsavedChanges} onClick={() => { duplicateStep(stepId) - setStepOverflowMenu(false) + setOpenedOverflowMenuId(null) }} > {t('duplicate')} @@ -155,7 +155,7 @@ export function StepOverflowMenu(props: StepOverflowMenuProps): JSX.Element { { confirmDelete() - setStepOverflowMenu(false) + setOpenedOverflowMenuId(null) }} > {t('delete')} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx index 502ccf68f06..d283468dc33 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/__tests__/StepOverflowMenu.test.tsx @@ -58,7 +58,7 @@ describe('StepOverflowMenu', () => { stepId: moveLiquidStepId, top: 0, menuRootRef: { current: null }, - setStepOverflowMenu: vi.fn(), + setOpenedOverflowMenuId: vi.fn(), multiSelectItemIds: [], handleEdit: vi.fn(), confirmDelete: mockConfirm, From e0c4ded72f4a8644bbe0cc02c2674581be894e66 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Thu, 14 Nov 2024 17:23:18 -0500 Subject: [PATCH 23/31] chore(release): internal release notes ot3@v2.2.0-alpha.1 (#16572) --- api/release-notes-internal.md | 4 ++++ app-shell/build/release-notes-internal.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 7a3d81e5cbf..1253f7e92fd 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -2,6 +2,10 @@ For more details about this release, please see the full [technical change log][ [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.2.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. + ## Internal Release 2.2.0-alpha.0 This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index b7780dc8c88..be1008ec824 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -1,6 +1,10 @@ For more details about this release, please see the full [technical changelog][]. [technical change log]: https://github.com/Opentrons/opentrons/releases +## Internal Release 2.2.0-alpha.1 + +This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. + ## Internal Release 2.2.0-alpha.0 This internal release, pulled from the `edge` branch, contains features being developed for 8.2.0. It's for internal testing only. From 53aad428c732de89643f9eba15bf036e225e5671 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Thu, 14 Nov 2024 17:23:53 -0500 Subject: [PATCH 24/31] test(analyses-snapshot): add capability to run against local code (#16520) --- .github/workflows/analyses-snapshot-test.yaml | 30 +++++-- analyses-snapshot-testing/Makefile | 80 +++++++++++-------- analyses-snapshot-testing/README.md | 29 ++++++- .../{Dockerfile => Dockerfile.analyze} | 8 +- .../citools/Dockerfile.base | 7 ++ .../citools/Dockerfile.local | 19 +++++ .../citools/Dockerfile.server | 8 +- .../citools/generate_analyses.py | 11 ++- 8 files changed, 135 insertions(+), 57 deletions(-) rename analyses-snapshot-testing/citools/{Dockerfile => Dockerfile.analyze} (75%) create mode 100644 analyses-snapshot-testing/citools/Dockerfile.base create mode 100644 analyses-snapshot-testing/citools/Dockerfile.local diff --git a/.github/workflows/analyses-snapshot-test.yaml b/.github/workflows/analyses-snapshot-test.yaml index 09539d873e9..fffdd6b667d 100644 --- a/.github/workflows/analyses-snapshot-test.yaml +++ b/.github/workflows/analyses-snapshot-test.yaml @@ -45,12 +45,13 @@ jobs: timeout-minutes: 15 runs-on: ubuntu-latest env: + BASE_IMAGE_NAME: opentrons-python-base:3.10 ANALYSIS_REF: ${{ github.event.inputs.ANALYSIS_REF || github.head_ref || 'edge' }} SNAPSHOT_REF: ${{ github.event.inputs.SNAPSHOT_REF || github.head_ref || 'edge' }} # If we're running because of workflow_dispatch, use the user input to decide # whether to open a PR on failure. Otherwise, there is no user input, # so we only open a PR if the PR has the label 'gen-analyses-snapshot-pr' - OPEN_PR_ON_FAILURE: ${{ (github.event_name == 'workflow_dispatch' && github.events.inputs.OPEN_PR_ON_FAILURE) || ((github.event_name != 'workflow_dispatch') && (contains(github.event.pull_request.labels.*.name, 'gen-analyses-snapshot-pr'))) }} + OPEN_PR_ON_FAILURE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.OPEN_PR_ON_FAILURE) || ((github.event_name != 'workflow_dispatch') && (contains(github.event.pull_request.labels.*.name, 'gen-analyses-snapshot-pr'))) }} PR_TARGET_BRANCH: ${{ github.event.pull_request.base.ref || 'not a pr'}} steps: - name: Checkout Repository @@ -71,9 +72,24 @@ jobs: echo "Analyses snapshots match ${{ env.PR_TARGET_BRANCH }} snapshots." fi - - name: Docker Build + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build base image + id: build_base_image + uses: docker/build-push-action@v6 + with: + context: analyses-snapshot-testing/citools + file: analyses-snapshot-testing/citools/Dockerfile.base + push: false + load: true + tags: ${{ env.BASE_IMAGE_NAME }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build analysis image working-directory: analyses-snapshot-testing - run: make build-opentrons-analysis + run: make build-opentrons-analysis BASE_IMAGE_NAME=${{ env.BASE_IMAGE_NAME }} ANALYSIS_REF=${{ env.ANALYSIS_REF }} CACHEBUST=${{ github.run_number }} - name: Set up Python 3.13 uses: actions/setup-python@v5 @@ -112,8 +128,8 @@ jobs: commit-message: 'fix(analyses-snapshot-testing): heal analyses snapshots' title: 'fix(analyses-snapshot-testing): heal ${{ env.ANALYSIS_REF }} snapshots' body: 'This PR was requested on the PR https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF}}' - base: ${{ env.SNAPSHOT_REF}} + branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF }}' + base: ${{ env.SNAPSHOT_REF }} - name: Comment on feature PR if: always() && steps.create_pull_request.outcome == 'success' && github.event_name == 'pull_request' @@ -135,5 +151,5 @@ jobs: commit-message: 'fix(analyses-snapshot-testing): heal ${{ env.ANALYSIS_REF }} snapshots' title: 'fix(analyses-snapshot-testing): heal ${{ env.ANALYSIS_REF }} snapshots' body: 'The ${{ env.ANALYSIS_REF }} overnight analyses snapshot test is failing. This PR was opened to alert us to the failure.' - branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF}}' - base: ${{ env.SNAPSHOT_REF}} + branch: 'analyses-snapshot-testing/${{ env.ANALYSIS_REF }}-from-${{ env.SNAPSHOT_REF }}' + base: ${{ env.SNAPSHOT_REF }} diff --git a/analyses-snapshot-testing/Makefile b/analyses-snapshot-testing/Makefile index 13c4e603f3c..de5e0381131 100644 --- a/analyses-snapshot-testing/Makefile +++ b/analyses-snapshot-testing/Makefile @@ -1,38 +1,56 @@ +BASE_IMAGE_NAME ?= opentrons-python-base:3.10 +CACHEBUST ?= $(shell date +%s) +ANALYSIS_REF ?= edge +PROTOCOL_NAMES ?= all +OVERRIDE_PROTOCOL_NAMES ?= all +OPENTRONS_VERSION ?= edge + +export OPENTRONS_VERSION # used for server +export ANALYSIS_REF # used for analysis and snapshot test +export PROTOCOL_NAMES # used for the snapshot test +export OVERRIDE_PROTOCOL_NAMES # used for the snapshot test + +ifeq ($(CI), true) + PYTHON=python +else + PYTHON=pyenv exec python +endif + .PHONY: black black: - python -m pipenv run python -m black . + $(PYTHON) -m pipenv run python -m black . .PHONY: black-check black-check: - python -m pipenv run python -m black . --check + $(PYTHON) -m pipenv run python -m black . --check .PHONY: ruff ruff: - python -m pipenv run python -m ruff check . --fix + $(PYTHON) -m pipenv run python -m ruff check . --fix .PHONY: ruff-check ruff-check: - python -m pipenv run python -m ruff check . + $(PYTHON) -m pipenv run python -m ruff check . .PHONY: mypy mypy: - python -m pipenv run python -m mypy automation tests citools + $(PYTHON) -m pipenv run python -m mypy automation tests citools .PHONY: lint lint: black-check ruff-check mypy .PHONY: format format: - @echo runnning black + @echo "Running black" $(MAKE) black - @echo running ruff + @echo "Running ruff" $(MAKE) ruff - @echo formatting the readme with yarn prettier + @echo "Formatting the readme with yarn prettier" $(MAKE) format-readme .PHONY: test-ci test-ci: - python -m pipenv run python -m pytest -m "emulated_alpha" + $(PYTHON) -m pipenv run python -m pytest -m "emulated_alpha" .PHONY: test-protocol-analysis test-protocol-analysis: @@ -40,66 +58,64 @@ test-protocol-analysis: .PHONY: setup setup: install-pipenv - python -m pipenv install + $(PYTHON) -m pipenv install .PHONY: teardown teardown: - python -m pipenv --rm + $(PYTHON) -m pipenv --rm .PHONY: format-readme format-readme: - yarn prettier --ignore-path .eslintignore --write analyses-snapshot-testing/**/*.md + yarn prettier --ignore-path .eslintignore --write analyses-snapshot-testing/**/*.md .github/workflows/analyses-snapshot-test.yaml .PHONY: install-pipenv install-pipenv: - python -m pip install -U pipenv - -ANALYSIS_REF ?= edge -PROTOCOL_NAMES ?= all -OVERRIDE_PROTOCOL_NAMES ?= all - -export ANALYSIS_REF -export PROTOCOL_NAMES -export OVERRIDE_PROTOCOL_NAMES + $(PYTHON) -m pip install -U pipenv .PHONY: snapshot-test snapshot-test: @echo "ANALYSIS_REF is $(ANALYSIS_REF)" @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" - python -m pipenv run pytest -k analyses_snapshot_test -vv + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test -vv .PHONY: snapshot-test-update snapshot-test-update: @echo "ANALYSIS_REF is $(ANALYSIS_REF)" @echo "PROTOCOL_NAMES is $(PROTOCOL_NAMES)" @echo "OVERRIDE_PROTOCOL_NAMES is $(OVERRIDE_PROTOCOL_NAMES)" - python -m pipenv run pytest -k analyses_snapshot_test --snapshot-update + $(PYTHON) -m pipenv run pytest -k analyses_snapshot_test --snapshot-update -CACHEBUST := $(shell date +%s) +.PHONY: build-base-image +build-base-image: + @echo "Building the base image $(BASE_IMAGE_NAME)" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) -f citools/Dockerfile.base -t $(BASE_IMAGE_NAME) citools/. .PHONY: build-opentrons-analysis build-opentrons-analysis: @echo "Building docker image for $(ANALYSIS_REF)" @echo "The image will be named opentrons-analysis:$(ANALYSIS_REF)" @echo "If you want to build a different version, run 'make build-opentrons-analysis ANALYSIS_REF='" - @echo "Cache is always busted to ensure latest version of the code is used" - docker build --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-analysis:$(ANALYSIS_REF) citools/. + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg ANALYSIS_REF=$(ANALYSIS_REF) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-analysis:$(ANALYSIS_REF) -f citools/Dockerfile.analyze citools/. + +.PHONY: local-build +local-build: + @echo "Building docker image for your local opentrons code" + @echo "The image will be named opentrons-analysis:local" + @echo "For a fresh build, run 'make local-build NO_CACHE=1'" + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) $(BUILD_FLAGS) -t opentrons-analysis:local -f citools/Dockerfile.local .. || true + @echo "Build complete" .PHONY: generate-protocols generate-protocols: - python -m pipenv run python -m automation.data.protocol_registry - - -OPENTRONS_VERSION ?= edge -export OPENTRONS_VERSION + $(PYTHON) -m pipenv run python -m automation.data.protocol_registry .PHONY: build-rs build-rs: @echo "Building docker image for opentrons-robot-server:$(OPENTRONS_VERSION)" @echo "Cache is always busted to ensure latest version of the code is used" @echo "If you want to build a different version, run 'make build-rs OPENTRONS_VERSION=chore_release-8.0.0'" - docker build --build-arg OPENTRONS_VERSION=$(OPENTRONS_VERSION) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-robot-server:$(OPENTRONS_VERSION) -f citools/Dockerfile.server . + docker build --build-arg BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) --build-arg OPENTRONS_VERSION=$(OPENTRONS_VERSION) --build-arg CACHEBUST=$(CACHEBUST) -t opentrons-robot-server:$(OPENTRONS_VERSION) -f citools/Dockerfile.server . .PHONY: run-flex run-flex: diff --git a/analyses-snapshot-testing/README.md b/analyses-snapshot-testing/README.md index 51a8e194ca1..78423b8447f 100644 --- a/analyses-snapshot-testing/README.md +++ b/analyses-snapshot-testing/README.md @@ -18,7 +18,10 @@ > This ALWAYS gets the remote code pushed to Opentrons/opentrons for the specified ANALYSIS_REF -`make build-opentrons-analysis ANALYSIS_REF=chore_release-8.0.0` +- build the base image + - `make build-base-image` +- build the opentrons-analysis image + - `make build-opentrons-analysis ANALYSIS_REF=release` ## Running the tests locally @@ -51,10 +54,28 @@ ```shell cd analyses-snapshot-testing \ -&& make build-rs OPENTRONS_VERSION=chore_release-8.0.0 \ -&& make run-rs OPENTRONS_VERSION=chore_release-8.0.0` +&& make build-base-image \ +&& make build-rs OPENTRONS_VERSION=release \ +&& make run-rs OPENTRONS_VERSION=release` ``` ### Default OPENTRONS_VERSION=edge in the Makefile so you can omit it if you want latest edge -`cd analyses-snapshot-testing && make build-rs && make run-rs` +```shell +cd analyses-snapshot-testing \ +&& make build-base-image \ +&& make build-rs \ +&& make run-rs +``` + +## Running the Analyses Battery against your local code + +> This copies in your local code to the container and runs the analyses battery against it. + +1. `make build-base-image` +1. `make build-local` +1. `make local-snapshot-test` + +You have the option to specify one or many protocols to run the analyses on. This is also described above [Running the tests against specific protocols](#running-the-tests-against-specific-protocols) + +- `make local-snapshot-test PROTOCOL_NAMES=Flex_S_v2_19_Illumina_DNA_PCR_Free OVERRIDE_PROTOCOL_NAMES=none` diff --git a/analyses-snapshot-testing/citools/Dockerfile b/analyses-snapshot-testing/citools/Dockerfile.analyze similarity index 75% rename from analyses-snapshot-testing/citools/Dockerfile rename to analyses-snapshot-testing/citools/Dockerfile.analyze index 123b7636652..1b85981cdaf 100644 --- a/analyses-snapshot-testing/citools/Dockerfile +++ b/analyses-snapshot-testing/citools/Dockerfile.analyze @@ -1,10 +1,6 @@ -# Use 3.10 just like the app does -FROM python:3.10-slim-bullseye +ARG BASE_IMAGE_NAME=opentrons-python-base:3.10 -# Update packages and install git -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y git libsystemd-dev +FROM ${BASE_IMAGE_NAME} # Define build arguments ARG ANALYSIS_REF=edge diff --git a/analyses-snapshot-testing/citools/Dockerfile.base b/analyses-snapshot-testing/citools/Dockerfile.base new file mode 100644 index 00000000000..086987e671b --- /dev/null +++ b/analyses-snapshot-testing/citools/Dockerfile.base @@ -0,0 +1,7 @@ +# Use Python 3.10 as the base image +FROM python:3.10-slim-bullseye + +# Update packages and install dependencies +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y git libsystemd-dev build-essential pkg-config network-manager diff --git a/analyses-snapshot-testing/citools/Dockerfile.local b/analyses-snapshot-testing/citools/Dockerfile.local new file mode 100644 index 00000000000..2346b4680c2 --- /dev/null +++ b/analyses-snapshot-testing/citools/Dockerfile.local @@ -0,0 +1,19 @@ +ARG BASE_IMAGE_NAME=opentrons-python-base:3.10 + +FROM ${BASE_IMAGE_NAME} + +# Set the working directory in the container +WORKDIR /opentrons + +# Copy everything from the build context into the /opentrons directory +# root directory .dockerignore file is respected +COPY . /opentrons + +# Install required packages from the copied code +RUN python -m pip install -U ./shared-data/python +RUN python -m pip install -U ./hardware[flex] +RUN python -m pip install -U ./api +RUN python -m pip install -U pandas==1.4.3 + +# The default command to keep the container running +CMD ["tail", "-f", "/dev/null"] diff --git a/analyses-snapshot-testing/citools/Dockerfile.server b/analyses-snapshot-testing/citools/Dockerfile.server index 6d4d9edcda3..0c44c1e04f0 100644 --- a/analyses-snapshot-testing/citools/Dockerfile.server +++ b/analyses-snapshot-testing/citools/Dockerfile.server @@ -1,10 +1,6 @@ -# Use Python 3.10 as the base image -FROM python:3.10-slim-bullseye +ARG BASE_IMAGE_NAME=opentrons-python-base:3.10 -# Update packages and install dependencies -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install -y git libsystemd-dev build-essential pkg-config network-manager +FROM ${BASE_IMAGE_NAME} # Define build arguments ARG OPENTRONS_VERSION=edge diff --git a/analyses-snapshot-testing/citools/generate_analyses.py b/analyses-snapshot-testing/citools/generate_analyses.py index 52aba70363b..7d550b47776 100644 --- a/analyses-snapshot-testing/citools/generate_analyses.py +++ b/analyses-snapshot-testing/citools/generate_analyses.py @@ -24,7 +24,7 @@ HOST_RESULTS: Path = Path(Path(__file__).parent.parent, "analysis_results") ANALYSIS_SUFFIX: str = "analysis.json" ANALYSIS_TIMEOUT_SECONDS: int = 30 -ANALYSIS_CONTAINER_INSTANCES: int = 5 +MAX_ANALYSIS_CONTAINER_INSTANCES: int = 5 console = Console() @@ -241,6 +241,12 @@ def analyze_against_image(tag: str, protocols: List[TargetProtocol], num_contain return protocols +def get_container_instances(protocol_len: int) -> int: + # Scaling linearly with the number of protocols + instances = max(1, min(MAX_ANALYSIS_CONTAINER_INSTANCES, protocol_len // 10)) + return instances + + def generate_analyses_from_test(tag: str, protocols: List[Protocol]) -> None: """Generate analyses from the tests.""" start_time = time.time() @@ -260,6 +266,7 @@ def generate_analyses_from_test(tag: str, protocols: List[Protocol]) -> None: protocol_custom_labware_paths_in_container(test_protocol), ) ) - analyze_against_image(tag, protocols_to_process, ANALYSIS_CONTAINER_INSTANCES) + instance_count = get_container_instances(len(protocols_to_process)) + analyze_against_image(tag, protocols_to_process, instance_count) end_time = time.time() console.print(f"Clock time to generate analyses: {end_time - start_time:.2f} seconds.") From 49733562dfbb2213705fa170843b5f04fe511752 Mon Sep 17 00:00:00 2001 From: Josh McVey Date: Fri, 15 Nov 2024 08:33:38 -0600 Subject: [PATCH 25/31] test(pd): typescript and test pattern for cypress (#16755) # Overview - Switch to Typescript - Map in test files - Use Enums or Objects to define content and locator strings. - Initial data driven pattern for Create Protocol Flow --- protocol-designer/Makefile | 45 +++- protocol-designer/cypress/README.md | 27 +++ protocol-designer/cypress/e2e/createNew.cy.ts | 43 ++++ protocol-designer/cypress/e2e/home.cy.ts | 10 + protocol-designer/cypress/e2e/import.cy.ts | 24 ++ .../cypress/e2e/migrations.cy.js | 201 --------------- .../cypress/e2e/migrations.cy.ts | 94 ++++++++ protocol-designer/cypress/e2e/settings.cy.js | 149 ------------ protocol-designer/cypress/e2e/settings.cy.ts | 53 ++++ protocol-designer/cypress/e2e/testfiles.cy.ts | 30 +++ .../cypress/e2e/urlNavigation.cy.ts | 16 ++ .../cypress/fixtures/garbage.txt | 1 + .../cypress/fixtures/invalid_json.txt | 7 + protocol-designer/cypress/mocks/file-saver.js | 6 - protocol-designer/cypress/support/commands.js | 117 --------- protocol-designer/cypress/support/commands.ts | 228 ++++++++++++++++++ .../cypress/support/createNew.ts | 131 ++++++++++ .../cypress/support/{e2e.js => e2e.ts} | 0 protocol-designer/cypress/support/import.ts | 142 +++++++++++ .../cypress/support/testFiles.ts | 73 ++++++ .../cypress/support/universalActions.ts | 16 ++ protocol-designer/cypress/support/utils.ts | 11 + protocol-designer/tsconfig.cypress.json | 14 ++ protocol-designer/tsconfig.json | 3 + protocol-designer/vite.config.ts | 9 - 25 files changed, 966 insertions(+), 484 deletions(-) create mode 100644 protocol-designer/cypress/README.md create mode 100644 protocol-designer/cypress/e2e/createNew.cy.ts create mode 100644 protocol-designer/cypress/e2e/home.cy.ts create mode 100644 protocol-designer/cypress/e2e/import.cy.ts delete mode 100644 protocol-designer/cypress/e2e/migrations.cy.js create mode 100644 protocol-designer/cypress/e2e/migrations.cy.ts delete mode 100644 protocol-designer/cypress/e2e/settings.cy.js create mode 100644 protocol-designer/cypress/e2e/settings.cy.ts create mode 100644 protocol-designer/cypress/e2e/testfiles.cy.ts create mode 100644 protocol-designer/cypress/e2e/urlNavigation.cy.ts create mode 100644 protocol-designer/cypress/fixtures/garbage.txt create mode 100644 protocol-designer/cypress/fixtures/invalid_json.txt delete mode 100644 protocol-designer/cypress/mocks/file-saver.js delete mode 100644 protocol-designer/cypress/support/commands.js create mode 100644 protocol-designer/cypress/support/commands.ts create mode 100644 protocol-designer/cypress/support/createNew.ts rename protocol-designer/cypress/support/{e2e.js => e2e.ts} (100%) create mode 100644 protocol-designer/cypress/support/import.ts create mode 100644 protocol-designer/cypress/support/testFiles.ts create mode 100644 protocol-designer/cypress/support/universalActions.ts create mode 100644 protocol-designer/cypress/support/utils.ts create mode 100644 protocol-designer/tsconfig.cypress.json diff --git a/protocol-designer/Makefile b/protocol-designer/Makefile index 14792b22b7b..dc201e472bc 100644 --- a/protocol-designer/Makefile +++ b/protocol-designer/Makefile @@ -59,9 +59,9 @@ serve: all # end to end tests .PHONY: test-e2e -test-e2e: +test-e2e: clean-downloads concurrently --no-color --kill-others --success first --names "protocol-designer-server,protocol-designer-tests" \ - "$(MAKE) dev CYPRESS=1" \ + "$(MAKE) dev" \ "wait-on http://localhost:5178/ && cypress run --browser chrome --headless --record false" .PHONY: test @@ -71,3 +71,44 @@ test: .PHONY: test-cov test-cov: make -C .. test-js-protocol-designer tests=$(tests) test_opts="$(test_opts)" cov_opts="$(cov_opts)" + +CYPRESS_ESLINT_GLOB := "cypress/**/*.{js,ts,tsx}" +CYPRESS_PRETTIER_GLOB := "cypress/**/*.{js,ts,tsx,md,json}" + +.PHONY: cy-lint-check +cy-lint-check: cy-lint-eslint-check cy-lint-prettier-check + @echo "Cypress lint check completed." + +.PHONY: cy-lint-fix +cy-lint-fix: cy-lint-eslint-fix cy-lint-prettier-fix + @echo "Cypress lint fix applied." + +.PHONY: cy-lint-eslint-check +cy-lint-eslint-check: + yarn eslint --ignore-path ../.eslintignore $(CYPRESS_ESLINT_GLOB) + @echo "Cypress ESLint check completed." + +.PHONY: cy-lint-eslint-fix +cy-lint-eslint-fix: + yarn eslint --fix --ignore-pattern ../.eslintignore $(CYPRESS_ESLINT_GLOB) + @echo "Cypress ESLint fix applied." + +.PHONY: cy-lint-prettier-check +cy-lint-prettier-check: + yarn prettier --ignore-path ../.eslintignore --check $(CYPRESS_PRETTIER_GLOB) + @echo "Cypress Prettier check completed." + +.PHONY: cy-lint-prettier-fix +cy-lint-prettier-fix: + yarn prettier --ignore-path ../.eslintignore --write $(CYPRESS_PRETTIER_GLOB) + @echo "Cypress Prettier fix applied." + +.PHONY: cy-ui +cy-ui: + @echo "Running Cypress UI" + @echo "Dev environment must be running" + yarn cypress open + +.PHONY: clean-downloads +clean-downloads: + shx rm -rf cypress/downloads \ No newline at end of file diff --git a/protocol-designer/cypress/README.md b/protocol-designer/cypress/README.md new file mode 100644 index 00000000000..2fb0666f3c6 --- /dev/null +++ b/protocol-designer/cypress/README.md @@ -0,0 +1,27 @@ +# Cypress in PD + +This is a guide to running Cypress tests in Protocol Designer. + +- `cat Makefile` to see all the available commands + +## Cypress Organization + +- `cypress/e2e` contains all the tests +- `cypress/support` contains all the support files + - `cypress/support/commands` contains added commands and may be used for actions on the home page and header + - use the files representing the different parts of the app to create reusable functions +- `../fixtures` (PD root fixtures) and `cypress/fixtures` contains all the fixtures (files) that we might want to use in tests + +## Fixtures + +We need to read data from files. `support/testFiles.ts` maps in files and provides an enum to reference them by. All files that need to be read in should be mapped through testFiles. + +## Tactics + +### Locators + + + +1. use a simple cy.contains() +1. try aria-\* attributes +1. data-testid attribute (then use getByTestId custom command) diff --git a/protocol-designer/cypress/e2e/createNew.cy.ts b/protocol-designer/cypress/e2e/createNew.cy.ts new file mode 100644 index 00000000000..be578415bee --- /dev/null +++ b/protocol-designer/cypress/e2e/createNew.cy.ts @@ -0,0 +1,43 @@ +import { + Actions, + Verifications, + runCreateTest, + verifyCreateProtocolPage, +} from '../support/createNew' +import { UniversalActions } from '../support/universalActions' + +describe('The Redesigned Create Protocol Landing Page', () => { + beforeEach(() => { + cy.visit('/') + }) + + it('content and step 1 flow works', () => { + cy.clickCreateNew() + cy.verifyCreateNewHeader() + verifyCreateProtocolPage() + const steps: Array = [ + Verifications.OnStep1, + Verifications.FlexSelected, + UniversalActions.Snapshot, + Actions.SelectOT2, + Verifications.OT2Selected, + UniversalActions.Snapshot, + Actions.SelectFlex, + Verifications.FlexSelected, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.OnStep2, + Verifications.NinetySixChannel, + UniversalActions.Snapshot, + Actions.GoBack, + Verifications.OnStep1, + Actions.SelectOT2, + Actions.Confirm, + Verifications.OnStep2, + Verifications.NotNinetySixChannel, + UniversalActions.Snapshot, + ] + + runCreateTest(steps) + }) +}) diff --git a/protocol-designer/cypress/e2e/home.cy.ts b/protocol-designer/cypress/e2e/home.cy.ts new file mode 100644 index 00000000000..211100898bf --- /dev/null +++ b/protocol-designer/cypress/e2e/home.cy.ts @@ -0,0 +1,10 @@ +describe('The Home Page', () => { + beforeEach(() => { + cy.visit('/') + }) + + it('successfully loads', () => { + cy.verifyFullHeader() + cy.verifyHomePage() + }) +}) diff --git a/protocol-designer/cypress/e2e/import.cy.ts b/protocol-designer/cypress/e2e/import.cy.ts new file mode 100644 index 00000000000..83ddaf0577d --- /dev/null +++ b/protocol-designer/cypress/e2e/import.cy.ts @@ -0,0 +1,24 @@ +import { TestFilePath, getTestFile } from '../support/testFiles' +import { + verifyOldProtocolModal, + verifyImportProtocolPage, +} from '../support/import' + +describe('The Import Page', () => { + beforeEach(() => { + cy.visit('/') + }) + + it('successfully loads a protocol exported on a previous version', () => { + const protocol = getTestFile(TestFilePath.DoItAllV7) + cy.importProtocol(protocol.path) + verifyOldProtocolModal() + verifyImportProtocolPage(protocol) + }) + + it('successfully loads a protocol exported on the current version', () => { + const protocol = getTestFile(TestFilePath.DoItAllV8) + cy.importProtocol(protocol.path) + verifyImportProtocolPage(protocol) + }) +}) diff --git a/protocol-designer/cypress/e2e/migrations.cy.js b/protocol-designer/cypress/e2e/migrations.cy.js deleted file mode 100644 index e5a8d632541..00000000000 --- a/protocol-designer/cypress/e2e/migrations.cy.js +++ /dev/null @@ -1,201 +0,0 @@ -import 'cypress-file-upload' -import cloneDeep from 'lodash/cloneDeep' -import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' -const semver = require('semver') - -describe('Protocol fixtures migrate and match snapshots', () => { - beforeEach(() => { - cy.visit('/') - }) - - const testCases = [ - { - title: 'example_1_1_0 (schema 1, PD version 1.1.1) -> PD 8.2.x, schema 8', - importFixture: '../../fixtures/protocol/1/example_1_1_0.json', - expectedExportFixture: - '../../fixtures/protocol/8/example_1_1_0MigratedToV8.json', - unusedHardware: true, - migrationModal: 'newLabwareDefs', - }, - { - title: 'doItAllV3 (schema 3, PD version 4.0.0) -> PD 8.2.x, schema 8', - importFixture: '../../fixtures/protocol/4/doItAllV3.json', - expectedExportFixture: - '../../fixtures/protocol/8/doItAllV3MigratedToV8.json', - unusedHardware: false, - migrationModal: 'v8.1', - }, - { - title: 'doItAllV4 (schema 4, PD version 4.0.0) -> PD 8.2.x, schema 8', - importFixture: '../../fixtures/protocol/4/doItAllV4.json', - expectedExportFixture: - '../../fixtures/protocol/8/doItAllV4MigratedToV8.json', - unusedHardware: false, - migrationModal: 'v8.1', - }, - { - title: - 'doItAllv7MigratedToV8 (schema 7, PD version 8.0.0) -> should migrate to 8.2.x, schema 8', - importFixture: '../../fixtures/protocol/7/doItAllV7.json', - expectedExportFixture: - '../../fixtures/protocol/8/doItAllV7MigratedToV8.json', - unusedHardware: false, - migrationModal: 'v8.1', - }, - { - title: - '96-channel full and column schema 8 -> should migrate to 8.2.x, schema 8', - importFixture: - '../../fixtures/protocol/8/ninetySixChannelFullAndColumn.json', - expectedExportFixture: - '../../fixtures/protocol/8/ninetySixChannelFullAndColumn.json', - migrationModal: null, - unusedHardware: false, - }, - { - title: - 'doItAllV8 flex robot -> reimported, should migrate to 8.2.x, schema 8', - importFixture: '../../fixtures/protocol/8/doItAllV8.json', - expectedExportFixture: '../../fixtures/protocol/8/doItAllV8.json', - migrationModal: null, - unusedHardware: false, - }, - { - title: - 'new advanced settings with multi temp => reimported, should not migrate and stay at 8.2.x, schema 8', - importFixture: - '../../fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json', - expectedExportFixture: - '../../fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json', - migrationModal: null, - unusedHardware: false, - }, - { - title: - 'thermocycler on Ot2 (schema 7, PD version 7.0.0) -> should migrate to 8.2.x, schema 8', - importFixture: '../../fixtures/protocol/7/thermocyclerOnOt2V7.json', - expectedExportFixture: - '../../fixtures/protocol/8/thermocyclerOnOt2V7MigratedToV8.json', - migrationModal: 'v8.1', - unusedHardware: true, - }, - ] - - testCases.forEach( - ({ - title, - importFixture, - expectedExportFixture, - unusedHardware, - migrationModal, - }) => { - it(title, () => { - cy.fixture(importFixture).then(fileContent => { - // TODO(IL, 2020-04-02): `cy.fixture` always parses .json files, though we want the plain text. - // So we have to use JSON.stringify. See https://github.com/cypress-io/cypress/issues/5395 - // Also, the latest version v4 of cypress-file-upload is too implicit to allow us to - // use the JSON.stringify workaround, so we're stuck on 3.5.3, - // see https://github.com/abramenal/cypress-file-upload/issues/175 - cy.get('[data-cy="landing-page"]') - .find('input[type=file]') - .upload({ - fileContent: JSON.stringify(fileContent), - fileName: 'fixture.json', - mimeType: 'application/json', - encoding: 'utf8', - }) - // wait until computation is done before proceeding, with generous timeout - cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( - 'not.exist' - ) - }) - - if (migrationModal) { - if (migrationModal === 'v8.1') { - cy.get('div') - .contains( - 'The default dispense height is now 1 mm from the bottom of the well' - ) - .should('exist') - cy.get('button').contains('Confirm', { matchCase: false }).click() - } else if (migrationModal === 'newLabwareDefs') { - cy.get('div') - .contains('Update protocol to use new labware definitions') - .should('exist') - cy.get('button').contains('Confirm', { matchCase: false }).click() - } else if (migrationModal === 'noBehaviorChange') { - cy.get('div') - .contains( - 'We have added new features since the last time this protocol was updated, but have not made any changes to existing protocol behavior' - ) - .should('exist') - cy.get('button').contains('Confirm', { matchCase: false }).click() - } - } - - cy.fixture(expectedExportFixture).then(expectedExportProtocol => { - cy.get('button').contains('Export').click() - - if (unusedHardware) { - cy.get('div') - .contains('Protocol has unused hardware') - .should('exist') - cy.get('button').contains('continue', { matchCase: false }).click() - } - - cy.window() - .its('__lastSavedFileBlob__') - .should('be.a', 'blob') - .should(async blob => { - const blobText = await blob.text() - const savedFile = JSON.parse(blobText) - const expectedFile = cloneDeep(expectedExportProtocol) - const version = semver.parse( - savedFile.designerApplication.version - ) - assert( - version != null, - `PD version ${version} is not valid semver` - ) - ;[savedFile, expectedFile].forEach(f => { - // Homogenize fields we don't want to compare - f.metadata.lastModified = 123 - f.designerApplication.data._internalAppBuildDate = 'Foo Date' - f.designerApplication.version = 'x.x.x' - - // currently stubbed because of the newly created trash id for movable trash support - Object.values( - f.designerApplication.data.savedStepForms - ).forEach(stepForm => { - if (stepForm.stepType === 'moveLiquid') { - stepForm.dropTip_location = 'trash drop tip location' - if (stepForm.blowout_location?.includes('trashBin')) { - stepForm.blowout_location = 'trash blowout location' - } - } - if (stepForm.stepType === 'mix') { - stepForm.dropTip_location = 'trash drop tip location' - stepForm.blowout_location = 'trash blowout location' - } - }) - f.commands.forEach(command => { - if ('key' in command) { - command.key = '123' - } - }) - }) - - expectDeepEqual(assert, savedFile, expectedFile) - }) - - cy.window() - .its('__lastSavedFileName__') - .should( - 'equal', - `${expectedExportProtocol.metadata.protocolName}.json` - ) - }) - }) - } - ) -}) diff --git a/protocol-designer/cypress/e2e/migrations.cy.ts b/protocol-designer/cypress/e2e/migrations.cy.ts new file mode 100644 index 00000000000..61470962bad --- /dev/null +++ b/protocol-designer/cypress/e2e/migrations.cy.ts @@ -0,0 +1,94 @@ +import 'cypress-file-upload' +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { MigrateTestCase, migrateAndMatchSnapshot } from '../support/import' +import { TestFilePath } from '../support/testFiles' + +describe('Protocol fixtures migrate and match snapshots', () => { + beforeEach(() => { + cy.visit('/') + }) + + const testCases: MigrateTestCase[] = [ + { + title: 'example_1_1_0 (schema 1, PD version 1.1.1) -> PD 8.2.x, schema 8', + importTestFile: TestFilePath.Example_1_1_0, + expectedTestFile: TestFilePath.Example_1_1_0V8, + unusedHardware: true, + migrationModal: 'newLabwareDefs', + }, + { + title: 'doItAllV3 (schema 3, PD version 4.0.0) -> PD 8.2.x, schema 8', + importTestFile: TestFilePath.DoItAllV3V4, + expectedTestFile: TestFilePath.DoItAllV3MigratedToV8, + unusedHardware: false, + migrationModal: 'v8.1', + }, + { + title: 'doItAllV4 (schema 4, PD version 4.0.0) -> PD 8.2.x, schema 8', + importTestFile: TestFilePath.DoItAllV4V4, + expectedTestFile: TestFilePath.DoItAllV4MigratedToV8, + unusedHardware: false, + migrationModal: 'v8.1', + }, + { + title: + 'doItAllv7MigratedToV8 (schema 7, PD version 8.0.0) -> should migrate to 8.2.x, schema 8', + importTestFile: TestFilePath.DoItAllV7, + expectedTestFile: TestFilePath.DoItAllV7MigratedToV8, + unusedHardware: false, + migrationModal: 'v8.1', + }, + { + title: + '96-channel full and column schema 8 -> should migrate to 8.2.x, schema 8', + importTestFile: TestFilePath.NinetySixChannelFullAndColumn, + expectedTestFile: TestFilePath.NinetySixChannelFullAndColumn, + unusedHardware: false, + migrationModal: null, + }, + { + title: + 'doItAllV8 flex robot -> reimported, should migrate to 8.2.x, schema 8', + importTestFile: TestFilePath.DoItAllV8, + expectedTestFile: TestFilePath.DoItAllV8, + unusedHardware: false, + migrationModal: null, + }, + { + title: + 'new advanced settings with multi temp => reimported, should not migrate and stay at 8.2.x, schema 8', + importTestFile: TestFilePath.NewAdvancedSettingsAndMultiTemp, + expectedTestFile: TestFilePath.NewAdvancedSettingsAndMultiTemp, + unusedHardware: false, + migrationModal: null, + }, + { + title: + 'thermocycler on Ot2 (schema 7, PD version 7.0.0) -> should migrate to 8.2.x, schema 8', + importTestFile: TestFilePath.ThermocyclerOnOt2V7, + expectedTestFile: TestFilePath.ThermocyclerOnOt2V7MigratedToV8, + migrationModal: 'v8.1', + unusedHardware: true, + }, + ] + + testCases.forEach( + ({ + title, + importTestFile, + expectedTestFile, + unusedHardware, + migrationModal, + }) => { + it(title, () => { + migrateAndMatchSnapshot({ + title, + importTestFile, + expectedTestFile, + unusedHardware, + migrationModal, + }) + }) + } + ) +}) diff --git a/protocol-designer/cypress/e2e/settings.cy.js b/protocol-designer/cypress/e2e/settings.cy.js deleted file mode 100644 index 4f52ef81fee..00000000000 --- a/protocol-designer/cypress/e2e/settings.cy.js +++ /dev/null @@ -1,149 +0,0 @@ -// TODO: refactor to test new settings page -describe('The Settings Page', () => { - // const exptlSettingText = 'Disable module placement restrictions' - // before(() => { - // cy.visit('/') - // }) - // it('Verify the settings page', () => { - // // displays the announcement modal and clicks "GOT IT!" to close it - // cy.closeAnnouncementModal() - // // contains a working settings button - // cy.openSettingsPage() - // cy.contains('App Settings') - // // contains an information section - // cy.get('h3').contains('Information').should('exist') - // // contains version information - // cy.contains('Protocol Designer Version').should('exist') - // // contains a hints section - // cy.get('h3').contains('Hints').should('exist') - // // contains a privacy section - // cy.get('h3').contains('Privacy').should('exist') - // // contains a share settings button in the pivacy section - // // It's toggled off by default - // cy.contains('Share sessions') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // Click it - // cy.contains('Share sessions').next().click() - // // Now it's toggled on - // cy.contains('Share sessions') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_on/) - // // Click it again - // cy.contains('Share sessions').next().click() - // // Now it's toggled off again - // cy.contains('Share sessions') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // contains an experimental settings section - // cy.get('h3').contains('Experimental Settings').should('exist') - // // contains a 'disable module placement restrictions' experimental feature - // // It's toggled off by default - // cy.contains(exptlSettingText) - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // Click it - // cy.contains(exptlSettingText).next().click() - // // We have to confirm this one - // cy.contains('Switching on an experimental feature').should('exist') - // cy.get('button').contains('Cancel').should('exist') - // cy.get('button').contains('Continue').should('exist') - // // Abort! - // cy.get('button').contains('Cancel').click() - // // Still toggled off - // cy.contains(exptlSettingText) - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // Click it again and confirm - // cy.contains(exptlSettingText).next().click() - // cy.get('button').contains('Continue').click() - // // Now it's toggled on - // cy.contains(exptlSettingText) - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_on/) - // // Click it again - // cy.contains(exptlSettingText).next().click() - // // We have to confirm to turn it off? - // // TODO That doesn't seem right... - // cy.get('button').contains('Continue').click() - // // Now it's toggled off again - // cy.contains(exptlSettingText) - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // contains a 'disable module placement restrictions' toggle in the experimental settings card - // // It's toggled off by default - // cy.contains('Disable module') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // Click it - // cy.contains('Disable module').next().click() - // // We have to confirm this one - // cy.contains('Switching on an experimental feature').should('exist') - // cy.get('button').contains('Cancel').should('exist') - // cy.get('button').contains('Continue').should('exist') - // // Abort! - // cy.get('button').contains('Cancel').click() - // // Still toggled off - // cy.contains('Disable module') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // Click it again and confirm - // cy.contains('Disable module').next().click() - // cy.get('button').contains('Continue').click() - // // Now it's toggled on - // cy.contains('Disable module') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_on/) - // // Click it again - // cy.contains('Disable module').next().click() - // // We have to confirm to turn it off - // cy.get('button').contains('Continue').click() - // // Now it's toggled off again - // cy.contains('Disable module') - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // // PD remembers when we enable things - // // Enable a button - // // We're not using the privacy button because that - // // interacts with analytics libraries, which might - // // not be accessible in a headless environment - // cy.contains(exptlSettingText).next().click() - // cy.get('button').contains('Continue').click() - // // Leave the settings page - // cy.get("button[id='NavTab_file']").contains('FILE').click() - // // Go back to settings - // cy.openSettingsPage() - // // The toggle is still on - // cy.contains(exptlSettingText) - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_on/) - // // PD remembers when we disable things - // // Disable a button - // // We're not using the privacy button because that - // // interacts with analytics libraries, which might - // // not be accessible in a headless environment - // cy.contains(exptlSettingText).next().click() - // cy.get('button').contains('Continue').click() - // // Leave the settings page - // cy.get("button[id='NavTab_file']").contains('FILE') - // // Go back to settings - // cy.openSettingsPage() - // // The toggle is still off - // cy.contains(exptlSettingText) - // .next() - // .should('have.attr', 'class') - // .and('match', /toggled_off/) - // }) -}) diff --git a/protocol-designer/cypress/e2e/settings.cy.ts b/protocol-designer/cypress/e2e/settings.cy.ts new file mode 100644 index 00000000000..5ce896aa883 --- /dev/null +++ b/protocol-designer/cypress/e2e/settings.cy.ts @@ -0,0 +1,53 @@ +describe('The Settings Page', () => { + before(() => { + cy.visit('/') + }) + + it('content and toggle state', () => { + // The settings page will not follow the same pattern as create and edit + // The Settings page is simple enough we need not abstract actions and validations into data + + // home page contains a working settings button + cy.openSettingsPage() + cy.verifySettingsPage() + // Timeline editing tips defaults to true + cy.getByAriaLabel('Settings_hotKeys') + .should('exist') + .should('be.visible') + .should('have.attr', 'aria-checked', 'true') + // Share sessions with Opentrons toggle defaults to off + cy.getByTestId('analyticsToggle') + .should('exist') + .should('be.visible') + .find('path[aria-roledescription="ot-toggle-input-off"]') + .should('exist') + // Toggle the share sessions with Opentrons setting + cy.getByTestId('analyticsToggle').click() + cy.getByTestId('analyticsToggle') + .find('path[aria-roledescription="ot-toggle-input-on"]') + .should('exist') + // Navigate away from the settings page + // Then return to see privacy toggle remains toggled on + cy.visit('/') + cy.openSettingsPage() + cy.getByTestId('analyticsToggle').find( + 'path[aria-roledescription="ot-toggle-input-on"]' + ) + // Toggle off editing timeline tips + // Navigate away from the settings page + // Then return to see timeline tips remains toggled on + cy.getByAriaLabel('Settings_hotKeys').click() + cy.getByAriaLabel('Settings_hotKeys').should( + 'have.attr', + 'aria-checked', + 'false' + ) + cy.visit('/') + cy.openSettingsPage() + cy.getByAriaLabel('Settings_hotKeys').should( + 'have.attr', + 'aria-checked', + 'false' + ) + }) +}) diff --git a/protocol-designer/cypress/e2e/testfiles.cy.ts b/protocol-designer/cypress/e2e/testfiles.cy.ts new file mode 100644 index 00000000000..70ad509855c --- /dev/null +++ b/protocol-designer/cypress/e2e/testfiles.cy.ts @@ -0,0 +1,30 @@ +import { TestFilePath, getTestFile } from '../support/testFiles' + +describe('Validate Test Files', () => { + it('should load and validate all test files', () => { + ;(Object.keys(TestFilePath) as Array).forEach( + key => { + const testFile = getTestFile(TestFilePath[key]) + + cy.log(`Loaded: ${testFile.basename}`) + expect(testFile).to.have.property('path') + + cy.readFile(testFile.path).then(fileContent => { + cy.log(`Loaded content for: ${testFile.basename}`) + + if ( + typeof fileContent === 'object' && + Boolean(fileContent?.metadata?.protocolName) + ) { + expect(fileContent.metadata.protocolName) + .to.be.a('string') + .and.have.length.greaterThan(0) + cy.log( + `Validated protocolName: ${fileContent.metadata.protocolName}` + ) + } + }) + } + ) + }) +}) diff --git a/protocol-designer/cypress/e2e/urlNavigation.cy.ts b/protocol-designer/cypress/e2e/urlNavigation.cy.ts new file mode 100644 index 00000000000..24249a4f75f --- /dev/null +++ b/protocol-designer/cypress/e2e/urlNavigation.cy.ts @@ -0,0 +1,16 @@ +describe('URL Navigation', () => { + it('settings', () => { + cy.visit('#/settings') + cy.verifySettingsPage() + }) + it('createNew', () => { + cy.visit('#/createNew') + // directly navigating sends you back to the home page + cy.verifyHomePage() + }) + it('overview', () => { + cy.visit('#/overview') + // directly navigating sends you back to the home page + cy.verifyHomePage() + }) +}) diff --git a/protocol-designer/cypress/fixtures/garbage.txt b/protocol-designer/cypress/fixtures/garbage.txt new file mode 100644 index 00000000000..56993df39af --- /dev/null +++ b/protocol-designer/cypress/fixtures/garbage.txt @@ -0,0 +1 @@ +Not a protocol diff --git a/protocol-designer/cypress/fixtures/invalid_json.txt b/protocol-designer/cypress/fixtures/invalid_json.txt new file mode 100644 index 00000000000..8bb73b625d0 --- /dev/null +++ b/protocol-designer/cypress/fixtures/invalid_json.txt @@ -0,0 +1,7 @@ +{ + "name": "Test Protocol", + "version": 1.0, + "steps": [ + { "step1": "initialize" }, + { "step2": "execute" + ] \ No newline at end of file diff --git a/protocol-designer/cypress/mocks/file-saver.js b/protocol-designer/cypress/mocks/file-saver.js deleted file mode 100644 index d4c7febe539..00000000000 --- a/protocol-designer/cypress/mocks/file-saver.js +++ /dev/null @@ -1,6 +0,0 @@ -// mock for 'file-saver' npm module - -export const saveAs = (blob, fileName) => { - global.__lastSavedFileBlob__ = blob - global.__lastSavedFileName__ = fileName -} diff --git a/protocol-designer/cypress/support/commands.js b/protocol-designer/cypress/support/commands.js deleted file mode 100644 index 09543c42330..00000000000 --- a/protocol-designer/cypress/support/commands.js +++ /dev/null @@ -1,117 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -import 'cypress-file-upload' -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) - -// -// General Custom Commands -// -Cypress.Commands.add('closeAnnouncementModal', () => { - // ComputingSpinner sometimes covers the announcement modal button and prevents the button click - // this will retry until the ComputingSpinner does not exist - cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( - 'not.exist' - ) - cy.get('button') - .contains('Got It!') - .should('be.visible') - .click({ force: true }) -}) - -// -// File Page Actions -// -Cypress.Commands.add('openFilePage', () => { - cy.get('button[id="NavTab_file"]').contains('FILE').click() -}) - -// -// Pipette Page Actions -// -Cypress.Commands.add( - 'choosePipettes', - (left_pipette_selector, right_pipette_selector) => { - cy.get('[id="PipetteSelect_left"]').click() - cy.get(left_pipette_selector).click() - cy.get('[id="PipetteSelect_right"]').click() - cy.get(right_pipette_selector).click() - } -) - -Cypress.Commands.add('selectTipRacks', (left, right) => { - if (left) { - cy.get("select[name*='left.tiprack']").select(left) - } - if (right) { - cy.get("select[name*='right.tiprack']").select(right) - } -}) - -// -// Liquid Page Actions -// -Cypress.Commands.add( - 'addLiquid', - (liquidName, liquidDesc, serializeLiquid = false) => { - cy.get('button').contains('New Liquid').click() - cy.get("input[name='name']").type(liquidName) - cy.get("input[name='description']").type(liquidDesc) - if (serializeLiquid) { - // force option used because checkbox is hidden - cy.get("input[name='serialize']").check({ force: true }) - } - cy.get('button').contains('save').click() - } -) - -// -// Design Page Actions -// -Cypress.Commands.add('openDesignPage', () => { - cy.get('button[id="NavTab_design"]').contains('DESIGN').parent().click() -}) -Cypress.Commands.add('addStep', stepName => { - cy.get('button').contains('Add Step').click() - cy.get('button').contains(stepName, { matchCase: false }).click() -}) - -// -// Settings Page Actions -// -Cypress.Commands.add('openSettingsPage', () => { - cy.get('button').contains('Settings').click() -}) - -// Advance Settings for Transfer Steps - -// Pre-wet tip enable/disable -Cypress.Commands.add('togglePreWetTip', () => { - cy.get('input[name="preWetTip"]').click({ force: true }) -}) - -// Mix settings select/deselect -Cypress.Commands.add('mixaspirate', () => { - cy.get('input[name="aspirate_mix_checkbox"]').click({ force: true }) -}) diff --git a/protocol-designer/cypress/support/commands.ts b/protocol-designer/cypress/support/commands.ts new file mode 100644 index 00000000000..b97c11f2bd2 --- /dev/null +++ b/protocol-designer/cypress/support/commands.ts @@ -0,0 +1,228 @@ +import 'cypress-file-upload' +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + getByTestId: (testId: string) => Cypress.Chainable> + getByAriaLabel: (value: string) => Cypress.Chainable> + verifyHeader: () => Cypress.Chainable + verifyFullHeader: () => Cypress.Chainable + verifyCreateNewHeader: () => Cypress.Chainable + clickCreateNew: () => Cypress.Chainable + closeAnnouncementModal: () => Cypress.Chainable + verifyHomePage: () => Cypress.Chainable + importProtocol: (protocolFile: string) => Cypress.Chainable + verifyImportPageOldProtocol: () => Cypress.Chainable + openFilePage: () => Cypress.Chainable + choosePipettes: ( + left_pipette_selector: string, + right_pipette_selector: string + ) => Cypress.Chainable + selectTipRacks: (left: string, right: string) => Cypress.Chainable + addLiquid: ( + liquidName: string, + liquidDesc: string, + serializeLiquid?: boolean + ) => Cypress.Chainable + openDesignPage: () => Cypress.Chainable + addStep: (stepName: string) => Cypress.Chainable + openSettingsPage: () => Cypress.Chainable + verifySettingsPage: () => Cypress.Chainable + verifyCreateNewPage: () => Cypress.Chainable + togglePreWetTip: () => Cypress.Chainable + mixaspirate: () => Cypress.Chainable + } + } +} + +// Only Header, Home, and Settings page actions are here +// due to their simplicity +// Create and Import page actions are in their respective files + +export const content = { + siteTitle: 'Opentrons Protocol Designer', + opentrons: 'Opentrons', + charSet: 'UTF-8', + header: 'Protocol Designer', + welcome: 'Welcome to Protocol Designer!', + appSettings: 'App settings', + privacy: 'Privacy', + shareSessions: 'Share sessions with Opentrons', +} + +export const locators = { + import: 'Import', + createNew: 'Create new', + createProtocol: 'Create a protocol', + editProtocol: 'Edit existing protocol', + settingsDataTestid: 'SettingsIconButton', + settings: 'Settings', + privacyPolicy: 'a[href="https://opentrons.com/privacy-policy"]', + eula: 'a[href="https://opentrons.com/eula"]', + privacyToggle: 'Settings_hotKeys', + analyticsToggleTestId: 'analyticsToggle', +} + +// General Custom Commands +Cypress.Commands.add( + 'getByTestId', + (testId: string): Cypress.Chainable> => { + return cy.get(`[data-testid="${testId}"]`) + } +) + +Cypress.Commands.add( + 'getByAriaLabel', + (value: string): Cypress.Chainable> => { + return cy.get(`[aria-label="${value}"]`) + } +) + +// Header Verifications +const verifyUniversal = (): void => { + cy.title().should('equal', content.siteTitle) + cy.document().should('have.property', 'charset').and('eq', content.charSet) + cy.contains(content.opentrons).should('be.visible') + cy.contains(content.header).should('be.visible') + cy.contains(locators.import).should('be.visible') +} + +Cypress.Commands.add('verifyFullHeader', () => { + verifyUniversal() + cy.contains(locators.createNew).should('be.visible') + cy.getByTestId(locators.settingsDataTestid).should('be.visible') +}) + +Cypress.Commands.add('verifyCreateNewHeader', () => { + verifyUniversal() +}) + +// Home Page +Cypress.Commands.add('verifyHomePage', () => { + cy.contains(content.welcome) + cy.contains('button', locators.createProtocol).should('be.visible') + cy.contains('label', locators.editProtocol).should('be.visible') + cy.getByTestId(locators.settingsDataTestid).should('be.visible') + cy.get(locators.privacyPolicy).should('exist').and('be.visible') + cy.get(locators.eula).should('exist').and('be.visible') +}) + +Cypress.Commands.add('clickCreateNew', () => { + cy.contains(locators.createProtocol).click() +}) + +// Header Import +Cypress.Commands.add('importProtocol', (protocolFilePath: string) => { + cy.contains(locators.import).click() + cy.get('[data-cy="landing-page"]') + .find('input[type=file]') + .selectFile(protocolFilePath, { force: true }) +}) + +// Settings Page Actions +Cypress.Commands.add('openSettingsPage', () => { + cy.getByTestId(locators.settingsDataTestid).click() +}) + +Cypress.Commands.add('verifySettingsPage', () => { + cy.verifyFullHeader() + cy.contains(locators.settings).should('exist').should('be.visible') + cy.contains(content.appSettings).should('exist').should('be.visible') + cy.contains(content.privacy).should('exist').should('be.visible') + cy.contains(content.shareSessions).should('exist').should('be.visible') + cy.getByAriaLabel(locators.privacyToggle).should('exist').should('be.visible') + cy.getByTestId(locators.analyticsToggleTestId) + .should('exist') + .should('be.visible') +}) + +/// ///////////////////////////////////////////////////////////////// +// Legacy Code Section +// This code is deprecated and should be removed +// as soon as possible once it's no longer needed +// as a reference during test migration. +/// ///////////////////////////////////////////////////////////////// + +Cypress.Commands.add('closeAnnouncementModal', () => { + // ComputingSpinner sometimes covers the announcement modal button and prevents the button click + // this will retry until the ComputingSpinner does not exist + cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( + 'not.exist' + ) + cy.get('button') + .contains('Got It!') + .should('be.visible') + .click({ force: true }) +}) + +// +// File Page Actions +// + +Cypress.Commands.add('openFilePage', () => { + cy.get('button[id="NavTab_file"]').contains('FILE').click() +}) + +// +// Pipette Page Actions +// + +Cypress.Commands.add( + 'choosePipettes', + (leftPipetteSelector, rightPipetteSelector) => { + cy.get('[id="PipetteSelect_left"]').click() + cy.get(leftPipetteSelector).click() + cy.get('[id="PipetteSelect_right"]').click() + cy.get(rightPipetteSelector).click() + } +) + +Cypress.Commands.add('selectTipRacks', (left, right) => { + if (left.length > 0) { + cy.get("select[name*='left.tiprack']").select(left) + } + if (right.length > 0) { + cy.get("select[name*='right.tiprack']").select(right) + } +}) + +// +// Liquid Page Actions +// +Cypress.Commands.add( + 'addLiquid', + (liquidName, liquidDesc, serializeLiquid = false) => { + cy.get('button').contains('New Liquid').click() + cy.get("input[name='name']").type(liquidName) + cy.get("input[name='description']").type(liquidDesc) + if (serializeLiquid) { + // force option used because checkbox is hidden + cy.get("input[name='serialize']").check({ force: true }) + } + cy.get('button').contains('save').click() + } +) + +// +// Design Page Actions +// + +Cypress.Commands.add('openDesignPage', () => { + cy.get('button[id="NavTab_design"]').contains('DESIGN').parent().click() +}) +Cypress.Commands.add('addStep', stepName => { + cy.get('button').contains('Add Step').click() + cy.get('button').contains(stepName, { matchCase: false }).click() +}) + +// Advance Settings for Transfer Steps + +// Pre-wet tip enable/disable +Cypress.Commands.add('togglePreWetTip', () => { + cy.get('input[name="preWetTip"]').click({ force: true }) +}) + +// Mix settings select/deselect +Cypress.Commands.add('mixaspirate', () => { + cy.get('input[name="aspirate_mix_checkbox"]').click({ force: true }) +}) diff --git a/protocol-designer/cypress/support/createNew.ts b/protocol-designer/cypress/support/createNew.ts new file mode 100644 index 00000000000..b5fde778339 --- /dev/null +++ b/protocol-designer/cypress/support/createNew.ts @@ -0,0 +1,131 @@ +import { executeUniversalAction, UniversalActions } from './universalActions' +import { isEnumValue } from './utils' + +export enum Actions { + SelectFlex = 'Select Opentrons Flex', + SelectOT2 = 'Select Opentrons OT-2', + Confirm = 'Confirm', + GoBack = 'Go back', +} + +export enum Verifications { + OnStep1 = 'On Step 1 page.', + OnStep2 = 'On Step 2 page.', + FlexSelected = 'Opentrons Flex selected.', + OT2Selected = 'Opentrons OT-2 selected.', + NinetySixChannel = '96-Channel option is available.', + NotNinetySixChannel = '96-Channel option is not available.', +} + +export enum Content { + Step1Title = 'Step 1', + Step2Title = 'Step 2', + AddPipette = 'Add a pipette', + NinetySixChannel = '96-Channel', + GoBack = 'Go back', + Confirm = 'Confirm', + OpentronsFlex = 'Opentrons Flex', + OpentronsOT2 = 'Opentrons OT-2', + LetsGetStarted = 'Let’s start with the basics', + WhatKindOfRobot = 'What kind of robot do you have?', +} + +export enum Locators { + Confirm = 'button:contains("Confirm")', + GoBack = 'button:contains("Go back")', + Step1Indicator = 'p:contains("Step 1")', + Step2Indicator = 'p:contains("Step 2")', + FlexOption = 'button:contains("Opentrons Flex")', + OT2Option = 'button:contains("Opentrons OT-2")', + NinetySixChannel = 'div:contains("96-Channel")', +} + +const executeAction = (action: Actions | UniversalActions): void => { + if (isEnumValue([UniversalActions], [action])) { + executeUniversalAction(action as UniversalActions) + return + } + + switch (action) { + case Actions.SelectFlex: + cy.contains(Content.OpentronsFlex).should('be.visible').click() + break + case Actions.SelectOT2: + cy.contains(Content.OpentronsOT2).should('be.visible').click() + break + case Actions.Confirm: + cy.contains(Content.Confirm).should('be.visible').click() + break + case Actions.GoBack: + cy.contains(Content.GoBack).should('be.visible').click() + break + default: + throw new Error(`Unrecognized action: ${action as string}`) + } +} + +const verifyStep = (verification: Verifications): void => { + switch (verification) { + case Verifications.OnStep1: + cy.contains(Content.Step1Title).should('be.visible') + break + case Verifications.OnStep2: + cy.contains(Content.Step2Title).should('be.visible') + cy.contains(Content.AddPipette).should('be.visible') + break + case Verifications.FlexSelected: + cy.contains(Content.OpentronsFlex).should( + 'have.css', + 'background-color', + 'rgb(0, 108, 250)' + ) + break + case Verifications.OT2Selected: + cy.contains(Content.OpentronsOT2).should( + 'have.css', + 'background-color', + 'rgb(0, 108, 250)' + ) + break + case Verifications.NinetySixChannel: + cy.contains(Content.NinetySixChannel).should('be.visible') + break + case Verifications.NotNinetySixChannel: + cy.contains(Content.NinetySixChannel).should('not.exist') + break + default: + throw new Error( + `Unrecognized verification: ${verification as Verifications}` + ) + } +} + +export const runCreateTest = ( + steps: Array +): void => { + const enumsToCheck = [Actions, Verifications, UniversalActions] + + if (!isEnumValue(enumsToCheck, steps)) { + throw new Error('One or more steps are unrecognized.') + } + + steps.forEach(step => { + if (isEnumValue([Actions], step)) { + executeAction(step as Actions) + } else if (isEnumValue([Verifications], step)) { + verifyStep(step as Verifications) + } else if (isEnumValue([UniversalActions], step)) { + executeAction(step as UniversalActions) + } + }) +} + +export const verifyCreateProtocolPage = (): void => { + // Verify step 1 and page content + cy.contains(Content.Step1Title).should('exist').should('be.visible') + cy.contains(Content.LetsGetStarted).should('exist').should('be.visible') + cy.contains(Content.WhatKindOfRobot).should('exist').should('be.visible') + cy.contains(Content.OpentronsFlex).should('exist').should('be.visible') + cy.contains(Content.OpentronsOT2).should('exist').should('be.visible') + cy.contains(Content.Confirm).should('exist').should('be.visible') +} diff --git a/protocol-designer/cypress/support/e2e.js b/protocol-designer/cypress/support/e2e.ts similarity index 100% rename from protocol-designer/cypress/support/e2e.js rename to protocol-designer/cypress/support/e2e.ts diff --git a/protocol-designer/cypress/support/import.ts b/protocol-designer/cypress/support/import.ts new file mode 100644 index 00000000000..829450c2189 --- /dev/null +++ b/protocol-designer/cypress/support/import.ts @@ -0,0 +1,142 @@ +// eslint-disable-next-line @typescript-eslint/consistent-type-imports +import { getTestFile, TestFile, TestFilePath } from './testFiles' +import path from 'path' +import semver from 'semver' +import cloneDeep from 'lodash/cloneDeep' +import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' + +export interface MigrateTestCase { + title: string + importTestFile: TestFilePath + expectedTestFile: TestFilePath + unusedHardware: boolean + migrationModal: 'newLabwareDefs' | 'v8.1' | 'noBehaviorChange' | null +} + +export const ContentStrings = { + newLabwareDefs: 'Update protocol to use new labware definitions', + v8_1: 'The default dispense height is now 1 mm from the bottom of the well', + noBehaviorChange: + 'We have added new features since the last time this protocol was updated, but have not made any changes to existing protocol behavior', + unusedHardwareWarning: 'Protocol has unused hardware', + exportButton: 'Export', + continueButton: 'continue', + continueWithExport: 'Continue with export', + migrationModal: + 'Your protocol was made in an older version of Protocol Designer', + confirmButton: 'Confirm', + cancelButton: 'Cancel', + protocolMetadata: 'Protocol Metadata', + instruments: 'Instruments', + liquidDefinitions: 'Liquid Definitions', + protocolStartingDeck: 'Protocol Starting Deck', +} + +export const LocatorStrings = { + modalShellArea: '[aria-label="ModalShell_ModalArea"]', + exportProtocol: `button:contains(${ContentStrings.exportButton})`, + continueButton: `button:contains(${ContentStrings.continueButton})`, +} + +export const verifyOldProtocolModal = (): void => { + cy.get(LocatorStrings.modalShellArea) + .should('exist') + .should('be.visible') + .within(() => { + cy.contains(ContentStrings.migrationModal) + .should('exist') + .and('be.visible') + cy.contains(ContentStrings.confirmButton).should('be.visible') + cy.contains(ContentStrings.cancelButton).should('be.visible') + cy.contains(ContentStrings.confirmButton).click() + }) +} + +export const verifyImportProtocolPage = (protocol: TestFile): void => { + cy.readFile(protocol.path).then(protocolRead => { + cy.contains(ContentStrings.protocolMetadata).should('be.visible') + cy.contains(ContentStrings.instruments).should('be.visible') + cy.contains(ContentStrings.protocolStartingDeck).should('be.visible') + cy.contains(String(protocolRead.metadata.protocolName)).should('be.visible') + }) +} + +export const migrateAndMatchSnapshot = ({ + importTestFile, + expectedTestFile, + unusedHardware, + migrationModal, +}: MigrateTestCase): void => { + const uploadProtocol: TestFile = getTestFile(importTestFile) + cy.importProtocol(uploadProtocol.path) + + if (migrationModal !== null) { + if (migrationModal === 'v8.1') { + cy.get('div').contains(ContentStrings.v8_1).should('exist') + } else if (migrationModal === 'newLabwareDefs') { + cy.get('div').contains(ContentStrings.newLabwareDefs).should('exist') + } else if (migrationModal === 'noBehaviorChange') { + cy.get('div').contains(ContentStrings.noBehaviorChange).should('exist') + } + cy.get('button') + .contains(ContentStrings.confirmButton, { matchCase: false }) + .click() + } + + cy.get(LocatorStrings.exportProtocol).click() + + if (unusedHardware) { + cy.get('div').contains(ContentStrings.unusedHardwareWarning).should('exist') + cy.contains(ContentStrings.continueWithExport).click() + } + + const expectedProtocol: TestFile = getTestFile(expectedTestFile) + + cy.readFile(expectedProtocol.path).then(expectedProtocolRead => { + const downloadedFilePath = path.join( + expectedProtocol.downloadsFolder, + `${expectedProtocolRead.metadata.protocolName}.json` + ) + cy.readFile(downloadedFilePath, { timeout: 5000 }).should('exist') + cy.readFile(downloadedFilePath).then(savedFile => { + const expectedFile = cloneDeep(expectedProtocolRead) + const version = semver.parse( + savedFile.designerApplication.version as string + ) + assert(version !== null, 'PD version is not valid semver') + + const files = [savedFile, expectedFile] + files.forEach(f => { + f.metadata.lastModified = 123 + f.designerApplication.data._internalAppBuildDate = 'Foo Date' + f.designerApplication.version = 'x.x.x' + + Object.values( + f.designerApplication.data.savedStepForms as Record + ).forEach(stepForm => { + const stepFormTyped = stepForm as { + stepType: string + dropTip_location?: string + blowout_location?: string + } + if (stepFormTyped.stepType === 'moveLiquid') { + stepFormTyped.dropTip_location = 'trash drop tip location' + if (stepFormTyped.blowout_location?.includes('trashBin') ?? false) { + stepFormTyped.blowout_location = 'trash blowout location' + } + } + if (stepFormTyped.stepType === 'mix') { + stepFormTyped.dropTip_location = 'trash drop tip location' + stepFormTyped.blowout_location = 'trash blowout location' + } + }) + + f.commands.forEach((command: { key: string }) => { + if ('key' in command) command.key = '123' + }) + }) + + expectDeepEqual(assert, savedFile, expectedFile) + }) + }) +} diff --git a/protocol-designer/cypress/support/testFiles.ts b/protocol-designer/cypress/support/testFiles.ts new file mode 100644 index 00000000000..d8364b67ed8 --- /dev/null +++ b/protocol-designer/cypress/support/testFiles.ts @@ -0,0 +1,73 @@ +import path from 'path' +import { isEnumValue } from './utils' + +// //////////////////////////////////////////// +// This is the data section where we map all the protocol files +// This allows for IDE . completion and type checking +// //////////////////////////////////////////// + +export enum TestFilePath { + // Define the path relative to the protocol-designer directory + // PD root project fixtures + DoItAllV3MigratedToV6 = 'fixtures/protocol/6/doItAllV3MigratedToV6.json', + Mix_6_0_0 = 'fixtures/protocol/6/mix_6_0_0.json', + PreFlexGrandfatheredProtocolV6 = 'fixtures/protocol/6/preFlexGrandfatheredProtocolMigratedFromV1_0_0.json', + DoItAllV4MigratedToV6 = 'fixtures/protocol/6/doItAllV4MigratedToV6.json', + Example_1_1_0V6 = 'fixtures/protocol/6/example_1_1_0MigratedFromV1_0_0.json', + DoItAllV3MigratedToV7 = 'fixtures/protocol/7/doItAllV3MigratedToV7.json', + Mix_7_0_0 = 'fixtures/protocol/7/mix_7_0_0.json', + DoItAllV7 = 'fixtures/protocol/7/doItAllV7.json', + DoItAllV4MigratedToV7 = 'fixtures/protocol/7/doItAllV4MigratedToV7.json', + Example_1_1_0V7 = 'fixtures/protocol/7/example_1_1_0MigratedFromV1_0_0.json', + MinimalProtocolOldTransfer = 'fixtures/protocol/1/minimalProtocolOldTransfer.json', + Example_1_1_0 = 'fixtures/protocol/1/example_1_1_0.json', + PreFlexGrandfatheredProtocolV1 = 'fixtures/protocol/1/preFlexGrandfatheredProtocol.json', + DoItAllV1 = 'fixtures/protocol/1/doItAll.json', + PreFlexGrandfatheredProtocolV4 = 'fixtures/protocol/4/preFlexGrandfatheredProtocolMigratedFromV1_0_0.json', + DoItAllV3V4 = 'fixtures/protocol/4/doItAllV3.json', + DoItAllV4V4 = 'fixtures/protocol/4/doItAllV4.json', + NinetySixChannelFullAndColumn = 'fixtures/protocol/8/ninetySixChannelFullAndColumn.json', + NewAdvancedSettingsAndMultiTemp = 'fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json', + Example_1_1_0V8 = 'fixtures/protocol/8/example_1_1_0MigratedToV8.json', + DoItAllV4MigratedToV8 = 'fixtures/protocol/8/doItAllV4MigratedToV8.json', + DoItAllV8 = 'fixtures/protocol/8/doItAllV8.json', + DoItAllV3MigratedToV8 = 'fixtures/protocol/8/doItAllV3MigratedToV8.json', + Mix_8_0_0 = 'fixtures/protocol/8/mix_8_0_0.json', + DoItAllV7MigratedToV8 = 'fixtures/protocol/8/doItAllV7MigratedToV8.json', + MixSettingsV5 = 'fixtures/protocol/5/mixSettings.json', + DoItAllV5 = 'fixtures/protocol/5/doItAllV5.json', + BatchEditV5 = 'fixtures/protocol/5/batchEdit.json', + MultipleLiquidsV5 = 'fixtures/protocol/5/multipleLiquids.json', + PreFlexGrandfatheredProtocolV5 = 'fixtures/protocol/5/preFlexGrandfatheredProtocolMigratedFromV1_0_0.json', + DoItAllV3V5 = 'fixtures/protocol/5/doItAllV3.json', + TransferSettingsV5 = 'fixtures/protocol/5/transferSettings.json', + Mix_5_0_X = 'fixtures/protocol/5/mix_5_0_x.json', + Example_1_1_0V5 = 'fixtures/protocol/5/example_1_1_0MigratedFromV1_0_0.json', + ThermocyclerOnOt2V7 = 'fixtures/protocol/7/thermocyclerOnOt2V7.json', + ThermocyclerOnOt2V7MigratedToV8 = 'fixtures/protocol/8/thermocyclerOnOt2V7MigratedToV8.json', + // cypress fixtures + GarbageTextFile = 'cypress/fixtures/garbage.txt', + Generic96TipRack200ul = 'cypress/fixtures/generic_96_tiprack_200ul.json', + InvalidLabware = 'cypress/fixtures/invalid_labware.json', + InvalidTipRack = 'cypress/fixtures/invalid_tip_rack.json', + InvalidTipRackTxt = 'cypress/fixtures/invalid_tip_rack.txt', + InvalidJson = 'cypress/fixtures/invalid_json.txt', // a file with invalid JSON may not have .json extension because cy.readfile will not read it. +} + +export interface TestFile { + path: string + downloadsFolder: string + basename: string +} + +export const getTestFile = (id: TestFilePath): TestFile => { + if (!isEnumValue([TestFilePath], [id])) { + throw new Error(`Invalid file path: ${id as string}`) + } + + return { + path: id.valueOf(), + basename: path.basename(id.valueOf()), + downloadsFolder: Cypress.config('downloadsFolder'), + } +} diff --git a/protocol-designer/cypress/support/universalActions.ts b/protocol-designer/cypress/support/universalActions.ts new file mode 100644 index 00000000000..f98abb442bc --- /dev/null +++ b/protocol-designer/cypress/support/universalActions.ts @@ -0,0 +1,16 @@ +export enum UniversalActions { + Snapshot = 'Take a visual testing snapshot', + // Other examples of things that could be universal actions: + // Clear the cache +} + +export const executeUniversalAction = (action: UniversalActions): void => { + switch (action) { + case UniversalActions.Snapshot: + // Placeholder for future implementation of visual testing snapshot + // Currently, this does nothing + break + default: + throw new Error(`Unrecognized universal action: ${action as string}`) + } +} diff --git a/protocol-designer/cypress/support/utils.ts b/protocol-designer/cypress/support/utils.ts new file mode 100644 index 00000000000..33be46e7c7d --- /dev/null +++ b/protocol-designer/cypress/support/utils.ts @@ -0,0 +1,11 @@ +export const isEnumValue = ( + enumObjs: T[], + values: unknown | unknown[] +): boolean => { + const valueArray = Array.isArray(values) ? values : [values] + return valueArray.every(value => + enumObjs.some(enumObj => + Object.values(enumObj).includes(value as T[keyof T]) + ) + ) +} diff --git a/protocol-designer/tsconfig.cypress.json b/protocol-designer/tsconfig.cypress.json new file mode 100644 index 00000000000..a23471dd5d0 --- /dev/null +++ b/protocol-designer/tsconfig.cypress.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "cypress", + "outDir": "cypress/lib", + "types": ["cypress", "node"], + "composite": true + }, + "include": [ + "cypress", + "cypress/**/*.ts" + ] + } + \ No newline at end of file diff --git a/protocol-designer/tsconfig.json b/protocol-designer/tsconfig.json index 6a2a9eac5bd..cca2f020439 100644 --- a/protocol-designer/tsconfig.json +++ b/protocol-designer/tsconfig.json @@ -12,6 +12,9 @@ }, { "path": "../step-generation" + }, + { + "path": "./tsconfig.cypress.json" } ], "compilerOptions": { diff --git a/protocol-designer/vite.config.ts b/protocol-designer/vite.config.ts index 6f26d077aa6..ed0a649b9b7 100644 --- a/protocol-designer/vite.config.ts +++ b/protocol-designer/vite.config.ts @@ -9,14 +9,6 @@ import lostCss from 'lost' import { versionForProject } from '../scripts/git-version.mjs' import type { UserConfig } from 'vite' -const testAliases: Record | { 'file-saver': string } = - process.env.CYPRESS === '1' - ? { - 'file-saver': - path.resolve(__dirname, 'cypress/mocks/file-saver.js') ?? '', - } - : {} - // eslint-disable-next-line import/no-default-export export default defineConfig( async (): Promise => { @@ -68,7 +60,6 @@ export default defineConfig( '@opentrons/step-generation': path.resolve( '../step-generation/src/index.ts' ), - ...testAliases, }, }, server: { From 9f672a664214e9812775686e86fa66ccf40ada12 Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:31:26 -0500 Subject: [PATCH 26/31] fix(opentrons-ai-client): Refresh chat goes to landing (#16845) # Overview If the user refreshes the chat page it will redirect them to the landing page instead of showing an empty chat. ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- .../src/pages/Chat/__tests__/Chat.test.tsx | 17 ++++++++++++++++- opentrons-ai-client/src/pages/Chat/index.tsx | 13 +++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx b/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx index 77874086534..ad17acd26fd 100644 --- a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx +++ b/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx @@ -1,15 +1,25 @@ import { screen } from '@testing-library/react' -import { describe, it, vi, beforeEach } from 'vitest' +import { describe, it, vi, beforeEach, expect } from 'vitest' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { PromptGuide } from '../../../molecules/PromptGuide' import { ChatFooter } from '../../../molecules/ChatFooter' import { Chat } from '../index' +import type { NavigateFunction } from 'react-router-dom' vi.mock('../../../molecules/PromptGuide') vi.mock('../../../molecules/ChatFooter') // Note (kk:05/20/2024) to avoid TypeError: scrollRef.current.scrollIntoView is not a function window.HTMLElement.prototype.scrollIntoView = vi.fn() +const mockNavigate = vi.fn() + +vi.mock('react-router-dom', async importOriginal => { + const reactRouterDom = await importOriginal() + return { + ...reactRouterDom, + useNavigate: () => mockNavigate, + } +}) const render = (): ReturnType => { return renderWithProviders(, { @@ -28,6 +38,11 @@ describe('Chat', () => { screen.getByText('mock ChatFooter') }) + it('should navigate to home if chatData is empty', () => { + render() + expect(mockNavigate).toHaveBeenCalledWith('/') + }) + it.skip('should not show the feedback modal when loading the page', () => { render() screen.getByText('Send feedback to Opentrons') diff --git a/opentrons-ai-client/src/pages/Chat/index.tsx b/opentrons-ai-client/src/pages/Chat/index.tsx index 7bedeb8dffe..ea70e76fe65 100644 --- a/opentrons-ai-client/src/pages/Chat/index.tsx +++ b/opentrons-ai-client/src/pages/Chat/index.tsx @@ -7,11 +7,14 @@ import { chatDataAtom, feedbackModalAtom, scrollToBottomAtom, + updateProtocolChatAtom, + createProtocolChatAtom, } from '../../resources/atoms' import { ChatDisplay } from '../../molecules/ChatDisplay' import { ChatFooter } from '../../molecules/ChatFooter' import styled from 'styled-components' import { FeedbackModal } from '../../molecules/FeedbackModal' +import { useNavigate } from 'react-router-dom' export interface InputType { userPrompt: string @@ -28,6 +31,16 @@ export function Chat(): JSX.Element | null { const scrollRef = useRef(null) const [showFeedbackModal] = useAtom(feedbackModalAtom) const [scrollToBottom] = useAtom(scrollToBottomAtom) + const navigate = useNavigate() + const [updateProtocolChat] = useAtom(updateProtocolChatAtom) + const [createProtocolChat] = useAtom(createProtocolChatAtom) + + // Redirect to home page if there is no prompt (user has refreshed the page) + useEffect(() => { + if (updateProtocolChat.prompt === '' && createProtocolChat.prompt === '') { + navigate('/') + } + }, []) useEffect(() => { if (scrollRef.current != null) From 450c562738bc91f470c2979a07f4fa21b8c06a99 Mon Sep 17 00:00:00 2001 From: connected-znaim <60662281+connected-znaim@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:31:38 -0500 Subject: [PATCH 27/31] =?UTF-8?q?fix(opentrons-ai-client):=20Made=20the=20?= =?UTF-8?q?Application=20and=20Description=20start=20on=20a=20new=20line?= =?UTF-8?q?=20for=20the=20crea=E2=80=A6=20(#16844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made the Application and Description start on a new line for the create new protocol prompt when displayed on the chat page # Overview BEFORE: Pipette mount list text is too large And description and application text starts right after the : instead of on a new line image Blue outline image AFTER: image No Blue outline: image ## Test Plan and Hands on Testing ## Changelog ## Review requests ## Risk assessment --- opentrons-ai-client/src/molecules/ChatDisplay/index.tsx | 1 - .../src/resources/utils/createProtocolUtils.tsx | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 7d01d282903..961fa93b445 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -262,5 +262,4 @@ const CodeWrapper = styled(Flex)` background-color: ${COLORS.grey20}; border-radius: ${BORDERS.borderRadius4}; overflow: auto; - border: 1px solid ${COLORS.blue35}; ` diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx index a2ccffd988b..06dd83061cb 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx @@ -208,9 +208,11 @@ export function generateChatPrompt( const prompt = `${t('create_protocol_prompt_robot', { robotType })}\n${t( 'application_title' - )}: ${scientificApplication}\n\n${t('description')}: ${description}\n\n${t( + )}: \n${scientificApplication}\n\n${t( + 'description' + )}: \n${description}\n\n${t( 'pipette_mounts' - )}:\n\n${pipetteMounts}\n${flexGripper}\n\n${t( + )}:\n\n${pipetteMounts}${flexGripper}\n\n${t( 'modules_title' )}:\n${modules}\n\n${t('labware_section_title')}:\n${labwares}\n\n${t( 'liquid_section_title' From 383f7ced15bd6c74588b5139ca98d8ebd961a123 Mon Sep 17 00:00:00 2001 From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:47:48 -0500 Subject: [PATCH 28/31] feat(abr-testing): protocol to test disposable lid with any pcr plate type (#16837) # Overview Protocol for testing `opentrons_tough_pcr_auto_sealing_lid` with any pcr plate ## Test Plan and Hands on Testing - Simulated profile with each lid type - Physically tested lid on thermocycler with biorad plate ## Changelog - added helper function for parameter with pcr plate types - added evaporation protocol - fixed abr1 bug where it resets a tip rack twice which causes the pipette to pick up from an empty slot - changed abr10 protocol to reflect comments about liquids at top ## Review requests ## Risk assessment - lid is currently not compatible with biorad plate or nest plate, but pr in progress for this. --- .../10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py | 19 ++- .../1_Simple Normalize Long Right.py | 2 - abr-testing/abr_testing/protocols/helpers.py | 26 ++++- .../10_ZymoBIOMICS Magbead Liquid Setup.py | 30 +++-- .../test_protocols/tc_biorad_evap_test.py | 109 ++++++++++++++++++ 5 files changed, 162 insertions(+), 24 deletions(-) create mode 100644 abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py diff --git a/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py b/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py index 43f0db962e8..9631b442694 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py +++ b/abr-testing/abr_testing/protocols/active_protocols/10_ZymoBIOMICS_Magbead_DNA_Cells_Flex.py @@ -94,7 +94,6 @@ def run(ctx: protocol_api.ProtocolContext) -> None: bind_time_1 = bind_time_2 = wash_time = 0.25 drybeads = 0.5 lysis_rep_1 = lysis_rep_2 = bead_reps_2 = 1 - PK_vol = 20.0 bead_vol = 25.0 starting_vol = lysis_vol + sample_vol binding_buffer_vol = bind_vol + bead_vol @@ -137,7 +136,7 @@ def run(ctx: protocol_api.ProtocolContext) -> None: """ lysis_ = res1.wells()[0] binding_buffer = res1.wells()[1:4] - bind2_res = res1.wells()[4:6] + bind2_res = res1.wells()[4:8] wash1 = res1.wells()[6:8] elution_solution = res1.wells()[-1] wash2 = res2.wells()[:6] @@ -149,16 +148,12 @@ def run(ctx: protocol_api.ProtocolContext) -> None: samps = sample_plate.wells()[: (8 * num_cols)] liquid_vols_and_wells: Dict[str, List[Dict[str, Well | List[Well] | float]]] = { - "Lysis": [{"well": lysis_, "volume": lysis_vol}], - "PK": [{"well": lysis_, "volume": PK_vol}], - "Beads": [{"well": binding_buffer, "volume": bead_vol}], - "Binding": [{"well": binding_buffer, "volume": bind_vol}], - "Binding 2": [{"well": bind2_res, "volume": bind2_vol}], - "Wash 1": [{"well": wash1_vol, "volume": wash1}], - "Wash 2": [{"well": wash2_vol, "volume": wash2}], - "Wash 3": [{"well": wash3_vol, "volume": wash3}], - "Final Elution": [{"well": elution_solution, "volume": elution_vol}], - "Samples": [{"well": samps, "volume": 0}], + "Lysis and PK": [{"well": lysis_, "volume": 12320.0}], + "Beads and Binding": [{"well": binding_buffer, "volume": 11875.0}], + "Binding 2": [{"well": bind2_res, "volume": 13500.0}], + "Final Elution": [{"well": elution_solution, "volume": 52000}], + "Samples": [{"well": samps, "volume": 0.0}], + "Reagents": [{"well": res2.wells(), "volume": 9000.0}], } flattened_list_of_wells = helpers.find_liquid_height_of_loaded_liquids( ctx, liquid_vols_and_wells, m1000 diff --git a/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py b/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py index 41af57f9dd4..525a82c3095 100644 --- a/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py +++ b/abr-testing/abr_testing/protocols/active_protocols/1_Simple Normalize Long Right.py @@ -121,8 +121,6 @@ def run(protocol: ProtocolContext) -> None: style=SINGLE, start="H1", tip_racks=[tiprack_x_1, tiprack_x_2, tiprack_x_3] ) helpers.find_liquid_height_of_all_wells(protocol, p1000, wells) - tiprack_x_1.reset() - sample_quant_csv = """ sample_plate_1, Sample_well,DYE,DILUENT sample_plate_1,A1,0,100 diff --git a/abr-testing/abr_testing/protocols/helpers.py b/abr-testing/abr_testing/protocols/helpers.py index 9e525faa314..12abbfa9b3f 100644 --- a/abr-testing/abr_testing/protocols/helpers.py +++ b/abr-testing/abr_testing/protocols/helpers.py @@ -14,7 +14,6 @@ ThermocyclerContext, TemperatureModuleContext, ) - from typing import List, Union, Dict from opentrons.hardware_control.modules.types import ThermocyclerStep from opentrons_shared_data.errors.exceptions import PipetteLiquidNotFoundError @@ -225,6 +224,31 @@ def create_hs_speed_parameter(parameters: ParameterContext) -> None: ) +def create_tc_compatible_labware_parameter(parameters: ParameterContext) -> None: + """Create parameter for labware type compatible with thermocycler.""" + parameters.add_str( + variable_name="labware_tc_compatible", + display_name="Labware Type for Thermocycler", + description="labware compatible with thermocycler.", + default="biorad_96_wellplate_200ul_pcr", + choices=[ + { + "display_name": "Armadillo_200ul", + "value": "armadillo_96_wellplate_200ul_pcr_full_skirt", + }, + {"display_name": "Bio-Rad_200ul", "value": "biorad_96_wellplate_200ul_pcr"}, + { + "display_name": "NEST_100ul", + "value": "nest_96_wellplate_100ul_pcr_full_skirt", + }, + { + "display_name": "Opentrons_200ul", + "value": "opentrons_96_wellplate_200ul_pcr_full_skirt", + }, + ], + ) + + # FUNCTIONS FOR COMMON MODULE SEQUENCES diff --git a/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py b/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py index e5c70194afa..422102e4321 100644 --- a/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py +++ b/abr-testing/abr_testing/protocols/liquid_setups/10_ZymoBIOMICS Magbead Liquid Setup.py @@ -29,23 +29,35 @@ def run(protocol: protocol_api.ProtocolContext) -> None: res1 = protocol.load_labware("nest_12_reservoir_15ml", "C3", "R1") res2 = protocol.load_labware("nest_12_reservoir_15ml", "B3", "R2") - lysis_and_pk = (3200 + 320) / 8 - beads_and_binding = (275 + 6600) / 8 - binding2 = 5500 / 8 - wash1 = 5500 / 8 - final_elution = 2100 / 8 + lysis_and_pk = 12320 / 8 + beads_and_binding = 11875 / 8 + binding2 = 13500 / 8 wash2 = 9000 / 8 - wash3 = 9000 / 8 + wash2_list = [wash2] * 12 # Fill up Plates # Res1 p1000.transfer( - volume=[lysis_and_pk, beads_and_binding, binding2, wash1, final_elution], + volume=[ + lysis_and_pk, + beads_and_binding, + beads_and_binding, + beads_and_binding, + binding2, + binding2, + binding2, + binding2, + binding2, + ], source=source_reservoir["A1"].bottom(z=0.2), dest=[ res1["A1"].top(), res1["A2"].top(), + res1["A3"].top(), + res1["A4"].top(), res1["A5"].top(), + res1["A6"].top(), res1["A7"].top(), + res1["A8"].top(), res1["A12"].top(), ], blow_out=True, @@ -54,9 +66,9 @@ def run(protocol: protocol_api.ProtocolContext) -> None: ) # Res2 p1000.transfer( - volume=[wash2, wash3], + volume=wash2_list, source=source_reservoir["A1"], - dest=[res2["A1"].top(), res2["A7"].top()], + dest=res2.wells(), blow_out=True, blowout_location="source well", trash=False, diff --git a/abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py b/abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py new file mode 100644 index 00000000000..4593c06f425 --- /dev/null +++ b/abr-testing/abr_testing/protocols/test_protocols/tc_biorad_evap_test.py @@ -0,0 +1,109 @@ +"""Test TC Disposable Lid with BioRad Plate.""" + +from opentrons.protocol_api import ( + ProtocolContext, + ParameterContext, + Well, + Labware, + InstrumentContext, +) +from typing import List +from abr_testing.protocols import helpers +from opentrons.protocol_api.module_contexts import ThermocyclerContext +from opentrons.hardware_control.modules.types import ThermocyclerStep + +metadata = {"protocolName": "Tough Auto Seal Lid Evaporation Test"} +requirements = {"robotType": "Flex", "apiLevel": "2.21"} + + +def add_parameters(parameters: ParameterContext) -> None: + """Add Parameters.""" + helpers.create_single_pipette_mount_parameter(parameters) + helpers.create_tc_lid_deck_riser_parameter(parameters) + helpers.create_tc_compatible_labware_parameter(parameters) + + +def _pcr_cycle(thermocycler: ThermocyclerContext) -> None: + """30x cycles of: 70° for 30s 72° for 30s 95° for 10s.""" + profile_TAG2: List[ThermocyclerStep] = [ + {"temperature": 70, "hold_time_seconds": 30}, + {"temperature": 72, "hold_time_seconds": 30}, + {"temperature": 95, "hold_time_seconds": 10}, + ] + thermocycler.execute_profile( + steps=profile_TAG2, repetitions=30, block_max_volume=50 + ) + + +def _fill_with_liquid_and_measure( + protocol: ProtocolContext, + pipette: InstrumentContext, + reservoir: Labware, + plate_in_cycler: Labware, +) -> None: + """Fill plate with 10 ul per well.""" + locations: List[Well] = [ + plate_in_cycler["A1"], + plate_in_cycler["A2"], + plate_in_cycler["A3"], + plate_in_cycler["A4"], + plate_in_cycler["A5"], + plate_in_cycler["A6"], + plate_in_cycler["A7"], + plate_in_cycler["A8"], + plate_in_cycler["A9"], + plate_in_cycler["A10"], + plate_in_cycler["A11"], + plate_in_cycler["A12"], + ] + volumes = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + protocol.pause("Weight Armadillo Plate, place on thermocycler") + # pipette 10uL into Armadillo wells + source_well: Well = reservoir["A1"] + pipette.distribute( + volume=volumes, + source=source_well, + dest=locations, + return_tips=True, + blow_out=False, + ) + protocol.pause("Weight Armadillo Plate, place on thermocycler, put on lid") + + +def run(ctx: ProtocolContext) -> None: + """Evaporation Test.""" + pipette_mount = ctx.params.pipette_mount # type: ignore[attr-defined] + deck_riser = ctx.params.deck_riser # type: ignore[attr-defined] + labware_tc_compatible = ctx.params.labware_tc_compatible # type: ignore[attr-defined] + tiprack_50 = ctx.load_labware("opentrons_flex_96_tiprack_50ul", "B2") + ctx.load_trash_bin("A3") + tc_mod: ThermocyclerContext = ctx.load_module( + helpers.tc_str + ) # type: ignore[assignment] + plate_in_cycler = tc_mod.load_labware(labware_tc_compatible) + p50 = ctx.load_instrument("flex_8channel_50", pipette_mount, tip_racks=[tiprack_50]) + unused_lids = helpers.load_disposable_lids(ctx, 5, ["D2"], deck_riser) + top_lid = unused_lids[0] + reservoir = ctx.load_labware("nest_12_reservoir_15ml", "A2") + tc_mod.open_lid() + tc_mod.set_block_temperature(4) + tc_mod.set_lid_temperature(105) + + # hold at 95° for 3 minutes + profile_TAG: List[ThermocyclerStep] = [{"temperature": 95, "hold_time_minutes": 3}] + # hold at 72° for 5min + profile_TAG3: List[ThermocyclerStep] = [{"temperature": 72, "hold_time_minutes": 5}] + tc_mod.open_lid() + _fill_with_liquid_and_measure(ctx, p50, reservoir, plate_in_cycler) + ctx.move_labware(top_lid, plate_in_cycler, use_gripper=True) + tc_mod.close_lid() + tc_mod.execute_profile(steps=profile_TAG, repetitions=1, block_max_volume=50) + _pcr_cycle(tc_mod) + tc_mod.execute_profile(steps=profile_TAG3, repetitions=1, block_max_volume=50) + # # # Cool to 4° + tc_mod.set_block_temperature(4) + tc_mod.set_lid_temperature(105) + # Open lid + tc_mod.open_lid() + ctx.move_labware(top_lid, "C2", use_gripper=True) + ctx.move_labware(top_lid, unused_lids[1], use_gripper=True) From 92b58064376d4a8b4f8abf4d1de7dbf92b1495ad Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:01:30 -0500 Subject: [PATCH 29/31] fix(protocol-designer): cleanup and add clear functionality to LiquidToolbox (#16843) This PR updates style according to latest designs and implements missing functionality in LiquidToolbox. Here, I add the ability to clear a specific liquid from its loaded wells from its liquid card, and update some other minor style issues. I also add an InfoScreen if there are no loaded liquids on the selected labware and no wells are selected. Lastly, I add prevention of highlighting labware column and row labels when clicking and dragging Closes RQA-3341 --- .../src/hardware-sim/Deck/RobotCoordsText.tsx | 18 +- .../Labware/labwareInternals/WellLabels.tsx | 1 + components/src/molecules/InfoScreen/index.tsx | 27 +- .../src/assets/localization/en/liquids.json | 3 + .../AssignLiquidsModal/LiquidCard.tsx | 102 ++++-- .../AssignLiquidsModal/LiquidToolbox.tsx | 290 +++++++++--------- 6 files changed, 264 insertions(+), 177 deletions(-) diff --git a/components/src/hardware-sim/Deck/RobotCoordsText.tsx b/components/src/hardware-sim/Deck/RobotCoordsText.tsx index 73240e3fbca..92dabd1cfc4 100644 --- a/components/src/hardware-sim/Deck/RobotCoordsText.tsx +++ b/components/src/hardware-sim/Deck/RobotCoordsText.tsx @@ -1,17 +1,31 @@ import type * as React from 'react' +import { css } from 'styled-components' export interface RobotCoordsTextProps extends React.ComponentProps<'text'> { x: number y: number children?: React.ReactNode + canHighlight?: boolean } /** SVG text reflected to use take robot coordinates as props */ // TODO: Ian 2019-05-07 reconcile this with Brian's version export function RobotCoordsText(props: RobotCoordsTextProps): JSX.Element { - const { x, y, children, ...additionalProps } = props + const { x, y, children, canHighlight = true, ...additionalProps } = props return ( - + {children} ) diff --git a/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx b/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx index bc6d0764768..59fa2723c34 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/WellLabels.tsx @@ -73,6 +73,7 @@ const Labels = (props: { ? highlightColor : fillColor } + canHighlight={false} > {(props.isLetterColumn === true ? /[A-Z]+/g : /\d+/g).exec( wellName diff --git a/components/src/molecules/InfoScreen/index.tsx b/components/src/molecules/InfoScreen/index.tsx index 5c5d64a0365..a70e2c409d7 100644 --- a/components/src/molecules/InfoScreen/index.tsx +++ b/components/src/molecules/InfoScreen/index.tsx @@ -1,23 +1,29 @@ import { BORDERS, COLORS } from '../../helix-design-system' -import { SPACING, TYPOGRAPHY } from '../../ui-style-constants/index' -import { LegacyStyledText } from '../../atoms/StyledText' +import { SPACING } from '../../ui-style-constants/index' +import { StyledText } from '../../atoms/StyledText' import { Icon } from '../../icons' import { Flex } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_COLUMN } from '../../styles' +import { ALIGN_CENTER, DIRECTION_COLUMN, JUSTIFY_CENTER } from '../../styles' interface InfoScreenProps { content: string + subContent?: string backgroundColor?: string + height?: string } export function InfoScreen({ content, + subContent, backgroundColor = COLORS.grey30, + height, }: InfoScreenProps): JSX.Element { return ( - - {content} - + + {content} + {subContent != null ? ( + + {subContent} + + ) : null} + ) } diff --git a/protocol-designer/src/assets/localization/en/liquids.json b/protocol-designer/src/assets/localization/en/liquids.json index 4f0d74264e3..ebd4800dacb 100644 --- a/protocol-designer/src/assets/localization/en/liquids.json +++ b/protocol-designer/src/assets/localization/en/liquids.json @@ -4,6 +4,7 @@ "clear_wells": "Clear wells", "click_and_drag": "Click and drag to select wells", "define_liquid": "Define a liquid", + "delete": "Delete", "delete_liquid": "Delete liquid", "description": "Description", "display_color": "Color", @@ -13,7 +14,9 @@ "liquids": "Liquids", "microliters": "µL", "name": "Name", + "no_liquids_added": "No liquids added", "no_liquids_defined": "No liquids defined", "save": "Save", + "select_wells_to_add": "Select wells to add liquid", "well": "Well" } diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx index 54ad8b67b66..a422b4b210e 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidCard.tsx @@ -1,10 +1,12 @@ import { useState } from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, - BORDERS, + ALIGN_FLEX_END, + Btn, COLORS, + CURSOR_POINTER, DIRECTION_COLUMN, Divider, Flex, @@ -13,13 +15,17 @@ import { ListItem, SPACING, StyledText, + TEXT_DECORATION_UNDERLINE, + Tag, } from '@opentrons/components' +import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import { removeWellsContents } from '../../labware-ingred/actions' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { getLabwareEntities } from '../../step-forms/selectors' import * as wellContentsSelectors from '../../top-selectors/well-contents' -import { LINE_CLAMP_TEXT_STYLE } from '../../atoms' +import type { SelectedContainerId } from '../../labware-ingred/reducers' import type { LiquidInfo } from './LiquidToolbox' interface LiquidCardProps { @@ -30,6 +36,7 @@ export function LiquidCard(props: LiquidCardProps): JSX.Element { const { info } = props const { name, color, liquidIndex } = info const { t } = useTranslation('liquids') + const dispatch = useDispatch() const [isExpanded, setIsExpanded] = useState(false) const labwareId = useSelector(labwareIngredSelectors.getSelectedLabwareId) const labwareEntities = useSelector(getLabwareEntities) @@ -72,38 +79,74 @@ export function LiquidCard(props: LiquidCardProps): JSX.Element { {} ) + const handleClearLiquid = ( + labwareId: SelectedContainerId, + wells: string[] + ): void => { + if (labwareId != null) { + dispatch( + removeWellsContents({ + labwareId, + wells, + }) + ) + } else { + console.error('Could not clear selected liquid - no labware ID') + } + } + return ( - - - - {name} - - + + + + {name} + + + {info.liquidIndex != null + ? liquidsWithDescriptions[info.liquidIndex].description + : null} + + + { + setIsExpanded(prev => !prev) + }} > - {info.liquidIndex != null - ? liquidsWithDescriptions[info.liquidIndex].description - : null} - + + - { - setIsExpanded(!isExpanded) + if (labwareId != null) { + handleClearLiquid(labwareId, fullWellsByLiquid[info.liquidIndex]) + } }} + alignSelf={ALIGN_FLEX_END} > - - + + {t('delete')} + + {isExpanded ? ( @@ -155,17 +198,12 @@ function WellContents(props: WellContentsProps): JSX.Element { const { t } = useTranslation('liquids') return ( - + {wellName} - {`${volume} ${t('microliters')}`} + ) diff --git a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx index 1b2067bf534..65c3243590d 100644 --- a/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx +++ b/protocol-designer/src/organisms/AssignLiquidsModal/LiquidToolbox.tsx @@ -8,6 +8,7 @@ import { DIRECTION_COLUMN, DropdownMenu, Flex, + InfoScreen, InputField, JUSTIFY_SPACE_BETWEEN, ListItem, @@ -233,7 +234,10 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { } confirmButtonText={t('shared:done')} - onConfirmClick={onClose} + onConfirmClick={() => { + dispatch(deselectAllWells()) + onClose() + }} onCloseClick={handleClearSelectedWells} height="calc(100vh - 64px)" closeButton={ @@ -245,151 +249,163 @@ export function LiquidToolbox(props: LiquidToolboxProps): JSX.Element { !(labwareId != null && selectedWells != null && selectionHasLiquids) } > -
- - {selectedWells.length > 0 ? ( - - - {t('add_liquid')} - - {liquidSelectionOptions.length === 0 ? ( - - - - {t('no_liquids_defined')} + {(liquidsInLabware != null && liquidsInLabware.length > 0) || + selectedWells.length > 0 ? ( + + + {selectedWells.length > 0 ? ( + + + + {t('add_liquid')} + + {liquidSelectionOptions.length === 0 ? ( + + + + {t('no_liquids_defined')} + + { + setDefineLiquidModal(true) + dispatch( + labwareIngredActions.createNewLiquidGroup() + ) + }} + > + + {t('define_liquid')} + + + + + ) : null} + + { + const fullOptions: DropdownOption[] = liquidSelectionOptions.map( + option => { + const liquid = liquids.find( + liquid => liquid.ingredientId === option.value + ) + + return { + name: option.name, + value: option.value, + liquidColor: liquid?.displayColor ?? '', + } + } + ) + const selectedLiquid = fullOptions.find( + option => option.value === selectedLiquidId + ) + const selectLiquidIdName = selectedLiquid?.name + const selectLiquidColor = selectedLiquid?.liquidColor + + return ( + + ) + }} + /> + + + + + {t('liquid_volume')} + ( + + )} + /> + + { - setDefineLiquidModal(true) - dispatch(labwareIngredActions.createNewLiquidGroup()) - }} + onClick={handleCancelForm} > - - {t('define_liquid')} + + {t('shared:cancel')} - - - ) : null} - - { - const fullOptions: DropdownOption[] = liquidSelectionOptions.map( - option => { - const liquid = liquids.find( - liquid => liquid.ingredientId === option.value - ) - - return { - name: option.name, - value: option.value, - liquidColor: liquid?.displayColor ?? '', - } + option.value === selectedLiquidId - ) - const selectLiquidIdName = selectedLiquid?.name - const selectLiquidColor = selectedLiquid?.liquidColor - - return ( - - ) - }} - /> - - - - - {t('liquid_volume')} + type="submit" + > + {t('save')} + + + + + ) : null} + + {liquidInfo.length > 0 ? ( + + {t('liquids_added')} - ( - - )} - /> - - - - - {t('shared:cancel')} - - - - {t('save')} - - + ) : null} + {liquidInfo.map(info => { + return + })} - ) : null} - - - {liquidInfo.length > 0 ? ( - - {t('liquids_added')} - - ) : null} - {liquidInfo.map(info => { - return - })} - - + + + ) : ( + + )} ) From 112ea831efe62f984a7b67ed1ba2793b0885f503 Mon Sep 17 00:00:00 2001 From: David Chau <46395074+ddcc4@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:35:35 -0500 Subject: [PATCH 30/31] fix(api): rename TouchTipParams -> LiquidClassTouchTipParams to avoid name conflict (#16848) # Overview Liquid classes defined a new class `TouchTipParams` to configure the touch-tip settings for a liquid. But there's already an existing class called [`TouchTipParams`](../blob/edge/api/src/opentrons/protocol_engine/commands/touch_tip.py) that's used as the argument to the Protocol Engine `touchTip` command. When we generate the [command schema](../blob/edge/shared-data/command/schemas/11.json), both classes get pulled in, and the names clash. This PR renames the liquid class `TouchTipParams` to `LiquidClassTouchTipParams` to avoid the conflict. ## Test Plan and Hands on Testing I ran `make -C api test`, everything seems to pass. ## Risk assessment If this breaks anything, it should only affect our team for now. --- .../liquid_classes/liquid_class_definition.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py index 8247bd2b760..0462ac5c0e4 100644 --- a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py +++ b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py @@ -82,9 +82,13 @@ def _validate_params( return v -class TouchTipParams(BaseModel): +class LiquidClassTouchTipParams(BaseModel): """Parameters for touch-tip.""" + # Note: Do not call this `TouchTipParams`, because that class name is used by the + # unrelated touchTip command in PE. Both classes are exported to things like the + # command schema JSON files, so the classes can't have the same name. + zOffset: _Number = Field( ..., description="Offset from the top of the well for touch-tip, in millimeters.", @@ -101,14 +105,14 @@ class TouchTipProperties(BaseModel): """Shared properties for the touch-tip function.""" enable: bool = Field(..., description="Whether touch-tip is enabled.") - params: Optional[TouchTipParams] = Field( + params: Optional[LiquidClassTouchTipParams] = Field( None, description="Parameters for the touch-tip function." ) @validator("params") def _validate_params( - cls, v: Optional[TouchTipParams], values: Dict[str, Any] - ) -> Optional[TouchTipParams]: + cls, v: Optional[LiquidClassTouchTipParams], values: Dict[str, Any] + ) -> Optional[LiquidClassTouchTipParams]: if v is None and values["enable"]: raise ValueError( "If enable is true parameters for touch tip must be defined." From 0110e172a68414dc0293bea38cb8698dd3a6f48c Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:41:37 -0500 Subject: [PATCH 31/31] fix(protocol-designer): form warnings always show in form (#16846) clsoes RQA-3598 --- .../src/organisms/Alerts/FormAlerts.tsx | 25 ++++++------------- .../Alerts/__tests__/FormAlerts.test.tsx | 3 +-- .../StepForm/StepFormToolbox.tsx | 22 +++++++--------- .../StepTools/MoveLiquidTools/index.tsx | 6 ++--- .../StepForm/StepTools/PauseTools/index.tsx | 12 +++------ .../StepTools/ThermocyclerTools/index.tsx | 6 ++--- .../Designer/ProtocolSteps/StepForm/types.ts | 2 +- 7 files changed, 29 insertions(+), 47 deletions(-) diff --git a/protocol-designer/src/organisms/Alerts/FormAlerts.tsx b/protocol-designer/src/organisms/Alerts/FormAlerts.tsx index f9d7166943d..d51d34ff5d8 100644 --- a/protocol-designer/src/organisms/Alerts/FormAlerts.tsx +++ b/protocol-designer/src/organisms/Alerts/FormAlerts.tsx @@ -29,7 +29,7 @@ import type { ProfileFormError } from '../../steplist/formLevel/profileErrors' import type { MakeAlert } from './types' interface FormAlertsProps { - showFormErrorsAndWarnings: boolean + showFormErrors: boolean focusedField?: StepFieldName | null dirtyFields?: StepFieldName[] page: number @@ -41,7 +41,7 @@ interface WarningType { } function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { - const { showFormErrorsAndWarnings, focusedField, dirtyFields, page } = props + const { showFormErrors, focusedField, dirtyFields, page } = props const { t } = useTranslation('alert') const dispatch = useDispatch() @@ -78,7 +78,7 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { dirtyFields: dirtyFields ?? [], errors: formLevelErrorsForUnsavedForm, page, - showErrors: showFormErrorsAndWarnings, + showErrors: showFormErrors, }) const profileItemsById: Record | null | undefined = @@ -180,28 +180,19 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { } } - if (showFormErrorsAndWarnings) { - return [...formErrors, ...formWarnings].length > 0 ? ( - - {formErrors.map((error, key) => makeAlert('error', error, key))} - {formWarnings.map((warning, key) => makeAlert('warning', warning, key))} - - ) : null - } - - return timelineWarnings.length > 0 ? ( + return [...formErrors, ...timelineWarnings, ...formWarnings].length > 0 ? ( + {showFormErrors + ? formErrors.map((error, key) => makeAlert('error', error, key)) + : null} {timelineWarnings.map((warning, key) => makeAlert('warning', warning, key) )} + {formWarnings.map((warning, key) => makeAlert('warning', warning, key))} ) : null } diff --git a/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx b/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx index 5c7428d6996..14110951a76 100644 --- a/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx +++ b/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx @@ -37,7 +37,7 @@ describe('FormAlerts', () => { props = { focusedField: null, dirtyFields: [], - showFormErrorsAndWarnings: false, + showFormErrors: false, page: 0, } vi.mocked(getFormLevelErrorsForUnsavedForm).mockReturnValue([]) @@ -64,7 +64,6 @@ describe('FormAlerts', () => { expect(vi.mocked(dismissTimelineWarning)).toHaveBeenCalled() }) it('renders a form level warning that is dismissible', () => { - props.showFormErrorsAndWarnings = true vi.mocked(getFormWarningsForSelectedStep).mockReturnValue([ { type: 'TIP_POSITIONED_LOW_IN_TUBE', diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index f8cffb8f6a5..b930af715cb 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -122,10 +122,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { })) const timeline = useSelector(getRobotStateTimeline) const [toolboxStep, setToolboxStep] = useState(0) - const [ - showFormErrorsAndWarnings, - setShowFormErrorsAndWarnings, - ] = useState(false) + const [showFormErrors, setShowFormErrors] = useState(false) const [tab, setTab] = useState('aspirate') const visibleFormWarnings = getVisibleFormWarnings({ focusedField, @@ -140,7 +137,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { ...dynamicFormLevelErrorsForUnsavedForm, ], page: toolboxStep, - showErrors: showFormErrorsAndWarnings, + showErrors: showFormErrors, }) const [isRename, setIsRename] = useState(false) const icon = stepIconsByType[formData.stepType] @@ -187,7 +184,6 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { }) } } - const handleSaveClick = (): void => { if (canSave) { const duration = new Date().getTime() - analyticsStartTime.getTime() @@ -212,7 +208,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { ) dispatch(analyticsEvent(stepDuration)) } else { - setShowFormErrorsAndWarnings(true) + setShowFormErrors(true) if (tab === 'aspirate' && isDispenseError && !isAspirateError) { setTab('dispense') } @@ -227,9 +223,9 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { if (isMultiStepToolbox && toolboxStep === 0) { if (!isErrorOnCurrentPage) { setToolboxStep(1) - setShowFormErrorsAndWarnings(false) + setShowFormErrors(false) } else { - setShowFormErrorsAndWarnings(true) + setShowFormErrors(true) handleScrollToTop() } } else { @@ -279,7 +275,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { width="100%" onClick={() => { setToolboxStep(0) - setShowFormErrorsAndWarnings(false) + setShowFormErrors(false) }} > {i18n.format(t('shared:back'), 'capitalize')} @@ -308,7 +304,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { { setTab('aspirate') - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }, } const dispenseTab = { @@ -110,7 +110,7 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { isActive: tab === 'dispense', onClick: () => { setTab('dispense') - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }, } const hideWellOrderField = diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx index df3e86b2bcb..0da95e20e01 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx @@ -34,11 +34,7 @@ import type { ChangeEvent } from 'react' import type { StepFormProps } from '../../types' export function PauseTools(props: StepFormProps): JSX.Element { - const { - propsForFields, - visibleFormErrors, - setShowFormErrorsAndWarnings, - } = props + const { propsForFields, visibleFormErrors, setShowFormErrors } = props const tempModuleLabwareOptions = useSelector( uiModuleSelectors.getTemperatureLabwareOptions @@ -101,7 +97,7 @@ export function PauseTools(props: StepFormProps): JSX.Element { ) => { propsForFields.pauseAction.updateValue(e.currentTarget.value) - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }} buttonLabel={t( 'form:step_edit_form.field.pauseAction.options.untilResume' @@ -113,7 +109,7 @@ export function PauseTools(props: StepFormProps): JSX.Element { ) => { propsForFields.pauseAction.updateValue(e.currentTarget.value) - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }} buttonLabel={t( 'form:step_edit_form.field.pauseAction.options.untilTime' @@ -125,7 +121,7 @@ export function PauseTools(props: StepFormProps): JSX.Element { ) => { propsForFields.pauseAction.updateValue(e.currentTarget.value) - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }} buttonLabel={t( 'form:step_edit_form.field.pauseAction.options.untilTemperature' diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/index.tsx index 96707eaf2f1..3e85004549e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/index.tsx @@ -25,7 +25,7 @@ export function ThermocyclerTools(props: StepFormProps): JSX.Element { showFormErrors = true, visibleFormErrors, focusedField, - setShowFormErrorsAndWarnings, + setShowFormErrors, } = props const { t } = useTranslation('form') @@ -49,7 +49,7 @@ export function ThermocyclerTools(props: StepFormProps): JSX.Element { onChange={() => { setContentType('thermocyclerState') propsForFields.thermocyclerFormType.updateValue('thermocyclerState') - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }} isSelected={contentType === 'thermocyclerState'} /> @@ -64,7 +64,7 @@ export function ThermocyclerTools(props: StepFormProps): JSX.Element { propsForFields.thermocyclerFormType.updateValue( 'thermocyclerProfile' ) - setShowFormErrorsAndWarnings?.(false) + setShowFormErrors?.(false) }} isSelected={contentType === 'thermocyclerProfile'} /> diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts index ac05085fa31..ffbfd8b32c3 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts @@ -30,7 +30,7 @@ export interface StepFormProps { visibleFormErrors: StepFormErrors showFormErrors: boolean focusedField?: string | null - setShowFormErrorsAndWarnings?: React.Dispatch> + setShowFormErrors?: React.Dispatch> tab: LiquidHandlingTab setTab: React.Dispatch> }