-
+
{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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] 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/68] =?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) => (
}
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)
}
>
-
+
+
+ ) : (
+
+ )}
>
)
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/68] 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/68] 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>
}
From 26d0198e8716d3bf144e3bfdb48b2f39cae940f4 Mon Sep 17 00:00:00 2001
From: koji
Date: Fri, 15 Nov 2024 14:56:04 -0500
Subject: [PATCH 32/68] fix(protocol-designer): fix decksetup tools position
when offdeck is selected (#16842)
* fix(protocol-designer): fix decksetup tools position when offdeck is selected
---
.../src/organisms/ProtocolNavBar/index.tsx | 11 +++++++++--
.../src/pages/Designer/Offdeck/Offdeck.tsx | 4 ++--
protocol-designer/src/pages/Designer/index.tsx | 1 +
3 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx
index f21efb0fd3a..6ae4b21ff45 100644
--- a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx
+++ b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx
@@ -30,6 +30,7 @@ interface ProtocolNavBarProps {
showLiquidOverflowMenu?: (liquidOverflowMenu: boolean) => void
isAddingHardwareOrLabware?: boolean
liquidPage?: boolean
+ isOffDeck?: boolean
}
export function ProtocolNavBar({
@@ -39,6 +40,7 @@ export function ProtocolNavBar({
hasTrashEntity,
showLiquidOverflowMenu,
liquidPage = false,
+ isOffDeck = false,
}: ProtocolNavBarProps): JSX.Element {
const { t } = useTranslation('starting_deck_state')
const metadata = useSelector(getFileMetadata)
@@ -54,13 +56,18 @@ export function ProtocolNavBar({
{metadata?.protocolName != null && metadata?.protocolName !== ''
? metadata?.protocolName
: t('untitled_protocol')}
-
- {isAddingHardwareOrLabware
+
+ {isAddingHardwareOrLabware || isOffDeck
? t('add_hardware_labware')
: t('edit_protocol')}
diff --git a/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx b/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx
index 66182e8dd93..c972c41d084 100644
--- a/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx
+++ b/protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx
@@ -110,9 +110,9 @@ export function OffDeck(props: DeckSetupTabType): JSX.Element {
}
return (
-
+
{selectedSlot.slot === 'offDeck' ? (
-
+
{tab === 'startingDeck' ? (
From 32c6e080e2ab9c7789e660ea7f666f89c0235a90 Mon Sep 17 00:00:00 2001
From: koji
Date: Fri, 15 Nov 2024 15:17:47 -0500
Subject: [PATCH 33/68] fix(protocol-designer): add Pseudo-classes to liquid
button (#16800)
* fix(protocol-designer): add Pseudo-classes to liquid button
---
components/src/atoms/ToggleGroup/index.tsx | 3 ++-
.../organisms/ProtocolNavBar/LiquidButton.tsx | 19 +++++++++++++++++++
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/components/src/atoms/ToggleGroup/index.tsx b/components/src/atoms/ToggleGroup/index.tsx
index 8d085574a18..99a5b24f73b 100644
--- a/components/src/atoms/ToggleGroup/index.tsx
+++ b/components/src/atoms/ToggleGroup/index.tsx
@@ -81,10 +81,11 @@ const ACTIVE_STYLE = css`
background-color: ${COLORS.blue50};
color: ${COLORS.white};
pointer-events: none;
+ border: 1px ${COLORS.blue50} solid;
`
const DEFAULT_STYLE = css`
background-color: ${COLORS.white};
color: ${COLORS.black90};
- outline: 1px ${COLORS.grey30} solid;
+ border: 1px ${COLORS.grey30} solid;
`
diff --git a/protocol-designer/src/organisms/ProtocolNavBar/LiquidButton.tsx b/protocol-designer/src/organisms/ProtocolNavBar/LiquidButton.tsx
index 2ebd74a3ae4..58a6641aff8 100644
--- a/protocol-designer/src/organisms/ProtocolNavBar/LiquidButton.tsx
+++ b/protocol-designer/src/organisms/ProtocolNavBar/LiquidButton.tsx
@@ -40,4 +40,23 @@ const LIQUID_BUTTON_STYLE = css`
align-items: ${ALIGN_CENTER};
border-radius: ${BORDERS.borderRadius8};
background-color: ${COLORS.grey30};
+
+ &:focus-visible {
+ outline-offset: 3px;
+ outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50};
+ }
+
+ &:active {
+ background-color: ${COLORS.grey40};
+ }
+
+ &:hover {
+ box-shadow: 0 0 0;
+ background-color: ${COLORS.grey35};
+ }
+
+ &:disabled {
+ background-color: ${COLORS.grey30};
+ color: ${COLORS.grey40};
+ }
`
From 99b9f76093bbd50001210be06fe59f1d52b94bd8 Mon Sep 17 00:00:00 2001
From: Alise Au <20424172+ahiuchingau@users.noreply.github.com>
Date: Fri, 15 Nov 2024 15:53:54 -0500
Subject: [PATCH 34/68] chore(hardware-testing): bump mypy slightly to fix
failed ci tests (#16858)
---
hardware-testing/Pipfile | 2 +-
hardware-testing/Pipfile.lock | 479 ++++++++++--------
.../hardware_testing/drivers/__init__.py | 2 +-
.../hardware_testing/drivers/asair_sensor.py | 4 +-
.../opentrons_api/helpers_ot3.py | 4 +-
.../test_connectivity.py | 6 +-
.../robot_assembly_qc_ot3/test_peripherals.py | 2 +-
.../robot_assembly_qc_ot3/test_signals.py | 2 +-
.../protocols/plate_reader_qc_protocol.py | 369 +++++++++++---
.../scripts/ABRAsairScript.py | 2 +-
10 files changed, 579 insertions(+), 293 deletions(-)
diff --git a/hardware-testing/Pipfile b/hardware-testing/Pipfile
index c4b15b68416..e3c824f42b2 100644
--- a/hardware-testing/Pipfile
+++ b/hardware-testing/Pipfile
@@ -19,7 +19,7 @@ atomicwrites = "==1.4.1"
colorama = "==0.4.4"
pytest = "==7.1.1"
pytest-cov = "==2.10.1"
-mypy = "==0.981"
+mypy = "==0.990"
black = "==22.3.0"
flake8 = "~=3.9.0"
flake8-annotations = "~=2.6.2"
diff --git a/hardware-testing/Pipfile.lock b/hardware-testing/Pipfile.lock
index 964ad78d21b..f8dcfd2f7df 100644
--- a/hardware-testing/Pipfile.lock
+++ b/hardware-testing/Pipfile.lock
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
- "sha256": "d22e237a7b020d3cf9efb022af3a5ebb7f6685b8fa255b79095a383e4b84eacf"
+ "sha256": "bfa6574dcab4bd350d77135bd8ecefd5e12ad6479664f932a7c0c544ecdf4c47"
},
"pipfile-spec": 6,
"requires": {
@@ -158,46 +158,38 @@
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
- "colorama": {
- "hashes": [
- "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
- "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
- ],
- "markers": "platform_system == 'Windows'",
- "version": "==0.4.6"
- },
"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"
+ "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"
],
"markers": "python_version >= '3.7'",
- "version": "==43.0.1"
+ "version": "==43.0.3"
},
"exceptiongroup": {
"hashes": [
@@ -227,6 +219,68 @@
"markers": "python_version >= '3.7'",
"version": "==4.17.3"
},
+ "msgpack": {
+ "hashes": [
+ "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982",
+ "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3",
+ "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40",
+ "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee",
+ "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693",
+ "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950",
+ "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151",
+ "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24",
+ "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305",
+ "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b",
+ "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c",
+ "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659",
+ "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d",
+ "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18",
+ "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746",
+ "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868",
+ "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2",
+ "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba",
+ "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228",
+ "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2",
+ "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273",
+ "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c",
+ "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653",
+ "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a",
+ "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596",
+ "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd",
+ "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8",
+ "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa",
+ "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85",
+ "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc",
+ "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836",
+ "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3",
+ "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58",
+ "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128",
+ "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db",
+ "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f",
+ "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77",
+ "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad",
+ "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13",
+ "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8",
+ "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b",
+ "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a",
+ "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543",
+ "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b",
+ "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce",
+ "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d",
+ "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a",
+ "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c",
+ "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f",
+ "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e",
+ "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011",
+ "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04",
+ "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480",
+ "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a",
+ "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d",
+ "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"
+ ],
+ "markers": "platform_system != 'Windows'",
+ "version": "==1.0.8"
+ },
"numpy": {
"hashes": [
"sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b",
@@ -291,11 +345,11 @@
},
"packaging": {
"hashes": [
- "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
- "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
],
"markers": "python_version >= '3.8'",
- "version": "==24.1"
+ "version": "==24.2"
},
"paramiko": {
"hashes": [
@@ -316,52 +370,52 @@
},
"pydantic": {
"hashes": [
- "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620",
- "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82",
- "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62",
- "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c",
- "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c",
- "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682",
- "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048",
- "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b",
- "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03",
- "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f",
- "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a",
- "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1",
- "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe",
- "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33",
- "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f",
- "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518",
- "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485",
- "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f",
- "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec",
- "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70",
- "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86",
- "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf",
- "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d",
- "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588",
- "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481",
- "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9",
- "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3",
- "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab",
- "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7",
- "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a",
- "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0",
- "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc",
- "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861",
- "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357",
- "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a",
- "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3",
- "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80",
- "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02",
- "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b",
- "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5",
- "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2",
- "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890",
- "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"
+ "sha256:0399094464ae7f28482de22383e667625e38e1516d6b213176df1acdd0c477ea",
+ "sha256:076c49e24b73d346c45f9282d00dbfc16eef7ae27c970583d499f11110d9e5b0",
+ "sha256:07d00ca5ef0de65dd274005433ce2bb623730271d495a7d190a91c19c5679d34",
+ "sha256:0890fbd7fec9e151c7512941243d830b2d6076d5df159a2030952d480ab80a4e",
+ "sha256:0bfb5b378b78229119d66ced6adac2e933c67a0aa1d0a7adffbe432f3ec14ce4",
+ "sha256:0d32227ea9a3bf537a2273fd2fdb6d64ab4d9b83acd9e4e09310a777baaabb98",
+ "sha256:11965f421f7eb026439d4eb7464e9182fe6d69c3d4d416e464a4485d1ba61ab6",
+ "sha256:1fc8cc264afaf47ae6a9bcbd36c018d0c6b89293835d7fb0e5e1a95898062d59",
+ "sha256:2206a1752d9fac011e95ca83926a269fb0ef5536f7e053966d058316e24d929f",
+ "sha256:22a1794e01591884741be56c6fba157c4e99dcc9244beb5a87bd4aa54b84ea8b",
+ "sha256:4739c206bfb6bb2bdc78dcd40bfcebb2361add4ceac6d170e741bb914e9eff0f",
+ "sha256:4a5d5b877c7d3d9e17399571a8ab042081d22fe6904416a8b20f8af5909e6c8f",
+ "sha256:566bebdbe6bc0ac593fa0f67d62febbad9f8be5433f686dc56401ba4aab034e3",
+ "sha256:570ad0aeaf98b5e33ff41af75aba2ef6604ee25ce0431ecd734a28e74a208555",
+ "sha256:573254d844f3e64093f72fcd922561d9c5696821ff0900a0db989d8c06ab0c25",
+ "sha256:5d4320510682d5a6c88766b2a286d03b87bd3562bf8d78c73d63bab04b21e7b4",
+ "sha256:6d8a38a44bb6a15810084316ed69c854a7c06e0c99c5429f1d664ad52cec353c",
+ "sha256:6eb56074b11a696e0b66c7181da682e88c00e5cebe6570af8013fcae5e63e186",
+ "sha256:7e66aa0fa7f8aa9d0a620361834f6eb60d01d3e9cea23ca1a92cda99e6f61dac",
+ "sha256:7ea24e8614f541d69ea72759ff635df0e612b7dc9d264d43f51364df310081a3",
+ "sha256:7f31742c95e3f9443b8c6fa07c119623e61d76603be9c0d390bcf7e888acabcb",
+ "sha256:83ee8c9916689f8e6e7d90161e6663ac876be2efd32f61fdcfa3a15e87d4e413",
+ "sha256:8b2cf5e26da84f2d2dee3f60a3f1782adedcee785567a19b68d0af7e1534bd1f",
+ "sha256:945407f4d08cd12485757a281fca0e5b41408606228612f421aa4ea1b63a095d",
+ "sha256:9c46f58ef2df958ed2ea7437a8be0897d5efe9ee480818405338c7da88186fb3",
+ "sha256:9d7d48fbc5289efd23982a0d68e973a1f37d49064ccd36d86de4543aff21e086",
+ "sha256:9f28a81978e936136c44e6a70c65bde7548d87f3807260f73aeffbf76fb94c2f",
+ "sha256:a415b9e95fa602b10808113967f72b2da8722061265d6af69268c111c254832d",
+ "sha256:a82746c6d6e91ca17e75f7f333ed41d70fce93af520a8437821dec3ee52dfb10",
+ "sha256:ad57004e5d73aee36f1e25e4e73a4bc853b473a1c30f652dc8d86b0a987ffce3",
+ "sha256:c6444368b651a14c2ce2fb22145e1496f7ab23cbdb978590d47c8d34a7bc0289",
+ "sha256:d216f8d0484d88ab72ab45d699ac669fe031275e3fa6553e3804e69485449fa0",
+ "sha256:d3449633c207ec3d2d672eedb3edbe753e29bd4e22d2e42a37a2c1406564c20f",
+ "sha256:d5b5b7c6bafaef90cbb7dafcb225b763edd71d9e22489647ee7df49d6d341890",
+ "sha256:d7a8a1dd68bac29f08f0a3147de1885f4dccec35d4ea926e6e637fac03cdb4b3",
+ "sha256:d8d72553d2f3f57ce547de4fa7dc8e3859927784ab2c88343f1fc1360ff17a08",
+ "sha256:dce355fe7ae53e3090f7f5fa242423c3a7b53260747aa398b4b3aaf8b25f41c3",
+ "sha256:e351df83d1c9cffa53d4e779009a093be70f1d5c6bb7068584086f6a19042526",
+ "sha256:ec5c44e6e9eac5128a9bfd21610df3b8c6b17343285cc185105686888dc81206",
+ "sha256:f5bb81fcfc6d5bff62cd786cbd87480a11d23f16d5376ad2e057c02b3b44df96",
+ "sha256:fd34012691fbd4e67bdf4accb1f0682342101015b78327eaae3543583fcd451e",
+ "sha256:fea36c2065b7a1d28c6819cc2e93387b43dd5d3cf5a1e82d8132ee23f36d1f10",
+ "sha256:ff09600cebe957ecbb4a27496fe34c1d449e7957ed20a202d5029a71a8af2e35"
],
"markers": "python_version >= '3.7'",
- "version": "==1.10.18"
+ "version": "==1.10.19"
},
"pynacl": {
"hashes": [
@@ -441,33 +495,13 @@
"markers": "python_full_version >= '3.6.0'",
"version": "==1.2.1"
},
- "pywin32": {
- "hashes": [
- "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d",
- "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65",
- "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e",
- "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b",
- "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4",
- "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040",
- "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a",
- "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36",
- "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8",
- "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e",
- "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802",
- "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a",
- "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407",
- "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"
- ],
- "markers": "platform_system == 'Windows' and platform_python_implementation == 'CPython'",
- "version": "==306"
- },
"setuptools": {
"hashes": [
- "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2",
- "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"
+ "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef",
+ "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"
],
- "markers": "python_version >= '3.8'",
- "version": "==75.1.0"
+ "markers": "python_version >= '3.9'",
+ "version": "==75.5.0"
},
"sniffio": {
"hashes": [
@@ -479,21 +513,21 @@
},
"types-paramiko": {
"hashes": [
- "sha256:6f9b311c63c16c74b923529315e6c75585b323f910b121568d4b3e47cedaf321",
- "sha256:e0e6c6c72abe922b035edd62741b4a1cd056ec50c548b2b9e17539bb27b2ba94"
+ "sha256:79dd9b2ee510b76a3b60d8ac1f3f348c45fcecf01347ca79e14db726bbfc442d",
+ "sha256:cda0aff4905fe8efe4b5448331a80e943d42a796bd4beb77a3eed3485bc96a85"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==3.5.0.20240918"
+ "version": "==3.5.0.20240928"
},
"types-pytz": {
"hashes": [
- "sha256:4433b5df4a6fc587bbed41716d86a5ba5d832b4378e506f40d34bc9c81df2c24",
- "sha256:a1eebf57ebc6e127a99d2fa2ba0a88d2b173784ef9b3defcc2004ab6855a44df"
+ "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7",
+ "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==2024.2.0.20240913"
+ "version": "==2024.2.0.20241003"
},
"typing-extensions": {
"hashes": [
@@ -652,89 +686,80 @@
},
"colorama": {
"hashes": [
- "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
- "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
+ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
+ "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
],
- "markers": "platform_system == 'Windows'",
- "version": "==0.4.6"
+ "index": "pypi",
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==0.4.4"
},
"coverage": {
"hashes": [
- "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca",
- "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d",
- "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6",
- "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989",
- "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c",
- "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b",
- "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223",
- "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f",
- "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56",
- "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3",
- "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8",
- "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb",
- "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388",
- "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0",
- "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a",
- "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8",
- "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f",
- "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a",
- "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962",
- "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8",
- "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391",
- "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc",
- "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2",
- "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155",
- "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb",
- "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0",
- "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c",
- "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a",
- "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004",
- "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060",
- "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232",
- "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93",
- "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129",
- "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163",
- "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de",
- "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6",
- "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23",
- "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569",
- "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d",
- "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778",
- "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d",
- "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36",
- "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a",
- "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6",
- "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34",
- "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704",
- "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106",
- "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9",
- "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862",
- "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b",
- "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255",
- "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16",
- "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3",
- "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133",
- "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb",
- "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657",
- "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d",
- "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca",
- "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36",
- "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c",
- "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e",
- "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff",
- "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7",
- "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5",
- "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02",
- "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c",
- "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df",
- "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3",
- "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a",
- "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959",
- "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234",
- "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"
+ "sha256:0266b62cbea568bd5e93a4da364d05de422110cbed5056d69339bd5af5685433",
+ "sha256:0573f5cbf39114270842d01872952d301027d2d6e2d84013f30966313cadb529",
+ "sha256:0ddcb70b3a3a57581b450571b31cb774f23eb9519c2aaa6176d3a84c9fc57671",
+ "sha256:108bb458827765d538abcbf8288599fee07d2743357bdd9b9dad456c287e121e",
+ "sha256:14045b8bfd5909196a90da145a37f9d335a5d988a83db34e80f41e965fb7cb42",
+ "sha256:1a5407a75ca4abc20d6252efeb238377a71ce7bda849c26c7a9bece8680a5d99",
+ "sha256:2bc3e45c16564cc72de09e37413262b9f99167803e5e48c6156bccdfb22c8327",
+ "sha256:2d608a7808793e3615e54e9267519351c3ae204a6d85764d8337bd95993581a8",
+ "sha256:34d23e28ccb26236718a3a78ba72744212aa383141961dd6825f6595005c8b06",
+ "sha256:37a15573f988b67f7348916077c6d8ad43adb75e478d0910957394df397d2874",
+ "sha256:3c0317288f032221d35fa4cbc35d9f4923ff0dfd176c79c9b356e8ef8ef2dff4",
+ "sha256:3c42ec2c522e3ddd683dec5cdce8e62817afb648caedad9da725001fa530d354",
+ "sha256:3c6b24007c4bcd0b19fac25763a7cac5035c735ae017e9a349b927cfc88f31c1",
+ "sha256:40cca284c7c310d622a1677f105e8507441d1bb7c226f41978ba7c86979609ab",
+ "sha256:46f21663e358beae6b368429ffadf14ed0a329996248a847a4322fb2e35d64d3",
+ "sha256:49ed5ee4109258973630c1f9d099c7e72c5c36605029f3a91fe9982c6076c82b",
+ "sha256:5c95e0fa3d1547cb6f021ab72f5c23402da2358beec0a8e6d19a368bd7b0fb37",
+ "sha256:5dd4e4a49d9c72a38d18d641135d2fb0bdf7b726ca60a103836b3d00a1182acd",
+ "sha256:5e444b8e88339a2a67ce07d41faabb1d60d1004820cee5a2c2b54e2d8e429a0f",
+ "sha256:60dcf7605c50ea72a14490d0756daffef77a5be15ed1b9fea468b1c7bda1bc3b",
+ "sha256:623e6965dcf4e28a3debaa6fcf4b99ee06d27218f46d43befe4db1c70841551c",
+ "sha256:673184b3156cba06154825f25af33baa2671ddae6343f23175764e65a8c4c30b",
+ "sha256:6cf96ceaa275f071f1bea3067f8fd43bec184a25a962c754024c973af871e1b7",
+ "sha256:70a56a2ec1869e6e9fa69ef6b76b1a8a7ef709972b9cc473f9ce9d26b5997ce3",
+ "sha256:77256ad2345c29fe59ae861aa11cfc74579c88d4e8dbf121cbe46b8e32aec808",
+ "sha256:796c9b107d11d2d69e1849b2dfe41730134b526a49d3acb98ca02f4985eeff7a",
+ "sha256:7c07de0d2a110f02af30883cd7dddbe704887617d5c27cf373362667445a4c76",
+ "sha256:7e61b0e77ff4dddebb35a0e8bb5a68bf0f8b872407d8d9f0c726b65dfabe2469",
+ "sha256:82c809a62e953867cf57e0548c2b8464207f5f3a6ff0e1e961683e79b89f2c55",
+ "sha256:850cfd2d6fc26f8346f422920ac204e1d28814e32e3a58c19c91980fa74d8289",
+ "sha256:87ea64b9fa52bf395272e54020537990a28078478167ade6c61da7ac04dc14bc",
+ "sha256:90746521206c88bdb305a4bf3342b1b7316ab80f804d40c536fc7d329301ee13",
+ "sha256:951aade8297358f3618a6e0660dc74f6b52233c42089d28525749fc8267dccd2",
+ "sha256:963e4a08cbb0af6623e61492c0ec4c0ec5c5cf74db5f6564f98248d27ee57d30",
+ "sha256:987a8e3da7da4eed10a20491cf790589a8e5e07656b6dc22d3814c4d88faf163",
+ "sha256:9c2eb378bebb2c8f65befcb5147877fc1c9fbc640fc0aad3add759b5df79d55d",
+ "sha256:a1ab9763d291a17b527ac6fd11d1a9a9c358280adb320e9c2672a97af346ac2c",
+ "sha256:a3b925300484a3294d1c70f6b2b810d6526f2929de954e5b6be2bf8caa1f12c1",
+ "sha256:acbb8af78f8f91b3b51f58f288c0994ba63c646bc1a8a22ad072e4e7e0a49f1c",
+ "sha256:ad32a981bcdedb8d2ace03b05e4fd8dace8901eec64a532b00b15217d3677dd2",
+ "sha256:aee9cf6b0134d6f932d219ce253ef0e624f4fa588ee64830fcba193269e4daa3",
+ "sha256:af05bbba896c4472a29408455fe31b3797b4d8648ed0a2ccac03e074a77e2314",
+ "sha256:b6cce5c76985f81da3769c52203ee94722cd5d5889731cd70d31fee939b74bf0",
+ "sha256:bb684694e99d0b791a43e9fc0fa58efc15ec357ac48d25b619f207c41f2fd384",
+ "sha256:c132b5a22821f9b143f87446805e13580b67c670a548b96da945a8f6b4f2efbb",
+ "sha256:c296263093f099da4f51b3dff1eff5d4959b527d4f2f419e16508c5da9e15e8c",
+ "sha256:c973b2fe4dc445cb865ab369df7521df9c27bf40715c837a113edaa2aa9faf45",
+ "sha256:cdd94501d65adc5c24f8a1a0eda110452ba62b3f4aeaba01e021c1ed9cb8f34a",
+ "sha256:d79d4826e41441c9a118ff045e4bccb9fdbdcb1d02413e7ea6eb5c87b5439d24",
+ "sha256:dbba8210f5067398b2c4d96b4e64d8fb943644d5eb70be0d989067c8ca40c0f8",
+ "sha256:df002e59f2d29e889c37abd0b9ee0d0e6e38c24f5f55d71ff0e09e3412a340ec",
+ "sha256:dfd14bcae0c94004baba5184d1c935ae0d1231b8409eb6c103a5fd75e8ecdc56",
+ "sha256:e25bacb53a8c7325e34d45dddd2f2fbae0dbc230d0e2642e264a64e17322a777",
+ "sha256:e2c8e3384c12dfa19fa9a52f23eb091a8fad93b5b81a41b14c17c78e23dd1d8b",
+ "sha256:e5f2a0f161d126ccc7038f1f3029184dbdf8f018230af17ef6fd6a707a5b881f",
+ "sha256:e69ad502f1a2243f739f5bd60565d14a278be58be4c137d90799f2c263e7049a",
+ "sha256:ead9b9605c54d15be228687552916c89c9683c215370c4a44f1f217d2adcc34d",
+ "sha256:f07ff574986bc3edb80e2c36391678a271d555f91fd1d332a1e0f4b5ea4b6ea9",
+ "sha256:f2c7a045eef561e9544359a0bf5784b44e55cefc7261a20e730baa9220c83413",
+ "sha256:f3e8796434a8106b3ac025fd15417315d7a58ee3e600ad4dbcfddc3f4b14342c",
+ "sha256:f63e21ed474edd23f7501f89b53280014436e383a14b9bd77a648366c81dce7b",
+ "sha256:fd49c01e5057a451c30c9b892948976f5d38f2cbd04dc556a82743ba8e27ed8c"
],
- "markers": "python_version >= '3.8'",
- "version": "==7.6.1"
+ "markers": "python_version >= '3.9'",
+ "version": "==7.6.7"
},
"flake8": {
"hashes": [
@@ -796,34 +821,40 @@
},
"mypy": {
"hashes": [
- "sha256:06e1eac8d99bd404ed8dd34ca29673c4346e76dd8e612ea507763dccd7e13c7a",
- "sha256:2ee3dbc53d4df7e6e3b1c68ac6a971d3a4fb2852bf10a05fda228721dd44fae1",
- "sha256:4bc460e43b7785f78862dab78674e62ec3cd523485baecfdf81a555ed29ecfa0",
- "sha256:64e1f6af81c003f85f0dfed52db632817dabb51b65c0318ffbf5ff51995bbb08",
- "sha256:6e35d764784b42c3e256848fb8ed1d4292c9fc0098413adb28d84974c095b279",
- "sha256:6ee196b1d10b8b215e835f438e06965d7a480f6fe016eddbc285f13955cca659",
- "sha256:756fad8b263b3ba39e4e204ee53042671b660c36c9017412b43af210ddee7b08",
- "sha256:77f8fcf7b4b3cc0c74fb33ae54a4cd00bb854d65645c48beccf65fa10b17882c",
- "sha256:794f385653e2b749387a42afb1e14c2135e18daeb027e0d97162e4b7031210f8",
- "sha256:8ad21d4c9d3673726cf986ea1d0c9fb66905258709550ddf7944c8f885f208be",
- "sha256:8e8e49aa9cc23aa4c926dc200ce32959d3501c4905147a66ce032f05cb5ecb92",
- "sha256:9f362470a3480165c4c6151786b5379351b790d56952005be18bdbdd4c7ce0ae",
- "sha256:a16a0145d6d7d00fbede2da3a3096dcc9ecea091adfa8da48fa6a7b75d35562d",
- "sha256:ad77c13037d3402fbeffda07d51e3f228ba078d1c7096a73759c9419ea031bf4",
- "sha256:b6ede64e52257931315826fdbfc6ea878d89a965580d1a65638ef77cb551f56d",
- "sha256:c9e0efb95ed6ca1654951bd5ec2f3fa91b295d78bf6527e026529d4aaa1e0c30",
- "sha256:ce65f70b14a21fdac84c294cde75e6dbdabbcff22975335e20827b3b94bdbf49",
- "sha256:d1debb09043e1f5ee845fa1e96d180e89115b30e47c5d3ce53bc967bab53f62d",
- "sha256:e178eaffc3c5cd211a87965c8c0df6da91ed7d258b5fc72b8e047c3771317ddb",
- "sha256:e1acf62a8c4f7c092462c738aa2c2489e275ed386320c10b2e9bff31f6f7e8d6",
- "sha256:e53773073c864d5f5cec7f3fc72fbbcef65410cde8cc18d4f7242dea60dac52e",
- "sha256:eb3978b191b9fa0488524bb4ffedf2c573340e8c2b4206fc191d44c7093abfb7",
- "sha256:f64d2ce043a209a297df322eb4054dfbaa9de9e8738291706eaafda81ab2b362",
- "sha256:fa38f82f53e1e7beb45557ff167c177802ba7b387ad017eab1663d567017c8ee"
+ "sha256:0680389c34284287fe00e82fc8bccdea9aff318f7e7d55b90d967a13a9606013",
+ "sha256:1767830da2d1afa4e62b684647af0ff79b401f004d7fa08bc5b0ce2d45bcd5ec",
+ "sha256:1ee5f99817ee70254e7eb5cf97c1b11dda29c6893d846c8b07bce449184e9466",
+ "sha256:262c543ef24deb10470a3c1c254bb986714e2b6b1a67d66daf836a548a9f316c",
+ "sha256:269f0dfb6463b8780333310ff4b5134425157ef0d2b1d614015adaf6d6a7eabd",
+ "sha256:2a3150d409609a775c8cb65dbe305c4edd7fe576c22ea79d77d1454acd9aeda8",
+ "sha256:2b6f85c2ad378e3224e017904a051b26660087b3b76490d533b7344f1546d3ff",
+ "sha256:3227f14fe943524f5794679156488f18bf8d34bfecd4623cf76bc55958d229c5",
+ "sha256:3ff201a0c6d3ea029d73b1648943387d75aa052491365b101f6edd5570d018ea",
+ "sha256:46897755f944176fbc504178422a5a2875bbf3f7436727374724842c0987b5af",
+ "sha256:47a9955214615108c3480a500cfda8513a0b1cd3c09a1ed42764ca0dd7b931dd",
+ "sha256:49082382f571c3186ce9ea0bd627cb1345d4da8d44a8377870f4442401f0a706",
+ "sha256:4a8a6c10f4c63fbf6ad6c03eba22c9331b3946a4cec97f008e9ffb4d3b31e8e2",
+ "sha256:6826d9c4d85bbf6d68cb279b561de6a4d8d778ca8e9ab2d00ee768ab501a9852",
+ "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d",
+ "sha256:7da0005e47975287a92b43276e460ac1831af3d23032c34e67d003388a0ce8d0",
+ "sha256:8798c8ed83aa809f053abff08664bdca056038f5a02af3660de00b7290b64c47",
+ "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6",
+ "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9",
+ "sha256:9b8f4a8213b1fd4b751e26b59ae0e0c12896568d7e805861035c7a15ed6dc9eb",
+ "sha256:9d851c09b981a65d9d283a8ccb5b1d0b698e580493416a10942ef1a04b19fd37",
+ "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa",
+ "sha256:aba38e3dd66bdbafbbfe9c6e79637841928ea4c79b32e334099463c17b0d90ef",
+ "sha256:b08541a06eed35b543ae1a6b301590eb61826a1eb099417676ddc5a42aa151c5",
+ "sha256:be88d665e76b452c26fb2bdc3d54555c01226fba062b004ede780b190a50f9db",
+ "sha256:c76c769c46a1e6062a84837badcb2a7b0cdb153d68601a61f60739c37d41cc74",
+ "sha256:cc6019808580565040cd2a561b593d7c3c646badd7e580e07d875eb1bf35c695",
+ "sha256:cd2dd3730ba894ec2a2082cc703fbf3e95a08479f7be84912e3131fc68809d46",
+ "sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4",
+ "sha256:d847dd23540e2912d9667602271e5ebf25e5788e7da46da5ffd98e7872616e8e"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
- "version": "==0.981"
+ "version": "==0.990"
},
"mypy-extensions": {
"hashes": [
@@ -835,11 +866,11 @@
},
"packaging": {
"hashes": [
- "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
- "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+ "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+ "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
],
"markers": "python_version >= '3.8'",
- "version": "==24.1"
+ "version": "==24.2"
},
"pathspec": {
"hashes": [
@@ -941,11 +972,11 @@
},
"tomli": {
"hashes": [
- "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
- "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+ "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8",
+ "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"
],
- "markers": "python_version >= '3.7'",
- "version": "==2.0.1"
+ "markers": "python_version < '3.11'",
+ "version": "==2.1.0"
},
"types-requests": {
"hashes": [
diff --git a/hardware-testing/hardware_testing/drivers/__init__.py b/hardware-testing/hardware_testing/drivers/__init__.py
index f1b4c991e2c..7dfc1c9ea51 100644
--- a/hardware-testing/hardware_testing/drivers/__init__.py
+++ b/hardware-testing/hardware_testing/drivers/__init__.py
@@ -4,7 +4,7 @@
from .radwag import RadwagScaleBase, RadwagScale, SimRadwagScale
-def list_ports_and_select(device_name: str = "", port_substr: str = None) -> str:
+def list_ports_and_select(device_name: str = "", port_substr: str = "") -> str:
"""List serial ports and display list for user to select from."""
ports = comports()
assert ports, "no serial ports found"
diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py
index ab61dcf4e5d..beb225f9d55 100644
--- a/hardware-testing/hardware_testing/drivers/asair_sensor.py
+++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py
@@ -42,7 +42,7 @@
class AsairSensorError(Exception):
"""Asair sensor error."""
- def __init__(self, ret_code: str = None) -> None:
+ def __init__(self, ret_code: str = "") -> None:
"""Constructor."""
super().__init__(ret_code)
@@ -75,7 +75,7 @@ def get_serial(self) -> str:
def BuildAsairSensor(
- simulate: bool, autosearch: bool = True, port_substr: str = None
+ simulate: bool, autosearch: bool = True, port_substr: str = ""
) -> AsairSensorBase:
"""Try to find and return an Asair sensor, if not found return a simulator."""
ui.print_title("Connecting to Environmental sensor")
diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py
index cf49ae8feff..b68e8e98343 100644
--- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py
+++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py
@@ -1014,13 +1014,13 @@ def set_pipette_offset_ot3(api: OT3API, mount: OT3Mount, offset: Point) -> None:
def get_gripper_offset_ot3(api: OT3API) -> Point:
"""Get gripper offset OT3."""
- assert api.has_gripper, "No gripper found"
+ assert api.has_gripper(), "No gripper found"
return api._gripper_handler._gripper._calibration_offset.offset # type: ignore[union-attr]
def set_gripper_offset_ot3(api: OT3API, offset: Point) -> None:
"""Set gripper offset OT3."""
- assert api.has_gripper, "No gripper found"
+ assert api.has_gripper(), "No gripper found"
api._gripper_handler._gripper._calibration_offset.offset = offset # type: ignore[union-attr]
diff --git a/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_connectivity.py b/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_connectivity.py
index 66e4bb72782..d986521c27d 100644
--- a/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_connectivity.py
+++ b/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_connectivity.py
@@ -385,9 +385,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None:
await _test_wifi(report, section)
else:
report(section, "wifi", ["", "", "0.0.0.0", CSVResult.PASS])
- assert nmcli.iface_info
- assert nmcli.configure
- assert nmcli.wifi_disconnect
+ assert nmcli.iface_info is not None
+ assert nmcli.configure is not None
+ assert nmcli.wifi_disconnect is not None
# USB-B-REAR
ui.print_header("USB-B-REAR")
diff --git a/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_peripherals.py b/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_peripherals.py
index b7ea527955e..f4d900a3279 100644
--- a/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_peripherals.py
+++ b/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_peripherals.py
@@ -69,7 +69,7 @@
async def _get_ip(api: OT3API) -> Optional[str]:
_ip: Optional[str] = None
if api.is_simulator:
- assert nmcli.iface_info
+ assert nmcli.iface_info is not None
_ip = "127.0.0.1"
else:
ethernet_status = await nmcli.iface_info(nmcli.NETWORK_IFACES.ETH_LL)
diff --git a/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_signals.py b/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_signals.py
index 419a5e6350c..5359d74c909 100644
--- a/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_signals.py
+++ b/hardware-testing/hardware_testing/production_qc/robot_assembly_qc_ot3/test_signals.py
@@ -76,7 +76,7 @@ async def _move_and_interrupt_with_signal(api: OT3API, sig_name: str) -> None:
runner = MoveGroupRunner(move_groups=[[_move_group_nsync]])
if api.is_simulator:
# test that the required functionality exists
- assert runner.run
+ assert runner.run is not None
else:
backend: OT3Controller = api._backend # type: ignore[assignment]
messenger = backend._messenger
diff --git a/hardware-testing/hardware_testing/protocols/plate_reader_qc_protocol.py b/hardware-testing/hardware_testing/protocols/plate_reader_qc_protocol.py
index c8ec7cc59af..9f81063be9d 100644
--- a/hardware-testing/hardware_testing/protocols/plate_reader_qc_protocol.py
+++ b/hardware-testing/hardware_testing/protocols/plate_reader_qc_protocol.py
@@ -1,3 +1,5 @@
+# flake8: noqa
+
from opentrons import protocol_api
import numpy as np
from typing import cast
@@ -5,8 +7,8 @@
# metadata
metadata = {
- 'protocolName': 'Absorbance Plate Reader Reference Plate QA',
- 'author': 'QA',
+ "protocolName": "Absorbance Plate Reader Reference Plate QA",
+ "author": "QA",
}
requirements = {
@@ -17,102 +19,355 @@
def convert_read_dictionary_to_array(read_data):
"""Convert a dictionary of read results to an array
-
+
Converts a dictionary of OD values, as formatted by the Opentrons API's
- plate reader read() function, to a 2D numpy.array of shape (8,12) for
+ plate reader read() function, to a 2D numpy.array of shape (8,12) for
further processing.
-
+
read_data: dict
a dictonary of read values with celll numbers for keys, e.g. 'A1'
"""
- data = np.empty((8,12))
+ data = np.empty((8, 12))
for key, value in read_data.items():
- row_index = ord(key[0]) - ord('A')
+ row_index = ord(key[0]) - ord("A")
column_index = int(key[1:]) - 1
data[row_index][column_index] = value
return data
-
+
def check_plate_reader_accuracy(read_data, flipped=False):
"""Check the accuracy of a measurement result returned by the read() method
-
+
data: dictionary of plate reader absorbance valurs
as returned by the absorbanceReaderV1 read() method
flipped: bool
True if reference plate was rotated 180 degrees for measurment
"""
-
+
# These are the hard-coded calibration values for Hellma 666-R013 with Serial
# Number 101934. If you're using a different reference plate you must update
- # these values with the ones provided by Hellma with your reference plate
- cal_values_450nm = np.array([
- [0. , 0. , 0.245 , 0.245 , 0.4973, 0.4973, 0.9897, 0.9897, 1.5258, 1.5258, 2.537 , 2.537 ],
- [0. , 0. , 0.2451, 0.2451, 0.4972, 0.4972, 0.9877, 0.9877, 1.5253, 1.5253, 2.535 , 2.535 ],
- [0. , 0. , 0.2451, 0.2451, 0.4973, 0.4973, 0.9871, 0.9871, 1.5246, 1.5246, 2.536 , 2.536 ],
- [0. , 0. , 0.2452, 0.2452, 0.4974, 0.4974, 0.9872, 0.9872, 1.525 , 1.525 , 2.535 , 2.535 ],
- [0. , 0. , 0.2452, 0.2452, 0.4976, 0.4976, 0.9872, 0.9872, 1.5248, 1.5248, 2.535 , 2.535 ],
- [0. , 0. , 0.2454, 0.2454, 0.4977, 0.4977, 0.9874, 0.9874, 1.5245, 1.5245, 2.536 , 2.536 ],
- [0. , 0. , 0.2453, 0.2453, 0.4977, 0.4977, 0.9876, 0.9876, 1.5245, 1.5245, 2.533 , 2.533 ],
- [0. , 0. , 0.2456, 0.2456, 0.4977, 0.4977, 0.9891, 0.9891, 1.5243, 1.5243, 2.533 , 2.533 ]
- ])
- cal_values_650nm = np.array([
- [0. , 0. , 0.2958, 0.2958, 0.5537, 0.5537, 0.9944, 0.9944, 1.4232, 1.4232, 2.372 , 2.372 ],
- [0. , 0. , 0.296 , 0.296 , 0.5535, 0.5535, 0.9924, 0.9924, 1.4235, 1.4235, 2.37 , 2.37 ],
- [0. , 0. , 0.296 , 0.296 , 0.5534, 0.5534, 0.9919, 0.9919, 1.4228, 1.4228, 2.37 , 2.37 ],
- [0. , 0. , 0.2961, 0.2961, 0.5534, 0.5534, 0.9918, 0.9918, 1.423 , 1.423 , 2.369 , 2.369 ],
- [0. , 0. , 0.2962, 0.2962, 0.5536, 0.5536, 0.9918, 0.9918, 1.4225, 1.4225, 2.369 , 2.369 ],
- [0. , 0. , 0.2964, 0.2964, 0.5535, 0.5535, 0.992 , 0.992 , 1.4223, 1.4223, 2.369 , 2.369 ],
- [0. , 0. , 0.2963, 0.2963, 0.5534, 0.5534, 0.9922, 0.9922, 1.4221, 1.4221, 2.368 , 2.368 ],
- [0. , 0. , 0.2965, 0.2965, 0.5533, 0.5533, 0.9938, 0.9938, 1.4222, 1.4222, 2.367 , 2.367 ]
- ])
+ # these values with the ones provided by Hellma with your reference plate
+ cal_values_450nm = np.array(
+ [
+ [
+ 0.0,
+ 0.0,
+ 0.245,
+ 0.245,
+ 0.4973,
+ 0.4973,
+ 0.9897,
+ 0.9897,
+ 1.5258,
+ 1.5258,
+ 2.537,
+ 2.537,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2451,
+ 0.2451,
+ 0.4972,
+ 0.4972,
+ 0.9877,
+ 0.9877,
+ 1.5253,
+ 1.5253,
+ 2.535,
+ 2.535,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2451,
+ 0.2451,
+ 0.4973,
+ 0.4973,
+ 0.9871,
+ 0.9871,
+ 1.5246,
+ 1.5246,
+ 2.536,
+ 2.536,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2452,
+ 0.2452,
+ 0.4974,
+ 0.4974,
+ 0.9872,
+ 0.9872,
+ 1.525,
+ 1.525,
+ 2.535,
+ 2.535,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2452,
+ 0.2452,
+ 0.4976,
+ 0.4976,
+ 0.9872,
+ 0.9872,
+ 1.5248,
+ 1.5248,
+ 2.535,
+ 2.535,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2454,
+ 0.2454,
+ 0.4977,
+ 0.4977,
+ 0.9874,
+ 0.9874,
+ 1.5245,
+ 1.5245,
+ 2.536,
+ 2.536,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2453,
+ 0.2453,
+ 0.4977,
+ 0.4977,
+ 0.9876,
+ 0.9876,
+ 1.5245,
+ 1.5245,
+ 2.533,
+ 2.533,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2456,
+ 0.2456,
+ 0.4977,
+ 0.4977,
+ 0.9891,
+ 0.9891,
+ 1.5243,
+ 1.5243,
+ 2.533,
+ 2.533,
+ ],
+ ]
+ )
+ cal_values_650nm = np.array(
+ [
+ [
+ 0.0,
+ 0.0,
+ 0.2958,
+ 0.2958,
+ 0.5537,
+ 0.5537,
+ 0.9944,
+ 0.9944,
+ 1.4232,
+ 1.4232,
+ 2.372,
+ 2.372,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.296,
+ 0.296,
+ 0.5535,
+ 0.5535,
+ 0.9924,
+ 0.9924,
+ 1.4235,
+ 1.4235,
+ 2.37,
+ 2.37,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.296,
+ 0.296,
+ 0.5534,
+ 0.5534,
+ 0.9919,
+ 0.9919,
+ 1.4228,
+ 1.4228,
+ 2.37,
+ 2.37,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2961,
+ 0.2961,
+ 0.5534,
+ 0.5534,
+ 0.9918,
+ 0.9918,
+ 1.423,
+ 1.423,
+ 2.369,
+ 2.369,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2962,
+ 0.2962,
+ 0.5536,
+ 0.5536,
+ 0.9918,
+ 0.9918,
+ 1.4225,
+ 1.4225,
+ 2.369,
+ 2.369,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2964,
+ 0.2964,
+ 0.5535,
+ 0.5535,
+ 0.992,
+ 0.992,
+ 1.4223,
+ 1.4223,
+ 2.369,
+ 2.369,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2963,
+ 0.2963,
+ 0.5534,
+ 0.5534,
+ 0.9922,
+ 0.9922,
+ 1.4221,
+ 1.4221,
+ 2.368,
+ 2.368,
+ ],
+ [
+ 0.0,
+ 0.0,
+ 0.2965,
+ 0.2965,
+ 0.5533,
+ 0.5533,
+ 0.9938,
+ 0.9938,
+ 1.4222,
+ 1.4222,
+ 2.367,
+ 2.367,
+ ],
+ ]
+ )
cal_tolerances = np.array(
- [0. , 0. , 0.0024, 0.0024, 0.0034, 0.0034, 0.0034, 0.0034, 0.0068, 0.0068, 0.012 , 0.012 ]
+ [
+ 0.0,
+ 0.0,
+ 0.0024,
+ 0.0024,
+ 0.0034,
+ 0.0034,
+ 0.0034,
+ 0.0034,
+ 0.0068,
+ 0.0068,
+ 0.012,
+ 0.012,
+ ]
)
-
+
# Calculate absolute accuracy tolerances for each cell
# The last two columns have a higher tolerance per the Byonoy datasheet
# because OD>2.0 and wavelength>=450nm on the Hellma plate
- accuracy_tolerances_450nm = np.zeros((8,12))
- accuracy_tolerances_450nm[:,:10] = cal_values_450nm[:,:10]*0.010 + cal_tolerances[:10] + 0.01
- accuracy_tolerances_450nm[:,10:] = cal_values_450nm[:,10:]*0.015 + cal_tolerances[10:] + 0.01
- accuracy_tolerances_650nm = np.zeros((8,12))
- accuracy_tolerances_650nm[:,:10] = cal_values_650nm[:,:10]*0.010 + cal_tolerances[:10] + 0.01
- accuracy_tolerances_650nm[:,10:] = cal_values_650nm[:,10:]*0.015 + cal_tolerances[10:] + 0.01
+ accuracy_tolerances_450nm = np.zeros((8, 12))
+ accuracy_tolerances_450nm[:, :10] = (
+ cal_values_450nm[:, :10] * 0.010 + cal_tolerances[:10] + 0.01
+ )
+ accuracy_tolerances_450nm[:, 10:] = (
+ cal_values_450nm[:, 10:] * 0.015 + cal_tolerances[10:] + 0.01
+ )
+ accuracy_tolerances_650nm = np.zeros((8, 12))
+ accuracy_tolerances_650nm[:, :10] = (
+ cal_values_650nm[:, :10] * 0.010 + cal_tolerances[:10] + 0.01
+ )
+ accuracy_tolerances_650nm[:, 10:] = (
+ cal_values_650nm[:, 10:] * 0.015 + cal_tolerances[10:] + 0.01
+ )
# Convert read result dictionary to numpy array for comparison
data_450nm = convert_read_dictionary_to_array(read_data[450])
data_650nm = convert_read_dictionary_to_array(read_data[650])
-
+
# Check accuracy
- if (flipped):
- within_tolerance_450nm = np.isclose(data_450nm, np.rot90(cal_values_450nm, 2), atol=np.rot90(accuracy_tolerances_450nm, 2))
- within_tolerance_650nm = np.isclose(data_650nm, np.rot90(cal_values_650nm, 2), atol=np.rot90(accuracy_tolerances_650nm, 2))
+ if flipped:
+ within_tolerance_450nm = np.isclose(
+ data_450nm,
+ np.rot90(cal_values_450nm, 2),
+ atol=np.rot90(accuracy_tolerances_450nm, 2),
+ )
+ within_tolerance_650nm = np.isclose(
+ data_650nm,
+ np.rot90(cal_values_650nm, 2),
+ atol=np.rot90(accuracy_tolerances_650nm, 2),
+ )
else:
- within_tolerance_450nm = np.isclose(data_450nm, cal_values_450nm, atol=accuracy_tolerances_450nm)
- within_tolerance_650nm = np.isclose(data_650nm, cal_values_650nm, atol=accuracy_tolerances_650nm)
+ within_tolerance_450nm = np.isclose(
+ data_450nm, cal_values_450nm, atol=accuracy_tolerances_450nm
+ )
+ within_tolerance_650nm = np.isclose(
+ data_650nm, cal_values_650nm, atol=accuracy_tolerances_650nm
+ )
- errors_450nm = np.count_nonzero(np.where(within_tolerance_450nm==False))
- errors_650nm = np.count_nonzero(np.where(within_tolerance_650nm==False))
+ errors_450nm = np.count_nonzero(np.where(within_tolerance_450nm == False))
+ errors_650nm = np.count_nonzero(np.where(within_tolerance_650nm == False))
msg = f"450nm Failures: {errors_450nm}, 650nm Failures: {errors_650nm}"
return msg
+
# protocol run function
def run(protocol: protocol_api.ProtocolContext):
HELLMA_PLATE_SLOT = "C2"
PLATE_READER_SLOT = "D3"
-
- plate_reader: AbsorbanceReaderContext = cast(AbsorbanceReaderContext, protocol.load_module("absorbanceReaderV1", PLATE_READER_SLOT))
+
+ plate_reader: AbsorbanceReaderContext = cast(
+ AbsorbanceReaderContext,
+ protocol.load_module("absorbanceReaderV1", PLATE_READER_SLOT),
+ )
hellma_plate = protocol.load_labware("hellma_reference_plate", HELLMA_PLATE_SLOT)
- tiprack_1000 = protocol.load_labware(load_name='opentrons_flex_96_tiprack_50ul', location="A2")
+ tiprack_1000 = protocol.load_labware(
+ load_name="opentrons_flex_96_tiprack_50ul", location="A2"
+ )
trash_labware = protocol.load_trash_bin("A3")
- #instrument = protocol.load_instrument("flex_8channel_50", "left", tip_racks=[tiprack_1000])
- instrument = protocol.load_instrument("flex_96channel_1000", "left", tip_racks=[tiprack_1000])
+ # instrument = protocol.load_instrument("flex_8channel_50", "left", tip_racks=[tiprack_1000])
+ instrument = protocol.load_instrument(
+ "flex_96channel_1000", "left", tip_racks=[tiprack_1000]
+ )
instrument.trash_container = trash_labware
# Initialize to multiple wavelengths
- plate_reader.initialize('multi', [450, 650])
+ plate_reader.initialize("multi", [450, 650])
plate_reader.open_lid()
protocol.move_labware(hellma_plate, plate_reader, use_gripper=True)
@@ -122,17 +377,17 @@ def run(protocol: protocol_api.ProtocolContext):
result = plate_reader.read()
msg = f"multi: {result}"
protocol.comment(msg=msg)
- #protocol.pause(msg=msg)
+ # protocol.pause(msg=msg)
# Place the Plate Reader lid back on using the Gripper.
plate_reader.open_lid()
protocol.move_labware(hellma_plate, HELLMA_PLATE_SLOT, use_gripper=True)
plate_reader.close_lid()
-
+
# Check and display accuracy
if result is not None:
- #msg = f"multi: {result}"
- #msg = f"multi: {result[450]}"
+ # msg = f"multi: {result}"
+ # msg = f"multi: {result[450]}"
msg = check_plate_reader_accuracy(result, flipped=False)
protocol.comment(msg=msg)
protocol.pause(msg=msg)
diff --git a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py
index 8eea871b9a3..41c70ed35a2 100644
--- a/hardware-testing/hardware_testing/scripts/ABRAsairScript.py
+++ b/hardware-testing/hardware_testing/scripts/ABRAsairScript.py
@@ -17,7 +17,7 @@ def execute(client: pmk.SSHClient, command: str, args: list) -> Optional[int]:
stderr_lines: List[str] = []
time.sleep(25)
- if stderr.channel.recv_ready:
+ if stderr.channel.recv_ready():
stderr_lines = stderr.readlines()
if stderr_lines != []:
print(f"{args[0]} ERROR: ", stderr_lines)
From a8c9c850178084e82014f33198c63ebd54ee6232 Mon Sep 17 00:00:00 2001
From: Jethary Alcid <66035149+jerader@users.noreply.github.com>
Date: Mon, 18 Nov 2024 09:48:26 -0500
Subject: [PATCH 35/68] fix(protocol-designer): incompatible tips modal copy
update (#16863)
closes RQA-3318
---
.../src/assets/localization/en/create_new_protocol.json | 2 +-
.../__tests__/IncompatibleTipsModal.test.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json
index 6ab28d9e76f..aeaa00c86c1 100644
--- a/protocol-designer/src/assets/localization/en/create_new_protocol.json
+++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json
@@ -11,7 +11,7 @@
"edit": "Edit",
"fixtures_added": "Fixtures added",
"fixtures_replace": "Fixtures replace standard deck slots and let you add functionality to your Flex.",
- "incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.",
+ "incompatible_tip_body": "Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.",
"incompatible_tips": "Incompatible tips",
"labware_name": "Labware name",
"left_right": "Left + Right",
diff --git a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx
index 2a1a8cf7e4e..96732965dea 100644
--- a/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx
+++ b/protocol-designer/src/organisms/IncompatibleTipsModal/__tests__/IncompatibleTipsModal.test.tsx
@@ -26,7 +26,7 @@ describe('IncompatibleTipsModal', () => {
render(props)
screen.getByText('Incompatible tips')
screen.getByText(
- 'Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.'
+ 'Protocol Designer only accepts custom JSON labware definitions made with our Labware Creator. Upload a valid file to continue.'
)
fireEvent.click(screen.getByText('Show more tip types'))
expect(vi.mocked(setFeatureFlags)).toHaveBeenCalled()
From 87440894e7f5fa8a62849297dadb04576d472805 Mon Sep 17 00:00:00 2001
From: Jethary Alcid <66035149+jerader@users.noreply.github.com>
Date: Mon, 18 Nov 2024 09:51:24 -0500
Subject: [PATCH 36/68] fix(protocol-designer): remove liquid defining
restrictions (#16864)
closes RQA-3260
---
.../__tests__/utils.test.ts | 21 ----------------
.../organisms/DefineLiquidsModal/index.tsx | 24 ++-----------------
.../src/organisms/DefineLiquidsModal/utils.ts | 8 -------
3 files changed, 2 insertions(+), 51 deletions(-)
delete mode 100644 protocol-designer/src/organisms/DefineLiquidsModal/__tests__/utils.test.ts
delete mode 100644 protocol-designer/src/organisms/DefineLiquidsModal/utils.ts
diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/__tests__/utils.test.ts b/protocol-designer/src/organisms/DefineLiquidsModal/__tests__/utils.test.ts
deleted file mode 100644
index 4e1016039b9..00000000000
--- a/protocol-designer/src/organisms/DefineLiquidsModal/__tests__/utils.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import { checkColor } from '../utils'
-
-describe('checkColor', () => {
- it('should return true for very dark colors', () => {
- expect(checkColor('#000000')).toBe(true)
- expect(checkColor('#0a0a0a')).toBe(true)
- })
-
- it('should return true for very light colors', () => {
- expect(checkColor('#ffffff')).toBe(true)
- expect(checkColor('#f5f5f5')).toBe(true)
- })
-
- it('should return false for colors with medium luminance', () => {
- expect(checkColor('#808080')).toBe(false)
- expect(checkColor('#ff0000')).toBe(false)
- expect(checkColor('#00ff00')).toBe(false)
- expect(checkColor('#0000ff')).toBe(false)
- })
-})
diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx
index 6b58895cdbb..fb1d775b702 100644
--- a/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx
+++ b/protocol-designer/src/organisms/DefineLiquidsModal/index.tsx
@@ -5,10 +5,7 @@ import { SketchPicker } from 'react-color'
import { yupResolver } from '@hookform/resolvers/yup'
import * as Yup from 'yup'
import { Controller, useForm } from 'react-hook-form'
-import {
- DEFAULT_LIQUID_COLORS,
- DEPRECATED_WHALE_GREY,
-} from '@opentrons/shared-data'
+import { DEFAULT_LIQUID_COLORS } from '@opentrons/shared-data'
import {
Btn,
COLORS,
@@ -30,7 +27,6 @@ import {
import * as labwareIngredActions from '../../labware-ingred/actions'
import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors'
import { swatchColors } from '../../components/swatchColors'
-import { checkColor } from './utils'
import { HandleEnter } from '../../atoms/HandleEnter'
import { DescriptionField, LINE_CLAMP_TEXT_STYLE } from '../../atoms'
@@ -47,25 +43,9 @@ interface LiquidEditFormValues {
[key: string]: unknown
}
-const BLACK = '#000000'
-const WHITE = '#ffffff'
-
-const INVALID_DISPLAY_COLORS = [BLACK, WHITE, DEPRECATED_WHALE_GREY]
-
const liquidEditFormSchema: any = Yup.object().shape({
name: Yup.string().required('liquid name is required'),
- displayColor: Yup.string().test(
- 'disallowed-color',
- 'Invalid display color',
- value => {
- if (value == null) {
- return true
- }
- return !INVALID_DISPLAY_COLORS.includes(value)
- ? !checkColor(value)
- : false
- }
- ),
+ displayColor: Yup.string(),
description: Yup.string(),
serialize: Yup.boolean(),
})
diff --git a/protocol-designer/src/organisms/DefineLiquidsModal/utils.ts b/protocol-designer/src/organisms/DefineLiquidsModal/utils.ts
deleted file mode 100644
index d55cbb4cf5b..00000000000
--- a/protocol-designer/src/organisms/DefineLiquidsModal/utils.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export function checkColor(hex: string): boolean {
- const cleanHex = hex.replace('#', '')
- const red = parseInt(cleanHex.slice(0, 2), 16)
- const green = parseInt(cleanHex.slice(2, 4), 16)
- const blue = parseInt(cleanHex.slice(4, 6), 16)
- const luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255
- return luminance < 0.1 || luminance > 0.9
-}
From 548ed2ca98f5f3f51e3188815bc9f89038106eeb Mon Sep 17 00:00:00 2001
From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com>
Date: Mon, 18 Nov 2024 11:10:37 -0500
Subject: [PATCH 37/68] fix(protocol-designer): fix spacing on starting deck
title (#16865)
Fix grid gap between title and materials list link on protocol starting
deck at ProtocolOverview
Closes RQA-3382
---
protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx b/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx
index 412b56c47f4..c7b345bf4da 100644
--- a/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx
+++ b/protocol-designer/src/pages/ProtocolOverview/StartingDeck.tsx
@@ -47,7 +47,7 @@ export function StartingDeck({
return (
<>
-
+
{t('starting_deck')}
From dcb5f4c2c54efd730609215e3bcac505a3455d1b Mon Sep 17 00:00:00 2001
From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com>
Date: Mon, 18 Nov 2024 12:09:26 -0500
Subject: [PATCH 38/68] fix(components, protocol-designer): fix background
color in file creation (#16854)
Update background color in new protocol wizard from grey20 to grey10
Closes RQA-3361
---
components/src/organisms/EndUserAgreementFooter/index.tsx | 2 +-
protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx | 2 +-
protocol-designer/src/pages/Landing/index.tsx | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/components/src/organisms/EndUserAgreementFooter/index.tsx b/components/src/organisms/EndUserAgreementFooter/index.tsx
index 5e40b205665..e2572805283 100644
--- a/components/src/organisms/EndUserAgreementFooter/index.tsx
+++ b/components/src/organisms/EndUserAgreementFooter/index.tsx
@@ -14,7 +14,7 @@ const EULA_URL = 'https://opentrons.com/eula'
export function EndUserAgreementFooter(): JSX.Element {
return (
+
Date: Mon, 18 Nov 2024 12:32:28 -0500
Subject: [PATCH 39/68] fix(components, protocol-designer): fix toolbox and
transfer tools styling (#16866)
Add title padding prop to toolbox and set default. Fix shared
DropdownMenu component spacing between title and dropdown
Closes RQA-3414
---
components/src/molecules/DropdownMenu/index.tsx | 8 ++------
components/src/organisms/Toolbox/index.tsx | 4 +++-
.../src/assets/localization/en/protocol_steps.json | 1 +
.../Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx | 4 +---
.../Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx | 4 +++-
.../Timeline/__tests__/TimelineToolbox.test.tsx | 2 +-
6 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx
index 851e759abca..af24cbf075d 100644
--- a/components/src/molecules/DropdownMenu/index.tsx
+++ b/components/src/molecules/DropdownMenu/index.tsx
@@ -205,15 +205,11 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element {
{title !== null ? (
-
+
diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json
index 42ece63c402..a9a45b9d2ed 100644
--- a/protocol-designer/src/assets/localization/en/protocol_steps.json
+++ b/protocol-designer/src/assets/localization/en/protocol_steps.json
@@ -133,6 +133,7 @@
}
},
"time": "Time",
+ "timeline": "Timeline",
"tiprack": "Tiprack",
"tip_handling": "Tip handling",
"tip_position": "{{prefix}} tip position",
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx
index 922220b7e81..3a1ddff44f0 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/SubstepsToolbox.tsx
@@ -5,7 +5,6 @@ import {
Flex,
Icon,
PrimaryButton,
- SPACING,
StyledText,
Toolbox,
} from '@opentrons/components'
@@ -62,7 +61,6 @@ export function SubstepsToolbox(
substeps.substepType === THERMOCYCLER_PROFILE ? (
}
onCloseClick={handleClose}
confirmButton={
@@ -81,7 +79,7 @@ export function SubstepsToolbox(
}
>
-
+
{substeps.substepType === THERMOCYCLER_PROFILE ? (
) : (
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx
index 76c5afa9540..8ec9a91a6f0 100644
--- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx
+++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/TimelineToolbox.tsx
@@ -63,9 +63,11 @@ export const TimelineToolbox = (): JSX.Element => {
width="19.5rem"
title={
- {t('protocol_timeline')}
+ {t('timeline')}
}
+ titlePadding={SPACING.spacing12}
+ childrenPadding={SPACING.spacing12}
confirmButton={formData != null ? undefined : }
>
{
})
it('renders 2 terminal item steps, a draggable step and presaved step with toolbox title', () => {
render()
- screen.getByText('Protocol timeline')
+ screen.getByText('Timeline')
screen.getByText('mock AddStepButton')
screen.getByText('mock PresavedStep')
screen.getByText('mock DraggableSteps')
From 1e6df83647e2e36043c0cb2e63a4238d4bd78bcd Mon Sep 17 00:00:00 2001
From: Rhyann Clarke <146747548+rclarke0@users.noreply.github.com>
Date: Mon, 18 Nov 2024 12:36:21 -0500
Subject: [PATCH 40/68] fix(abr-testing): added error handling for run log
parsing (#16867)
# Overview
Added error handling to run log parsing when run logs are empty due to
failed protocol analysis
## Test Plan and Hands on Testing
tested both scripts for 3 days using `make abr-setup` command.
## Changelog
- `get_run_logs.py` added error handling for key error
- `abr_google_drive.py` added error handling for json parsing
## Review requests
## Risk assessment
---
abr-testing/abr_testing/data_collection/abr_google_drive.py | 6 +++++-
abr-testing/abr_testing/data_collection/get_run_logs.py | 2 +-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/abr-testing/abr_testing/data_collection/abr_google_drive.py b/abr-testing/abr_testing/data_collection/abr_google_drive.py
index 6b9d7dd7ebe..6552534c4ae 100644
--- a/abr-testing/abr_testing/data_collection/abr_google_drive.py
+++ b/abr-testing/abr_testing/data_collection/abr_google_drive.py
@@ -48,7 +48,11 @@ def create_data_dictionary(
file_path = os.path.join(storage_directory, filename)
if file_path.endswith(".json"):
with open(file_path) as file:
- file_results = json.load(file)
+ try:
+ file_results = json.load(file)
+ except json.decoder.JSONDecodeError:
+ print(f"Skipped file {file_path} bc no data.")
+ continue
else:
continue
if not isinstance(file_results, dict):
diff --git a/abr-testing/abr_testing/data_collection/get_run_logs.py b/abr-testing/abr_testing/data_collection/get_run_logs.py
index 24d5aaf4f3b..964a8a06e18 100644
--- a/abr-testing/abr_testing/data_collection/get_run_logs.py
+++ b/abr-testing/abr_testing/data_collection/get_run_logs.py
@@ -17,7 +17,7 @@ def get_run_ids_from_robot(ip: str) -> Set[str]:
f"http://{ip}:31950/runs", headers={"opentrons-version": "3"}
)
run_data = response.json()
- run_list = run_data["data"]
+ run_list = run_data.get("data", "")
except requests.exceptions.RequestException:
print(f"Could not connect to robot with IP {ip}")
run_list = []
From f4331374a380dfb066531cc03e30f76368357c1f Mon Sep 17 00:00:00 2001
From: Alise Au <20424172+ahiuchingau@users.noreply.github.com>
Date: Mon, 18 Nov 2024 12:45:57 -0500
Subject: [PATCH 41/68] feat(hardware-testing): flex stacker EVT diagnostic
script for connectivity (#16835)
Setting qc test suite for flex-stacker & adding connectivity script.
---
hardware-testing/Makefile | 4 +
.../modules/flex_stacker_evt_qc/__init__.py | 1 +
.../modules/flex_stacker_evt_qc/__main__.py | 74 +++++++++++++++
.../modules/flex_stacker_evt_qc/config.py | 45 +++++++++
.../modules/flex_stacker_evt_qc/driver.py | 93 +++++++++++++++++++
.../flex_stacker_evt_qc/test_connectivity.py | 78 ++++++++++++++++
6 files changed, 295 insertions(+)
create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__init__.py
create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__main__.py
create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py
create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py
create mode 100644 hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_connectivity.py
diff --git a/hardware-testing/Makefile b/hardware-testing/Makefile
index 87edd408aa7..45b50b5a579 100755
--- a/hardware-testing/Makefile
+++ b/hardware-testing/Makefile
@@ -166,6 +166,10 @@ test-liquid-sense:
.PHONY: test-integration
test-integration: test-production-qc test-examples test-scripts test-gravimetric
+.PHONY: test-stacker
+test-stacker:
+ $(python) -m hardware_testing.modules.flex_stacker_evt_qc --simulate
+
.PHONY: lint
lint:
$(python) -m mypy hardware_testing tests
diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__init__.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__init__.py
new file mode 100644
index 00000000000..b79a4e5b836
--- /dev/null
+++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__init__.py
@@ -0,0 +1 @@
+"""FLEX Stacker QC scripts for EVT."""
diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__main__.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__main__.py
new file mode 100644
index 00000000000..2c4890023d4
--- /dev/null
+++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/__main__.py
@@ -0,0 +1,74 @@
+"""FLEX Stacker EVT QC."""
+from os import environ
+
+# NOTE: this is required to get WIFI test to work
+if "OT_SYSTEM_VERSION" not in environ:
+ environ["OT_SYSTEM_VERSION"] = "0.0.0"
+
+import argparse
+import asyncio
+from pathlib import Path
+from typing import Tuple
+
+from hardware_testing.data import ui
+from hardware_testing.data.csv_report import CSVReport
+
+from .config import TestSection, TestConfig, build_report, TESTS
+from .driver import FlexStacker
+
+
+def build_stacker_report(is_simulating: bool) -> Tuple[CSVReport, FlexStacker]:
+ """Report setup for FLEX Stacker qc script."""
+ test_name = Path(__file__).parent.name.replace("_", "-")
+ ui.print_title(test_name.upper())
+
+ stacker = FlexStacker.build_simulator() if is_simulating else FlexStacker.build()
+
+ report = build_report(test_name)
+ report.set_operator(
+ "simulating" if is_simulating else input("enter OPERATOR name: ")
+ )
+ info = stacker.get_device_info()
+ if not is_simulating:
+ barcode = input("SCAN device barcode: ").strip()
+ else:
+ barcode = "STACKER-SIMULATOR-SN"
+ report.set_tag(info.sn)
+ report.set_device_id(info.sn, barcode)
+ return report, stacker
+
+
+async def _main(cfg: TestConfig) -> None:
+ # BUILD REPORT
+ report, stacker = build_stacker_report(cfg.simulate)
+
+ # RUN TESTS
+ for section, test_run in cfg.tests.items():
+ ui.print_title(section.value)
+ test_run(stacker, report, section.value)
+
+ # SAVE REPORT
+ ui.print_title("DONE")
+ report.save_to_disk()
+ report.print_results()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--simulate", action="store_true")
+ # add each test-section as a skippable argument (eg: --skip-connectivity)
+ for s in TestSection:
+ parser.add_argument(f"--skip-{s.value.lower()}", action="store_true")
+ parser.add_argument(f"--only-{s.value.lower()}", action="store_true")
+ args = parser.parse_args()
+ _t_sections = {s: f for s, f in TESTS if getattr(args, f"only_{s.value.lower()}")}
+ if _t_sections:
+ assert (
+ len(list(_t_sections.keys())) < 2
+ ), 'use "--only" for just one test, not multiple tests'
+ else:
+ _t_sections = {
+ s: f for s, f in TESTS if not getattr(args, f"skip_{s.value.lower()}")
+ }
+ _config = TestConfig(simulate=args.simulate, tests=_t_sections)
+ asyncio.run(_main(_config))
diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py
new file mode 100644
index 00000000000..e8bc37da959
--- /dev/null
+++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/config.py
@@ -0,0 +1,45 @@
+"""Config."""
+from dataclasses import dataclass
+import enum
+from typing import Dict, Callable
+
+from hardware_testing.data.csv_report import CSVReport, CSVSection
+
+from . import (
+ test_connectivity,
+)
+
+
+class TestSection(enum.Enum):
+ """Test Section."""
+
+ CONNECTIVITY = "CONNECTIVITY"
+
+
+@dataclass
+class TestConfig:
+ """Test Config."""
+
+ simulate: bool
+ tests: Dict[TestSection, Callable]
+
+
+TESTS = [
+ (
+ TestSection.CONNECTIVITY,
+ test_connectivity.run,
+ ),
+]
+
+
+def build_report(test_name: str) -> CSVReport:
+ """Build report."""
+ return CSVReport(
+ test_name=test_name,
+ sections=[
+ CSVSection(
+ title=TestSection.CONNECTIVITY.value,
+ lines=test_connectivity.build_csv_lines(),
+ )
+ ],
+ )
diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py
new file mode 100644
index 00000000000..04d833fa8a5
--- /dev/null
+++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/driver.py
@@ -0,0 +1,93 @@
+"""FLEX Stacker Driver."""
+from dataclasses import dataclass
+import serial # type: ignore[import]
+from serial.tools.list_ports import comports # type: ignore[import]
+import re
+from enum import Enum
+
+STACKER_VID = 0x483
+STACKER_PID = 0xEF24
+STACKER_FREQ = 115200
+
+
+class HardwareRevision(Enum):
+ """Hardware Revision."""
+
+ NFF = "nff"
+ EVT = "a1"
+
+
+@dataclass
+class StackerInfo:
+ """Stacker Info."""
+
+ fw: str
+ hw: HardwareRevision
+ sn: str
+
+
+class FlexStacker:
+ """FLEX Stacker Driver."""
+
+ @classmethod
+ def build(cls, port: str = "") -> "FlexStacker":
+ """Build FLEX Stacker driver."""
+ if not port:
+ for i in comports():
+ if i.vid == STACKER_VID and i.pid == STACKER_PID:
+ port = i.device
+ break
+ assert port, "could not find connected FLEX Stacker"
+ return cls(port)
+
+ @classmethod
+ def build_simulator(cls, port: str = "") -> "FlexStacker":
+ """Build FLEX Stacker simulator."""
+ return cls(port, simulating=True)
+
+ def __init__(self, port: str, simulating: bool = False) -> None:
+ """Constructor."""
+ self._simulating = simulating
+ if not self._simulating:
+ self._serial = serial.Serial(port, baudrate=STACKER_FREQ)
+
+ def _send_and_recv(self, msg: str, guard_ret: str = "") -> str:
+ """Internal utility to send a command and receive the response."""
+ assert self._simulating
+ self._serial.write(msg.encode())
+ ret = self._serial.readline()
+ if guard_ret:
+ if not ret.startswith(guard_ret.encode()):
+ raise RuntimeError(f"Incorrect Response: {ret}")
+ if ret.startswith("ERR".encode()):
+ raise RuntimeError(ret)
+ return ret.decode()
+
+ def get_device_info(self) -> StackerInfo:
+ """Get Device Info."""
+ if self._simulating:
+ return StackerInfo(
+ "STACKER-FW", HardwareRevision.EVT, "STACKER-SIMULATOR-SN"
+ )
+
+ _DEV_INFO_RE = re.compile(
+ "^M115 FW:(?P.+) HW:Opentrons-flex-stacker-(?P.+) SerialNo:(?P.+) OK\n"
+ )
+ res = self._send_and_recv("M115\n", "M115 FW:")
+ m = _DEV_INFO_RE.match(res)
+ if not m:
+ raise RuntimeError(f"Incorrect Response: {res}")
+ return StackerInfo(
+ m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
+ )
+
+ def set_serial_number(self, sn: str) -> None:
+ """Set Serial Number."""
+ if self._simulating:
+ return
+ self._send_and_recv(f"M996 {sn}\n", "M996 OK")
+
+ def __del__(self) -> None:
+ """Close serial port."""
+ if not self._simulating:
+ self._serial.close()
diff --git a/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_connectivity.py b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_connectivity.py
new file mode 100644
index 00000000000..86a0bda991d
--- /dev/null
+++ b/hardware-testing/hardware_testing/modules/flex_stacker_evt_qc/test_connectivity.py
@@ -0,0 +1,78 @@
+"""Test Connectivity."""
+from typing import List, Union
+
+from hardware_testing.data import ui
+from hardware_testing.data.csv_report import (
+ CSVReport,
+ CSVLine,
+ CSVLineRepeating,
+ CSVResult,
+)
+
+from .driver import FlexStacker, HardwareRevision
+
+
+def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]:
+ """Build CSV Lines."""
+ return [
+ CSVLine("usb-get-device-info", [str, str, str, CSVResult]),
+ CSVLine("eeprom-set-serial-number", [str, str, CSVResult]),
+ CSVLine("led-blinking", [bool, CSVResult]),
+ ]
+
+
+def test_gcode(driver: FlexStacker, report: CSVReport) -> None:
+ """Send and receive response for GCODE M115."""
+ success = True
+ info = driver.get_device_info()
+ print("Hardware Revision: ", info.hw, "\n")
+ if info is None or info.hw != HardwareRevision.EVT:
+ print("Hardware Revision must be EVT")
+ success = False
+ report(
+ "CONNECTIVITY",
+ "usb-get-device-info",
+ [info.fw, info.hw, info.sn, CSVResult.from_bool(success)],
+ )
+
+
+def test_eeprom(driver: FlexStacker, report: CSVReport) -> None:
+ """Set serial number and make sure device info is updated accordingly."""
+ success = True
+ if not driver._simulating:
+ serial = input("enter Serial Number: ")
+ else:
+ serial = "STACKER-SIMULATOR-SN"
+ driver.set_serial_number(serial)
+ info = driver.get_device_info()
+ if info.sn != serial:
+ print("Serial number is not set properly")
+ success = False
+ report(
+ "CONNECTIVITY",
+ "eeprom-set-serial-number",
+ [serial, info.sn, CSVResult.from_bool(success)],
+ )
+
+
+def test_leds(driver: FlexStacker, report: CSVReport) -> None:
+ """Prompt tester to verify the status led is blinking."""
+ if not driver._simulating:
+ is_blinking = ui.get_user_answer("Is the status LED blinking?")
+ else:
+ is_blinking = True
+ report(
+ "CONNECTIVITY", "led-blinking", [is_blinking, CSVResult.from_bool(is_blinking)]
+ )
+
+
+def run(driver: FlexStacker, report: CSVReport, section: str) -> None:
+ """Run."""
+ ui.print_header("USB Communication")
+ test_gcode(driver, report)
+
+ ui.print_header("EEPROM Communication")
+ test_eeprom(driver, report)
+
+ ui.print_header("LED Blinking")
+ test_leds(driver, report)
From b2d1066d26b19a09bb80352037ccce72c3b781b5 Mon Sep 17 00:00:00 2001
From: Jethary Alcid <66035149+jerader@users.noreply.github.com>
Date: Mon, 18 Nov 2024 13:27:46 -0500
Subject: [PATCH 42/68] fix(protocol-designer): deck setup slot details
responsiveness (#16862)
closes RQA-3602 RQA-3327
---
.../ListItemChildren/ListItemDescriptor.tsx | 16 +++-
.../RobotCoordinateSpaceWithRef.tsx | 12 +--
.../__tests__/utils.test.ts | 25 ------
.../organisms/SlotDetailsContainer/index.tsx | 29 +------
.../organisms/SlotDetailsContainer/utils.ts | 82 -------------------
.../src/organisms/SlotInformation/index.tsx | 46 +++++++++--
.../Designer/DeckSetup/DeckSetupContainer.tsx | 52 ++++++++++--
.../Designer/DeckSetup/SlotOverflowMenu.tsx | 7 +-
.../src/pages/Designer/DeckSetup/utils.ts | 34 ++++++++
.../src/pages/Designer/index.tsx | 2 +-
10 files changed, 138 insertions(+), 167 deletions(-)
delete mode 100644 protocol-designer/src/organisms/SlotDetailsContainer/__tests__/utils.test.ts
delete mode 100644 protocol-designer/src/organisms/SlotDetailsContainer/utils.ts
diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx
index dcedecaa9f8..7b7620457c2 100644
--- a/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx
+++ b/components/src/atoms/ListItem/ListItemChildren/ListItemDescriptor.tsx
@@ -1,7 +1,9 @@
import { Flex } from '../../../primitives'
import {
ALIGN_FLEX_START,
+ DIRECTION_COLUMN,
DIRECTION_ROW,
+ JUSTIFY_FLEX_START,
JUSTIFY_SPACE_BETWEEN,
} from '../../../styles'
import { SPACING } from '../../../ui-style-constants'
@@ -10,19 +12,27 @@ interface ListItemDescriptorProps {
type: 'default' | 'large'
description: JSX.Element
content: JSX.Element
+ changeFlexDirection?: boolean
}
export const ListItemDescriptor = (
props: ListItemDescriptorProps
): JSX.Element => {
- const { description, content, type } = props
+ const { description, content, type, changeFlexDirection = false } = props
+ let justifyContent = 'none'
+ if (type === 'default' && changeFlexDirection) {
+ justifyContent = JUSTIFY_FLEX_START
+ } else if (type === 'default') {
+ justifyContent = JUSTIFY_SPACE_BETWEEN
+ }
+
return (
{description}
diff --git a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx
index a777299fb1c..78ab942ed47 100644
--- a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx
+++ b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx
@@ -34,17 +34,7 @@ export function RobotCoordinateSpaceWithRef(
(acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }),
{}
)
-
- const PADDING = deckDef.otId === 'ot2_standard' ? 5 : 10
- if (deckDef.otId === 'ot2_standard') {
- wholeDeckViewBox = `${viewBoxOriginX - PADDING} ${
- viewBoxOriginY + PADDING * 5
- } ${deckXDimension + PADDING * 2} ${deckYDimension - PADDING * 10}`
- } else {
- wholeDeckViewBox = `${viewBoxOriginX + PADDING * 2} ${
- viewBoxOriginY - PADDING
- } ${deckXDimension + PADDING * 4} ${deckYDimension + PADDING * 3}`
- }
+ wholeDeckViewBox = `${viewBoxOriginX} ${viewBoxOriginY} ${deckXDimension} ${deckYDimension}`
}
return (
diff --git a/protocol-designer/src/organisms/SlotDetailsContainer/__tests__/utils.test.ts b/protocol-designer/src/organisms/SlotDetailsContainer/__tests__/utils.test.ts
deleted file mode 100644
index eef8e3153c2..00000000000
--- a/protocol-designer/src/organisms/SlotDetailsContainer/__tests__/utils.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { describe, it, expect } from 'vitest'
-import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data'
-import { getXPosition } from '../utils'
-
-describe('getXPosition', () => {
- it('should return the right position 600 for FLEX robot type and slot 3', () => {
- expect(getXPosition('3', FLEX_ROBOT_TYPE, false)).toBe('600')
- })
-
- it('should return the right position 700 for FLEX robot type and slot 4', () => {
- expect(getXPosition('4', FLEX_ROBOT_TYPE, true)).toBe('700')
- })
-
- it('should return the left position for FLEX robot type and slot 1', () => {
- expect(getXPosition('1', FLEX_ROBOT_TYPE, false)).toBe('-400')
- })
-
- it('should return the right position for OT2 robot type and slot 6', () => {
- expect(getXPosition('6', OT2_ROBOT_TYPE, false)).toBe('420')
- })
-
- it('should return the left position for OT2 robot type and slot 2', () => {
- expect(getXPosition('2', OT2_ROBOT_TYPE, false)).toBe('-300')
- })
-})
diff --git a/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx b/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx
index b0b408a4964..4a8403bdf90 100644
--- a/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx
+++ b/protocol-designer/src/organisms/SlotDetailsContainer/index.tsx
@@ -1,20 +1,15 @@
-import { useLocation } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { getModuleDisplayName } from '@opentrons/shared-data'
-import { RobotCoordsForeignObject } from '@opentrons/components'
import * as wellContentsSelectors from '../../top-selectors/well-contents'
-import { getAdditionalEquipmentEntities } from '../../step-forms/selectors'
import { selectors } from '../../labware-ingred/selectors'
import { selectors as uiLabwareSelectors } from '../../ui/labware'
import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations'
import { SlotInformation } from '../../organisms/SlotInformation'
-import { getXPosition } from './utils'
import type { DeckSlotId, RobotType } from '@opentrons/shared-data'
import type { ContentsByWell } from '../../labware-ingred/types'
-const SLOT_DETAIL_Y_POSITION = '-10'
interface SlotDetailContainerProps {
robotType: RobotType
slot: DeckSlotId | null
@@ -26,17 +21,12 @@ export function SlotDetailsContainer(
): JSX.Element | null {
const { robotType, slot, offDeckLabwareId } = props
const { t } = useTranslation('shared')
- const location = useLocation()
const deckSetup = useSelector(getDeckSetupForActiveItem)
const allWellContentsForActiveItem = useSelector(
wellContentsSelectors.getAllWellContentsForActiveItem
)
const nickNames = useSelector(uiLabwareSelectors.getLabwareNicknamesById)
const allIngredNamesIds = useSelector(selectors.allIngredientNamesIds)
- const additionalEquipment = useSelector(getAdditionalEquipmentEntities)
- const hasStagingArea = Object.values(additionalEquipment).some(
- item => item.name === 'stagingArea'
- )
if (slot == null || (slot === 'offDeck' && offDeckLabwareId == null)) {
return null
@@ -108,24 +98,7 @@ export function SlotDetailsContainer(
}
}
- return location.pathname === '/designer' && slot !== 'offDeck' ? (
-
-
-
- ) : (
+ return (
{
- if (robotType === FLEX_ROBOT_TYPE) {
- if (FLEX_TOP_ROW_SLOTS.includes(slot)) {
- return Y_POSITIONS.FLEX.TOP
- } else if (FLEX_TOP_MIDDLE_ROW_SLOTS.includes(slot)) {
- return Y_POSITIONS.FLEX.TOP_MIDDLE
- } else if (FLEX_BOTTOM_MIDDLE_ROW_SLOTS.includes(slot)) {
- return Y_POSITIONS.FLEX.BOTTOM_MIDDLE
- } else {
- return Y_POSITIONS.FLEX.BOTTOM
- }
- } else {
- if (OT2_TOP_ROW_SLOTS.includes(slot)) {
- return Y_POSITIONS.OT2.TOP
- } else if (OT2_TOP_MIDDLE_ROW_SLOTS.includes(slot)) {
- return Y_POSITIONS.OT2.TOP_MIDDLE
- } else if (OT2_BOTTOM_MIDDLE_ROW_SLOTS.includes(slot)) {
- return Y_POSITIONS.OT2.BOTTOM_MIDDLE
- } else {
- return Y_POSITIONS.OT2.BOTTOM
- }
- }
-}
-
-export const getXPosition = (
- slot: string,
- robotType: RobotType,
- hasStagingArea: boolean
-): string => {
- const POSITION_MAP = {
- FLEX: {
- right: (slot: string) => (hasStagingArea ? '700' : '600'),
- left: '-400',
- regex: /[34]/,
- },
- OT2: {
- right: '420',
- left: '-300',
- regex: /[369]/,
- },
- }
-
- const { right, left, regex } =
- robotType === FLEX_ROBOT_TYPE ? POSITION_MAP.FLEX : POSITION_MAP.OT2
-
- return regex.test(slot)
- ? typeof right === 'function'
- ? right(slot)
- : right
- : left
-}
diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx
index 1e6a29746f6..0ee7205fd97 100644
--- a/protocol-designer/src/organisms/SlotInformation/index.tsx
+++ b/protocol-designer/src/organisms/SlotInformation/index.tsx
@@ -12,8 +12,16 @@ import {
StyledText,
TYPOGRAPHY,
} from '@opentrons/components'
-import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data'
+import {
+ FLEX_ROBOT_TYPE,
+ getModuleDisplayName,
+ TC_MODULE_LOCATION_OT2,
+ TC_MODULE_LOCATION_OT3,
+ THERMOCYCLER_MODULE_V1,
+ THERMOCYCLER_MODULE_V2,
+} from '@opentrons/shared-data'
import { LINE_CLAMP_TEXT_STYLE } from '../../atoms'
+import { useDeckSetupWindowBreakPoint } from '../../pages/Designer/DeckSetup/utils'
import type { FC } from 'react'
import type { RobotType } from '@opentrons/shared-data'
@@ -38,15 +46,28 @@ export const SlotInformation: FC = ({
fixtures = [],
}) => {
const { t } = useTranslation('shared')
+ const breakPointSize = useDeckSetupWindowBreakPoint()
+ const pathLocation = useLocation()
const isOffDeck = location === 'offDeck'
+ const tcDisplayLocation =
+ robotType === FLEX_ROBOT_TYPE
+ ? TC_MODULE_LOCATION_OT3
+ : TC_MODULE_LOCATION_OT2
+ const modifiedLocation =
+ modules.includes(getModuleDisplayName(THERMOCYCLER_MODULE_V2)) ||
+ modules.includes(getModuleDisplayName(THERMOCYCLER_MODULE_V1))
+ ? tcDisplayLocation
+ : location
+
return (
- {isOffDeck ? null : }
+ {isOffDeck ? null : }
{t(isOffDeck ? 'labware_detail' : 'slot_detail')}
@@ -55,11 +76,16 @@ export const SlotInformation: FC = ({
{liquids.length > 1 ? (
{liquids.join(', ')}
@@ -92,11 +118,10 @@ interface StackInfoListProps {
}
function StackInfoList({ title, items }: StackInfoListProps): JSX.Element {
- const pathLocation = useLocation()
return (
{items.length > 0 ? (
@@ -121,20 +146,27 @@ interface StackInfoProps {
function StackInfo({ title, stackInformation }: StackInfoProps): JSX.Element {
const { t } = useTranslation('shared')
+ const breakPointSize = useDeckSetupWindowBreakPoint()
+
return (
{stackInformation ?? t('none')}
}
description={
-
+
{title}
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx
index 2e45768cf4d..921e9c1fc55 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx
+++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'
import {
ALIGN_CENTER,
BORDERS,
+ Box,
COLORS,
DIRECTION_COLUMN,
DeckFromLayers,
@@ -37,6 +38,7 @@ import { DeckSetupDetails } from './DeckSetupDetails'
import {
animateZoom,
getCutoutIdForAddressableArea,
+ useDeckSetupWindowBreakPoint,
zoomInOnCoordinate,
} from './utils'
import { DeckSetupTools } from './DeckSetupTools'
@@ -68,11 +70,30 @@ const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [
]
export const lightFill = COLORS.grey35
export const darkFill = COLORS.grey60
+const LEFT_SLOTS = [
+ 'A1',
+ 'A2',
+ 'B1',
+ 'B2',
+ 'C1',
+ 'C2',
+ 'D1',
+ 'D2',
+ '1',
+ '2',
+ '4',
+ '5',
+ '7',
+ '8',
+ '10',
+ '11',
+]
export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element {
const { tab } = props
const activeDeckSetup = useSelector(getDeckSetupForActiveItem)
const dispatch = useDispatch()
+ const breakPointSize = useDeckSetupWindowBreakPoint()
const zoomIn = useSelector(selectors.getZoomedInSlot)
const _disableCollisionWarnings = useSelector(getDisableModuleRestrictions)
const robotType = useSelector(getRobotType)
@@ -97,6 +118,7 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element {
aE.location === WASTE_CHUTE_CUTOUT &&
wasteChuteFixtures.length > 0
)
+
const hasWasteChute =
wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0
@@ -112,6 +134,7 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element {
const initialViewBox = `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`
const [viewBox, setViewBox] = useState(initialViewBox)
+
const [hoveredLabware, setHoveredLabware] = useState(null)
const [hoveredModule, setHoveredModule] = useState(null)
const [hoveredFixture, setHoveredFixture] = useState(null)
@@ -179,7 +202,7 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element {
width="100%"
height={zoomIn.slot != null ? '75vh' : '70vh'}
flexDirection={DIRECTION_COLUMN}
- padding={SPACING.spacing40}
+ padding={SPACING.spacing24}
maxHeight="39.375rem" // this is to block deck view from enlarging
>
+
+ {hoverSlot != null &&
+ breakPointSize !== 'small' &&
+ LEFT_SLOTS.includes(hoverSlot) ? (
+
+ ) : null}
+
0}
/>
- {hoverSlot != null ? (
-
- ) : null}
>
)}
+
+ {hoverSlot != null &&
+ breakPointSize !== 'small' &&
+ !LEFT_SLOTS.includes(hoverSlot) ? (
+
+ ) : null}
+
{zoomIn.slot != null && zoomIn.cutout != null ? (
diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx
index b29d3b90dfa..36cd92f4ec6 100644
--- a/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx
+++ b/protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx
@@ -61,7 +61,7 @@ const BOTTOM_SLOT_Y_POSITION = -70
const TOP_SLOT_Y_POSITION = 50
const TOP_SLOT_Y_POSITION_ALL_BUTTONS = 110
const TOP_SLOT_Y_POSITION_2_BUTTONS = 35
-
+const STAGING_AREA_SLOTS = ['A4', 'B4', 'C4', 'D4']
interface SlotOverflowMenuProps {
// can be off-deck id or deck slot
location: DeckSlotId | string
@@ -317,7 +317,10 @@ export function SlotOverflowMenu(
return menuListSlotPosition != null ? (
{
+ const handleResize = (): void => {
+ setWindowSize({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ })
+ }
+
+ window.addEventListener('resize', handleResize)
+
+ return () => {
+ window.removeEventListener('resize', handleResize)
+ }
+ }, [])
+
+ let size: BreakPoint = 'large'
+ if (windowSize.width <= 1024 && windowSize.width > 800) {
+ size = 'medium'
+ } else if (windowSize.width <= 800) {
+ size = 'small'
+ }
+
+ return size
+}
diff --git a/protocol-designer/src/pages/Designer/index.tsx b/protocol-designer/src/pages/Designer/index.tsx
index a1ec3a27e4a..3469a843eb4 100644
--- a/protocol-designer/src/pages/Designer/index.tsx
+++ b/protocol-designer/src/pages/Designer/index.tsx
@@ -165,7 +165,7 @@ export function Designer(): JSX.Element {
From 2ee196eb9b87d67e313f344ef51079b4a9258028 Mon Sep 17 00:00:00 2001
From: Ed Cormany
Date: Mon, 18 Nov 2024 13:39:16 -0500
Subject: [PATCH 43/68] docs(api): fix typo in deck extents table (#16830)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Overview
Fixing a typo in the deck extents table for partial pickup. Thanks to
## Test Plan and Hands on Testing
[Sandbox](http://sandbox.docs.opentrons.com/docs-fix-deck-extents-typo/v2/pipettes/partial_tip_pickup.html#deck-extents)
## Changelog
1 character
## Review requests
A≠D
## Risk assessment
zero
---
api/docs/v2/pipettes/partial_tip_pickup.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/docs/v2/pipettes/partial_tip_pickup.rst b/api/docs/v2/pipettes/partial_tip_pickup.rst
index 9890cc7d345..719262760b9 100644
--- a/api/docs/v2/pipettes/partial_tip_pickup.rst
+++ b/api/docs/v2/pipettes/partial_tip_pickup.rst
@@ -399,7 +399,7 @@ The following table summarizes the limitations in place along each side of the d
* - A1–D1 (left edge)
- Rightmost column
- None (all wells accessible)
- * - A1–D3 (back edge)
+ * - A1–A3 (back edge)
- Frontmost row
- Rows A–G
* - A3–D3 (right edge)
From 25add49d3c052c37195a12d96abfa5e67961e03b Mon Sep 17 00:00:00 2001
From: David Chau <46395074+ddcc4@users.noreply.github.com>
Date: Mon, 18 Nov 2024 13:54:14 -0500
Subject: [PATCH 44/68] feat(api): implement loadLiquidClass command in PE
(#16814)
# Overview
This is the second half of AUTH-851.
This implements the `loadLiquidClass()` command using the liquid class
store from the previous PR.
Liquid classes are read-only in the Protocol Engine, so each
`liquidClassId` refers to one specific liquid class definition. Each
mutation of a liquid class needs to be stored under a different
`liquidClassId`. The caller can specify the `liquidClassId` if they want
to, or else we will generate one for them.
So there are 4 cases we need to handle:
1. Caller did not specify a `liquidClassId` and the liquid class is new:
Generate a new `liquidClassId`.
2. Caller did not specify a `liquidClassId` but the liquid class has
already been stored: Reuse the existing `liquidClassId`.
3. Caller specified a `liquidClassId` that we haven't seen before: Store
the liquid class under the new `liquidClassId`.
4. Caller specified a `liquidClassId` that's already been loaded: Check
that the incoming liquid class matches the one we previously stored,
and:
a. If the incoming liquid class exactly matches the existing one, do
nothing.
b. If they don't match, raise an error.
## Test Plan and Hands on Testing
I added 5 test cases to cover each of the scenarios above.
## Review requests
Could someone teach me what `StateView.get_summary()` is supposed to do?
## Risk assessment
Low risk, liquid classes are not released yet and these changes should
be dev-only. This change makes the Protocol Engine state marginally
larger.
---
.../commands/command_unions.py | 13 +
.../commands/load_liquid_class.py | 137 +++
.../protocol_engine/errors/__init__.py | 2 +
.../protocol_engine/errors/exceptions.py | 12 +
.../opentrons/protocol_engine/state/state.py | 14 +
.../commands/test_load_liquid_class.py | 161 ++++
.../state/test_liquid_class_store.py | 5 +-
shared-data/command/schemas/11.json | 903 ++++++++++++++++++
8 files changed, 1244 insertions(+), 3 deletions(-)
create mode 100644 api/src/opentrons/protocol_engine/commands/load_liquid_class.py
create mode 100644 api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py
diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py
index 30f00b4bb95..7db8045f0cb 100644
--- a/api/src/opentrons/protocol_engine/commands/command_unions.py
+++ b/api/src/opentrons/protocol_engine/commands/command_unions.py
@@ -136,6 +136,14 @@
LoadLiquidCommandType,
)
+from .load_liquid_class import (
+ LoadLiquidClass,
+ LoadLiquidClassParams,
+ LoadLiquidClassCreate,
+ LoadLiquidClassResult,
+ LoadLiquidClassCommandType,
+)
+
from .load_module import (
LoadModule,
LoadModuleParams,
@@ -347,6 +355,7 @@
LoadLabware,
ReloadLabware,
LoadLiquid,
+ LoadLiquidClass,
LoadModule,
LoadPipette,
MoveLabware,
@@ -429,6 +438,7 @@
LoadLabwareParams,
ReloadLabwareParams,
LoadLiquidParams,
+ LoadLiquidClassParams,
LoadModuleParams,
LoadPipetteParams,
MoveLabwareParams,
@@ -509,6 +519,7 @@
LoadLabwareCommandType,
ReloadLabwareCommandType,
LoadLiquidCommandType,
+ LoadLiquidClassCommandType,
LoadModuleCommandType,
LoadPipetteCommandType,
MoveLabwareCommandType,
@@ -590,6 +601,7 @@
LoadLabwareCreate,
ReloadLabwareCreate,
LoadLiquidCreate,
+ LoadLiquidClassCreate,
LoadModuleCreate,
LoadPipetteCreate,
MoveLabwareCreate,
@@ -672,6 +684,7 @@
LoadLabwareResult,
ReloadLabwareResult,
LoadLiquidResult,
+ LoadLiquidClassResult,
LoadModuleResult,
LoadPipetteResult,
MoveLabwareResult,
diff --git a/api/src/opentrons/protocol_engine/commands/load_liquid_class.py b/api/src/opentrons/protocol_engine/commands/load_liquid_class.py
new file mode 100644
index 00000000000..bd267abe567
--- /dev/null
+++ b/api/src/opentrons/protocol_engine/commands/load_liquid_class.py
@@ -0,0 +1,137 @@
+"""LoadLiquidClass stores the liquid class settings used for a transfer into the Protocol Engine."""
+from __future__ import annotations
+
+from typing import Optional, Type, TYPE_CHECKING
+from typing_extensions import Literal
+from pydantic import BaseModel, Field
+
+from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
+from ..errors import LiquidClassDoesNotExistError
+from ..errors.error_occurrence import ErrorOccurrence
+from ..errors.exceptions import LiquidClassRedefinitionError
+from ..state.update_types import LiquidClassLoadedUpdate, StateUpdate
+from ..types import LiquidClassRecord
+
+if TYPE_CHECKING:
+ from ..state.state import StateView
+ from ..resources import ModelUtils
+
+LoadLiquidClassCommandType = Literal["loadLiquidClass"]
+
+
+class LoadLiquidClassParams(BaseModel):
+ """The liquid class transfer properties to store."""
+
+ liquidClassId: Optional[str] = Field(
+ None,
+ description="Unique identifier for the liquid class to store. "
+ "If you do not supply a liquidClassId, we will generate one.",
+ )
+ liquidClassRecord: LiquidClassRecord = Field(
+ ...,
+ description="The liquid class to store.",
+ )
+
+
+class LoadLiquidClassResult(BaseModel):
+ """Result from execution of LoadLiquidClass command."""
+
+ liquidClassId: str = Field(
+ ...,
+ description="The ID for the liquid class that was loaded, either the one you "
+ "supplied or the one we generated.",
+ )
+
+
+class LoadLiquidClassImplementation(
+ AbstractCommandImpl[LoadLiquidClassParams, SuccessData[LoadLiquidClassResult]]
+):
+ """Load Liquid Class command implementation."""
+
+ def __init__(
+ self, state_view: StateView, model_utils: ModelUtils, **kwargs: object
+ ) -> None:
+ self._state_view = state_view
+ self._model_utils = model_utils
+
+ async def execute(
+ self, params: LoadLiquidClassParams
+ ) -> SuccessData[LoadLiquidClassResult]:
+ """Store the liquid class in the Protocol Engine."""
+ liquid_class_id: Optional[str]
+ already_loaded = False
+
+ if params.liquidClassId:
+ liquid_class_id = params.liquidClassId
+ if self._liquid_class_id_already_loaded(
+ liquid_class_id, params.liquidClassRecord
+ ):
+ already_loaded = True
+ else:
+ liquid_class_id = (
+ self._state_view.liquid_classes.get_id_for_liquid_class_record(
+ params.liquidClassRecord
+ ) # if liquidClassRecord was already loaded, reuse the existing ID
+ )
+ if liquid_class_id:
+ already_loaded = True
+ else:
+ liquid_class_id = self._model_utils.generate_id()
+
+ if already_loaded:
+ state_update = StateUpdate() # liquid class already loaded, do nothing
+ else:
+ state_update = StateUpdate(
+ liquid_class_loaded=LiquidClassLoadedUpdate(
+ liquid_class_id=liquid_class_id,
+ liquid_class_record=params.liquidClassRecord,
+ )
+ )
+
+ return SuccessData(
+ public=LoadLiquidClassResult(liquidClassId=liquid_class_id),
+ state_update=state_update,
+ )
+
+ def _liquid_class_id_already_loaded(
+ self, liquid_class_id: str, liquid_class_record: LiquidClassRecord
+ ) -> bool:
+ """Check if the liquid_class_id has already been loaded.
+
+ If it has, make sure that liquid_class_record matches the previously loaded definition.
+ """
+ try:
+ existing_liquid_class_record = self._state_view.liquid_classes.get(
+ liquid_class_id
+ )
+ except LiquidClassDoesNotExistError:
+ return False
+
+ if liquid_class_record != existing_liquid_class_record:
+ raise LiquidClassRedefinitionError(
+ f"Liquid class {liquid_class_id} conflicts with previously loaded definition."
+ )
+ return True
+
+
+class LoadLiquidClass(
+ BaseCommand[LoadLiquidClassParams, LoadLiquidClassResult, ErrorOccurrence]
+):
+ """Load Liquid Class command resource model."""
+
+ commandType: LoadLiquidClassCommandType = "loadLiquidClass"
+ params: LoadLiquidClassParams
+ result: Optional[LoadLiquidClassResult]
+
+ _ImplementationCls: Type[
+ LoadLiquidClassImplementation
+ ] = LoadLiquidClassImplementation
+
+
+class LoadLiquidClassCreate(BaseCommandCreate[LoadLiquidClassParams]):
+ """Load Liquid Class command creation request."""
+
+ commandType: LoadLiquidClassCommandType = "loadLiquidClass"
+ params: LoadLiquidClassParams
+
+ _CommandCls: Type[LoadLiquidClass] = LoadLiquidClass
diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py
index c6adf4bfc16..2706a4bc862 100644
--- a/api/src/opentrons/protocol_engine/errors/__init__.py
+++ b/api/src/opentrons/protocol_engine/errors/__init__.py
@@ -79,6 +79,7 @@
StorageLimitReachedError,
InvalidLiquidError,
LiquidClassDoesNotExistError,
+ LiquidClassRedefinitionError,
)
from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError
@@ -166,4 +167,5 @@
"InvalidDispenseVolumeError",
"StorageLimitReachedError",
"LiquidClassDoesNotExistError",
+ "LiquidClassRedefinitionError",
]
diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py
index e5a17ea4da2..7c16156b4bb 100644
--- a/api/src/opentrons/protocol_engine/errors/exceptions.py
+++ b/api/src/opentrons/protocol_engine/errors/exceptions.py
@@ -1167,3 +1167,15 @@ def __init__(
wrapping: Optional[Sequence[EnumeratedError]] = None,
) -> None:
super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
+
+
+class LiquidClassRedefinitionError(ProtocolEngineError):
+ """Raised when attempting to load a liquid class that conflicts with a liquid class already loaded."""
+
+ def __init__(
+ self,
+ message: Optional[str] = None,
+ details: Optional[Dict[str, Any]] = None,
+ wrapping: Optional[Sequence[EnumeratedError]] = None,
+ ) -> None:
+ super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping)
diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py
index 6743e1f44fc..47b15e4eb3b 100644
--- a/api/src/opentrons/protocol_engine/state/state.py
+++ b/api/src/opentrons/protocol_engine/state/state.py
@@ -25,6 +25,7 @@
from .pipettes import PipetteState, PipetteStore, PipetteView
from .modules import ModuleState, ModuleStore, ModuleView
from .liquids import LiquidState, LiquidView, LiquidStore
+from .liquid_classes import LiquidClassState, LiquidClassStore, LiquidClassView
from .tips import TipState, TipView, TipStore
from .wells import WellState, WellView, WellStore
from .geometry import GeometryView
@@ -49,6 +50,7 @@ class State:
pipettes: PipetteState
modules: ModuleState
liquids: LiquidState
+ liquid_classes: LiquidClassState
tips: TipState
wells: WellState
files: FileState
@@ -64,6 +66,7 @@ class StateView(HasState[State]):
_pipettes: PipetteView
_modules: ModuleView
_liquid: LiquidView
+ _liquid_classes: LiquidClassView
_tips: TipView
_wells: WellView
_geometry: GeometryView
@@ -101,6 +104,11 @@ def liquid(self) -> LiquidView:
"""Get state view selectors for liquid state."""
return self._liquid
+ @property
+ def liquid_classes(self) -> LiquidClassView:
+ """Get state view selectors for liquid class state."""
+ return self._liquid_classes
+
@property
def tips(self) -> TipView:
"""Get state view selectors for tip state."""
@@ -148,6 +156,7 @@ def get_summary(self) -> StateSummary:
wells=self._wells.get_all(),
hasEverEnteredErrorRecovery=self._commands.get_has_entered_recovery_mode(),
files=self._state.files.file_ids,
+ # TODO(dc): Do we want to just dump all the liquid classes into the summary?
)
@@ -213,6 +222,7 @@ def __init__(
module_calibration_offsets=module_calibration_offsets,
)
self._liquid_store = LiquidStore()
+ self._liquid_class_store = LiquidClassStore()
self._tip_store = TipStore()
self._well_store = WellStore()
self._file_store = FileStore()
@@ -224,6 +234,7 @@ def __init__(
self._labware_store,
self._module_store,
self._liquid_store,
+ self._liquid_class_store,
self._tip_store,
self._well_store,
self._file_store,
@@ -342,6 +353,7 @@ def _get_next_state(self) -> State:
pipettes=self._pipette_store.state,
modules=self._module_store.state,
liquids=self._liquid_store.state,
+ liquid_classes=self._liquid_class_store.state,
tips=self._tip_store.state,
wells=self._well_store.state,
files=self._file_store.state,
@@ -359,6 +371,7 @@ def _initialize_state(self) -> None:
self._pipettes = PipetteView(state.pipettes)
self._modules = ModuleView(state.modules)
self._liquid = LiquidView(state.liquids)
+ self._liquid_classes = LiquidClassView(state.liquid_classes)
self._tips = TipView(state.tips)
self._wells = WellView(state.wells)
self._files = FileView(state.files)
@@ -391,6 +404,7 @@ def _update_state_views(self) -> None:
self._pipettes._state = next_state.pipettes
self._modules._state = next_state.modules
self._liquid._state = next_state.liquids
+ self._liquid_classes._state = next_state.liquid_classes
self._tips._state = next_state.tips
self._wells._state = next_state.wells
self._change_notifier.notify()
diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py b/api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py
new file mode 100644
index 00000000000..041a7b2f8ca
--- /dev/null
+++ b/api/tests/opentrons/protocol_engine/commands/test_load_liquid_class.py
@@ -0,0 +1,161 @@
+"""Test load-liquid command."""
+from decoy import Decoy
+import pytest
+
+from opentrons_shared_data.liquid_classes.liquid_class_definition import (
+ LiquidClassSchemaV1,
+)
+from opentrons.protocol_engine.commands.command import SuccessData
+from opentrons.protocol_engine.commands.load_liquid_class import (
+ LoadLiquidClassImplementation,
+ LoadLiquidClassParams,
+ LoadLiquidClassResult,
+)
+from opentrons.protocol_engine.errors import (
+ LiquidClassDoesNotExistError,
+ LiquidClassRedefinitionError,
+)
+from opentrons.protocol_engine.resources.model_utils import ModelUtils
+from opentrons.protocol_engine.state import update_types
+from opentrons.protocol_engine.state.state import StateView
+from opentrons.protocol_engine.types import LiquidClassRecord
+
+
+@pytest.fixture
+def liquid_class_record(
+ minimal_liquid_class_def2: LiquidClassSchemaV1,
+) -> LiquidClassRecord:
+ """A dummy LiquidClassRecord for testing."""
+ pipette_0 = minimal_liquid_class_def2.byPipette[0]
+ by_tip_type_0 = pipette_0.byTipType[0]
+ return LiquidClassRecord(
+ liquidClassName=minimal_liquid_class_def2.liquidClassName,
+ pipetteModel=pipette_0.pipetteModel,
+ tiprack=by_tip_type_0.tiprack,
+ aspirate=by_tip_type_0.aspirate,
+ singleDispense=by_tip_type_0.singleDispense,
+ multiDispense=by_tip_type_0.multiDispense,
+ )
+
+
+async def test_load_liquid_class_new_liquid_class_no_id(
+ decoy: Decoy,
+ state_view: StateView,
+ model_utils: ModelUtils,
+ liquid_class_record: LiquidClassRecord,
+) -> None:
+ """Load a new liquid class with no liquidClassId specified. Should assign a new ID."""
+ subject = LoadLiquidClassImplementation(
+ state_view=state_view, model_utils=model_utils
+ )
+ decoy.when(model_utils.generate_id()).then_return("new-generated-id")
+
+ params = LoadLiquidClassParams(liquidClassRecord=liquid_class_record)
+ result = await subject.execute(params)
+ assert result == SuccessData(
+ public=LoadLiquidClassResult(liquidClassId="new-generated-id"),
+ state_update=update_types.StateUpdate(
+ liquid_class_loaded=update_types.LiquidClassLoadedUpdate(
+ liquid_class_id="new-generated-id",
+ liquid_class_record=liquid_class_record,
+ )
+ ),
+ )
+
+
+async def test_load_liquid_class_existing_liquid_class_no_id(
+ decoy: Decoy,
+ state_view: StateView,
+ model_utils: ModelUtils,
+ liquid_class_record: LiquidClassRecord,
+) -> None:
+ """Load an existing liquid class with no liquidClassId specified. Should find and reuse existing ID."""
+ subject = LoadLiquidClassImplementation(
+ state_view=state_view, model_utils=model_utils
+ )
+ decoy.when(
+ state_view.liquid_classes.get_id_for_liquid_class_record(liquid_class_record)
+ ).then_return("existing-id")
+
+ params = LoadLiquidClassParams(liquidClassRecord=liquid_class_record)
+ result = await subject.execute(params)
+ assert result == SuccessData(
+ public=LoadLiquidClassResult(liquidClassId="existing-id"),
+ state_update=update_types.StateUpdate(), # no state change since liquid class already loaded
+ )
+
+
+async def test_load_liquid_class_new_liquid_class_specified_id(
+ decoy: Decoy,
+ state_view: StateView,
+ model_utils: ModelUtils,
+ liquid_class_record: LiquidClassRecord,
+) -> None:
+ """Load a new liquid class with the liquidClassId specified. Should store the new liquid class."""
+ subject = LoadLiquidClassImplementation(
+ state_view=state_view, model_utils=model_utils
+ )
+ decoy.when(state_view.liquid_classes.get("liquid-class-1")).then_raise(
+ LiquidClassDoesNotExistError()
+ )
+
+ params = LoadLiquidClassParams(
+ liquidClassId="liquid-class-1", liquidClassRecord=liquid_class_record
+ )
+ result = await subject.execute(params)
+ assert result == SuccessData(
+ public=LoadLiquidClassResult(liquidClassId="liquid-class-1"),
+ state_update=update_types.StateUpdate(
+ liquid_class_loaded=update_types.LiquidClassLoadedUpdate(
+ liquid_class_id="liquid-class-1",
+ liquid_class_record=liquid_class_record,
+ )
+ ),
+ )
+
+
+async def test_load_liquid_class_existing_liquid_class_specified_id(
+ decoy: Decoy,
+ state_view: StateView,
+ model_utils: ModelUtils,
+ liquid_class_record: LiquidClassRecord,
+) -> None:
+ """Load a liquid class with a liquidClassId that was already loaded before. Should be a no-op."""
+ subject = LoadLiquidClassImplementation(
+ state_view=state_view, model_utils=model_utils
+ )
+ decoy.when(state_view.liquid_classes.get("liquid-class-1")).then_return(
+ liquid_class_record
+ )
+
+ params = LoadLiquidClassParams(
+ liquidClassId="liquid-class-1", liquidClassRecord=liquid_class_record
+ )
+ result = await subject.execute(params)
+ assert result == SuccessData(
+ public=LoadLiquidClassResult(liquidClassId="liquid-class-1"),
+ state_update=update_types.StateUpdate(), # no state change since liquid class already loaded
+ )
+
+
+async def test_load_liquid_class_conflicting_definition_for_id(
+ decoy: Decoy,
+ state_view: StateView,
+ model_utils: ModelUtils,
+ liquid_class_record: LiquidClassRecord,
+) -> None:
+ """Should raise when we try to load a modified liquid class with the same liquidClassId."""
+ subject = LoadLiquidClassImplementation(
+ state_view=state_view, model_utils=model_utils
+ )
+ decoy.when(state_view.liquid_classes.get("liquid-class-1")).then_return(
+ liquid_class_record
+ )
+
+ new_liquid_class_record = liquid_class_record.copy(deep=True)
+ new_liquid_class_record.aspirate.offset.x += 123 # make it different
+ params = LoadLiquidClassParams(
+ liquidClassId="liquid-class-1", liquidClassRecord=new_liquid_class_record
+ )
+ with pytest.raises(LiquidClassRedefinitionError):
+ await subject.execute(params)
diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_class_store.py b/api/tests/opentrons/protocol_engine/state/test_liquid_class_store.py
index f9032acdb94..aac74fd6133 100644
--- a/api/tests/opentrons/protocol_engine/state/test_liquid_class_store.py
+++ b/api/tests/opentrons/protocol_engine/state/test_liquid_class_store.py
@@ -5,7 +5,7 @@
LiquidClassSchemaV1,
)
from opentrons.protocol_engine import actions
-from opentrons.protocol_engine.commands import Comment
+from opentrons.protocol_engine.commands.load_liquid_class import LoadLiquidClass
from opentrons.protocol_engine.state import update_types
from opentrons.protocol_engine.state.liquid_classes import LiquidClassStore
from opentrons.protocol_engine.types import LiquidClassRecord
@@ -34,8 +34,7 @@ def test_handles_add_liquid_class(
subject.handle_action(
actions.SucceedCommandAction(
- # TODO(dc): this is a placeholder command, LoadLiquidClassCommand coming soon
- command=Comment.construct(), # type: ignore[call-arg]
+ command=LoadLiquidClass.construct(), # type: ignore[call-arg]
state_update=update_types.StateUpdate(
liquid_class_loaded=update_types.LiquidClassLoadedUpdate(
liquid_class_id="liquid-class-id",
diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json
index 54db9e28fc2..27de3c75b54 100644
--- a/shared-data/command/schemas/11.json
+++ b/shared-data/command/schemas/11.json
@@ -22,6 +22,7 @@
"loadLabware": "#/definitions/LoadLabwareCreate",
"reloadLabware": "#/definitions/ReloadLabwareCreate",
"loadLiquid": "#/definitions/LoadLiquidCreate",
+ "loadLiquidClass": "#/definitions/LoadLiquidClassCreate",
"loadModule": "#/definitions/LoadModuleCreate",
"loadPipette": "#/definitions/LoadPipetteCreate",
"moveLabware": "#/definitions/MoveLabwareCreate",
@@ -139,6 +140,9 @@
{
"$ref": "#/definitions/LoadLiquidCreate"
},
+ {
+ "$ref": "#/definitions/LoadLiquidClassCreate"
+ },
{
"$ref": "#/definitions/LoadModuleCreate"
},
@@ -1691,6 +1695,905 @@
},
"required": ["params"]
},
+ "PositionReference": {
+ "title": "PositionReference",
+ "description": "Positional reference for liquid handling operations.",
+ "enum": ["well-bottom", "well-top", "well-center", "liquid-meniscus"]
+ },
+ "Coordinate": {
+ "title": "Coordinate",
+ "description": "Three-dimensional coordinates.",
+ "type": "object",
+ "properties": {
+ "x": {
+ "title": "X",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "y": {
+ "title": "Y",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "z": {
+ "title": "Z",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ }
+ },
+ "required": ["x", "y", "z"]
+ },
+ "DelayParams": {
+ "title": "DelayParams",
+ "description": "Parameters for delay.",
+ "type": "object",
+ "properties": {
+ "duration": {
+ "title": "Duration",
+ "description": "Duration of delay, in seconds.",
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "required": ["duration"]
+ },
+ "DelayProperties": {
+ "title": "DelayProperties",
+ "description": "Shared properties for delay..",
+ "type": "object",
+ "properties": {
+ "enable": {
+ "title": "Enable",
+ "description": "Whether delay is enabled.",
+ "type": "boolean"
+ },
+ "params": {
+ "title": "Params",
+ "description": "Parameters for the delay function.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayParams"
+ }
+ ]
+ }
+ },
+ "required": ["enable"]
+ },
+ "Submerge": {
+ "title": "Submerge",
+ "description": "Shared properties for the submerge function before aspiration or dispense.",
+ "type": "object",
+ "properties": {
+ "positionReference": {
+ "description": "Position reference for submerge.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PositionReference"
+ }
+ ]
+ },
+ "offset": {
+ "title": "Offset",
+ "description": "Relative offset for submerge.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Coordinate"
+ }
+ ]
+ },
+ "speed": {
+ "title": "Speed",
+ "description": "Speed of submerging, in millimeters per second.",
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ },
+ "delay": {
+ "title": "Delay",
+ "description": "Delay settings for submerge.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayProperties"
+ }
+ ]
+ }
+ },
+ "required": ["positionReference", "offset", "speed", "delay"]
+ },
+ "LiquidClassTouchTipParams": {
+ "title": "LiquidClassTouchTipParams",
+ "description": "Parameters for touch-tip.",
+ "type": "object",
+ "properties": {
+ "zOffset": {
+ "title": "Zoffset",
+ "description": "Offset from the top of the well for touch-tip, in millimeters.",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "mmToEdge": {
+ "title": "Mmtoedge",
+ "description": "Offset away from the the well edge, in millimeters.",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ },
+ "speed": {
+ "title": "Speed",
+ "description": "Touch-tip speed, in millimeters per second.",
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "required": ["zOffset", "mmToEdge", "speed"]
+ },
+ "TouchTipProperties": {
+ "title": "TouchTipProperties",
+ "description": "Shared properties for the touch-tip function.",
+ "type": "object",
+ "properties": {
+ "enable": {
+ "title": "Enable",
+ "description": "Whether touch-tip is enabled.",
+ "type": "boolean"
+ },
+ "params": {
+ "title": "Params",
+ "description": "Parameters for the touch-tip function.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/LiquidClassTouchTipParams"
+ }
+ ]
+ }
+ },
+ "required": ["enable"]
+ },
+ "RetractAspirate": {
+ "title": "RetractAspirate",
+ "description": "Shared properties for the retract function after aspiration.",
+ "type": "object",
+ "properties": {
+ "positionReference": {
+ "description": "Position reference for retract after aspirate.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PositionReference"
+ }
+ ]
+ },
+ "offset": {
+ "title": "Offset",
+ "description": "Relative offset for retract after aspirate.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Coordinate"
+ }
+ ]
+ },
+ "speed": {
+ "title": "Speed",
+ "description": "Speed of retraction, in millimeters per second.",
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ },
+ "airGapByVolume": {
+ "title": "Airgapbyvolume",
+ "description": "Settings for air gap keyed by target aspiration volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "touchTip": {
+ "title": "Touchtip",
+ "description": "Touch tip settings for retract after aspirate.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/TouchTipProperties"
+ }
+ ]
+ },
+ "delay": {
+ "title": "Delay",
+ "description": "Delay settings for retract after aspirate.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayProperties"
+ }
+ ]
+ }
+ },
+ "required": [
+ "positionReference",
+ "offset",
+ "speed",
+ "airGapByVolume",
+ "touchTip",
+ "delay"
+ ]
+ },
+ "MixParams": {
+ "title": "MixParams",
+ "description": "Parameters for mix.",
+ "type": "object",
+ "properties": {
+ "repetitions": {
+ "title": "Repetitions",
+ "description": "Number of mixing repetitions.",
+ "minimum": 0,
+ "type": "integer"
+ },
+ "volume": {
+ "title": "Volume",
+ "description": "Volume used for mixing, in microliters.",
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "number"
+ }
+ ]
+ }
+ },
+ "required": ["repetitions", "volume"]
+ },
+ "MixProperties": {
+ "title": "MixProperties",
+ "description": "Mixing properties.",
+ "type": "object",
+ "properties": {
+ "enable": {
+ "title": "Enable",
+ "description": "Whether mix is enabled.",
+ "type": "boolean"
+ },
+ "params": {
+ "title": "Params",
+ "description": "Parameters for the mix function.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/MixParams"
+ }
+ ]
+ }
+ },
+ "required": ["enable"]
+ },
+ "AspirateProperties": {
+ "title": "AspirateProperties",
+ "description": "Properties specific to the aspirate function.",
+ "type": "object",
+ "properties": {
+ "submerge": {
+ "title": "Submerge",
+ "description": "Submerge settings for aspirate.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Submerge"
+ }
+ ]
+ },
+ "retract": {
+ "title": "Retract",
+ "description": "Pipette retract settings after an aspirate.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/RetractAspirate"
+ }
+ ]
+ },
+ "positionReference": {
+ "description": "Position reference for aspiration.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PositionReference"
+ }
+ ]
+ },
+ "offset": {
+ "title": "Offset",
+ "description": "Relative offset for aspiration.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Coordinate"
+ }
+ ]
+ },
+ "flowRateByVolume": {
+ "title": "Flowratebyvolume",
+ "description": "Settings for flow rate keyed by target aspiration volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "preWet": {
+ "title": "Prewet",
+ "description": "Whether to perform a pre-wet action.",
+ "type": "boolean"
+ },
+ "mix": {
+ "title": "Mix",
+ "description": "Mixing settings for before an aspirate",
+ "allOf": [
+ {
+ "$ref": "#/definitions/MixProperties"
+ }
+ ]
+ },
+ "delay": {
+ "title": "Delay",
+ "description": "Delay settings after an aspirate",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayProperties"
+ }
+ ]
+ }
+ },
+ "required": [
+ "submerge",
+ "retract",
+ "positionReference",
+ "offset",
+ "flowRateByVolume",
+ "preWet",
+ "mix",
+ "delay"
+ ]
+ },
+ "BlowoutLocation": {
+ "title": "BlowoutLocation",
+ "description": "Location for blowout during a transfer function.",
+ "enum": ["source", "destination", "trash"]
+ },
+ "BlowoutParams": {
+ "title": "BlowoutParams",
+ "description": "Parameters for blowout.",
+ "type": "object",
+ "properties": {
+ "location": {
+ "description": "Location well or trash entity for blow out.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/BlowoutLocation"
+ }
+ ]
+ },
+ "flowRate": {
+ "title": "Flowrate",
+ "description": "Flow rate for blow out, in microliters per second.",
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "required": ["location", "flowRate"]
+ },
+ "BlowoutProperties": {
+ "title": "BlowoutProperties",
+ "description": "Blowout properties.",
+ "type": "object",
+ "properties": {
+ "enable": {
+ "title": "Enable",
+ "description": "Whether blow-out is enabled.",
+ "type": "boolean"
+ },
+ "params": {
+ "title": "Params",
+ "description": "Parameters for the blowout function.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/BlowoutParams"
+ }
+ ]
+ }
+ },
+ "required": ["enable"]
+ },
+ "RetractDispense": {
+ "title": "RetractDispense",
+ "description": "Shared properties for the retract function after dispense.",
+ "type": "object",
+ "properties": {
+ "positionReference": {
+ "description": "Position reference for retract after dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PositionReference"
+ }
+ ]
+ },
+ "offset": {
+ "title": "Offset",
+ "description": "Relative offset for retract after dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Coordinate"
+ }
+ ]
+ },
+ "speed": {
+ "title": "Speed",
+ "description": "Speed of retraction, in millimeters per second.",
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ },
+ "airGapByVolume": {
+ "title": "Airgapbyvolume",
+ "description": "Settings for air gap keyed by target aspiration volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "blowout": {
+ "title": "Blowout",
+ "description": "Blowout properties for retract after dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/BlowoutProperties"
+ }
+ ]
+ },
+ "touchTip": {
+ "title": "Touchtip",
+ "description": "Touch tip settings for retract after dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/TouchTipProperties"
+ }
+ ]
+ },
+ "delay": {
+ "title": "Delay",
+ "description": "Delay settings for retract after dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayProperties"
+ }
+ ]
+ }
+ },
+ "required": [
+ "positionReference",
+ "offset",
+ "speed",
+ "airGapByVolume",
+ "blowout",
+ "touchTip",
+ "delay"
+ ]
+ },
+ "SingleDispenseProperties": {
+ "title": "SingleDispenseProperties",
+ "description": "Properties specific to the single-dispense function.",
+ "type": "object",
+ "properties": {
+ "submerge": {
+ "title": "Submerge",
+ "description": "Submerge settings for single dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Submerge"
+ }
+ ]
+ },
+ "retract": {
+ "title": "Retract",
+ "description": "Pipette retract settings after a single dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/RetractDispense"
+ }
+ ]
+ },
+ "positionReference": {
+ "description": "Position reference for single dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PositionReference"
+ }
+ ]
+ },
+ "offset": {
+ "title": "Offset",
+ "description": "Relative offset for single dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Coordinate"
+ }
+ ]
+ },
+ "flowRateByVolume": {
+ "title": "Flowratebyvolume",
+ "description": "Settings for flow rate keyed by target dispense volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "mix": {
+ "title": "Mix",
+ "description": "Mixing settings for after a dispense",
+ "allOf": [
+ {
+ "$ref": "#/definitions/MixProperties"
+ }
+ ]
+ },
+ "pushOutByVolume": {
+ "title": "Pushoutbyvolume",
+ "description": "Settings for pushout keyed by target dispense volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "delay": {
+ "title": "Delay",
+ "description": "Delay after dispense, in seconds.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayProperties"
+ }
+ ]
+ }
+ },
+ "required": [
+ "submerge",
+ "retract",
+ "positionReference",
+ "offset",
+ "flowRateByVolume",
+ "mix",
+ "pushOutByVolume",
+ "delay"
+ ]
+ },
+ "MultiDispenseProperties": {
+ "title": "MultiDispenseProperties",
+ "description": "Properties specific to the multi-dispense function.",
+ "type": "object",
+ "properties": {
+ "submerge": {
+ "title": "Submerge",
+ "description": "Submerge settings for multi-dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Submerge"
+ }
+ ]
+ },
+ "retract": {
+ "title": "Retract",
+ "description": "Pipette retract settings after a multi-dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/RetractDispense"
+ }
+ ]
+ },
+ "positionReference": {
+ "description": "Position reference for multi-dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/PositionReference"
+ }
+ ]
+ },
+ "offset": {
+ "title": "Offset",
+ "description": "Relative offset for single multi-dispense.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/Coordinate"
+ }
+ ]
+ },
+ "flowRateByVolume": {
+ "title": "Flowratebyvolume",
+ "description": "Settings for flow rate keyed by target dispense volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "conditioningByVolume": {
+ "title": "Conditioningbyvolume",
+ "description": "Settings for conditioning volume keyed by target dispense volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "disposalByVolume": {
+ "title": "Disposalbyvolume",
+ "description": "Settings for disposal volume keyed by target dispense volume.",
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 0
+ },
+ {
+ "type": "number",
+ "minimum": 0.0
+ }
+ ]
+ }
+ },
+ "delay": {
+ "title": "Delay",
+ "description": "Delay settings after each dispense",
+ "allOf": [
+ {
+ "$ref": "#/definitions/DelayProperties"
+ }
+ ]
+ }
+ },
+ "required": [
+ "submerge",
+ "retract",
+ "positionReference",
+ "offset",
+ "flowRateByVolume",
+ "conditioningByVolume",
+ "disposalByVolume",
+ "delay"
+ ]
+ },
+ "LiquidClassRecord": {
+ "title": "LiquidClassRecord",
+ "description": "LiquidClassRecord is our internal representation of an (immutable) liquid class.\n\nConceptually, a liquid class record is the tuple (name, pipette, tip, transfer properties).\nWe consider two liquid classes to be the same if every entry in that tuple is the same; and liquid\nclasses are different if any entry in the tuple is different.\n\nThis class defines the tuple via inheritance so that we can reuse the definitions from shared_data.",
+ "type": "object",
+ "properties": {
+ "tiprack": {
+ "title": "Tiprack",
+ "description": "The name of tiprack whose tip will be used when handling this specific liquid class with this pipette",
+ "type": "string"
+ },
+ "aspirate": {
+ "title": "Aspirate",
+ "description": "Aspirate parameters for this tip type.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/AspirateProperties"
+ }
+ ]
+ },
+ "singleDispense": {
+ "title": "Singledispense",
+ "description": "Single dispense parameters for this tip type.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/SingleDispenseProperties"
+ }
+ ]
+ },
+ "multiDispense": {
+ "title": "Multidispense",
+ "description": "Optional multi-dispense parameters for this tip type.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/MultiDispenseProperties"
+ }
+ ]
+ },
+ "liquidClassName": {
+ "title": "Liquidclassname",
+ "description": "Identifier for the liquid of this liquid class, e.g. glycerol50.",
+ "type": "string"
+ },
+ "pipetteModel": {
+ "title": "Pipettemodel",
+ "description": "Identifier for the pipette of this liquid class.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "tiprack",
+ "aspirate",
+ "singleDispense",
+ "liquidClassName",
+ "pipetteModel"
+ ]
+ },
+ "LoadLiquidClassParams": {
+ "title": "LoadLiquidClassParams",
+ "description": "The liquid class transfer properties to store.",
+ "type": "object",
+ "properties": {
+ "liquidClassId": {
+ "title": "Liquidclassid",
+ "description": "Unique identifier for the liquid class to store. If you do not supply a liquidClassId, we will generate one.",
+ "type": "string"
+ },
+ "liquidClassRecord": {
+ "title": "Liquidclassrecord",
+ "description": "The liquid class to store.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/LiquidClassRecord"
+ }
+ ]
+ }
+ },
+ "required": ["liquidClassRecord"]
+ },
+ "LoadLiquidClassCreate": {
+ "title": "LoadLiquidClassCreate",
+ "description": "Load Liquid Class command creation request.",
+ "type": "object",
+ "properties": {
+ "commandType": {
+ "title": "Commandtype",
+ "default": "loadLiquidClass",
+ "enum": ["loadLiquidClass"],
+ "type": "string"
+ },
+ "params": {
+ "$ref": "#/definitions/LoadLiquidClassParams"
+ },
+ "intent": {
+ "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/CommandIntent"
+ }
+ ]
+ },
+ "key": {
+ "title": "Key",
+ "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.",
+ "type": "string"
+ }
+ },
+ "required": ["params"]
+ },
"ModuleModel": {
"title": "ModuleModel",
"description": "All available modules' models.",
From 1e8ac747c5c273332f28fb1709583a993534c0d0 Mon Sep 17 00:00:00 2001
From: Seth Foster
Date: Mon, 18 Nov 2024 14:12:46 -0500
Subject: [PATCH 45/68] refactor(api): create micro-operations for motion and
pipetting (#16857)
Make some intermediates for `move_to_well` and the pipetting operations
that can handle and format exceptions and pass defined errors upstream.
In addition, make some state update changes that will pave the way for
having multiple micro-operations that can fail in each command, and get
rid of maybes.
Specifically,
- You can now chain `StateUpdate` setter calls
- New classmethod `StateUpdate.reduce()` that does a reduce over a group
of state updates
- New "micro operations" in `pipetting_common` and new `movement_common`
that basically just do exception handling for things like
`pipetting.aspirate_in_place()` and `movement.move_to_wells()` so that
the commands can treat defined errors entirely as data
- Give up on the `Maybe` experiment because it's really awful to chain
async calls in python
## testing
- No behavior should have changed. Some
`state_update_if_false_positive`s are now full overrides instead of just
extensions. Tests should cover this.
Works toward EXEC-830
---
.../protocol_engine/commands/aspirate.py | 113 ++++----
.../commands/aspirate_in_place.py | 120 ++++----
.../protocol_engine/commands/blow_out.py | 69 ++---
.../commands/blow_out_in_place.py | 54 ++--
.../protocol_engine/commands/command.py | 241 ----------------
.../protocol_engine/commands/dispense.py | 106 +++----
.../commands/dispense_in_place.py | 108 ++++----
.../protocol_engine/commands/drop_tip.py | 44 ++-
.../protocol_engine/commands/liquid_probe.py | 23 +-
.../protocol_engine/commands/move_relative.py | 2 +-
.../commands/move_to_addressable_area.py | 2 +
.../move_to_addressable_area_for_drop_tip.py | 2 +
.../commands/move_to_coordinates.py | 3 +-
.../protocol_engine/commands/move_to_well.py | 22 +-
.../commands/movement_common.py | 140 ++++++++++
.../protocol_engine/commands/pick_up_tip.py | 63 +++--
.../commands/pipetting_common.py | 259 ++++++++++--------
.../commands/prepare_to_aspirate.py | 24 +-
.../protocol_engine/commands/robot/move_to.py | 2 +-
.../protocol_engine/commands/touch_tip.py | 19 +-
.../protocol_engine/state/update_types.py | 117 +++++---
.../protocol_engine/commands/test_aspirate.py | 24 ++
.../commands/test_aspirate_in_place.py | 13 +-
.../protocol_engine/commands/test_blow_out.py | 21 ++
.../commands/test_blow_out_in_place.py | 5 +-
.../protocol_engine/commands/test_dispense.py | 20 ++
.../commands/test_dispense_in_place.py | 4 +
.../protocol_engine/commands/test_drop_tip.py | 24 ++
.../commands/test_liquid_probe.py | 10 +
.../commands/test_move_to_well.py | 2 +
.../commands/test_pick_up_tip.py | 20 ++
.../commands/test_touch_tip.py | 5 +
32 files changed, 876 insertions(+), 805 deletions(-)
create mode 100644 api/src/opentrons/protocol_engine/commands/movement_common.py
diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py
index 32cca1a8521..1dce8e8c2ca 100644
--- a/api/src/opentrons/protocol_engine/commands/aspirate.py
+++ b/api/src/opentrons/protocol_engine/commands/aspirate.py
@@ -1,7 +1,7 @@
"""Aspirate command request, result, and implementation models."""
+
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type, Union
-from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
from typing_extensions import Literal
from .pipetting_common import (
@@ -9,9 +9,13 @@
PipetteIdMixin,
AspirateVolumeMixin,
FlowRateMixin,
- LiquidHandlingWellLocationMixin,
BaseLiquidHandlingResult,
+ aspirate_in_place,
+)
+from .movement_common import (
+ LiquidHandlingWellLocationMixin,
DestinationPositionResult,
+ move_to_well,
)
from .command import (
AbstractCommandImpl,
@@ -20,7 +24,6 @@
DefinedErrorData,
SuccessData,
)
-from ..errors.error_occurrence import ErrorOccurrence
from opentrons.hardware_control import HardwareControlAPI
@@ -29,9 +32,6 @@
WellLocation,
WellOrigin,
CurrentWell,
- DeckPoint,
- AspiratedFluid,
- FluidKind,
)
if TYPE_CHECKING:
@@ -99,7 +99,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn:
)
current_well = None
- state_update = StateUpdate()
if not ready_to_aspirate:
await self._movement.move_to_well(
@@ -119,7 +118,8 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn:
well_name=well_name,
)
- position = await self._movement.move_to_well(
+ move_result = await move_to_well(
+ movement=self._movement,
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
@@ -127,66 +127,61 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn:
current_well=current_well,
operation_volume=-params.volume,
)
- deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
- state_update.set_pipette_location(
+
+ aspirate_result = await aspirate_in_place(
pipette_id=pipette_id,
- new_labware_id=labware_id,
- new_well_name=well_name,
- new_deck_point=deck_point,
+ volume=params.volume,
+ flow_rate=params.flowRate,
+ location_if_error={
+ "retryLocation": (
+ move_result.public.position.x,
+ move_result.public.position.y,
+ move_result.public.position.z,
+ )
+ },
+ command_note_adder=self._command_note_adder,
+ pipetting=self._pipetting,
+ model_utils=self._model_utils,
)
-
- try:
- volume_aspirated = await self._pipetting.aspirate_in_place(
- pipette_id=pipette_id,
- volume=params.volume,
- flow_rate=params.flowRate,
- command_note_adder=self._command_note_adder,
- )
- except PipetteOverpressureError as e:
- state_update.set_liquid_operated(
- labware_id=labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- labware_id, well_name, pipette_id
- ),
- volume_added=CLEAR,
- )
- state_update.set_fluid_unknown(pipette_id=params.pipetteId)
+ if isinstance(aspirate_result, DefinedErrorData):
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": (position.x, position.y, position.z)},
+ public=aspirate_result.public,
+ state_update=StateUpdate.reduce(
+ move_result.state_update, aspirate_result.state_update
+ ).set_liquid_operated(
+ labware_id=labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ labware_id,
+ well_name,
+ params.pipetteId,
+ ),
+ volume_added=CLEAR,
),
- state_update=state_update,
- )
- else:
- state_update.set_liquid_operated(
- labware_id=labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- labware_id, well_name, pipette_id
+ state_update_if_false_positive=StateUpdate.reduce(
+ move_result.state_update,
+ aspirate_result.state_update_if_false_positive,
),
- volume_added=-volume_aspirated
- * self._state_view.geometry.get_nozzles_per_well(
- labware_id, well_name, pipette_id
- ),
- )
- state_update.set_fluid_aspirated(
- pipette_id=params.pipetteId,
- fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume_aspirated),
)
+ else:
return SuccessData(
public=AspirateResult(
- volume=volume_aspirated,
- position=deck_point,
+ volume=aspirate_result.public.volume,
+ position=move_result.public.position,
+ ),
+ state_update=StateUpdate.reduce(
+ move_result.state_update, aspirate_result.state_update
+ ).set_liquid_operated(
+ labware_id=labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ labware_id, well_name, pipette_id
+ ),
+ volume_added=-aspirate_result.public.volume
+ * self._state_view.geometry.get_nozzles_per_well(
+ labware_id,
+ well_name,
+ params.pipetteId,
+ ),
),
- state_update=state_update,
)
diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py
index 55c35d5d19f..7fc7b62dc45 100644
--- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py
+++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py
@@ -4,8 +4,6 @@
from typing import TYPE_CHECKING, Optional, Type, Union
from typing_extensions import Literal
-from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
-
from opentrons.hardware_control import HardwareControlAPI
from .pipetting_common import (
@@ -14,6 +12,7 @@
FlowRateMixin,
BaseLiquidHandlingResult,
OverpressureError,
+ aspirate_in_place,
)
from .command import (
AbstractCommandImpl,
@@ -22,10 +21,9 @@
SuccessData,
DefinedErrorData,
)
-from ..errors.error_occurrence import ErrorOccurrence
from ..errors.exceptions import PipetteNotReadyToAspirateError
-from ..state.update_types import StateUpdate, CLEAR
-from ..types import CurrentWell, AspiratedFluid, FluidKind
+from ..state.update_types import CLEAR
+from ..types import CurrentWell
if TYPE_CHECKING:
from ..execution import PipettingHandler, GantryMover
@@ -94,83 +92,71 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn:
" so the plunger can be reset in a known safe position."
)
- state_update = StateUpdate()
+ current_position = await self._gantry_mover.get_position(params.pipetteId)
current_location = self._state_view.pipettes.get_current_location()
- try:
- current_position = await self._gantry_mover.get_position(params.pipetteId)
- volume = await self._pipetting.aspirate_in_place(
- pipette_id=params.pipetteId,
- volume=params.volume,
- flow_rate=params.flowRate,
- command_note_adder=self._command_note_adder,
- )
- except PipetteOverpressureError as e:
+ result = await aspirate_in_place(
+ pipette_id=params.pipetteId,
+ volume=params.volume,
+ flow_rate=params.flowRate,
+ location_if_error={
+ "retryLocation": (
+ current_position.x,
+ current_position.y,
+ current_position.z,
+ )
+ },
+ command_note_adder=self._command_note_adder,
+ pipetting=self._pipetting,
+ model_utils=self._model_utils,
+ )
+ if isinstance(result, DefinedErrorData):
if (
isinstance(current_location, CurrentWell)
and current_location.pipette_id == params.pipetteId
):
- state_update.set_liquid_operated(
- labware_id=current_location.labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- current_location.labware_id,
- current_location.well_name,
- params.pipetteId,
+ return DefinedErrorData(
+ public=result.public,
+ state_update=result.state_update.set_liquid_operated(
+ labware_id=current_location.labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ current_location.labware_id,
+ current_location.well_name,
+ params.pipetteId,
+ ),
+ volume_added=CLEAR,
),
- volume_added=CLEAR,
+ state_update_if_false_positive=result.state_update_if_false_positive,
)
- 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:
+ return result
else:
- state_update.set_fluid_aspirated(
- pipette_id=params.pipetteId,
- fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume),
- )
if (
isinstance(current_location, CurrentWell)
and current_location.pipette_id == params.pipetteId
):
- state_update.set_liquid_operated(
- labware_id=current_location.labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- current_location.labware_id,
- current_location.well_name,
- params.pipetteId,
- ),
- volume_added=-volume
- * self._state_view.geometry.get_nozzles_per_well(
- current_location.labware_id,
- current_location.well_name,
- params.pipetteId,
+ return SuccessData(
+ public=AspirateInPlaceResult(volume=result.public.volume),
+ state_update=result.state_update.set_liquid_operated(
+ labware_id=current_location.labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ current_location.labware_id,
+ current_location.well_name,
+ params.pipetteId,
+ ),
+ volume_added=-result.public.volume
+ * self._state_view.geometry.get_nozzles_per_well(
+ current_location.labware_id,
+ current_location.well_name,
+ params.pipetteId,
+ ),
),
)
-
- return SuccessData(
- public=AspirateInPlaceResult(volume=volume),
- state_update=state_update,
- )
+ else:
+ return SuccessData(
+ public=AspirateInPlaceResult(volume=result.public.volume),
+ state_update=result.state_update,
+ )
class AspirateInPlace(
diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py
index c450fa894ed..2469036186c 100644
--- a/api/src/opentrons/protocol_engine/commands/blow_out.py
+++ b/api/src/opentrons/protocol_engine/commands/blow_out.py
@@ -1,19 +1,17 @@
"""Blow-out command request, result, and implementation models."""
+
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type, Union
-from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
from typing_extensions import Literal
-from ..state.update_types import StateUpdate
-from ..types import DeckPoint
from .pipetting_common import (
OverpressureError,
PipetteIdMixin,
FlowRateMixin,
- WellLocationMixin,
- DestinationPositionResult,
+ blow_out_in_place,
)
+from .movement_common import WellLocationMixin, DestinationPositionResult, move_to_well
from .command import (
AbstractCommandImpl,
BaseCommand,
@@ -22,6 +20,7 @@
SuccessData,
)
from ..errors.error_occurrence import ErrorOccurrence
+from ..state.update_types import StateUpdate
from opentrons.hardware_control import HardwareControlAPI
@@ -73,53 +72,43 @@ def __init__(
async def execute(self, params: BlowOutParams) -> _ExecuteReturn:
"""Move to and blow-out the requested well."""
- state_update = StateUpdate()
-
- x, y, z = await self._movement.move_to_well(
+ move_result = await move_to_well(
+ movement=self._movement,
pipette_id=params.pipetteId,
labware_id=params.labwareId,
well_name=params.wellName,
well_location=params.wellLocation,
)
- deck_point = DeckPoint.construct(x=x, y=y, z=z)
- state_update.set_pipette_location(
+ blow_out_result = await blow_out_in_place(
pipette_id=params.pipetteId,
- new_labware_id=params.labwareId,
- new_well_name=params.wellName,
- new_deck_point=deck_point,
+ flow_rate=params.flowRate,
+ location_if_error={
+ "retryLocation": (
+ move_result.public.position.x,
+ move_result.public.position.y,
+ move_result.public.position.z,
+ )
+ },
+ pipetting=self._pipetting,
+ model_utils=self._model_utils,
)
- try:
- await self._pipetting.blow_out_in_place(
- pipette_id=params.pipetteId, flow_rate=params.flowRate
- )
- except PipetteOverpressureError as e:
- state_update.set_fluid_unknown(pipette_id=params.pipetteId)
+ if isinstance(blow_out_result, DefinedErrorData):
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": (
- x,
- y,
- z,
- )
- },
+ public=blow_out_result.public,
+ state_update=StateUpdate.reduce(
+ move_result.state_update, blow_out_result.state_update
+ ),
+ state_update_if_false_positive=StateUpdate.reduce(
+ move_result.state_update,
+ blow_out_result.state_update_if_false_positive,
),
- state_update=state_update,
)
else:
- state_update.set_fluid_empty(pipette_id=params.pipetteId)
return SuccessData(
- public=BlowOutResult(position=deck_point),
- state_update=state_update,
+ public=BlowOutResult(position=move_result.public.position),
+ state_update=StateUpdate.reduce(
+ move_result.state_update, blow_out_result.state_update
+ ),
)
diff --git a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py
index 04a38b8915c..f5f648bcec8 100644
--- a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py
+++ b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py
@@ -2,7 +2,6 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type, Union
-from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
from typing_extensions import Literal
from pydantic import BaseModel
@@ -10,6 +9,7 @@
OverpressureError,
PipetteIdMixin,
FlowRateMixin,
+ blow_out_in_place,
)
from .command import (
AbstractCommandImpl,
@@ -19,7 +19,6 @@
SuccessData,
)
from ..errors.error_occurrence import ErrorOccurrence
-from ..state import update_types
from opentrons.hardware_control import HardwareControlAPI
@@ -73,38 +72,25 @@ def __init__(
async def execute(self, params: BlowOutInPlaceParams) -> _ExecuteReturn:
"""Blow-out without moving the pipette."""
- state_update = update_types.StateUpdate()
- try:
- current_position = await self._gantry_mover.get_position(params.pipetteId)
- await self._pipetting.blow_out_in_place(
- pipette_id=params.pipetteId, flow_rate=params.flowRate
- )
- 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=BlowOutInPlaceResult(), state_update=state_update)
+ current_position = await self._gantry_mover.get_position(params.pipetteId)
+ result = await blow_out_in_place(
+ pipette_id=params.pipetteId,
+ flow_rate=params.flowRate,
+ location_if_error={
+ "retryLocation": (
+ current_position.x,
+ current_position.y,
+ current_position.z,
+ )
+ },
+ pipetting=self._pipetting,
+ model_utils=self._model_utils,
+ )
+ if isinstance(result, DefinedErrorData):
+ return result
+ return SuccessData(
+ public=BlowOutInPlaceResult(), state_update=result.state_update
+ )
class BlowOutInPlace(
diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py
index fe47c9dbbcc..e47ae9f3a37 100644
--- a/api/src/opentrons/protocol_engine/commands/command.py
+++ b/api/src/opentrons/protocol_engine/commands/command.py
@@ -1,6 +1,5 @@
"""Base command data model and type definitions."""
-
from __future__ import annotations
import dataclasses
@@ -8,7 +7,6 @@
from datetime import datetime
import enum
from typing import (
- cast,
TYPE_CHECKING,
Generic,
Optional,
@@ -16,11 +14,6 @@
List,
Type,
Union,
- Callable,
- Awaitable,
- Literal,
- Final,
- TypeAlias,
)
from pydantic import BaseModel, Field
@@ -248,240 +241,6 @@ 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/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py
index ae4a483f8ad..78b9ebed6b3 100644
--- a/api/src/opentrons/protocol_engine/commands/dispense.py
+++ b/api/src/opentrons/protocol_engine/commands/dispense.py
@@ -4,20 +4,22 @@
from typing import TYPE_CHECKING, Optional, Type, Union
from typing_extensions import Literal
-from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
from pydantic import Field
-from ..types import DeckPoint
from ..state.update_types import StateUpdate, CLEAR
from .pipetting_common import (
PipetteIdMixin,
DispenseVolumeMixin,
FlowRateMixin,
- LiquidHandlingWellLocationMixin,
BaseLiquidHandlingResult,
- DestinationPositionResult,
OverpressureError,
+ dispense_in_place,
+)
+from .movement_common import (
+ LiquidHandlingWellLocationMixin,
+ DestinationPositionResult,
+ move_to_well,
)
from .command import (
AbstractCommandImpl,
@@ -26,7 +28,6 @@
DefinedErrorData,
SuccessData,
)
-from ..errors.error_occurrence import ErrorOccurrence
if TYPE_CHECKING:
from ..execution import MovementHandler, PipettingHandler
@@ -78,7 +79,6 @@ def __init__(
async def execute(self, params: DispenseParams) -> _ExecuteReturn:
"""Move to and dispense to the requested well."""
- state_update = StateUpdate()
well_location = params.wellLocation
labware_id = params.labwareId
well_name = params.wellName
@@ -86,72 +86,76 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn:
# TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well()
- position = await self._movement.move_to_well(
+ move_result = await move_to_well(
+ movement=self._movement,
pipette_id=params.pipetteId,
labware_id=labware_id,
well_name=well_name,
well_location=well_location,
)
- deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
- state_update.set_pipette_location(
+ dispense_result = await dispense_in_place(
pipette_id=params.pipetteId,
- new_labware_id=labware_id,
- new_well_name=well_name,
- new_deck_point=deck_point,
+ volume=volume,
+ flow_rate=params.flowRate,
+ push_out=params.pushOut,
+ location_if_error={
+ "retryLocation": (
+ move_result.public.position.x,
+ move_result.public.position.y,
+ move_result.public.position.z,
+ )
+ },
+ pipetting=self._pipetting,
+ model_utils=self._model_utils,
)
- try:
- volume = await self._pipetting.dispense_in_place(
- pipette_id=params.pipetteId,
- volume=volume,
- flow_rate=params.flowRate,
- push_out=params.pushOut,
- )
- except PipetteOverpressureError as e:
- state_update.set_liquid_operated(
- labware_id=labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- labware_id, well_name, params.pipetteId
- ),
- volume_added=CLEAR,
- )
- state_update.set_fluid_unknown(pipette_id=params.pipetteId)
+ if isinstance(dispense_result, DefinedErrorData):
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": (position.x, position.y, position.z)},
+ public=dispense_result.public,
+ state_update=(
+ StateUpdate.reduce(
+ move_result.state_update, dispense_result.state_update
+ ).set_liquid_operated(
+ labware_id=labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ labware_id, well_name, params.pipetteId
+ ),
+ volume_added=CLEAR,
+ )
+ ),
+ state_update_if_false_positive=StateUpdate.reduce(
+ move_result.state_update,
+ dispense_result.state_update_if_false_positive,
),
- state_update=state_update,
)
else:
volume_added = (
self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume(
- pipette_id=params.pipetteId, volume=volume
+ pipette_id=params.pipetteId, volume=dispense_result.public.volume
)
)
if volume_added is not None:
volume_added *= self._state_view.geometry.get_nozzles_per_well(
labware_id, well_name, params.pipetteId
)
- state_update.set_liquid_operated(
- labware_id=labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- labware_id, well_name, params.pipetteId
- ),
- volume_added=volume_added if volume_added is not None else CLEAR,
- )
- state_update.set_fluid_ejected(pipette_id=params.pipetteId, volume=volume)
return SuccessData(
- public=DispenseResult(volume=volume, position=deck_point),
- state_update=state_update,
+ public=DispenseResult(
+ volume=dispense_result.public.volume,
+ position=move_result.public.position,
+ ),
+ state_update=(
+ StateUpdate.reduce(
+ move_result.state_update, dispense_result.state_update
+ ).set_liquid_operated(
+ labware_id=labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ labware_id, well_name, params.pipetteId
+ ),
+ volume_added=volume_added
+ if volume_added is not None
+ else CLEAR,
+ )
+ ),
)
diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py
index e7a631df670..fc1f9e19610 100644
--- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py
+++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py
@@ -5,14 +5,13 @@
from typing_extensions import Literal
from pydantic import Field
-from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
-
from .pipetting_common import (
PipetteIdMixin,
DispenseVolumeMixin,
FlowRateMixin,
BaseLiquidHandlingResult,
OverpressureError,
+ dispense_in_place,
)
from .command import (
AbstractCommandImpl,
@@ -21,8 +20,7 @@
SuccessData,
DefinedErrorData,
)
-from ..errors.error_occurrence import ErrorOccurrence
-from ..state.update_types import StateUpdate, CLEAR
+from ..state.update_types import CLEAR
from ..types import CurrentWell
if TYPE_CHECKING:
@@ -75,63 +73,51 @@ def __init__(
async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn:
"""Dispense without moving the pipette."""
- state_update = StateUpdate()
current_location = self._state_view.pipettes.get_current_location()
- try:
- current_position = await self._gantry_mover.get_position(params.pipetteId)
- volume = await self._pipetting.dispense_in_place(
- pipette_id=params.pipetteId,
- volume=params.volume,
- flow_rate=params.flowRate,
- push_out=params.pushOut,
- )
- except PipetteOverpressureError as e:
+ current_position = await self._gantry_mover.get_position(params.pipetteId)
+ result = await dispense_in_place(
+ pipette_id=params.pipetteId,
+ volume=params.volume,
+ flow_rate=params.flowRate,
+ push_out=params.pushOut,
+ location_if_error={
+ "retryLocation": (
+ current_position.x,
+ current_position.y,
+ current_position.z,
+ )
+ },
+ pipetting=self._pipetting,
+ model_utils=self._model_utils,
+ )
+ if isinstance(result, DefinedErrorData):
if (
isinstance(current_location, CurrentWell)
and current_location.pipette_id == params.pipetteId
):
- state_update.set_liquid_operated(
- labware_id=current_location.labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- current_location.labware_id,
- current_location.well_name,
- params.pipetteId,
+ return DefinedErrorData(
+ public=result.public,
+ state_update=result.state_update.set_liquid_operated(
+ labware_id=current_location.labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ current_location.labware_id,
+ current_location.well_name,
+ params.pipetteId,
+ ),
+ volume_added=CLEAR,
),
- volume_added=CLEAR,
+ state_update_if_false_positive=result.state_update_if_false_positive,
)
- 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:
+ return result
else:
- state_update.set_fluid_ejected(pipette_id=params.pipetteId, volume=volume)
if (
isinstance(current_location, CurrentWell)
and current_location.pipette_id == params.pipetteId
):
volume_added = (
self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume(
- pipette_id=params.pipetteId, volume=volume
+ pipette_id=params.pipetteId, volume=result.public.volume
)
)
if volume_added is not None:
@@ -140,19 +126,25 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn:
current_location.well_name,
params.pipetteId,
)
- state_update.set_liquid_operated(
- labware_id=current_location.labware_id,
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
- current_location.labware_id,
- current_location.well_name,
- params.pipetteId,
+ return SuccessData(
+ public=DispenseInPlaceResult(volume=result.public.volume),
+ state_update=result.state_update.set_liquid_operated(
+ labware_id=current_location.labware_id,
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
+ current_location.labware_id,
+ current_location.well_name,
+ params.pipetteId,
+ ),
+ volume_added=volume_added
+ if volume_added is not None
+ else CLEAR,
),
- volume_added=volume_added if volume_added is not None else CLEAR,
)
- return SuccessData(
- public=DispenseInPlaceResult(volume=volume),
- state_update=state_update,
- )
+ else:
+ return SuccessData(
+ public=DispenseInPlaceResult(volume=result.public.volume),
+ state_update=result.state_update,
+ )
class DispenseInPlace(
diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py
index ad0954c5a32..6b8b5132504 100644
--- a/api/src/opentrons/protocol_engine/commands/drop_tip.py
+++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py
@@ -1,4 +1,5 @@
"""Drop tip command request, result, and implementation models."""
+
from __future__ import annotations
from pydantic import Field
@@ -8,13 +9,13 @@
from opentrons.protocol_engine.errors.exceptions import TipAttachedError
from opentrons.protocol_engine.resources.model_utils import ModelUtils
-from ..state import update_types
-from ..types import DropTipWellLocation, DeckPoint
+from ..state.update_types import StateUpdate
+from ..types import DropTipWellLocation
from .pipetting_common import (
PipetteIdMixin,
- DestinationPositionResult,
TipPhysicallyAttachedError,
)
+from .movement_common import DestinationPositionResult, move_to_well
from .command import (
AbstractCommandImpl,
BaseCommand,
@@ -95,8 +96,6 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn:
well_name = params.wellName
home_after = params.homeAfter
- state_update = update_types.StateUpdate()
-
if params.alternateDropLocation:
well_location = self._state_view.geometry.get_next_tip_drop_location(
labware_id=labware_id,
@@ -116,19 +115,13 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn:
partially_configured=is_partially_configured,
)
- position = await self._movement_handler.move_to_well(
+ move_result = await move_to_well(
+ movement=self._movement_handler,
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
well_location=tip_drop_location,
)
- deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
- state_update.set_pipette_location(
- pipette_id=pipette_id,
- new_labware_id=labware_id,
- new_well_name=well_name,
- new_deck_point=deck_point,
- )
try:
await self._tip_handler.drop_tip(
@@ -146,24 +139,23 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn:
)
],
)
- state_update_if_false_positive = update_types.StateUpdate()
- state_update_if_false_positive.update_pipette_tip_state(
- pipette_id=params.pipetteId, tip_geometry=None
- )
- state_update.set_fluid_unknown(pipette_id=pipette_id)
return DefinedErrorData(
public=error,
- state_update=state_update,
- state_update_if_false_positive=state_update_if_false_positive,
+ state_update=StateUpdate.reduce(
+ StateUpdate(), move_result.state_update
+ ).set_fluid_unknown(pipette_id=pipette_id),
+ state_update_if_false_positive=move_result.state_update.update_pipette_tip_state(
+ pipette_id=params.pipetteId, tip_geometry=None
+ ),
)
else:
- state_update.set_fluid_unknown(pipette_id=pipette_id)
- state_update.update_pipette_tip_state(
- pipette_id=params.pipetteId, tip_geometry=None
- )
return SuccessData(
- public=DropTipResult(position=deck_point),
- state_update=state_update,
+ public=DropTipResult(position=move_result.public.position),
+ state_update=move_result.state_update.set_fluid_unknown(
+ pipette_id=pipette_id
+ ).update_pipette_tip_state(
+ pipette_id=params.pipetteId, tip_geometry=None
+ ),
)
diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py
index f7d6b652a8c..b6c51613263 100644
--- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py
+++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py
@@ -23,8 +23,11 @@
from .pipetting_common import (
LiquidNotFoundError,
PipetteIdMixin,
+)
+from .movement_common import (
WellLocationMixin,
DestinationPositionResult,
+ move_to_well,
)
from .command import (
AbstractCommandImpl,
@@ -117,8 +120,6 @@ async def _execute_common(
"Either the front right or back left nozzle must have a tip attached to probe liquid height."
)
- state_update = update_types.StateUpdate()
-
# May raise TipNotAttachedError.
aspirated_volume = state_view.pipettes.get_aspirated_volume(pipette_id)
@@ -142,19 +143,13 @@ async def _execute_common(
)
# liquid_probe process start position
- position = await movement.move_to_well(
+ move_result = await move_to_well(
+ movement=movement,
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
well_location=params.wellLocation,
)
- deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
- state_update.set_pipette_location(
- pipette_id=pipette_id,
- new_labware_id=labware_id,
- new_well_name=well_name,
- new_deck_point=deck_point,
- )
try:
z_pos = await pipetting.liquid_probe_in_place(
@@ -165,11 +160,15 @@ async def _execute_common(
)
except PipetteLiquidNotFoundError as exception:
return _ExecuteCommonResult(
- z_pos_or_error=exception, state_update=state_update, deck_point=deck_point
+ z_pos_or_error=exception,
+ state_update=move_result.state_update,
+ deck_point=move_result.public.position,
)
else:
return _ExecuteCommonResult(
- z_pos_or_error=z_pos, state_update=state_update, deck_point=deck_point
+ z_pos_or_error=z_pos,
+ state_update=move_result.state_update,
+ deck_point=move_result.public.position,
)
diff --git a/api/src/opentrons/protocol_engine/commands/move_relative.py b/api/src/opentrons/protocol_engine/commands/move_relative.py
index 9133725727d..cc2ae7c597a 100644
--- a/api/src/opentrons/protocol_engine/commands/move_relative.py
+++ b/api/src/opentrons/protocol_engine/commands/move_relative.py
@@ -9,7 +9,7 @@
from ..types import MovementAxis, DeckPoint
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ..errors.error_occurrence import ErrorOccurrence
-from .pipetting_common import DestinationPositionResult
+from .movement_common import DestinationPositionResult
if TYPE_CHECKING:
from ..execution import MovementHandler
diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py
index 8247f54a266..f3dd3f57c56 100644
--- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py
+++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py
@@ -12,6 +12,8 @@
from ..resources import fixture_validation
from .pipetting_common import (
PipetteIdMixin,
+)
+from .movement_common import (
MovementMixin,
DestinationPositionResult,
)
diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py
index 1c151f1e605..835209c4386 100644
--- a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py
+++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py
@@ -10,6 +10,8 @@
from ..resources import fixture_validation
from .pipetting_common import (
PipetteIdMixin,
+)
+from .movement_common import (
MovementMixin,
DestinationPositionResult,
)
diff --git a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py
index d7a0919d238..99df6be868b 100644
--- a/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py
+++ b/api/src/opentrons/protocol_engine/commands/move_to_coordinates.py
@@ -8,7 +8,8 @@
from ..state import update_types
from ..types import DeckPoint
-from .pipetting_common import PipetteIdMixin, MovementMixin, DestinationPositionResult
+from .pipetting_common import PipetteIdMixin
+from .movement_common import MovementMixin, DestinationPositionResult
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ..errors.error_occurrence import ErrorOccurrence
diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py
index 49ab10111a4..67819cb0d58 100644
--- a/api/src/opentrons/protocol_engine/commands/move_to_well.py
+++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py
@@ -1,18 +1,20 @@
"""Move to well command request, result, and implementation models."""
+
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type
from typing_extensions import Literal
-from ..types import DeckPoint
from .pipetting_common import (
PipetteIdMixin,
+)
+from .movement_common import (
WellLocationMixin,
MovementMixin,
DestinationPositionResult,
+ move_to_well,
)
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ..errors.error_occurrence import ErrorOccurrence
-from ..state import update_types
from ..errors import LabwareIsTipRackError
if TYPE_CHECKING:
@@ -52,8 +54,6 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul
well_name = params.wellName
well_location = params.wellLocation
- state_update = update_types.StateUpdate()
-
if (
self._state_view.labware.is_tiprack(labware_id)
and well_location.volumeOffset
@@ -62,7 +62,8 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul
"Cannot specify a WellLocation with a volumeOffset with movement to a tip rack"
)
- x, y, z = await self._movement.move_to_well(
+ move_result = await move_to_well(
+ movement=self._movement,
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
@@ -71,17 +72,10 @@ async def execute(self, params: MoveToWellParams) -> SuccessData[MoveToWellResul
minimum_z_height=params.minimumZHeight,
speed=params.speed,
)
- deck_point = DeckPoint.construct(x=x, y=y, z=z)
- state_update.set_pipette_location(
- pipette_id=pipette_id,
- new_labware_id=labware_id,
- new_well_name=well_name,
- new_deck_point=deck_point,
- )
return SuccessData(
- public=MoveToWellResult(position=deck_point),
- state_update=state_update,
+ public=MoveToWellResult(position=move_result.public.position),
+ state_update=move_result.state_update,
)
diff --git a/api/src/opentrons/protocol_engine/commands/movement_common.py b/api/src/opentrons/protocol_engine/commands/movement_common.py
new file mode 100644
index 00000000000..4d5d38544f6
--- /dev/null
+++ b/api/src/opentrons/protocol_engine/commands/movement_common.py
@@ -0,0 +1,140 @@
+"""Common movement base models."""
+
+from __future__ import annotations
+
+from typing import Optional, Union, TYPE_CHECKING
+from pydantic import BaseModel, Field
+from ..types import WellLocation, LiquidHandlingWellLocation, DeckPoint, CurrentWell
+from ..state.update_types import StateUpdate
+from .command import SuccessData
+
+
+if TYPE_CHECKING:
+ from ..execution.movement import MovementHandler
+
+
+class WellLocationMixin(BaseModel):
+ """Mixin for command requests that take a location that's somewhere in a well."""
+
+ labwareId: str = Field(
+ ...,
+ description="Identifier of labware to use.",
+ )
+ wellName: str = Field(
+ ...,
+ description="Name of well to use in labware.",
+ )
+ wellLocation: WellLocation = Field(
+ default_factory=WellLocation,
+ description="Relative well location at which to perform the operation",
+ )
+
+
+class LiquidHandlingWellLocationMixin(BaseModel):
+ """Mixin for command requests that take a location that's somewhere in a well."""
+
+ labwareId: str = Field(
+ ...,
+ description="Identifier of labware to use.",
+ )
+ wellName: str = Field(
+ ...,
+ description="Name of well to use in labware.",
+ )
+ wellLocation: LiquidHandlingWellLocation = Field(
+ default_factory=LiquidHandlingWellLocation,
+ description="Relative well location at which to perform the operation",
+ )
+
+
+class MovementMixin(BaseModel):
+ """Mixin for command requests that move a pipette."""
+
+ minimumZHeight: Optional[float] = Field(
+ None,
+ description=(
+ "Optional minimal Z margin in mm."
+ " If this is larger than the API's default safe Z margin,"
+ " it will make the arc higher. If it's smaller, it will have no effect."
+ ),
+ )
+
+ forceDirect: bool = Field(
+ False,
+ description=(
+ "If true, moving from one labware/well to another"
+ " will not arc to the default safe z,"
+ " but instead will move directly to the specified location."
+ " This will also force the `minimumZHeight` param to be ignored."
+ " A 'direct' movement is in X/Y/Z simultaneously."
+ ),
+ )
+
+ speed: Optional[float] = Field(
+ None,
+ description=(
+ "Override the travel speed in mm/s."
+ " This controls the straight linear speed of motion."
+ ),
+ )
+
+
+class DestinationPositionResult(BaseModel):
+ """Mixin for command results that move a pipette."""
+
+ # todo(mm, 2024-08-02): Consider deprecating or redefining this.
+ #
+ # This is here because opentrons.protocol_engine needed it for internal bookkeeping
+ # and, at the time, we didn't have a way to do that without adding this to the
+ # public command results. Its usefulness to callers outside
+ # opentrons.protocol_engine is questionable because they would need to know which
+ # critical point is in play, and I think that can change depending on obscure
+ # things like labware quirks.
+ position: DeckPoint = Field(
+ DeckPoint(x=0, y=0, z=0),
+ description=(
+ "The (x,y,z) coordinates of the pipette's critical point in deck space"
+ " after the move was completed."
+ ),
+ )
+
+
+MoveToWellOperationReturn = SuccessData[DestinationPositionResult]
+
+
+async def move_to_well(
+ movement: MovementHandler,
+ pipette_id: str,
+ labware_id: str,
+ well_name: str,
+ well_location: Optional[Union[WellLocation, LiquidHandlingWellLocation]] = None,
+ current_well: Optional[CurrentWell] = None,
+ force_direct: bool = False,
+ minimum_z_height: Optional[float] = None,
+ speed: Optional[float] = None,
+ operation_volume: Optional[float] = None,
+) -> MoveToWellOperationReturn:
+ """Execute a move to well microoperation."""
+ position = await movement.move_to_well(
+ pipette_id=pipette_id,
+ labware_id=labware_id,
+ well_name=well_name,
+ well_location=well_location,
+ current_well=current_well,
+ force_direct=force_direct,
+ minimum_z_height=minimum_z_height,
+ speed=speed,
+ operation_volume=operation_volume,
+ )
+ deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
+ return SuccessData(
+ public=DestinationPositionResult(
+ position=deck_point,
+ ),
+ state_update=StateUpdate().set_pipette_location(
+ pipette_id=pipette_id,
+ new_labware_id=labware_id,
+ new_well_name=well_name,
+ new_deck_point=deck_point,
+ ),
+ )
diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py
index 86967c6502f..928a4a9ced8 100644
--- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py
+++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py
@@ -1,4 +1,5 @@
"""Pick up tip command request, result, and implementation models."""
+
from __future__ import annotations
from opentrons_shared_data.errors import ErrorCodes
from pydantic import Field
@@ -9,11 +10,11 @@
from ..errors import ErrorOccurrence, PickUpTipTipNotAttachedError
from ..resources import ModelUtils
from ..state import update_types
-from ..types import PickUpTipWellLocation, DeckPoint
+from ..types import PickUpTipWellLocation
from .pipetting_common import (
PipetteIdMixin,
- DestinationPositionResult,
)
+from .movement_common import DestinationPositionResult, move_to_well
from .command import (
AbstractCommandImpl,
BaseCommand,
@@ -115,24 +116,16 @@ async def execute(
labware_id = params.labwareId
well_name = params.wellName
- state_update = update_types.StateUpdate()
-
well_location = self._state_view.geometry.convert_pick_up_tip_well_location(
well_location=params.wellLocation
)
- position = await self._movement.move_to_well(
+ move_result = await move_to_well(
+ movement=self._movement,
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
well_location=well_location,
)
- deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z)
- state_update.set_pipette_location(
- pipette_id=pipette_id,
- new_labware_id=labware_id,
- new_well_name=well_name,
- new_deck_point=deck_point,
- )
try:
tip_geometry = await self._tip_handler.pick_up_tip(
@@ -141,16 +134,28 @@ async def execute(
well_name=well_name,
)
except PickUpTipTipNotAttachedError as e:
- state_update_if_false_positive = update_types.StateUpdate()
- state_update_if_false_positive.update_pipette_tip_state(
- pipette_id=pipette_id,
- tip_geometry=e.tip_geometry,
+ state_update_if_false_positive = (
+ update_types.StateUpdate.reduce(
+ update_types.StateUpdate(), move_result.state_update
+ )
+ .update_pipette_tip_state(
+ pipette_id=pipette_id,
+ tip_geometry=e.tip_geometry,
+ )
+ .set_fluid_empty(pipette_id=pipette_id)
+ .mark_tips_as_used(
+ pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
+ )
)
- state_update_if_false_positive.set_fluid_empty(pipette_id=pipette_id)
- state_update.mark_tips_as_used(
- pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
+ state_update = (
+ update_types.StateUpdate.reduce(
+ update_types.StateUpdate(), move_result.state_update
+ )
+ .mark_tips_as_used(
+ pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
+ )
+ .set_fluid_unknown(pipette_id=pipette_id)
)
- state_update.set_fluid_unknown(pipette_id=pipette_id)
return DefinedErrorData(
public=TipPhysicallyMissingError(
id=self._model_utils.generate_id(),
@@ -167,20 +172,22 @@ async def execute(
state_update_if_false_positive=state_update_if_false_positive,
)
else:
- state_update.update_pipette_tip_state(
- pipette_id=pipette_id,
- tip_geometry=tip_geometry,
- )
- state_update.mark_tips_as_used(
- pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
+ state_update = (
+ move_result.state_update.update_pipette_tip_state(
+ pipette_id=pipette_id,
+ tip_geometry=tip_geometry,
+ )
+ .mark_tips_as_used(
+ pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
+ )
+ .set_fluid_empty(pipette_id=pipette_id)
)
- state_update.set_fluid_empty(pipette_id=pipette_id)
return SuccessData(
public=PickUpTipResult(
tipVolume=tip_geometry.volume,
tipLength=tip_geometry.length,
tipDiameter=tip_geometry.diameter,
- position=deck_point,
+ position=move_result.public.position,
),
state_update=state_update,
)
diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py
index 6e0064211fa..ee69a3e3764 100644
--- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py
+++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py
@@ -1,19 +1,21 @@
"""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, TYPE_CHECKING
+from typing import Literal, Tuple, TypedDict, TYPE_CHECKING
from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence
+from opentrons.protocol_engine.types import AspiratedFluid, FluidKind
from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
-from .command import Maybe, DefinedErrorData, SuccessData
+from .command import 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
+ from ..notes import CommandNoteAdder
class PipetteIdMixin(BaseModel):
@@ -59,72 +61,6 @@ class FlowRateMixin(BaseModel):
)
-class WellLocationMixin(BaseModel):
- """Mixin for command requests that take a location that's somewhere in a well."""
-
- labwareId: str = Field(
- ...,
- description="Identifier of labware to use.",
- )
- wellName: str = Field(
- ...,
- description="Name of well to use in labware.",
- )
- wellLocation: WellLocation = Field(
- default_factory=WellLocation,
- description="Relative well location at which to perform the operation",
- )
-
-
-class LiquidHandlingWellLocationMixin(BaseModel):
- """Mixin for command requests that take a location that's somewhere in a well."""
-
- labwareId: str = Field(
- ...,
- description="Identifier of labware to use.",
- )
- wellName: str = Field(
- ...,
- description="Name of well to use in labware.",
- )
- wellLocation: LiquidHandlingWellLocation = Field(
- default_factory=LiquidHandlingWellLocation,
- description="Relative well location at which to perform the operation",
- )
-
-
-class MovementMixin(BaseModel):
- """Mixin for command requests that move a pipette."""
-
- minimumZHeight: Optional[float] = Field(
- None,
- description=(
- "Optional minimal Z margin in mm."
- " If this is larger than the API's default safe Z margin,"
- " it will make the arc higher. If it's smaller, it will have no effect."
- ),
- )
-
- forceDirect: bool = Field(
- False,
- description=(
- "If true, moving from one labware/well to another"
- " will not arc to the default safe z,"
- " but instead will move directly to the specified location."
- " This will also force the `minimumZHeight` param to be ignored."
- " A 'direct' movement is in X/Y/Z simultaneously."
- ),
- )
-
- speed: Optional[float] = Field(
- None,
- description=(
- "Override the travel speed in mm/s."
- " This controls the straight linear speed of motion."
- ),
- )
-
-
class BaseLiquidHandlingResult(BaseModel):
"""Base properties of a liquid handling result."""
@@ -135,26 +71,6 @@ class BaseLiquidHandlingResult(BaseModel):
)
-class DestinationPositionResult(BaseModel):
- """Mixin for command results that move a pipette."""
-
- # todo(mm, 2024-08-02): Consider deprecating or redefining this.
- #
- # This is here because opentrons.protocol_engine needed it for internal bookkeeping
- # and, at the time, we didn't have a way to do that without adding this to the
- # public command results. Its usefulness to callers outside
- # opentrons.protocol_engine is questionable because they would need to know which
- # critical point is in play, and I think that can change depending on obscure
- # things like labware quirks.
- position: DeckPoint = Field(
- DeckPoint(x=0, y=0, z=0),
- description=(
- "The (x,y,z) coordinates of the pipette's critical point in deck space"
- " after the move was completed."
- ),
- )
-
-
class ErrorLocationInfo(TypedDict):
"""Holds a retry location for in-place error recovery."""
@@ -211,42 +127,153 @@ class TipPhysicallyAttachedError(ErrorOccurrence):
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:
+) -> SuccessData[BaseModel] | DefinedErrorData[OverpressureError]:
"""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,
- )
+ return 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=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
+ )
+ else:
+ return SuccessData(
+ public=BaseModel(),
+ state_update=StateUpdate().set_fluid_empty(pipette_id=pipette_id),
+ )
+
+
+async def aspirate_in_place(
+ pipette_id: str,
+ volume: float,
+ flow_rate: float,
+ location_if_error: ErrorLocationInfo,
+ command_note_adder: CommandNoteAdder,
+ pipetting: PipettingHandler,
+ model_utils: ModelUtils,
+) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
+ """Execute an aspirate in place microoperation."""
+ try:
+ volume_aspirated = await pipetting.aspirate_in_place(
+ pipette_id=pipette_id,
+ volume=volume,
+ flow_rate=flow_rate,
+ command_note_adder=command_note_adder,
+ )
+ except PipetteOverpressureError as e:
+ return 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=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
+ )
+ else:
+ return SuccessData(
+ public=BaseLiquidHandlingResult(
+ volume=volume_aspirated,
+ ),
+ state_update=StateUpdate().set_fluid_aspirated(
+ pipette_id=pipette_id,
+ fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume_aspirated),
+ ),
+ )
+
+
+async def dispense_in_place(
+ pipette_id: str,
+ volume: float,
+ flow_rate: float,
+ push_out: float | None,
+ location_if_error: ErrorLocationInfo,
+ pipetting: PipettingHandler,
+ model_utils: ModelUtils,
+) -> SuccessData[BaseLiquidHandlingResult] | DefinedErrorData[OverpressureError]:
+ """Dispense-in-place as a microoperation."""
+ try:
+ volume = await pipetting.dispense_in_place(
+ pipette_id=pipette_id,
+ volume=volume,
+ flow_rate=flow_rate,
+ push_out=push_out,
+ )
+ except PipetteOverpressureError as e:
+ return 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=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
+ )
+ else:
+ return SuccessData(
+ public=BaseLiquidHandlingResult(volume=volume),
+ state_update=StateUpdate().set_fluid_ejected(
+ pipette_id=pipette_id, volume=volume
+ ),
+ )
+
+
+async def blow_out_in_place(
+ pipette_id: str,
+ flow_rate: float,
+ location_if_error: ErrorLocationInfo,
+ pipetting: PipettingHandler,
+ model_utils: ModelUtils,
+) -> SuccessData[BaseModel] | DefinedErrorData[OverpressureError]:
+ """Execute a blow-out-in-place micro-operation."""
+ try:
+ await pipetting.blow_out_in_place(pipette_id=pipette_id, flow_rate=flow_rate)
+ except PipetteOverpressureError as e:
+ return 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=StateUpdate().set_fluid_unknown(pipette_id=pipette_id),
)
else:
- state_update.set_fluid_empty(pipette_id=pipette_id)
- return PrepareForAspirateReturn.from_result(
- SuccessData(public=BaseModel(), state_update=state_update)
+ return SuccessData(
+ public=BaseModel(),
+ state_update=StateUpdate().set_fluid_empty(pipette_id=pipette_id),
)
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 38f3a60516a..cabcb2039eb 100644
--- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py
+++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py
@@ -12,7 +12,6 @@
BaseCommandCreate,
DefinedErrorData,
SuccessData,
- Maybe,
)
from ..errors.error_occurrence import ErrorOccurrence
@@ -42,11 +41,6 @@ class PrepareToAspirateResult(BaseModel):
]
-_ExecuteMaybe = Maybe[
- SuccessData[PrepareToAspirateResult], DefinedErrorData[OverpressureError]
-]
-
-
class PrepareToAspirateImplementation(
AbstractCommandImpl[PrepareToAspirateParams, _ExecuteReturn]
):
@@ -63,11 +57,11 @@ 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
- )
+ def _transform_result(
+ self, result: SuccessData[BaseModel]
+ ) -> SuccessData[PrepareToAspirateResult]:
+ return SuccessData(
+ public=PrepareToAspirateResult(), state_update=result.state_update
)
async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn:
@@ -85,7 +79,13 @@ async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn:
)
},
)
- return prepare_result.and_then(self._transform_result).unwrap()
+ if isinstance(prepare_result, DefinedErrorData):
+ return prepare_result
+ else:
+ return SuccessData(
+ public=PrepareToAspirateResult(),
+ state_update=prepare_result.state_update,
+ )
class PrepareToAspirate(
diff --git a/api/src/opentrons/protocol_engine/commands/robot/move_to.py b/api/src/opentrons/protocol_engine/commands/robot/move_to.py
index 44b8c4fdbe2..199d5be5079 100644
--- a/api/src/opentrons/protocol_engine/commands/robot/move_to.py
+++ b/api/src/opentrons/protocol_engine/commands/robot/move_to.py
@@ -5,7 +5,7 @@
from pydantic import BaseModel, Field
from opentrons.types import MountType
-from ..pipetting_common import DestinationPositionResult
+from ..movement_common import DestinationPositionResult
from ..command import (
AbstractCommandImpl,
BaseCommand,
diff --git a/api/src/opentrons/protocol_engine/commands/touch_tip.py b/api/src/opentrons/protocol_engine/commands/touch_tip.py
index 48c947abcbd..c7a5f278e52 100644
--- a/api/src/opentrons/protocol_engine/commands/touch_tip.py
+++ b/api/src/opentrons/protocol_engine/commands/touch_tip.py
@@ -1,10 +1,11 @@
"""Touch tip command request, result, and implementation models."""
+
from __future__ import annotations
from pydantic import Field
from typing import TYPE_CHECKING, Optional, Type
from typing_extensions import Literal
-from opentrons.protocol_engine.state import update_types
+from opentrons.types import Point
from ..errors import TouchTipDisabledError, LabwareIsTipRackError
from ..types import DeckPoint
@@ -12,8 +13,11 @@
from ..errors.error_occurrence import ErrorOccurrence
from .pipetting_common import (
PipetteIdMixin,
+)
+from .movement_common import (
WellLocationMixin,
DestinationPositionResult,
+ move_to_well,
)
if TYPE_CHECKING:
@@ -71,8 +75,6 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]:
labware_id = params.labwareId
well_name = params.wellName
- state_update = update_types.StateUpdate()
-
if self._state_view.labware.get_has_quirk(labware_id, "touchTipDisabled"):
raise TouchTipDisabledError(
f"Touch tip not allowed on labware {labware_id}"
@@ -81,7 +83,8 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]:
if self._state_view.labware.is_tiprack(labware_id):
raise LabwareIsTipRackError("Cannot touch tip on tip rack")
- center_point = await self._movement.move_to_well(
+ center_result = await move_to_well(
+ movement=self._movement,
pipette_id=pipette_id,
labware_id=labware_id,
well_name=well_name,
@@ -97,7 +100,11 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]:
labware_id=labware_id,
well_name=well_name,
radius=params.radius,
- center_point=center_point,
+ center_point=Point(
+ center_result.public.position.x,
+ center_result.public.position.y,
+ center_result.public.position.z,
+ ),
)
final_point = await self._gantry_mover.move_to(
@@ -108,7 +115,7 @@ async def execute(self, params: TouchTipParams) -> SuccessData[TouchTipResult]:
final_deck_point = DeckPoint.construct(
x=final_point.x, y=final_point.y, z=final_point.z
)
- state_update.set_pipette_location(
+ state_update = center_result.state_update.set_pipette_location(
pipette_id=pipette_id,
new_labware_id=labware_id,
new_well_name=well_name,
diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py
index aed6e637f3f..2f217120143 100644
--- a/api/src/opentrons/protocol_engine/state/update_types.py
+++ b/api/src/opentrons/protocol_engine/state/update_types.py
@@ -1,9 +1,9 @@
"""Structures to represent changes that commands want to make to engine state."""
-
import dataclasses
import enum
import typing
+from typing_extensions import Self
from datetime import datetime
from opentrons.hardware_control.nozzle_manager import NozzleMap
@@ -275,9 +275,13 @@ class StateUpdate:
pipette_tip_state: PipetteTipStateUpdate | NoChangeType = NO_CHANGE
- pipette_aspirated_fluid: PipetteAspiratedFluidUpdate | PipetteEjectedFluidUpdate | PipetteUnknownFluidUpdate | PipetteEmptyFluidUpdate | NoChangeType = (
- NO_CHANGE
- )
+ pipette_aspirated_fluid: (
+ PipetteAspiratedFluidUpdate
+ | PipetteEjectedFluidUpdate
+ | PipetteUnknownFluidUpdate
+ | PipetteEmptyFluidUpdate
+ | NoChangeType
+ ) = NO_CHANGE
labware_location: LabwareLocationUpdate | NoChangeType = NO_CHANGE
@@ -295,40 +299,61 @@ class StateUpdate:
liquid_class_loaded: LiquidClassLoadedUpdate | NoChangeType = NO_CHANGE
+ @classmethod
+ def reduce(cls: typing.Type[Self], *args: Self) -> Self:
+ """Fuse multiple state updates into a single one.
+
+ State updates that are later in the parameter list are preferred to those that are earlier;
+ NO_CHANGE is ignored.
+ """
+ fields = dataclasses.fields(cls)
+ changes_dicts = [
+ {
+ field.name: update.__dict__[field.name]
+ for field in fields
+ if update.__dict__[field.name] != NO_CHANGE
+ }
+ for update in args
+ ]
+ changes = {}
+ for changes_dict in changes_dicts:
+ changes.update(changes_dict)
+ return cls(**changes)
+
# These convenience functions let the caller avoid the boilerplate of constructing a
# complicated dataclass tree.
@typing.overload
def set_pipette_location(
- self,
+ self: Self,
*,
pipette_id: str,
new_labware_id: str,
new_well_name: str,
new_deck_point: DeckPoint,
- ) -> None:
+ ) -> Self:
"""Schedule a pipette's location to be set to a well."""
@typing.overload
def set_pipette_location(
- self,
+ self: Self,
*,
pipette_id: str,
new_addressable_area_name: str,
new_deck_point: DeckPoint,
- ) -> None:
+ ) -> Self:
"""Schedule a pipette's location to be set to an addressable area."""
pass
def set_pipette_location( # noqa: D102
- self,
+ self: Self,
*,
pipette_id: str,
new_labware_id: str | NoChangeType = NO_CHANGE,
new_well_name: str | NoChangeType = NO_CHANGE,
new_addressable_area_name: str | NoChangeType = NO_CHANGE,
new_deck_point: DeckPoint,
- ) -> None:
+ ) -> Self:
if new_addressable_area_name != NO_CHANGE:
self.pipette_location = PipetteLocationUpdate(
pipette_id=pipette_id,
@@ -347,33 +372,36 @@ def set_pipette_location( # noqa: D102
new_location=Well(labware_id=new_labware_id, well_name=new_well_name),
new_deck_point=new_deck_point,
)
+ return self
- def clear_all_pipette_locations(self) -> None:
+ def clear_all_pipette_locations(self) -> Self:
"""Mark all pipettes as having an unknown location."""
self.pipette_location = CLEAR
+ return self
def set_labware_location(
- self,
+ self: Self,
*,
labware_id: str,
new_location: LabwareLocation,
new_offset_id: str | None,
- ) -> None:
+ ) -> Self:
"""Set a labware's location. See `LabwareLocationUpdate`."""
self.labware_location = LabwareLocationUpdate(
labware_id=labware_id,
new_location=new_location,
offset_id=new_offset_id,
)
+ return self
def set_loaded_labware(
- self,
+ self: Self,
definition: LabwareDefinition,
labware_id: str,
offset_id: typing.Optional[str],
display_name: typing.Optional[str],
location: LabwareLocation,
- ) -> None:
+ ) -> Self:
"""Add a new labware to state. See `LoadedLabwareUpdate`."""
self.loaded_labware = LoadedLabwareUpdate(
definition=definition,
@@ -382,14 +410,15 @@ def set_loaded_labware(
new_location=location,
display_name=display_name,
)
+ return self
def set_load_pipette(
- self,
+ self: Self,
pipette_id: str,
pipette_name: PipetteNameType,
mount: MountType,
liquid_presence_detection: typing.Optional[bool],
- ) -> None:
+ ) -> Self:
"""Add a new pipette to state. See `LoadPipetteUpdate`."""
self.loaded_pipette = LoadPipetteUpdate(
pipette_id=pipette_id,
@@ -397,61 +426,69 @@ def set_load_pipette(
mount=mount,
liquid_presence_detection=liquid_presence_detection,
)
+ return self
def update_pipette_config(
- self,
+ self: Self,
pipette_id: str,
config: pipette_data_provider.LoadedStaticPipetteData,
serial_number: str,
- ) -> None:
+ ) -> Self:
"""Update a pipette's config. See `PipetteConfigUpdate`."""
self.pipette_config = PipetteConfigUpdate(
pipette_id=pipette_id, config=config, serial_number=serial_number
)
+ return self
- def update_pipette_nozzle(self, pipette_id: str, nozzle_map: NozzleMap) -> None:
+ def update_pipette_nozzle(
+ self: Self, pipette_id: str, nozzle_map: NozzleMap
+ ) -> Self:
"""Update a pipette's nozzle map. See `PipetteNozzleMapUpdate`."""
self.pipette_nozzle_map = PipetteNozzleMapUpdate(
pipette_id=pipette_id, nozzle_map=nozzle_map
)
+ return self
def update_pipette_tip_state(
- self, pipette_id: str, tip_geometry: typing.Optional[TipGeometry]
- ) -> None:
+ self: Self, pipette_id: str, tip_geometry: typing.Optional[TipGeometry]
+ ) -> Self:
"""Update a pipette's tip state. See `PipetteTipStateUpdate`."""
self.pipette_tip_state = PipetteTipStateUpdate(
pipette_id=pipette_id, tip_geometry=tip_geometry
)
+ return self
def mark_tips_as_used(
- self, pipette_id: str, labware_id: str, well_name: str
- ) -> None:
+ self: Self, pipette_id: str, labware_id: str, well_name: str
+ ) -> Self:
"""Mark tips in a tip rack as used. See `TipsUsedUpdate`."""
self.tips_used = TipsUsedUpdate(
pipette_id=pipette_id, labware_id=labware_id, well_name=well_name
)
+ return self
def set_liquid_loaded(
- self,
+ self: Self,
labware_id: str,
volumes: typing.Dict[str, float],
last_loaded: datetime,
- ) -> None:
+ ) -> Self:
"""Add liquid volumes to well state. See `LoadLiquidUpdate`."""
self.liquid_loaded = LiquidLoadedUpdate(
labware_id=labware_id,
volumes=volumes,
last_loaded=last_loaded,
)
+ return self
def set_liquid_probed(
- self,
+ self: Self,
labware_id: str,
well_name: str,
last_probed: datetime,
height: float | ClearType,
volume: float | ClearType,
- ) -> None:
+ ) -> Self:
"""Add a liquid height and volume to well state. See `ProbeLiquidUpdate`."""
self.liquid_probed = LiquidProbedUpdate(
labware_id=labware_id,
@@ -460,43 +497,53 @@ def set_liquid_probed(
volume=volume,
last_probed=last_probed,
)
+ return self
def set_liquid_operated(
- self, labware_id: str, well_names: list[str], volume_added: float | ClearType
- ) -> None:
+ self: Self,
+ labware_id: str,
+ well_names: list[str],
+ volume_added: float | ClearType,
+ ) -> Self:
"""Update liquid volumes in well state. See `OperateLiquidUpdate`."""
self.liquid_operated = LiquidOperatedUpdate(
labware_id=labware_id,
well_names=well_names,
volume_added=volume_added,
)
+ return self
- def set_fluid_aspirated(self, pipette_id: str, fluid: AspiratedFluid) -> None:
+ def set_fluid_aspirated(self: Self, pipette_id: str, fluid: AspiratedFluid) -> Self:
"""Update record of fluid held inside a pipette. See `PipetteAspiratedFluidUpdate`."""
self.pipette_aspirated_fluid = PipetteAspiratedFluidUpdate(
type="aspirated", pipette_id=pipette_id, fluid=fluid
)
+ return self
- def set_fluid_ejected(self, pipette_id: str, volume: float) -> None:
+ def set_fluid_ejected(self: Self, pipette_id: str, volume: float) -> Self:
"""Update record of fluid held inside a pipette. See `PipetteEjectedFluidUpdate`."""
self.pipette_aspirated_fluid = PipetteEjectedFluidUpdate(
type="ejected", pipette_id=pipette_id, volume=volume
)
+ return self
- def set_fluid_unknown(self, pipette_id: str) -> None:
+ def set_fluid_unknown(self: Self, pipette_id: str) -> Self:
"""Update record of fluid held inside a pipette. See `PipetteUnknownFluidUpdate`."""
self.pipette_aspirated_fluid = PipetteUnknownFluidUpdate(
type="unknown", pipette_id=pipette_id
)
+ return self
- def set_fluid_empty(self, pipette_id: str) -> None:
+ def set_fluid_empty(self: Self, pipette_id: str) -> Self:
"""Update record fo fluid held inside a pipette. See `PipetteEmptyFluidUpdate`."""
self.pipette_aspirated_fluid = PipetteEmptyFluidUpdate(
type="empty", pipette_id=pipette_id
)
+ return self
- def set_absorbance_reader_lid(self, module_id: str, is_lid_on: bool) -> None:
+ def set_absorbance_reader_lid(self: Self, module_id: str, is_lid_on: bool) -> Self:
"""Update an absorbance reader's lid location. See `AbsorbanceReaderLidUpdate`."""
self.absorbance_reader_lid = AbsorbanceReaderLidUpdate(
module_id=module_id, is_lid_on=is_lid_on
)
+ return self
diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py
index 724b55df3c5..8c2c2b06439 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py
@@ -106,6 +106,9 @@ async def test_aspirate_implementation_no_prep(
well_name="A3",
well_location=location,
current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
operation_volume=-50,
),
).then_return(Point(x=1, y=2, z=3))
@@ -195,6 +198,9 @@ async def test_aspirate_implementation_with_prep(
labware_id="123",
well_name="A3",
),
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
operation_volume=-50,
),
).then_return(Point(x=1, y=2, z=3))
@@ -285,6 +291,9 @@ async def test_aspirate_raises_volume_error(
well_name="A3",
well_location=location,
current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
operation_volume=-50,
),
).then_return(Point(1, 2, 3))
@@ -358,6 +367,9 @@ async def test_overpressure_error(
well_name=well_name,
well_location=well_location,
current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
operation_volume=-50,
),
).then_return(position)
@@ -400,6 +412,15 @@ async def test_overpressure_error(
pipette_id=pipette_id
),
),
+ state_update_if_false_positive=update_types.StateUpdate(
+ pipette_location=update_types.PipetteLocationUpdate(
+ pipette_id=pipette_id,
+ new_location=update_types.Well(
+ labware_id=labware_id, well_name=well_name
+ ),
+ new_deck_point=DeckPoint(x=position.x, y=position.y, z=position.z),
+ ),
+ ),
)
@@ -450,6 +471,9 @@ async def test_aspirate_implementation_meniscus(
well_name="A3",
well_location=location,
current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
operation_volume=-50,
),
).then_return(Point(x=1, y=2, z=3))
diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py
index 1404d804707..48dba2e0c3e 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py
@@ -88,6 +88,7 @@ def subject(
)
async def test_aspirate_in_place_implementation(
decoy: Decoy,
+ gantry_mover: GantryMover,
pipetting: PipettingHandler,
state_store: StateStore,
hardware_api: HardwareAPI,
@@ -131,6 +132,10 @@ async def test_aspirate_in_place_implementation(
)
).then_return(123)
+ decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return(
+ Point(1, 2, 3)
+ )
+
decoy.when(state_store.pipettes.get_current_location()).then_return(location)
result = await subject.execute(params=data)
@@ -164,6 +169,7 @@ async def test_aspirate_in_place_implementation(
async def test_handle_aspirate_in_place_request_not_ready_to_aspirate(
decoy: Decoy,
+ gantry_mover: GantryMover,
pipetting: PipettingHandler,
state_store: StateStore,
hardware_api: HardwareAPI,
@@ -175,7 +181,9 @@ async def test_handle_aspirate_in_place_request_not_ready_to_aspirate(
volume=123,
flowRate=1.234,
)
-
+ decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return(
+ Point(1, 2, 3)
+ )
decoy.when(
pipetting.get_is_ready_to_aspirate(
pipette_id="pipette-id-abc",
@@ -196,6 +204,7 @@ async def test_aspirate_raises_volume_error(
pipetting: PipettingHandler,
subject: AspirateInPlaceImplementation,
mock_command_note_adder: CommandNoteAdder,
+ gantry_mover: GantryMover,
) -> None:
"""Should raise an assertion error for volume larger than working volume."""
data = AspirateInPlaceParams(
@@ -203,7 +212,7 @@ async def test_aspirate_raises_volume_error(
volume=50,
flowRate=1.23,
)
-
+ decoy.when(await gantry_mover.get_position("abc")).then_return(Point(x=1, y=2, z=3))
decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True)
decoy.when(
diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py
index d053aac0f0d..86940b15266 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py
@@ -1,4 +1,5 @@
"""Test blow-out command."""
+
from datetime import datetime
from decoy import Decoy, matchers
@@ -69,6 +70,11 @@ async def test_blow_out_implementation(
labware_id="labware-id",
well_name="C6",
well_location=location,
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=1, y=2, z=3))
@@ -136,6 +142,11 @@ async def test_overpressure_error(
labware_id="labware-id",
well_name="C6",
well_location=location,
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=1, y=2, z=3))
@@ -161,4 +172,14 @@ async def test_overpressure_error(
pipette_id="pipette-id"
),
),
+ state_update_if_false_positive=update_types.StateUpdate(
+ pipette_location=update_types.PipetteLocationUpdate(
+ pipette_id="pipette-id",
+ new_location=update_types.Well(
+ labware_id="labware-id",
+ well_name="C6",
+ ),
+ new_deck_point=DeckPoint(x=1, y=2, z=3),
+ ),
+ ),
)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py
index bc4ab782f64..97e8e8c0851 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py
@@ -43,6 +43,7 @@ def subject(
async def test_blow_out_in_place_implementation(
decoy: Decoy,
+ gantry_mover: GantryMover,
subject: BlowOutInPlaceImplementation,
pipetting: PipettingHandler,
) -> None:
@@ -51,9 +52,11 @@ async def test_blow_out_in_place_implementation(
pipetteId="pipette-id",
flowRate=1.234,
)
+ decoy.when(await gantry_mover.get_position("pipette-id")).then_return(
+ Point(1, 2, 3)
+ )
result = await subject.execute(data)
-
assert result == SuccessData(
public=BlowOutInPlaceResult(),
state_update=update_types.StateUpdate(
diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py
index 7983343aebd..6744add9b4a 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py
@@ -71,6 +71,11 @@ async def test_dispense_implementation(
labware_id="labware-id-abc123",
well_name="A3",
well_location=well_location,
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=1, y=2, z=3))
@@ -174,6 +179,11 @@ async def test_overpressure_error(
labware_id=labware_id,
well_name=well_name,
well_location=well_location,
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
),
).then_return(position)
@@ -213,4 +223,14 @@ async def test_overpressure_error(
pipette_id="pipette-id"
),
),
+ state_update_if_false_positive=update_types.StateUpdate(
+ pipette_location=update_types.PipetteLocationUpdate(
+ pipette_id="pipette-id",
+ new_location=update_types.Well(
+ labware_id="labware-id",
+ well_name="well-name",
+ ),
+ new_deck_point=DeckPoint.construct(x=1, y=2, z=3),
+ ),
+ ),
)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py
index 569cc69ecce..bc39fba4a00 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py
@@ -61,6 +61,7 @@ def subject(
)
async def test_dispense_in_place_implementation(
decoy: Decoy,
+ gantry_mover: GantryMover,
pipetting: PipettingHandler,
state_view: StateView,
subject: DispenseInPlaceImplementation,
@@ -101,6 +102,9 @@ async def test_dispense_in_place_implementation(
stateupdateLabware, stateupdateWell, "pipette-id-abc"
)
).then_return(["A3", "A4"])
+ decoy.when(await gantry_mover.get_position("pipette-id-abc")).then_return(
+ Point(1, 2, 3)
+ )
result = await subject.execute(data)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py
index 9217a4a4287..126d55defef 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py
@@ -1,4 +1,5 @@
"""Test drop tip commands."""
+
from datetime import datetime
import pytest
@@ -122,6 +123,11 @@ async def test_drop_tip_implementation(
labware_id="123",
well_name="A3",
well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)),
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=111, y=222, z=333))
@@ -203,6 +209,11 @@ async def test_drop_tip_with_alternating_locations(
labware_id="123",
well_name="A3",
well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)),
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=111, y=222, z=333))
@@ -269,6 +280,11 @@ async def test_tip_attached_error(
labware_id="123",
well_name="A3",
well_location=WellLocation(offset=WellOffset(x=4, y=5, z=6)),
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=111, y=222, z=333))
decoy.when(
@@ -306,5 +322,13 @@ async def test_tip_attached_error(
pipette_id="abc",
tip_geometry=None,
),
+ pipette_location=update_types.PipetteLocationUpdate(
+ pipette_id="abc",
+ new_location=update_types.Well(
+ labware_id="123",
+ well_name="A3",
+ ),
+ new_deck_point=DeckPoint(x=111, y=222, z=333),
+ ),
),
)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py
index 1f55dc95daf..d13ededae85 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_liquid_probe.py
@@ -127,6 +127,11 @@ async def test_liquid_probe_implementation(
labware_id="123",
well_name="A3",
well_location=location,
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
),
).then_return(Point(x=1, y=2, z=3))
@@ -212,6 +217,11 @@ async def test_liquid_not_found_error(
labware_id=labware_id,
well_name=well_name,
well_location=well_location,
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
),
).then_return(position)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py
index fdfcfb45af7..61863363656 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py
@@ -54,6 +54,8 @@ async def test_move_to_well_implementation(
force_direct=True,
minimum_z_height=4.56,
speed=7.89,
+ current_well=None,
+ operation_volume=None,
)
).then_return(Point(x=9, y=8, z=7))
diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py
index 5fb97a2f78f..00dad1557d2 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py
@@ -57,6 +57,11 @@ async def test_success(
labware_id="labware-id",
well_name="A3",
well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)),
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=111, y=222, z=333))
@@ -137,6 +142,11 @@ async def test_tip_physically_missing_error(
labware_id="labware-id",
well_name="well-name",
well_location=WellLocation(offset=WellOffset()),
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=111, y=222, z=333))
decoy.when(
@@ -177,5 +187,15 @@ async def test_tip_physically_missing_error(
pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate(
pipette_id="pipette-id"
),
+ tips_used=update_types.TipsUsedUpdate(
+ pipette_id="pipette-id", labware_id="labware-id", well_name="well-name"
+ ),
+ pipette_location=update_types.PipetteLocationUpdate(
+ pipette_id="pipette-id",
+ new_location=update_types.Well(
+ labware_id="labware-id", well_name="well-name"
+ ),
+ new_deck_point=DeckPoint(x=111, y=222, z=333),
+ ),
),
)
diff --git a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py
index d00f44fd108..c0ec729aefe 100644
--- a/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py
+++ b/api/tests/opentrons/protocol_engine/commands/test_touch_tip.py
@@ -73,6 +73,11 @@ async def test_touch_tip_implementation(
labware_id="123",
well_name="A3",
well_location=WellLocation(offset=WellOffset(x=1, y=2, z=3)),
+ current_well=None,
+ force_direct=False,
+ minimum_z_height=None,
+ speed=None,
+ operation_volume=None,
)
).then_return(Point(x=1, y=2, z=3))
From f01b437263cc989b144a0d66186abce81b3a945f Mon Sep 17 00:00:00 2001
From: Jethary Alcid <66035149+jerader@users.noreply.github.com>
Date: Mon, 18 Nov 2024 15:45:25 -0500
Subject: [PATCH 46/68] fix(protocol-designer): remove console warnings
(#16872)
---
.../src/organisms/BlockingHintModal/index.tsx | 2 +-
.../src/organisms/ProtocolNavBar/index.tsx | 15 +++++++++++----
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/protocol-designer/src/organisms/BlockingHintModal/index.tsx b/protocol-designer/src/organisms/BlockingHintModal/index.tsx
index 13a57c3b18c..4ed10b98dee 100644
--- a/protocol-designer/src/organisms/BlockingHintModal/index.tsx
+++ b/protocol-designer/src/organisms/BlockingHintModal/index.tsx
@@ -69,7 +69,7 @@ export function BlockingHintModal(props: HintProps): JSX.Element {
{t('hint.dont_show_again')}
-
+
{t('shared:cancel')}
diff --git a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx
index 6ae4b21ff45..cb47c8a20e6 100644
--- a/protocol-designer/src/organisms/ProtocolNavBar/index.tsx
+++ b/protocol-designer/src/organisms/ProtocolNavBar/index.tsx
@@ -8,6 +8,7 @@ import {
COLORS,
DIRECTION_COLUMN,
Flex,
+ isntStyleProp,
JUSTIFY_SPACE_BETWEEN,
SecondaryButton,
SPACING,
@@ -21,7 +22,7 @@ import { LINE_CLAMP_TEXT_STYLE } from '../../atoms'
import { useKitchen } from '../Kitchen/hooks'
import { LiquidButton } from './LiquidButton'
-import type { TabProps } from '@opentrons/components'
+import type { StyleProps, TabProps } from '@opentrons/components'
interface ProtocolNavBarProps {
hasZoomInSlot?: boolean
@@ -105,10 +106,16 @@ const NavContainer = styled(Flex)`
box-shadow: 0px 1px 3px 0px ${COLORS.black90}${COLORS.opacity20HexCode};
`
-const MetadataContainer = styled(Flex)<{ isAddingHardwareOrLabware: boolean }>`
+interface MetadataProps extends StyleProps {
+ isAddingHardwareOrLabware: boolean
+}
+const MetadataContainer = styled.div.withConfig({
+ shouldForwardProp: p => isntStyleProp(p) && p !== 'isAddingHardwareOrLabware',
+})`
+ display: flex;
flex-direction: ${DIRECTION_COLUMN};
- text-align: ${({ isAddingHardwareOrLabware }) =>
- isAddingHardwareOrLabware === true
+ text-align: ${props =>
+ props.isAddingHardwareOrLabware === true
? TYPOGRAPHY.textAlignLeft
: TYPOGRAPHY.textAlignCenter};
From f0e45b807e01e5d57e6431b530d3218bd72c83e5 Mon Sep 17 00:00:00 2001
From: Shlok Amin
Date: Mon, 18 Nov 2024 15:59:56 -0500
Subject: [PATCH 47/68] feat(app, opentrons-ai, labware-library): update flex
deck riser image (#16870)
closes RQA-3601
---
.../labware/opentrons_flex_deck_riser.png | Bin 17859 -> 690224 bytes
.../Labware/LabwareDetails/labware-images.ts | 3 +++
.../components/labware-ui/labware-images.ts | 2 +-
.../src/images/opentrons_flex_deck_riser.jpg | Bin 1009921 -> 0 bytes
.../src/images/opentrons_flex_deck_riser.png | Bin 0 -> 690224 bytes
.../labwares/opentrons_flex_deck_riser.jpg | Bin 1009921 -> 0 bytes
.../labwares/opentrons_flex_deck_riser.png | Bin 0 -> 690224 bytes
.../LabwareDiagram/labware-images.ts | 2 +-
8 files changed, 5 insertions(+), 2 deletions(-)
delete mode 100644 labware-library/src/images/opentrons_flex_deck_riser.jpg
create mode 100644 labware-library/src/images/opentrons_flex_deck_riser.png
delete mode 100644 opentrons-ai-client/src/assets/images/labwares/opentrons_flex_deck_riser.jpg
create mode 100644 opentrons-ai-client/src/assets/images/labwares/opentrons_flex_deck_riser.png
diff --git a/app/src/assets/images/labware/opentrons_flex_deck_riser.png b/app/src/assets/images/labware/opentrons_flex_deck_riser.png
index a06f26bf4458e6acd562ae72631c3607be13675a..1fecdf8ca4b677cbc16d18f868d1940fc5888595 100644
GIT binary patch
literal 690224
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