From f1b82deea353720cd98403b4bc641bfaee42a990 Mon Sep 17 00:00:00 2001 From: Gui Date: Thu, 25 Apr 2024 07:48:54 -0700 Subject: [PATCH 01/10] Form for creating Sequences (#1170) * [create-seq-form] chore: adding form validation * chore: formatting * chore: add a helpful msg * [create-seq-form] chore: trim space in name * chore: formatting --------- Co-authored-by: Joey Yu --- .../components/TestSequencerInfo.tsx | 4 +- .../components/modals/CreateSequenceModal.tsx | 175 ++++++++++++++++++ .../modals/TestSequencerProjectModal.tsx | 108 ----------- 3 files changed, 177 insertions(+), 110 deletions(-) create mode 100644 src/renderer/routes/test_sequencer_panel/components/modals/CreateSequenceModal.tsx delete mode 100644 src/renderer/routes/test_sequencer_panel/components/modals/TestSequencerProjectModal.tsx diff --git a/src/renderer/routes/test_sequencer_panel/components/TestSequencerInfo.tsx b/src/renderer/routes/test_sequencer_panel/components/TestSequencerInfo.tsx index 2d19d1d58..db272620c 100644 --- a/src/renderer/routes/test_sequencer_panel/components/TestSequencerInfo.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/TestSequencerInfo.tsx @@ -11,10 +11,10 @@ import SequencerKeyboardShortcuts from "@/renderer/routes/test_sequencer_panel/S import { ControlButton } from "./ControlButton"; import { DesignBar } from "./DesignBar"; import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState"; -import { TestSequencerProjectModal } from "./modals/TestSequencerProjectModal"; import { ImportTestModal } from "./modals/ImportTestModal"; import { ErrorModal } from "./modals/ErrorModal"; import { RenameTestModal } from "./modals/RenameTestModal"; +import { CreateSequenceModal } from "./modals/CreateSequenceModal"; const TestSequencerView = () => { const { backendGlobalState } = useDisplayedSequenceState(); @@ -40,7 +40,7 @@ const TestSequencerView = () => { }} className="overflow-y-auto" > - + diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/CreateSequenceModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/CreateSequenceModal.tsx new file mode 100644 index 000000000..7dca49ea4 --- /dev/null +++ b/src/renderer/routes/test_sequencer_panel/components/modals/CreateSequenceModal.tsx @@ -0,0 +1,175 @@ +import { Dialog, DialogContent } from "@/renderer/components/ui/dialog"; +import { Button } from "@/renderer/components/ui/button"; +import { useState } from "react"; +import { Input } from "@/renderer/components/ui/input"; +import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState"; +import { useCreateSequence } from "@/renderer/hooks/useTestSequencerProject"; +import { InterpreterType } from "@/renderer/types/test-sequencer"; +import { PathInput } from "@/renderer/components/ui/path-input"; +import { useSequencerModalStore } from "@/renderer/stores/modal"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/renderer/components/ui/form"; + +const formSchema = z.object({ + name: z + .string() + .min(1) + .max(50) + .regex(/\S/, { + message: "The sequence name should not contain white spaces", + }) + .transform((val) => val.trim()), + description: z.string().max(100), + projectPath: z.string().min(1).regex(/\S/, { + message: "The project path should not contain white spaces", + }), +}); + +export const CreateSequenceModal = () => { + const { isCreateProjectModalOpen, setIsCreateProjectModalOpen } = + useSequencerModalStore(); + const { elems } = useDisplayedSequenceState(); + const handleCreate = useCreateSequence(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [interpreterPath, setInterpreterPath] = useState(""); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [type, setType] = useState("flojoy"); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + description: "", + projectPath: "", + }, + }); + + function handleSubmit(values: z.infer) { + handleCreate( + { + name: values.name, + description: values.description, + elems: elems, + projectPath: values.projectPath, + interpreter: { + type: type, + path: interpreterPath === "" ? null : interpreterPath, + requirementsPath: "flojoy_requirements.txt", + }, + }, + setIsCreateProjectModalOpen, + ); + } + + return ( + + +

New Sequence

+
+ + ( + + Sequence Name + + + + + + )} + /> + ( + + Description + + + + + + )} + /> + ( + + Project Root Directory + + + + + + )} + /> + { + //
+ //
+ // + //
+ // {setInterpreterPath(event.target.value)}} + // /> + //
+ } + + + +
+
+ ); +}; diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/TestSequencerProjectModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/TestSequencerProjectModal.tsx deleted file mode 100644 index dd2677180..000000000 --- a/src/renderer/routes/test_sequencer_panel/components/modals/TestSequencerProjectModal.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Dialog, DialogContent } from "@/renderer/components/ui/dialog"; -import { Button } from "@/renderer/components/ui/button"; -import { useState } from "react"; -import { Input } from "@/renderer/components/ui/input"; -import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState"; -import { useCreateSequence } from "@/renderer/hooks/useTestSequencerProject"; -import { InterpreterType } from "@/renderer/types/test-sequencer"; -import { PathInput } from "@/renderer/components/ui/path-input"; -import { useSequencerModalStore } from "@/renderer/stores/modal"; - -export const TestSequencerProjectModal = () => { - const { isCreateProjectModalOpen, setIsCreateProjectModalOpen } = - useSequencerModalStore(); - const { elems } = useDisplayedSequenceState(); - const handleCreate = useCreateSequence(); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [projectDirPath, setProjectDirPath] = useState(""); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [interpreterPath, setInterpreterPath] = useState(""); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [type, setType] = useState("flojoy"); - - function handleSubmit() { - handleCreate( - { - name: name, - description: description, - elems: elems, - projectPath: projectDirPath, - interpreter: { - type: type, - path: interpreterPath === "" ? null : interpreterPath, - requirementsPath: "flojoy_requirements.txt", - }, - }, - setIsCreateProjectModalOpen, - ); - } - - return ( - - -

- New Sequence -

- { - setName(e.target.value); - }} - /> - { - setDescription(e.target.value); - }} - /> - { - setProjectDirPath(event.target.value); - }} - pickerType="directory" - allowDirectoryCreation={true} - /> - { - //
- //
- // - //
- // {setInterpreterPath(event.target.value)}} - // /> - //
- } - -
-
- ); -}; From 2f405d4122596a28a4653ff88b9948a7bf3d51d6 Mon Sep 17 00:00:00 2001 From: Gui Date: Thu, 25 Apr 2024 08:35:54 -0700 Subject: [PATCH 02/10] Sequencer Gallery (#1169) * chore: Sequence Gallery UI * [sequencer-gallery] chore: Working import demo sequence * [sequencer-gallery] Export & Expected Demo * [sequencer-gallery] ui: removed weird yellow and added better comment in example * chore: formatting * fix: Eslint * [sequencer-gallery] chore: using test profile workflow to load example * [sequencer-gallery] chore: remove "use" prefix as react thinks it's a hook * [sequencer-gallery] chore: bundle example with app * [sequencer-gallery] fix: space in sequence name * chore: formatting * [sequencer-gallery] chore: using process.resourcesPath * [sequencer-gallery] chore: adding CI test for Gallery + testing injected min & max * chore: formatting * chore(gallery): fix @39bytes comment * try bumping pnpm version in pacakge.json --------- Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com> Co-authored-by: JeffDotPng --- electron-builder.yaml | 2 + .../Conditional_Demo.tjoy | 1 + .../flojoy_requirements.txt | 0 .../test.py | 28 ++++++ .../Export_&_Expected_Demo.tjoy | 1 + .../flojoy_requirements.txt | 0 .../test.py | 48 ++++++++++ package.json | 2 +- playwright-test/15_sequences_gallery.spec.ts | 69 +++++++++++++++ playwright-test/selectors.ts | 1 + src/api/index.ts | 2 + src/main/utils.ts | 5 ++ src/renderer/assets/FlojoyTheme.ts | 2 +- src/renderer/hooks/useTestSequencerProject.ts | 55 ++++++++++++ .../components/DesignBar.tsx | 18 +++- .../components/data-table/TestTable.tsx | 5 +- .../modals/SequencerGalleryModal.tsx | 88 +++++++++++++++++++ .../utils/SequenceHandler.ts | 3 + src/renderer/types/test-sequencer.ts | 4 +- 19 files changed, 327 insertions(+), 7 deletions(-) create mode 100644 examples/test-sequencer-conditional-example/Conditional_Demo.tjoy create mode 100644 examples/test-sequencer-conditional-example/flojoy_requirements.txt create mode 100644 examples/test-sequencer-conditional-example/test.py create mode 100644 examples/test-sequencer-expected-exported-example/Export_&_Expected_Demo.tjoy create mode 100644 examples/test-sequencer-expected-exported-example/flojoy_requirements.txt create mode 100644 examples/test-sequencer-expected-exported-example/test.py create mode 100644 playwright-test/15_sequences_gallery.spec.ts create mode 100644 src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx diff --git a/electron-builder.yaml b/electron-builder.yaml index 590714b0b..a36db7144 100644 --- a/electron-builder.yaml +++ b/electron-builder.yaml @@ -29,6 +29,8 @@ extraResources: to: "poetry.lock" - from: "blocks" to: "blocks" + - from: "examples" + to: "examples" mac: icon: ./public/favicon.icns diff --git a/examples/test-sequencer-conditional-example/Conditional_Demo.tjoy b/examples/test-sequencer-conditional-example/Conditional_Demo.tjoy new file mode 100644 index 000000000..8f3abc995 --- /dev/null +++ b/examples/test-sequencer-conditional-example/Conditional_Demo.tjoy @@ -0,0 +1 @@ +{"name":"Conditional_Demo","description":"Example","elems":[{"type":"test","id":"ca204090-41bb-4236-97ba-623c4257ef0a","groupId":"41717403-d6ba-49c3-b451-a4a0ae815b2c","path":"test.py::test_will_pass","testName":"test_will_pass","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"5ec59132-f8f5-45be-b0ad-31124a3a6ba0","role":"start","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"if","condition":" $test_will_pass"},{"type":"test","id":"67014fb0-9a93-433b-8398-df103de3dcc1","groupId":"8a8ecab5-b765-4944-a46c-63111fd59e03","path":"test.py::test_for_example_1","testName":"test_for_example_1","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"test","id":"7d6d00ee-f418-49d4-9bcd-5b9cc2e28470","groupId":"e2de5be6-ba8c-4aa8-b6ac-b32b1f30dec1","path":"test.py::test_will_fail","testName":"test_will_fail","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"5ec59132-f8f5-45be-b0ad-31124a3a6ba0","role":"start","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"if","condition":" $test_will_fail"},{"type":"test","id":"b4927c15-00e8-4ea4-8788-af395be14775","groupId":"b824ead8-84ad-4de3-88c1-21e4b46343fe","path":"test.py::test_for_example_2","testName":"test_for_example_2","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"d5b1cf12-f50d-4f5a-88d4-b7a9f982a447","role":"between","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"else","condition":""},{"type":"test","id":"5ad4da63-40fe-45a2-ab1e-235fd067bde2","groupId":"8f02f062-19f5-448f-83b4-1fcc4e9dc07a","path":"test.py::test_for_example_3","testName":"test_for_example_3","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"c45616fd-cc5a-4469-9bb6-5f3a8ae74bd7","role":"end","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"end","condition":""},{"type":"conditional","id":"d5b1cf12-f50d-4f5a-88d4-b7a9f982a447","role":"between","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"else","condition":""},{"type":"test","id":"01cafc7f-0c21-47b9-8246-305b36a0c23a","groupId":"2e320c36-4b8b-48ec-97af-732e623c3776","path":"test.py::test_for_example_4","testName":"test_for_example_4","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"c45616fd-cc5a-4469-9bb6-5f3a8ae74bd7","role":"end","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"end","condition":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/src/renderer/data/apps/sequencer/conditional/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}} diff --git a/examples/test-sequencer-conditional-example/flojoy_requirements.txt b/examples/test-sequencer-conditional-example/flojoy_requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/examples/test-sequencer-conditional-example/test.py b/examples/test-sequencer-conditional-example/test.py new file mode 100644 index 000000000..ac1b4a8a7 --- /dev/null +++ b/examples/test-sequencer-conditional-example/test.py @@ -0,0 +1,28 @@ +""" +Simple test file to demo how to build conditional tests +- With pytest, all test files should start with 'test_' to be recognized +""" + + +def test_will_pass(): + assert True + + +def test_will_fail(): + assert False + + +def test_for_example_1(): + assert 1 == 1 + + +def test_for_example_2(): + assert 2 == 2 + + +def test_for_example_3(): + assert 3 == 3 + + +def test_for_example_4(): + assert 4 == 4 diff --git a/examples/test-sequencer-expected-exported-example/Export_&_Expected_Demo.tjoy b/examples/test-sequencer-expected-exported-example/Export_&_Expected_Demo.tjoy new file mode 100644 index 000000000..0296bb559 --- /dev/null +++ b/examples/test-sequencer-expected-exported-example/Export_&_Expected_Demo.tjoy @@ -0,0 +1 @@ +{"name":"Export_&_Expected_Demo","description":"Consult the Test Step Code!","elems":[{"type":"test","id":"acb47b74-d1dc-4f4b-b3ef-506acb2f53e1","groupId":"f90f758a-a705-4dd0-b3b6-4ae032d22ea3","path":"test.py::test_min_max","testName":"test_min_max","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","minValue":5,"maxValue":10,"unit":""},{"type":"test","id":"53bb5746-c720-45c4-8121-9dc469d07927","groupId":"397475ec-c600-4478-8725-b7b8e824d599","path":"test.py::test_min","testName":"test_min","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","minValue":5,"unit":""},{"type":"test","id":"55b82abb-37fb-4e32-9379-3609642d01af","groupId":"0377f366-edb7-4a51-97cd-2867bd944cd2","path":"test.py::test_max","testName":"test_max","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","maxValue":7,"unit":""},{"type":"test","id":"029a20ad-4c3f-47a5-82aa-2d558eda418a","groupId":"97ba9e80-3133-4dfa-9806-770d47ffb1e4","path":"test.py::test_export_dataframe","testName":"test_export_dataframe","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z"},{"type":"test","id":"2a75451e-49f6-4810-827e-2b9769d12bc4","groupId":"096274ef-132f-498a-a67a-a279c65b9a8a","path":"test.py::test_export","testName":"test_export","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z"}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/src/renderer/data/apps/sequencer/expected_exported_values/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}} diff --git a/examples/test-sequencer-expected-exported-example/flojoy_requirements.txt b/examples/test-sequencer-expected-exported-example/flojoy_requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/examples/test-sequencer-expected-exported-example/test.py b/examples/test-sequencer-expected-exported-example/test.py new file mode 100644 index 000000000..1d764c5bb --- /dev/null +++ b/examples/test-sequencer-expected-exported-example/test.py @@ -0,0 +1,48 @@ +from flojoy_cloud import test_sequencer +import pandas as pd + + +def test_min_max(): + value = 6.15 + test_sequencer.export(value) + assert test_sequencer.is_in_range(value) + + +def test_min(): + value = 6.15 + # If not Max value is defined, the value will be checked against the Min value. + test_sequencer.export(value) + assert test_sequencer.is_in_range(value) + + +def test_max(): + value = 6.15 + test_sequencer.export(value) + + assert test_sequencer.is_in_range(value) + # If multiple assert statements are defined and one of them fails: + # - the rest of the assert statements will not be executed, and the result will + # be reported to the sequencer. + # - the sequencer will report the error, and the test will be marked as failed. + assert 0 < value + + +def test_export_dataframe(): + df = pd.DataFrame({"value": [6.15, 6.15, 6.15]}) + # Boolean and DataFrame values will be exported to the Cloud. + test_sequencer.export(df) + + assert df is not None + + +def test_export(): + value = 6.15 + # Always export as early as possible to avoid missing data. + test_sequencer.export(value) + assert 12 < value # <-- FAIL + + # Only the last executed export statement will be exported to the Cloud and + # reported to the sequencer. + test_sequencer.export(20) + + assert 0 < value diff --git a/package.json b/package.json index 23cdc75e1..5d8b4051e 100644 --- a/package.json +++ b/package.json @@ -171,5 +171,5 @@ ], "all": true }, - "packageManager": "pnpm@9.0.5" + "packageManager": "pnpm@9.0.6" } diff --git a/playwright-test/15_sequences_gallery.spec.ts b/playwright-test/15_sequences_gallery.spec.ts new file mode 100644 index 000000000..e1442b30b --- /dev/null +++ b/playwright-test/15_sequences_gallery.spec.ts @@ -0,0 +1,69 @@ +import { test, expect, Page, ElectronApplication } from "@playwright/test"; +import { _electron as electron } from "playwright"; +import { + STARTUP_TIMEOUT, + getExecutablePath, + mockDialogMessage, + standbyStatus, +} from "./utils"; +import { Selectors } from "./selectors"; + +test.describe("Load a demo test sequence", () => { + let window: Page; + let app: ElectronApplication; + test.beforeAll(async () => { + test.setTimeout(STARTUP_TIMEOUT); + const executablePath = getExecutablePath(); + app = await electron.launch({ executablePath }); + await mockDialogMessage(app); + window = await app.firstWindow(); + await expect( + window.locator("code", { hasText: standbyStatus }), + ).toBeVisible({ timeout: STARTUP_TIMEOUT }); + await window.getByTestId(Selectors.closeWelcomeModalBtn).click(); + // Switch to sequencer tab + await window.getByTestId(Selectors.testSequencerTabBtn).click(); + }); + + test.afterAll(async () => { + await app.close(); + }); + + test("Should load and run a sequence", async () => { + await expect(window.getByTestId(Selectors.newDropdown)).toBeEnabled({ + timeout: 15000, + }); + + // Open the sequence gallery + await window.getByTestId(Selectors.newDropdown).click(); + await window.getByTestId(Selectors.openSequenceGalleryBtn).click(); + + // Open a sequence + await window + .getByTestId("test_step_with_expected_and_exported_values") + .nth(1) + .click(); + + // Expect sequence and tests to be loaded + await expect( + window.locator("div", { hasText: "Export_&_Expected_Demo" }).first(), + ).toBeVisible(); + + // Expect test steps to bey loaded + await expect( + window.locator("div", { hasText: "test_min_max" }).first(), + ).toBeVisible(); + + // Run the sequence + await window.getByTestId(Selectors.runBtn).click(); + await window.waitForTimeout(10000); + + // Check the status + await expect(window.getByTestId(Selectors.globalStatusBadge)).toContainText( + "FAIL", + ); + await expect(window.getByTestId("status-test_min_max")).toContainText( + "PASS", + ); + }); +}); diff --git a/playwright-test/selectors.ts b/playwright-test/selectors.ts index ff2009122..a6edc2090 100644 --- a/playwright-test/selectors.ts +++ b/playwright-test/selectors.ts @@ -43,6 +43,7 @@ export enum Selectors { pytestBtn = "pytest-btn", newDropdown = "new-dropdown", importTestBtn = "import-test-button", + openSequenceGalleryBtn = "seq-gallery-btn", globalStatusBadge = "global-status-badge", newSeqModalNameInput = "new-seq-modal-name-input", newSeqModalDescInput = "new-seq-modal-desc-input", diff --git a/src/api/index.ts b/src/api/index.ts index a3314bb44..17c45fd13 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -148,11 +148,13 @@ export default { openAllFilesInFolder: ( folderPath: string, allowedExtensions: string[] = ["json"], + relativeToResources: boolean = false, ): Promise<{ filePath: string; fileContent: string }[] | undefined> => ipcRenderer.invoke( API.openAllFilesInFolderPicker, folderPath, allowedExtensions, + relativeToResources, ), getFileContent: (filepath: string): Promise => diff --git a/src/main/utils.ts b/src/main/utils.ts index f2b4b6185..35355d36f 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -190,7 +190,12 @@ export const openAllFilesInFolderPicker = ( _, folderPath: string, allowedExtensions: string[] = ["json"], + relativeToResources: boolean = false, ): { filePath: string; fileContent: string }[] | undefined => { + // Append the current working directory if the path is relative + if (relativeToResources) { + folderPath = join(process.resourcesPath, folderPath); + } // Return multiple files or all files with the allowed extensions if a folder is selected if (!fs.existsSync(folderPath) || !fs.lstatSync(folderPath).isDirectory()) { return undefined; diff --git a/src/renderer/assets/FlojoyTheme.ts b/src/renderer/assets/FlojoyTheme.ts index bc21231c8..81d2b32b2 100644 --- a/src/renderer/assets/FlojoyTheme.ts +++ b/src/renderer/assets/FlojoyTheme.ts @@ -12,7 +12,7 @@ export const flojoySyntaxTheme: SyntaxTheme = { background: "rgb(var(--color-modal))", }, "hljs-comment": { - color: "rgb(var(--foreground))", + color: "rgb(var(--color-accent4))", fontStyle: "italic", }, "hljs-quote": { diff --git a/src/renderer/hooks/useTestSequencerProject.ts b/src/renderer/hooks/useTestSequencerProject.ts index 14bc58454..ed2b08236 100644 --- a/src/renderer/hooks/useTestSequencerProject.ts +++ b/src/renderer/hooks/useTestSequencerProject.ts @@ -118,6 +118,61 @@ export const useImportSequences = () => { return handleImport; }; +export const useImportAllSequencesInFolder = () => { + const manager = usePrepareStateManager(); + const { isAdmin } = useWithPermission(); + + const handleImport = async (path: string, relative: boolean = false) => { + async function importSequences(): Promise> { + // Confirmation if admin + if (!isAdmin()) { + return err( + Error( + "Admin only, Connect to Flojoy Cloud and select a Test Profile", + ), + ); + } + + // Find .tjoy files from the profile + const result = await window.api.openAllFilesInFolder( + path, + ["tjoy"], + relative, + ); + if (result === undefined) { + return err(Error(`Failed to find the directory ${path}`)); + } + if (!result || result.length === 0) { + return err(Error("No .tjoy file found in the selected directory")); + } + + // Import them in the sequencer + await Promise.all( + result.map(async (res, idx) => { + const { filePath, fileContent } = res; + const result = await importSequence( + filePath, + fileContent, + manager, + idx !== 0, + ); + if (result.isErr()) return err(result.error); + }), + ); + + return ok(undefined); + } + + toastResultPromise(importSequences(), { + loading: `Importing Sequences...`, + success: () => `Sequences imported`, + error: (e) => `${e}`, + }); + }; + + return handleImport; +}; + export const useLoadTestProfile = () => { const manager = usePrepareStateManager(); const { isAdmin } = useWithPermission(); diff --git a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx index de4b98f31..249e9bb84 100644 --- a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx @@ -28,6 +28,7 @@ import { HoverCardTrigger, } from "@/renderer/components/ui/hover-card"; import _ from "lodash"; +import { SequencerGalleryModal } from "./modals/SequencerGalleryModal"; export function DesignBar() { const { setIsImportTestModalOpen, setIsCreateProjectModalOpen } = @@ -63,9 +64,14 @@ export function DesignBar() { }, [elems, sequences, cycleRuns]); const [displayTotal, setDisplayTotal] = useState(false); + const [isGalleryOpen, setIsGalleryOpen] = useState(false); return (
+
{isAdmin() && ( @@ -110,15 +116,23 @@ export function DesignBar() { Import Sequence - + setIsGalleryOpen(true)} + data-testid="seq-gallery-btn" + > - Sequence Gallery + Import Example + +
)} diff --git a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx index 402f8696c..d04b44300 100644 --- a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx @@ -267,7 +267,10 @@ export function TestTable() { header: () =>
Status
, cell: ({ row }) => { return row.original.type === "test" ? ( -
+
{typeof mapStatusToDisplay[row.original.status] === "function" ? mapStatusToDisplay[row.original.status](row.original.error) : mapStatusToDisplay[row.original.status]} diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx new file mode 100644 index 000000000..071f387ea --- /dev/null +++ b/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx @@ -0,0 +1,88 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/renderer/components/ui/dialog"; +import { ScrollArea } from "@/renderer/components/ui/scroll-area"; +import { Separator } from "@/renderer/components/ui/separator"; +import { Button } from "@/renderer/components/ui/button"; +import { useImportAllSequencesInFolder } from "@/renderer/hooks/useTestSequencerProject"; + +type SequencerGalleryModalProps = { + isGalleryOpen: boolean; + setIsGalleryOpen: (open: boolean) => void; +}; + +export const SequencerGalleryModal = ({ + isGalleryOpen, + setIsGalleryOpen, +}: SequencerGalleryModalProps) => { + const importSequence = useImportAllSequencesInFolder(); + + const handleSequenceLoad = (relativePath: string) => { + importSequence(relativePath, true); + setIsGalleryOpen(false); + }; + + const data = [ + { + title: "Creating Sequences with Conditional", + description: + "Learn how to create a simple sequence with conditional logic.", + dirPath: "examples/test-sequencer-conditional-example/", + }, + { + title: "Test Step with Expected and Exported Values", + description: + "Learn how to inject the minimum and maximum expected values into a test and export the result.", + dirPath: "examples/test-sequencer-expected-exported-example/", + }, + ]; + + return ( + + + + +
Sequence Gallery
+
+
+ + {data.map((seqExample) => ( + <> + +
+
+
+
+ {seqExample.title} +
+
+ {seqExample.description} +
+
+
+
+ +
+ + ))} + + +
+ ); +}; diff --git a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts index bb539dabe..fe5dcfd3f 100644 --- a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts +++ b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts @@ -229,6 +229,9 @@ async function saveToDisk( } async function installDeps(sequence: TestSequencerProject): Promise { + if (sequence.interpreter.requirementsPath === null) { + return true; + } const success = await window.api.poetryInstallRequirementsUserGroup( sequence.projectPath + sequence.interpreter.requirementsPath, ); diff --git a/src/renderer/types/test-sequencer.ts b/src/renderer/types/test-sequencer.ts index a711fdf3e..9041fe679 100644 --- a/src/renderer/types/test-sequencer.ts +++ b/src/renderer/types/test-sequencer.ts @@ -145,8 +145,8 @@ export type InterpreterType = z.infer; export const Interpreter = z.object({ type: InterpreterType, - path: z.union([z.null(), z.string()]), - requirementsPath: z.union([z.null(), z.string()]), + path: z.string().nullable(), + requirementsPath: z.string().nullable(), }); export type Interpreter = z.infer; From 5ad968d8bb6aff003e2b4dab2c7544a6aeeef2c8 Mon Sep 17 00:00:00 2001 From: Gui Date: Thu, 25 Apr 2024 13:50:10 -0700 Subject: [PATCH 03/10] Stu-346 placeholder test (#1159) * [stu-346-placeholder-test] feat: new Test type: "Placeholder" * [stu-346-placeholder-test] feat: Change the path and test type of placeholder * chore: all test step can be edited * fix: css overflow * chore: display error as toast * formatting * fix: Eslint * chore: python formatting * Update src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com> * chore: EsLint fix * chore: removing dead code * chore: fix Eslint * chore: Eslint fix * chore: formatting * fix: new return Result when test are duplicated doesn't close Modal. -> Fix test * [stu-346-placeholder-test] chore: placeholder test => Using form * [stu-346-placeholder-test] chore: Using object as input for better clarity * chore: formatting * chore: pruned useless abstraction * build: remove duplicated div * chore: formatting --------- Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com> --- captain/models/test_sequencer.py | 1 + .../utils/test_sequencer/run_test_sequence.py | 22 +++ .../13_create_test_sequence.spec.ts | 19 +-- src/renderer/hooks/useTestImport.ts | 58 ++++++- src/renderer/hooks/useTestSequencerState.ts | 56 ++++--- .../components/DesignBar.tsx | 32 ++-- .../components/data-table/TestTable.tsx | 74 ++++++++- .../components/modals/ChangeLinkedTest.tsx | 149 +++++++++++++++++ .../modals/CreatePlaceholderTestModal.tsx | 154 ++++++++++++++++++ .../components/modals/ImportTestModal.tsx | 4 +- .../utils/SequenceHandler.ts | 44 ++--- src/renderer/types/test-sequencer.ts | 8 +- 12 files changed, 527 insertions(+), 94 deletions(-) create mode 100644 src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx create mode 100644 src/renderer/routes/test_sequencer_panel/components/modals/CreatePlaceholderTestModal.tsx diff --git a/captain/models/test_sequencer.py b/captain/models/test_sequencer.py index b48eab714..0c02e7408 100644 --- a/captain/models/test_sequencer.py +++ b/captain/models/test_sequencer.py @@ -19,6 +19,7 @@ class TestTypes(StrEnum): python = "python" flojoy = "flojoy" matlab = "matlab" + placeholder = "placeholder" class StatusTypes(StrEnum): diff --git a/captain/utils/test_sequencer/run_test_sequence.py b/captain/utils/test_sequencer/run_test_sequence.py index 8d40f064b..1783f2e00 100644 --- a/captain/utils/test_sequencer/run_test_sequence.py +++ b/captain/utils/test_sequencer/run_test_sequence.py @@ -165,6 +165,27 @@ def _run_pytest(node: TestNode) -> Extract: ) +@_with_stream_test_result +def _run_placeholder(node: TestNode) -> Extract: + """ + @params file_path: path to the file + @returns: + bool: result of the test + float: time taken to execute the test + str: error message if any + """ + return ( + lambda _: None, + TestResult( + node, + False, + 0, + "Placeholder test not implemented", + utcnow_str(), + ), + ) + + def _eval_condition( result_dict: dict[str, TestResult], condition: str, identifiers: set[str] ): @@ -244,6 +265,7 @@ def get_next_children_from_context(context: Context): { TestTypes.python: (None, _run_python), TestTypes.pytest: (None, _run_pytest), + TestTypes.placeholder: (None, _run_placeholder), }, ), "conditional": ( diff --git a/playwright-test/13_create_test_sequence.spec.ts b/playwright-test/13_create_test_sequence.spec.ts index ae0471e56..13bb93178 100644 --- a/playwright-test/13_create_test_sequence.spec.ts +++ b/playwright-test/13_create_test_sequence.spec.ts @@ -67,25 +67,8 @@ test.describe("Create a test sequence", () => { await expect(window.getByTestId(Selectors.newDropdown)).toBeEnabled({ timeout: 15000, }); - await window.getByTestId(Selectors.newDropdown).click(); - await window.getByTestId(Selectors.importTestBtn).click(); - - // Select the fixture file - const customTestFile = join(__dirname, "fixtures/custom-sequences/test.py"); - await app.evaluate(async ({ dialog }, customTestFile) => { - dialog.showOpenDialog = () => - Promise.resolve({ filePaths: [customTestFile], canceled: false }); - }, customTestFile); - - // Click on Pytest test to open modal - await window.getByTestId(Selectors.pytestBtn).click(); - - // Expect test to be loaded - await expect( - window.locator("div", { hasText: "test_one" }).first(), - ).toBeVisible(); - // Ctrl/meta + p key shortcut to save the sequence + // Ctrl/meta + s key shortcut to save the sequence if (process.platform === "darwin") { await window.keyboard.press("Meta+s"); } else { diff --git a/src/renderer/hooks/useTestImport.ts b/src/renderer/hooks/useTestImport.ts index d5f7cdbde..8fdeddce7 100644 --- a/src/renderer/hooks/useTestImport.ts +++ b/src/renderer/hooks/useTestImport.ts @@ -17,16 +17,16 @@ function parseDiscoverContainer( settings: ImportTestSettings, ) { return map(data.response, (container) => { - const new_elem = createNewTest( - container.testName, - container.path, - settings.importType, - ); + const new_elem = createNewTest({ + name: container.testName, + path: container.path, + type: settings.importType, + }); return new_elem; }); } -export const useTestImport = () => { +export const useDiscoverAndImportTests = () => { const { addNewElems } = useDisplayedSequenceState(); const { openErrorModal } = useSequencerModalStore(); @@ -125,3 +125,49 @@ export const useTestImport = () => { return openFilePicker; }; + +export const useDiscoverPytestElements = () => { + const handleUserDepInstall = useCallback(async (depName: string) => { + const promise = () => window.api.poetryInstallDepUserGroup(depName); + toast.promise(promise, { + loading: `Installing ${depName}...`, + success: () => { + return `${depName} has been added.`; + }, + error: + "Could not install the library. Please consult the Dependency Manager in the settings.", + }); + }, []); + + async function getTests(path: string) { + const res = await discoverPytest(path, false); + if (res.isErr()) { + return err(res.error); + } + const data = res.value; + if (data.error) { + return err(Error(data.error)); + } + for (const lib of data.missingLibraries) { + toast.error(`Missing Python Library: ${lib}`, { + action: { + label: "Install", + onClick: () => { + handleUserDepInstall(lib); + }, + }, + }); + return err(Error("Please retry after installing the missing libraries.")); + } + const newElems = parseDiscoverContainer(data, { + importAsOneRef: false, + importType: "pytest", + }); + if (newElems.length === 0) { + return err(Error("No tests were found in the specified file.")); + } + return ok(newElems); + } + + return getTests; +}; diff --git a/src/renderer/hooks/useTestSequencerState.ts b/src/renderer/hooks/useTestSequencerState.ts index 4e461f697..cc91fc329 100644 --- a/src/renderer/hooks/useTestSequencerState.ts +++ b/src/renderer/hooks/useTestSequencerState.ts @@ -17,7 +17,7 @@ import useWithPermission from "@/renderer/hooks/useWithPermission"; import { useSequencerStore } from "@/renderer/stores/sequencer"; import { useShallow } from "zustand/react/shallow"; import { v4 as uuidv4 } from "uuid"; -import { Err, Ok, Result } from "neverthrow"; +import { Err, Result, err, ok } from "neverthrow"; import { verifyElementCompatibleWithSequence } from "@/renderer/routes/test_sequencer_panel/utils/SequenceHandler"; import { toast } from "sonner"; import { SendJsonMessage } from "react-use-websocket/dist/lib/types"; @@ -28,6 +28,7 @@ import { testSequenceStopRequest, } from "../routes/test_sequencer_panel/models/models"; import { produce } from "immer"; +import { z } from "zod"; // sync this with the definition of setElems export type SetElemsFn = { @@ -98,34 +99,38 @@ const validateElements = ( return !validators.some((validator) => !validator(elems), validators); }; -export function createNewTest( - name: string, - path: string, +export const NewTest = z.object({ + name: z.string(), + path: z.string(), type: TestType, - exportToCloud?: boolean, - id?: string, - groupId?: string, - minValue?: number, - maxValue?: number, - unit?: string, -): Test { + exportToCloud: z.boolean().optional(), + id: z.string().optional(), + groupId: z.string().optional(), + minValue: z.number().optional(), + maxValue: z.number().optional(), + unit: z.string().optional(), +}); + +export type NewTest = z.infer; + +export function createNewTest(test: NewTest): Test { const newTest: Test = { type: "test", - id: id || uuidv4(), - groupId: groupId || uuidv4(), - path: path, - testName: name, + id: test.id || uuidv4(), + groupId: test.groupId || uuidv4(), + path: test.path, + testName: test.name, runInParallel: false, - testType: type, + testType: test.type, status: "pending", completionTime: undefined, error: null, isSavedToCloud: false, - exportToCloud: exportToCloud === undefined ? true : exportToCloud, + exportToCloud: test.exportToCloud === undefined ? true : test.exportToCloud, createdAt: new Date().toISOString(), - minValue: minValue, - maxValue: maxValue, - unit: unit, + minValue: test.minValue, + maxValue: test.maxValue, + unit: test.unit, }; return newTest; } @@ -172,7 +177,7 @@ export function useDisplayedSequenceState() { p: | TestSequenceElement[] | ((elems: TestSequenceElement[]) => TestSequenceElement[]), - ) { + ): Result { let candidateElems: TestSequenceElement[]; // handle overloads @@ -189,7 +194,7 @@ export function useDisplayedSequenceState() { ); if (!res) { console.error("Validation failed"); - return; + return err(new Error("Validation failed")); } // PASS @@ -198,13 +203,14 @@ export function useDisplayedSequenceState() { // creates tree to send to backend setTree(createTestSequenceTree(candidateElems)); + return ok(undefined); } const setElemsWithPermissions = withPermissionCheck(setElems); async function AddNewElems( newElems: TestSequenceElement[], - ): Promise> { + ): Promise> { // Validate with project if (project !== null) { const result = await verifyElementCompatibleWithSequence( @@ -216,8 +222,8 @@ export function useDisplayedSequenceState() { } } // Add new elements - setElems((elems) => [...elems, ...newElems]); - return new Ok(null); + const result = setElems((elems) => [...elems, ...newElems]); + return result; } const addNewElemsWithPermissions = withPermissionCheck(AddNewElems); diff --git a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx index 249e9bb84..2c5dc7c74 100644 --- a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx @@ -28,6 +28,7 @@ import { HoverCardTrigger, } from "@/renderer/components/ui/hover-card"; import _ from "lodash"; +import { CreatePlaceholderTestModal } from "./modals/CreatePlaceholderTestModal"; import { SequencerGalleryModal } from "./modals/SequencerGalleryModal"; export function DesignBar() { @@ -64,10 +65,18 @@ export function DesignBar() { }, [elems, sequences, cycleRuns]); const [displayTotal, setDisplayTotal] = useState(false); + const [ + isCreatePlaceholderTestModalOpen, + setIsCreatePlaceholderTestModalOpen, + ] = useState(false); const [isGalleryOpen, setIsGalleryOpen] = useState(false); return (
+ New Test + { + setIsCreatePlaceholderTestModalOpen(true); + }} + data-testid="placeholder-test-button" + > + + New Placeholder + { setIsCreateProjectModalOpen(true); @@ -136,17 +157,6 @@ export function DesignBar() {
)} - {/* Comming soon - - */} {sequences.length <= 1 ? ( diff --git a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx index d04b44300..1785dbeeb 100644 --- a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx @@ -58,6 +58,8 @@ import useWithPermission from "@/renderer/hooks/useWithPermission"; import { useSequencerModalStore } from "@/renderer/stores/modal"; import { WriteMinMaxModal } from "@/renderer/routes/test_sequencer_panel/components/modals/WriteMinMaxModal"; import { toast } from "sonner"; +import { ChangeLinkedTestModal } from "../modals/ChangeLinkedTest"; +import { ImportType } from "../modals/ImportTestModal"; import { useSequencerStore } from "@/renderer/stores/sequencer"; import { useShallow } from "zustand/react/shallow"; @@ -349,6 +351,8 @@ export function TestTable() { }, }); + // Remove ---------------------- + const handleClickRemoveTests = () => { onRemoveTest(map(Object.keys(rowSelection), (idxStr) => parseInt(idxStr))); setRowSelection([]); @@ -366,6 +370,8 @@ export function TestTable() { }); }; + // Conditional ------------------ + const addConditionalAfterIdx = useRef(-1); const [showWriteConditionalModal, setShowWriteConditionalModal] = @@ -411,6 +417,8 @@ export function TestTable() { setShowWriteConditionalModal(true); }; + // Edit MinMax ------------------ + const onClickWriteMinMax = (idx: number) => { writeMinMaxForIdx.current = idx; setShowWriteMinMaxModal(true); @@ -434,6 +442,26 @@ export function TestTable() { }); }; + // Change linked test ------------ + + const [openLinkedTestModal, setOpenLinkedTestModal] = useState(false); + const testRef = useRef(-1); + + const handleChangeLinkedTest = (newPath: string, testType: ImportType) => { + setElems((data) => { + const new_data = [...data]; + const test = new_data[testRef.current] as Test; + new_data[testRef.current] = { + ...test, + path: newPath, + testType: testType, + }; + return new_data; + }); + }; + + // Context Menu ------------------ + const getSpecificContextMenuItems = (row: Row) => { switch (row.original.type) { case "test": @@ -470,6 +498,11 @@ export function TestTable() { setModalOpen={setShowWriteMinMaxModal} handleWrite={onSubmitWriteMinMax} /> + {openPyTestFileModal && ( Add Conditional + {row.original.type === "test" && + row.original.testType !== "placeholder" && ( + <> + + { + setOpenPyTestFileModal(true); + setTestToDisplay(row.original as Test); + }} + > + Consult Code + + + )} + {row.original.type === "test" && + row.original.testType === "placeholder" && ( + <> + + + Not Linked to any code + + + )} {row.original.type === "test" && ( <> - - { - setOpenPyTestFileModal(true); - setTestToDisplay(row.original as Test); - }} - > - Consult Code - { @@ -617,6 +664,15 @@ export function TestTable() { > Edit Expected Value + { + setOpenLinkedTestModal(true); + testRef.current = parseInt(row.id); + setTestToDisplay(row.original as Test); + }} + > + Change executable + )} diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx new file mode 100644 index 000000000..a39362c0b --- /dev/null +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx @@ -0,0 +1,149 @@ +import { Button } from "@/renderer/components/ui/button"; +import { Dialog, DialogContent } from "@/renderer/components/ui/dialog"; +import { Separator } from "@/renderer/components/ui/separator"; +import { useAppStore } from "@/renderer/stores/app"; +import { ExternalLinkIcon } from "lucide-react"; +import { useState } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { ImportType } from "./ImportTestModal"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/renderer/components/ui/select"; +import { useDiscoverPytestElements } from "@/renderer/hooks/useTestImport"; +import { TestSequenceElement } from "@/renderer/types/test-sequencer"; +import { toast } from "sonner"; + +export const ChangeLinkedTestModal = ({ + isModalOpen, + setModalOpen, + handleSubmit, +}: { + isModalOpen: boolean; + setModalOpen: (value: boolean) => void; + handleSubmit: (path: string, testType: ImportType) => void; +}) => { + const [availableTests, setAvailableTests] = useState( + [], + ); + const [selectedPath, setSelectedPath] = useState(""); + + const { setIsDepManagerModalOpen } = useAppStore( + useShallow((state) => ({ + setIsDepManagerModalOpen: state.setIsDepManagerModalOpen, + })), + ); + + const discoverPytestElement = useDiscoverPytestElements(); + + const handleDiscoverPytestElements = async (filePath: string) => { + const result = await discoverPytestElement(filePath); + if (result.isOk()) { + setAvailableTests(result.value); + if (result.value.length > 0) { + setSelectedPath(result.value[0].path); + } + } else { + console.error(result.error); + } + }; + + const handleFilePicker = async () => { + const res = await window.api.openTestPicker(); + if (!res) return; + if (res.filePath) { + await handleDiscoverPytestElements(res.filePath); + } + }; + + const handleSubmitByType = (testType: ImportType) => { + if (testType === "pytest") { + if (selectedPath === "") { + toast.error("Please select a test to link to"); + } + handleSubmit(selectedPath, testType); + } else { + window.api.openTestPicker().then((result) => { + if (!result) { + return; + } + const { filePath } = result; + handleSubmit(filePath, testType); + }); + } + setModalOpen(false); + }; + + return ( + + +

+ Select a test to link to +

+

Pytest

+
+ +
+ +
+
+ + +

Python Script

+ +
+
+
+ +
+
+ +
+ ); +}; diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/CreatePlaceholderTestModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/CreatePlaceholderTestModal.tsx new file mode 100644 index 000000000..b92b68137 --- /dev/null +++ b/src/renderer/routes/test_sequencer_panel/components/modals/CreatePlaceholderTestModal.tsx @@ -0,0 +1,154 @@ +import { Button } from "@/renderer/components/ui/button"; +import { Dialog, DialogContent } from "@/renderer/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/renderer/components/ui/form"; +import { Input } from "@/renderer/components/ui/input"; +import { + createNewTest, + useDisplayedSequenceState, +} from "@/renderer/hooks/useTestSequencerState"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const formSchema = z.object({ + name: z.string().min(1).regex(/\S/), + min: z.coerce.number().optional(), + max: z.coerce.number().optional(), + unit: z.string().optional(), +}); + +export const CreatePlaceholderTestModal = ({ + isModalOpen, + setModalOpen, +}: { + isModalOpen: boolean; + setModalOpen: (value: boolean) => void; +}) => { + const { addNewElems } = useDisplayedSequenceState(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + unit: undefined, + min: undefined, + max: undefined, + }, + }); + + async function onSubmit(values: z.infer) { + const res = await addNewElems([ + createNewTest({ + name: values.name, + path: "", + type: "placeholder", + exportToCloud: false, + minValue: values.min, + maxValue: values.max, + unit: values.unit, + }), + ]); + if (res.isErr()) { + return; + } + setModalOpen(false); + } + + return ( + + +

+ Create a placeholder test step +

+

+ {" "} + This will create a new test step with the given name and expected + value without being link to any code. The code can be added later. +

+ +
+ + ( + + Test Name + + + + + + )} + /> + +
+

+ Expected Value +

+
+
+ ( + + Minimum + + + + + + )} + /> +
+ +
+ ( + + Maximun + + + + + + )} + /> +
+ +
+ ( + + Displayed Unit + + + + + + )} + /> +
+
+
+ + + +
+
+ ); +}; diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx index dfea5358a..4df0c7234 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx @@ -2,7 +2,7 @@ import { Button } from "@/renderer/components/ui/button"; import { Checkbox } from "@/renderer/components/ui/checkbox"; import { Dialog, DialogContent } from "@/renderer/components/ui/dialog"; import { Separator } from "@/renderer/components/ui/separator"; -import { useTestImport } from "@/renderer/hooks/useTestImport"; +import { useDiscoverAndImportTests } from "@/renderer/hooks/useTestImport"; import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState"; import { useAppStore } from "@/renderer/stores/app"; import { useSequencerModalStore } from "@/renderer/stores/modal"; @@ -28,7 +28,7 @@ export const ImportTestModal = () => { })), ); - const openFilePicker = useTestImport(); + const openFilePicker = useDiscoverAndImportTests(); const { setIsLocked } = useDisplayedSequenceState(); const handleImportTest = (importType: ImportType) => { diff --git a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts index fe5dcfd3f..d2924237e 100644 --- a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts +++ b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts @@ -275,17 +275,17 @@ async function createExportableSequenceElementsFromTestSequencerElements( } const elements = [...elems].map((elem) => { return elem.type === "test" - ? createNewTest( - removeBaseFolderFromName(elem.testName, baseFolder), - elem.path.replaceAll(baseFolder, ""), - elem.testType, - elem.exportToCloud, - elem.id, - elem.groupId, - elem.minValue, - elem.maxValue, - elem.unit, - ) + ? createNewTest({ + name: removeBaseFolderFromName(elem.testName, baseFolder), + path: elem.path.replaceAll(baseFolder, ""), + type: elem.testType, + exportToCloud: elem.exportToCloud, + id: elem.id, + groupId: elem.groupId, + minValue: elem.minValue, + maxValue: elem.maxValue, + unit: elem.unit, + }) : { ...elem, condition: elem.condition.replaceAll(baseFolder, ""), @@ -301,17 +301,17 @@ async function createTestSequencerElementsFromSequenceElements( ): Promise> { const elements: TestSequenceElement[] = [...sequence.elems].map((elem) => { return elem.type === "test" - ? createNewTest( - removeBaseFolderFromName(elem.testName, baseFolder), - baseFolder + elem.path, - elem.testType, - elem.exportToCloud, - elem.id, - elem.groupId, - elem.minValue, - elem.maxValue, - elem.unit, - ) + ? createNewTest({ + name: removeBaseFolderFromName(elem.testName, baseFolder), + path: baseFolder + elem.path, + type: elem.testType, + exportToCloud: elem.exportToCloud, + id: elem.id, + groupId: elem.groupId, + minValue: elem.minValue, + maxValue: elem.maxValue, + unit: elem.unit, + }) : { ...elem, }; diff --git a/src/renderer/types/test-sequencer.ts b/src/renderer/types/test-sequencer.ts index 9041fe679..9d4b8a2a5 100644 --- a/src/renderer/types/test-sequencer.ts +++ b/src/renderer/types/test-sequencer.ts @@ -4,7 +4,13 @@ export type LockedContextType = { isLocked: boolean; }; -export const TestType = z.enum(["pytest", "python", "flojoy", "matlab"]); +export const TestType = z.enum([ + "pytest", + "python", + "flojoy", + "matlab", + "placeholder", +]); export type TestType = z.infer; export const ResultType = z.enum(["pass", "fail", "aborted"]); From 0ff0c5c7eae8648f9076a53b51ff86eacf7bc281 Mon Sep 17 00:00:00 2001 From: Gui Date: Thu, 25 Apr 2024 14:30:52 -0700 Subject: [PATCH 04/10] feat(cloud panel): new refresh button (#1174) * feat(cloud panel): new refresh button * chore(cloud panel): formatting --------- Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com> --- .../components/CloudPanel.tsx | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/renderer/routes/test_sequencer_panel/components/CloudPanel.tsx b/src/renderer/routes/test_sequencer_panel/components/CloudPanel.tsx index 0d6f154d0..aedffc24e 100644 --- a/src/renderer/routes/test_sequencer_panel/components/CloudPanel.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/CloudPanel.tsx @@ -39,6 +39,8 @@ import { getGlobalStatus } from "./DesignBar"; import { useSequencerStore } from "@/renderer/stores/sequencer"; import { Autocomplete } from "@/renderer/components/ui/autocomplete"; import { useLoadTestProfile } from "@/renderer/hooks/useTestSequencerProject"; +import { RefreshCcw } from "lucide-react"; +import { Separator } from "@/renderer/components/ui/separator"; export function CloudPanel() { const queryClient = useQueryClient(); @@ -346,12 +348,6 @@ export function CloudPanel() { {projectsQuery.data.length === 0 && (
No Test Profile found -
)} {projectsQuery.data.map((option) => ( @@ -360,6 +356,20 @@ export function CloudPanel() { {option.label} ))} + + @@ -375,13 +385,10 @@ export function CloudPanel() { {stationsQuery.data.length === 0 && (
No station found - {stationsQuery.isFetching ? ( -

Loading...

+ {projectsQuery.data.length === 0 ? ( +

Select a test profile to load the available stations.

) : ( -

- {" "} - Select a test profile to load the available stations{" "} -

+

No station found for the selected test profile.

)}
)} @@ -390,6 +397,20 @@ export function CloudPanel() { {option.label} ))} + +
From 05239a51528357f2ecc0504e10c33c2e78aa1a7a Mon Sep 17 00:00:00 2001 From: Gui Date: Thu, 25 Apr 2024 14:45:19 -0700 Subject: [PATCH 05/10] Robot Framework Support (#1173) * [stu-346-placeholder-test] feat: new Test type: "Placeholder" * [stu-346-placeholder-test] feat: Change the path and test type of placeholder * chore: all test step can be edited * fix: css overflow * chore: display error as toast * formatting * fix: Eslint * chore: python formatting * Update src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com> * chore: wip * chore: EsLint fix * chore: removing dead code * chore: fix Eslint * chore: Eslint fix * chore: formatting * chore: Sequence Gallery UI * fix: new return Result when test are duplicated doesn't close Modal. -> Fix test * [sequencer-gallery] chore: Working import demo sequence * [sequencer-gallery] Export & Expected Demo * [sequencer-gallery] ui: removed weird yellow and added better comment in example * chore: formatting * fix: Eslint * [stu-346-placeholder-test] chore: placeholder test => Using form * [stu-346-placeholder-test] chore: Using object as input for better clarity * chore: formatting * [sequencer-gallery] chore: using test profile workflow to load example * [sequencer-gallery] chore: remove "use" prefix as react thinks it's a hook * [sequencer-gallery] chore: bundle example with app * [sequencer-gallery] fix: space in sequence name * chore: formatting * [sequencer-gallery] chore: using process.resourcesPath * [sequencer-gallery] chore: adding CI test for Gallery + testing injected min & max * chore: formatting * chore(robot): discoverer * chore(robot): Robot import frontend - experimental * chore(robot): robot framework => import in batches * chore(robot): handle Robot framework in change executable * chore(robot): formatting * chore(robot): sample code for robot example * chore(robot): Robot Framework Sequence Example * chore(linter): fix E711 * chore(robot): cleanup * chore(robot): formatting * chore(robot): adding @itsjoeoui pattern matching recommendation * chore(robot): exposing discoverable test type as global type * chore(robot): formatting * chore(robot): format * chore(robot): fix esLint * chore(robot): fix esLint part #2 --------- Co-authored-by: Jeff Zhang <47371088+39bytes@users.noreply.github.com> --- captain/models/test_sequencer.py | 2 + captain/routes/test_sequence.py | 29 +++++- captain/utils/pytest/discover_tests.py | 27 ++++++ .../utils/test_sequencer/run_test_sequence.py | 39 ++++++++ .../Robot_Sequence.tjoy | 1 + .../TestExample.robot | 19 ++++ .../calculate.py | 5 + .../flojoy_requirements.txt | 1 + poetry.lock | 16 +++- pyproject.toml | 2 + src/main/ipc-main-handlers.ts | 2 +- src/renderer/hooks/useTestImport.ts | 63 ++++++++---- src/renderer/hooks/useTestSequencerState.ts | 2 + src/renderer/lib/api.ts | 11 ++- .../components/DesignBar.tsx | 13 ++- .../components/data-table/TestTable.tsx | 7 +- .../components/modals/ChangeLinkedTest.tsx | 96 +++++++++++-------- .../components/modals/ImportTestModal.tsx | 10 +- .../modals/SequencerGalleryModal.tsx | 6 ++ src/renderer/types/test-sequencer.ts | 4 +- 20 files changed, 278 insertions(+), 77 deletions(-) create mode 100644 examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy create mode 100644 examples/test-sequencer-robot-framework-example/TestExample.robot create mode 100644 examples/test-sequencer-robot-framework-example/calculate.py create mode 100644 examples/test-sequencer-robot-framework-example/flojoy_requirements.txt diff --git a/captain/models/test_sequencer.py b/captain/models/test_sequencer.py index 0c02e7408..d0239b60a 100644 --- a/captain/models/test_sequencer.py +++ b/captain/models/test_sequencer.py @@ -20,6 +20,7 @@ class TestTypes(StrEnum): flojoy = "flojoy" matlab = "matlab" placeholder = "placeholder" + robotframework = "robotframework" class StatusTypes(StrEnum): @@ -66,6 +67,7 @@ class Test(BaseModel): max_value: Optional[float] = Field(None, alias="maxValue") measured_value: Optional[float] = Field(None, alias="measuredValue") unit: Optional[str] = Field(None, alias="unit") + args: Optional[List[str]] = Field(None, alias="args") class Role(StrEnum): diff --git a/captain/routes/test_sequence.py b/captain/routes/test_sequence.py index 51b2fa216..84971ea4b 100644 --- a/captain/routes/test_sequence.py +++ b/captain/routes/test_sequence.py @@ -3,7 +3,10 @@ import pydantic from captain.models.pytest.pytest_models import TestDiscoverContainer from captain.models.test_sequencer import TestSequenceRun -from captain.utils.pytest.discover_tests import discover_pytest_file +from captain.utils.pytest.discover_tests import ( + discover_pytest_file, + discover_robot_file, +) from captain.utils.config import ts_manager from captain.utils.test_sequencer.handle_data import handle_data from captain.utils.logger import logger @@ -34,13 +37,13 @@ async def websocket_endpoint(websocket: WebSocket, socket_id: str): logger.info(f"Client {socket_id} is disconnected") -class DiscoverPytestParams(BaseModel): +class DiscoverParams(BaseModel): path: str one_file: bool = Field(..., alias="oneFile") -@router.get("/discover-pytest/") -async def discover_pytest(params: DiscoverPytestParams = Depends()): +@router.get("/discover/pytest/") +async def discover_pytest(params: DiscoverParams = Depends()): path = params.path one_file = params.one_file return_val, missing_lib, errors = [], [], [] # For passing info between threads @@ -55,3 +58,21 @@ async def discover_pytest(params: DiscoverPytestParams = Depends()): missing_libraries=missing_lib, error=errors[0] if len(errors) > 0 else None, ) + + +@router.get("/discover/robot/") +async def discover_robot(params: DiscoverParams = Depends()): + path = params.path + one_file = params.one_file + return_val, errors = [], [] # For passing info between threads + thread = Thread( + target=discover_robot_file, + args=(path, one_file, return_val, errors), + ) + thread.start() + thread.join() + return TestDiscoverContainer( + response=return_val, + missing_libraries=[], + error=errors[0] if len(errors) > 0 else None, + ) diff --git a/captain/utils/pytest/discover_tests.py b/captain/utils/pytest/discover_tests.py index c6b183306..b84c16b60 100644 --- a/captain/utils/pytest/discover_tests.py +++ b/captain/utils/pytest/discover_tests.py @@ -1,3 +1,4 @@ +import logging import os from captain.utils.logger import logger from typing import List, Union @@ -12,6 +13,7 @@ from captain.utils.import_utils import unload_module import re import pathlib +from robot.running.builder import TestSuiteBuilder def extract_error(report: RootModel): @@ -84,3 +86,28 @@ def dfs( ) else: return_val.extend(test_list) + + +def discover_robot_file(path: str, one_file: bool, return_val: list, errors: list): + try: + builder = TestSuiteBuilder() + suite = builder.build(path) + logging.info( + f"Suite: {suite} - suites in it: {suite.suites} - tests in it: {suite.tests}" + ) + if one_file: + return_val.append( + TestDiscoveryResponse( + test_name=suite.full_name, path=pathlib.Path(path).as_posix() + ) + ) + else: + for test in suite.tests: + return_val.append( + TestDiscoveryResponse( + test_name=test.full_name, path=pathlib.Path(path).as_posix() + ) + ) + + except Exception as e: + errors.append(str(e)) diff --git a/captain/utils/test_sequencer/run_test_sequence.py b/captain/utils/test_sequencer/run_test_sequence.py index 1783f2e00..c17221c43 100644 --- a/captain/utils/test_sequencer/run_test_sequence.py +++ b/captain/utils/test_sequencer/run_test_sequence.py @@ -186,6 +186,44 @@ def _run_placeholder(node: TestNode) -> Extract: ) +@_with_stream_test_result +def _run_robotframework(node: TestNode) -> Extract: + """ + runs python file. + @params file_path: path to the file + @returns: + bool: result of the test + float: time taken to execute the test + str: error message if any + """ + start_time = time.time() + logger.info(f"[Robot Framework Runner] Running {node.path}") + if node.args is not None: + cmd = ["robot", "--test", *node.args, node.path] + else: + cmd = ["robot", node.path] + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + logger.info(f"[Robot Framework Runner] Running {result}") + end_time = time.time() + if result.returncode == 0: + is_pass = True + else: + logger.info( + f"TEST {node.path} FAILED:\nSTDOUT: {result.stdout.decode()}\nSTDERR: {result.stderr.decode()}" + ) + is_pass = False + return ( + lambda _: None, + TestResult( + node, + is_pass, + end_time - start_time, + result.stderr.decode() if not is_pass else None, + utcnow_str(), + ), + ) + + def _eval_condition( result_dict: dict[str, TestResult], condition: str, identifiers: set[str] ): @@ -266,6 +304,7 @@ def get_next_children_from_context(context: Context): TestTypes.python: (None, _run_python), TestTypes.pytest: (None, _run_pytest), TestTypes.placeholder: (None, _run_placeholder), + TestTypes.robotframework: (None, _run_robotframework), }, ), "conditional": ( diff --git a/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy new file mode 100644 index 000000000..4c6042803 --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy @@ -0,0 +1 @@ +{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"35d63b42-c0b9-4a2e-900e-e327404a8c41","groupId":"40216fc8-785a-4610-913e-90bd54f157af","path":"TestExample.robot","testName":"TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z"},{"type":"test","id":"36374991-c721-40c6-8a35-68a996fe6ed4","groupId":"d8a898b4-a012-4177-9f9a-d07c3ab5f84e","path":"TestExample.robot","testName":"TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z","minValue":2,"maxValue":4.2,"unit":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}} \ No newline at end of file diff --git a/examples/test-sequencer-robot-framework-example/TestExample.robot b/examples/test-sequencer-robot-framework-example/TestExample.robot new file mode 100644 index 000000000..935164cb1 --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/TestExample.robot @@ -0,0 +1,19 @@ +*** Settings *** +Library flojoy_cloud.test_sequencer +Library calculate.py + +*** Test Cases *** +TEST EXPORT + ${result} Calculate 3 + 1 + # Export the `result` so it's display in the sequencer + # + this value will be upload to Flojoy Cloud + Export ${result} + Should Not Be Equal 4 ${result} + +TEST ASSERT + ${result} Calculate 1 + 1 + Export ${result} + # Call the `is_in_range` from the test_sequencer + ${ok} Is In Range ${result} + Should Be True ${ok} + diff --git a/examples/test-sequencer-robot-framework-example/calculate.py b/examples/test-sequencer-robot-framework-example/calculate.py new file mode 100644 index 000000000..4df04fb4e --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/calculate.py @@ -0,0 +1,5 @@ +def calculate(term): + if term == "": + return 0 + else: + return eval(term) diff --git a/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt b/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt new file mode 100644 index 000000000..f1e0ac08a --- /dev/null +++ b/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt @@ -0,0 +1 @@ +psutil==5.9.8 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 9c377d09a..dd3dabbc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -2533,7 +2533,6 @@ files = [ {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] @@ -4047,6 +4046,17 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "robotframework" +version = "7.0" +description = "Generic automation framework for acceptance testing and robotic process automation (RPA)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "robotframework-7.0-py3-none-any.whl", hash = "sha256:865f427c4e4ec8c0b71a24dedbdad6668adfecc9fce04d77d02e1b8e54b77f41"}, + {file = "robotframework-7.0.zip", hash = "sha256:04623f758346c917db182e17591ffa474090560c02ed5a64343902e72b7b4bd5"}, +] + [[package]] name = "rpds-py" version = "0.18.0" @@ -5421,4 +5431,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "c90e45542824c888f1fc21254f20d704111fe23fde01d1200109427233736f49" +content-hash = "4cbeb9ef3f59318e9d5ca01d662c776c37627aa04dd98157c36d26bf87b43356" diff --git a/pyproject.toml b/pyproject.toml index 1cb7483e4..d8646857c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ nimodinst = "^1.4.7" flojoy-cloud = "^0.2.1" bcrypt = "^4.1.2" tinymovr = "^1.6.5" +robotframework = "^7.0" [tool.poetry.group.blocks.dependencies] scikit-image = "^0.22.0" @@ -75,6 +76,7 @@ optional = true scikit-learn = "^1.3.2" [tool.poetry.group.user.dependencies] +psutil = "5.9.8" [build-system] requires = ["poetry-core"] diff --git a/src/main/ipc-main-handlers.ts b/src/main/ipc-main-handlers.ts index 59af85398..6b252b129 100644 --- a/src/main/ipc-main-handlers.ts +++ b/src/main/ipc-main-handlers.ts @@ -154,7 +154,7 @@ export const registerIpcMainHandlers = () => { ipcMain.handle( API.openTestPicker, - async (e) => await openFilePicker(e, "Test", ["json", "py"]), + async (e) => await openFilePicker(e, "Test", ["json", "py", "robot"]), ); ipcMain.handle(API.openEditorWindow, (_, filepath) => { createEditorWindow(filepath); diff --git a/src/renderer/hooks/useTestImport.ts b/src/renderer/hooks/useTestImport.ts index 8fdeddce7..deefc0a0a 100644 --- a/src/renderer/hooks/useTestImport.ts +++ b/src/renderer/hooks/useTestImport.ts @@ -1,16 +1,22 @@ -import { TestDiscoverContainer } from "@/renderer/types/test-sequencer"; +import { + TestDiscoverContainer, +} from "@/renderer/types/test-sequencer"; import { createNewTest, useDisplayedSequenceState, } from "./useTestSequencerState"; import { map } from "lodash"; -import { ImportTestSettings } from "@/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal"; +import { + ImportTestSettings, + discoverableTestTypes as DiscoverableTestTypes, +} from "@/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal"; import { toast } from "sonner"; import { useCallback } from "react"; -import { discoverPytest } from "@/renderer/lib/api"; +import { discoverPytest, discoverRobot } from "@/renderer/lib/api"; import { useSequencerModalStore } from "../stores/modal"; import { toastResultPromise } from "../utils/report-error"; import { Result, err, ok } from "neverthrow"; +import { match } from "ts-pattern"; function parseDiscoverContainer( data: TestDiscoverContainer, @@ -21,6 +27,10 @@ function parseDiscoverContainer( name: container.testName, path: container.path, type: settings.importType, + args: + settings.importType === "robotframework" && !settings.importAsOneRef + ? [container.testName] + : undefined, }); return new_elem; }); @@ -47,20 +57,27 @@ export const useDiscoverAndImportTests = () => { settings: ImportTestSettings, setModalOpen: (val: boolean) => void, ): Promise> { - let data: TestDiscoverContainer; - if (settings.importType == "python") { - data = { - response: [{ testName: path, path: path }], - missingLibraries: [], - error: null, - }; - } else { - const res = await discoverPytest(path, settings.importAsOneRef); - if (res.isErr()) { - return err(res.error); - } - data = res.value; + const dataResponse = await match(settings.importType) + .with("python", async () => { + return ok({ + response: [{ testName: path, path: path }], + missingLibraries: [], + error: null, + }); + }) + .with( + "pytest", + async () => await discoverPytest(path, settings.importAsOneRef), + ) + .with( + "robotframework", + async () => await discoverRobot(path, settings.importAsOneRef), + ) + .exhaustive(); + if (dataResponse.isErr()) { + return err(dataResponse.error); } + const data = dataResponse.value; if (data.error) { return err(Error(data.error)); } @@ -126,7 +143,7 @@ export const useDiscoverAndImportTests = () => { return openFilePicker; }; -export const useDiscoverPytestElements = () => { +export const useDiscoverElements = () => { const handleUserDepInstall = useCallback(async (depName: string) => { const promise = () => window.api.poetryInstallDepUserGroup(depName); toast.promise(promise, { @@ -140,7 +157,15 @@ export const useDiscoverPytestElements = () => { }, []); async function getTests(path: string) { - const res = await discoverPytest(path, false); + let res: Result; + let type: DiscoverableTestTypes; + if (path.endsWith(".robot")) { + res = await discoverRobot(path, false); + type = "robotframework"; + } else { + res = await discoverPytest(path, false); + type = "pytest"; + } if (res.isErr()) { return err(res.error); } @@ -161,7 +186,7 @@ export const useDiscoverPytestElements = () => { } const newElems = parseDiscoverContainer(data, { importAsOneRef: false, - importType: "pytest", + importType: type, }); if (newElems.length === 0) { return err(Error("No tests were found in the specified file.")); diff --git a/src/renderer/hooks/useTestSequencerState.ts b/src/renderer/hooks/useTestSequencerState.ts index cc91fc329..570d9f48f 100644 --- a/src/renderer/hooks/useTestSequencerState.ts +++ b/src/renderer/hooks/useTestSequencerState.ts @@ -109,6 +109,7 @@ export const NewTest = z.object({ minValue: z.number().optional(), maxValue: z.number().optional(), unit: z.string().optional(), + args: z.string().array().optional(), }); export type NewTest = z.infer; @@ -131,6 +132,7 @@ export function createNewTest(test: NewTest): Test { minValue: test.minValue, maxValue: test.maxValue, unit: test.unit, + args: test.args, }; return newTest; } diff --git a/src/renderer/lib/api.ts b/src/renderer/lib/api.ts index 2a60fad49..42ad21952 100644 --- a/src/renderer/lib/api.ts +++ b/src/renderer/lib/api.ts @@ -139,7 +139,16 @@ export const setLogLevel = async (level: string) => { }; export const discoverPytest = async (path: string, oneFile: boolean) => { - return get("discover-pytest", TestDiscoverContainer, { + return get("discover/pytest", TestDiscoverContainer, { + searchParams: { + path, + oneFile, + }, + }); +}; + +export const discoverRobot = async (path: string, oneFile: boolean) => { + return get("discover/robot", TestDiscoverContainer, { searchParams: { path, oneFile, diff --git a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx index 2c5dc7c74..7a560938e 100644 --- a/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx @@ -2,7 +2,14 @@ import { useSequencerModalStore } from "@/renderer/stores/modal"; import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState"; import { Button } from "@/renderer/components/ui/button"; import { ACTIONS_HEIGHT } from "@/renderer/routes/common/Layout"; -import { FlaskConical, Import, LayoutGrid, Plus, Route } from "lucide-react"; +import { + FlaskConical, + Import, + LayoutGrid, + Plus, + Route, + TestTube, +} from "lucide-react"; import { StatusType, Test, @@ -65,11 +72,11 @@ export function DesignBar() { }, [elems, sequences, cycleRuns]); const [displayTotal, setDisplayTotal] = useState(false); + const [isGalleryOpen, setIsGalleryOpen] = useState(false); const [ isCreatePlaceholderTestModalOpen, setIsCreatePlaceholderTestModalOpen, ] = useState(false); - const [isGalleryOpen, setIsGalleryOpen] = useState(false); return (
@@ -103,7 +110,7 @@ export function DesignBar() { }} data-testid="import-test-button" > - diff --git a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx index 1785dbeeb..32a341b0c 100644 --- a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx @@ -447,7 +447,11 @@ export function TestTable() { const [openLinkedTestModal, setOpenLinkedTestModal] = useState(false); const testRef = useRef(-1); - const handleChangeLinkedTest = (newPath: string, testType: ImportType) => { + const handleChangeLinkedTest = ( + newPath: string, + testType: ImportType, + args: string[] | undefined, + ) => { setElems((data) => { const new_data = [...data]; const test = new_data[testRef.current] as Test; @@ -455,6 +459,7 @@ export function TestTable() { ...test, path: newPath, testType: testType, + args: args, }; return new_data; }); diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx index a39362c0b..55862aef2 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ChangeLinkedTest.tsx @@ -15,7 +15,7 @@ import { SelectTrigger, SelectValue, } from "@/renderer/components/ui/select"; -import { useDiscoverPytestElements } from "@/renderer/hooks/useTestImport"; +import { useDiscoverElements } from "@/renderer/hooks/useTestImport"; import { TestSequenceElement } from "@/renderer/types/test-sequencer"; import { toast } from "sonner"; @@ -26,29 +26,33 @@ export const ChangeLinkedTestModal = ({ }: { isModalOpen: boolean; setModalOpen: (value: boolean) => void; - handleSubmit: (path: string, testType: ImportType) => void; + handleSubmit: ( + path: string, + testType: ImportType, + args: string[] | undefined, + ) => void; }) => { const [availableTests, setAvailableTests] = useState( [], ); - const [selectedPath, setSelectedPath] = useState(""); - + const [selectedTestName, setSelectedPath] = useState(""); const { setIsDepManagerModalOpen } = useAppStore( useShallow((state) => ({ setIsDepManagerModalOpen: state.setIsDepManagerModalOpen, })), ); - const discoverPytestElement = useDiscoverPytestElements(); + const discoverElement = useDiscoverElements(); - const handleDiscoverPytestElements = async (filePath: string) => { - const result = await discoverPytestElement(filePath); + const handleDiscoverElements = async (filePath: string) => { + const result = await discoverElement(filePath); if (result.isOk()) { setAvailableTests(result.value); if (result.value.length > 0) { setSelectedPath(result.value[0].path); } } else { + toast.error(`Failed to discover tests: ${result.error}`); console.error(result.error); } }; @@ -57,35 +61,46 @@ export const ChangeLinkedTestModal = ({ const res = await window.api.openTestPicker(); if (!res) return; if (res.filePath) { - await handleDiscoverPytestElements(res.filePath); + await handleDiscoverElements(res.filePath); } }; - const handleSubmitByType = (testType: ImportType) => { - if (testType === "pytest") { - if (selectedPath === "") { - toast.error("Please select a test to link to"); - } - handleSubmit(selectedPath, testType); - } else { - window.api.openTestPicker().then((result) => { - if (!result) { - return; - } - const { filePath } = result; - handleSubmit(filePath, testType); - }); + const handleSubmitIndividualTest = () => { + if (selectedTestName === "") { + toast.error("Please select a test to link to"); + } + const test = availableTests.find( + (test) => test.type === "test" && test.testName === selectedTestName, + ); + if (test?.type !== "test" || test.testType === "placeholder") { + return; } + handleSubmit(test.path, test.testType, test.args); setModalOpen(false); }; + const handleSubmitPythonScript = () => { + window.api.openTestPicker().then((result) => { + if (!result) { + return; + } + const { filePath } = result; + if (!filePath.endsWith(".py")) { + toast.error("Please select a Python file"); + return; + } + handleSubmit(filePath, "python", undefined); + setModalOpen(false); + }); + }; + return (

Select a test to link to

-

Pytest

+

Pytest & Robot Framework

-
- -
+

Python Script

-
diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx index 4df0c7234..98a2fb9bc 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx @@ -15,7 +15,8 @@ export type ImportTestSettings = { importType: ImportType; }; -export type ImportType = "pytest" | "python"; +export type discoverableTestTypes = "pytest" | "robotframework"; +export type ImportType = discoverableTestTypes | "python"; export const ImportTestModal = () => { const { isImportTestModalOpen, setIsImportTestModalOpen } = @@ -57,6 +58,13 @@ export const ImportTestModal = () => { > Pytest & Unittest + diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx index 071f387ea..86af7720f 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx @@ -38,6 +38,12 @@ export const SequencerGalleryModal = ({ "Learn how to inject the minimum and maximum expected values into a test and export the result.", dirPath: "examples/test-sequencer-expected-exported-example/", }, + { + title: "Robot Framework & Flojoy", + description: + "Learn how to inject the minimum and maximum expected values into a robot test and export the result.", + dirPath: "examples/test-sequencer-robot-framework-example/", + }, ]; return ( diff --git a/src/renderer/types/test-sequencer.ts b/src/renderer/types/test-sequencer.ts index 9d4b8a2a5..1c9fdde0d 100644 --- a/src/renderer/types/test-sequencer.ts +++ b/src/renderer/types/test-sequencer.ts @@ -7,9 +7,8 @@ export type LockedContextType = { export const TestType = z.enum([ "pytest", "python", - "flojoy", - "matlab", "placeholder", + "robotframework", ]); export type TestType = z.infer; @@ -78,6 +77,7 @@ export const Test = z.object({ maxValue: z.number().optional(), measuredValue: z.number().optional(), unit: z.string().optional(), + args: z.string().array().optional(), }); export type Test = z.infer; From dee7efff41c093bca9d29e84f8559d456cc92473 Mon Sep 17 00:00:00 2001 From: Gui Date: Fri, 26 Apr 2024 14:32:01 -0700 Subject: [PATCH 06/10] Bump Flojoy_Cloud deps (#1175) * chore: now using * chore: lock deps * chore: format ? * fix(test-ci): double pytest-btn data-testid * ci(debug-test): adding screenshot in failing test * ci(debug-test): small delay after loading a sequence * ci(debug-test): Timeout before loading * chore: remove deps from example since it's taking too long for CI --- playwright-test/14_load_test_sequence.spec.ts | 9 + .../custom-sequences/complexe_sequence.tjoy | 2 +- .../custom-sequences/flojoy_requirements.txt | 0 poetry.lock | 609 ++++++++---------- pyproject.toml | 2 +- src/renderer/hooks/useTestImport.ts | 4 +- .../components/modals/ImportTestModal.tsx | 2 +- 7 files changed, 290 insertions(+), 338 deletions(-) delete mode 100644 playwright-test/fixtures/custom-sequences/flojoy_requirements.txt diff --git a/playwright-test/14_load_test_sequence.spec.ts b/playwright-test/14_load_test_sequence.spec.ts index a7b3df14d..802114302 100644 --- a/playwright-test/14_load_test_sequence.spec.ts +++ b/playwright-test/14_load_test_sequence.spec.ts @@ -49,6 +49,15 @@ test.describe("Create a test sequence", () => { await window.keyboard.press("Control+o"); } + // Small delay + await window.waitForTimeout(10000); + + // To Debug CI + await window.screenshot({ + path: "test-results/load-complex-sequence.jpeg", + fullPage: true, + }); + // Expect sequence and tests to be loaded await expect( window.locator("div", { hasText: "test_one" }).first(), diff --git a/playwright-test/fixtures/custom-sequences/complexe_sequence.tjoy b/playwright-test/fixtures/custom-sequences/complexe_sequence.tjoy index 383f5bfd5..e19349d9d 100644 --- a/playwright-test/fixtures/custom-sequences/complexe_sequence.tjoy +++ b/playwright-test/fixtures/custom-sequences/complexe_sequence.tjoy @@ -1 +1 @@ -{"name":"complexe_sequence","description":"Playwright test","elems":[{"type":"test","id":"1ed858aa-7e42-4cce-8fc5-cf20efd87ec3","groupId":"3970ad57-dc41-4f7c-8428-67eef733604e","path":"test.py::test_one","testName":"test.py::test_one","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"if","role":"start","id":"aba131bf-e575-485a-afa7-d1924e06e46d","groupId":"588e2d35-d0db-4aa4-b773-04424c6bbf78","condition":"$test.py::test_one"},{"type":"test","id":"18f0505f-3c21-43ca-a1b9-bbc063d1161d","groupId":"405748f9-0bdc-431c-a046-a9faf03a4da4","path":"test.py::test_two","testName":"test.py::test_two","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"if","role":"start","id":"aba131bf-e575-485a-afa7-d1924e06e46d","groupId":"20eacdc3-aafd-422f-b16e-70f7b1a7fa3e","condition":" $test.py::test_one & $test.py::test_two"},{"type":"test","id":"97b89851-cf12-4465-af1e-8464c11ec01d","groupId":"a7901d8a-07c4-4821-aff1-f233b78fdf31","path":"test.py::test_three","testName":"test.py::test_three","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"else","role":"between","id":"ba854dfb-f2c8-41b4-b0f4-733ef82cbd34","groupId":"20eacdc3-aafd-422f-b16e-70f7b1a7fa3e","condition":""},{"type":"test","id":"5a2fb469-9460-4517-b9ec-213627cf717e","groupId":"f329f7b3-b645-489d-bfcf-5c01f1d34b5b","path":"test.py::test_four_will_fail","testName":"test.py::test_four_will_fail","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"end","role":"end","id":"3c33d7bd-712e-494a-ba9f-bf66f39ff77b","groupId":"20eacdc3-aafd-422f-b16e-70f7b1a7fa3e","condition":""},{"type":"conditional","conditionalType":"else","role":"between","id":"ba854dfb-f2c8-41b4-b0f4-733ef82cbd34","groupId":"588e2d35-d0db-4aa4-b773-04424c6bbf78","condition":""},{"type":"conditional","conditionalType":"end","role":"end","id":"3c33d7bd-712e-494a-ba9f-bf66f39ff77b","groupId":"588e2d35-d0db-4aa4-b773-04424c6bbf78","condition":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/playwright-test/fixtures/custom-sequences/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}} \ No newline at end of file +{"name":"complexe_sequence","description":"Playwright test","elems":[{"type":"test","id":"1ed858aa-7e42-4cce-8fc5-cf20efd87ec3","groupId":"3970ad57-dc41-4f7c-8428-67eef733604e","path":"test.py::test_one","testName":"test.py::test_one","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"if","role":"start","id":"aba131bf-e575-485a-afa7-d1924e06e46d","groupId":"588e2d35-d0db-4aa4-b773-04424c6bbf78","condition":"$test.py::test_one"},{"type":"test","id":"18f0505f-3c21-43ca-a1b9-bbc063d1161d","groupId":"405748f9-0bdc-431c-a046-a9faf03a4da4","path":"test.py::test_two","testName":"test.py::test_two","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"if","role":"start","id":"aba131bf-e575-485a-afa7-d1924e06e46d","groupId":"20eacdc3-aafd-422f-b16e-70f7b1a7fa3e","condition":" $test.py::test_one & $test.py::test_two"},{"type":"test","id":"97b89851-cf12-4465-af1e-8464c11ec01d","groupId":"a7901d8a-07c4-4821-aff1-f233b78fdf31","path":"test.py::test_three","testName":"test.py::test_three","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"else","role":"between","id":"ba854dfb-f2c8-41b4-b0f4-733ef82cbd34","groupId":"20eacdc3-aafd-422f-b16e-70f7b1a7fa3e","condition":""},{"type":"test","id":"5a2fb469-9460-4517-b9ec-213627cf717e","groupId":"f329f7b3-b645-489d-bfcf-5c01f1d34b5b","path":"test.py::test_four_will_fail","testName":"test.py::test_four_will_fail","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true},{"type":"conditional","conditionalType":"end","role":"end","id":"3c33d7bd-712e-494a-ba9f-bf66f39ff77b","groupId":"20eacdc3-aafd-422f-b16e-70f7b1a7fa3e","condition":""},{"type":"conditional","conditionalType":"else","role":"between","id":"ba854dfb-f2c8-41b4-b0f4-733ef82cbd34","groupId":"588e2d35-d0db-4aa4-b773-04424c6bbf78","condition":""},{"type":"conditional","conditionalType":"end","role":"end","id":"3c33d7bd-712e-494a-ba9f-bf66f39ff77b","groupId":"588e2d35-d0db-4aa4-b773-04424c6bbf78","condition":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/playwright-test/fixtures/custom-sequences/","interpreter":{"type":"flojoy","path":null,"requirementsPath": null}} diff --git a/playwright-test/fixtures/custom-sequences/flojoy_requirements.txt b/playwright-test/fixtures/custom-sequences/flojoy_requirements.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/poetry.lock b/poetry.lock index dd3dabbc2..318d4d4ee 100644 --- a/poetry.lock +++ b/poetry.lock @@ -138,13 +138,13 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p [[package]] name = "avlos" -version = "0.7.0" +version = "0.8.3" description = "Avlos Remote Object Templating System" optional = false python-versions = ">=3.9" files = [ - {file = "Avlos-0.7.0-py3-none-any.whl", hash = "sha256:d182a7ea898050ab7323c0c4246e7871b811074a72cf45917ff550f5d67d43fb"}, - {file = "Avlos-0.7.0.tar.gz", hash = "sha256:bf61beeda5e59688e533ddfb8972770fad271c6acbef83ae2f055e9e10891355"}, + {file = "Avlos-0.8.3-py3-none-any.whl", hash = "sha256:8b894419ef6ae2c4119dc6698658405d915305e8334e30b829b355c5070fa41e"}, + {file = "Avlos-0.8.3.tar.gz", hash = "sha256:d090f29610a29f0b81d864df112a62b7fe8d48e81d04bf582f1973b1bfd82c0a"}, ] [package.dependencies] @@ -153,7 +153,9 @@ jinja2 = "*" marshmallow = "*" pint = "*" pyyaml = "*" -rstcheck = "*" + +[package.extras] +devel = ["rstcheck"] [[package]] name = "azure-core" @@ -233,17 +235,17 @@ typecheck = ["mypy"] [[package]] name = "boto3" -version = "1.34.84" +version = "1.34.92" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.84-py3-none-any.whl", hash = "sha256:7a02f44af32095946587d748ebeb39c3fa15b9d7275307ff612a6760ead47e04"}, - {file = "boto3-1.34.84.tar.gz", hash = "sha256:91e6343474173e9b82f603076856e1d5b7b68f44247bdd556250857a3f16b37b"}, + {file = "boto3-1.34.92-py3-none-any.whl", hash = "sha256:db7bbb1c6059e99b74dcf634e497b04addcac4c527ae2b2696e47c39eccc6c50"}, + {file = "boto3-1.34.92.tar.gz", hash = "sha256:684cba753d64978a486e8ea9645d53de0d4e3b4a3ab1495b26bd04b9541cea2d"}, ] [package.dependencies] -botocore = ">=1.34.84,<1.35.0" +botocore = ">=1.34.92,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -252,13 +254,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.84" +version = "1.34.92" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.84-py3-none-any.whl", hash = "sha256:da1ae0a912e69e10daee2a34dafd6c6c106450d20b8623665feceb2d96c173eb"}, - {file = "botocore-1.34.84.tar.gz", hash = "sha256:a2b309bf5594f0eb6f63f355ade79ba575ce8bf672e52e91da1a7933caa245e6"}, + {file = "botocore-1.34.92-py3-none-any.whl", hash = "sha256:4211a22a1f6c6935e70cbb84c2cd93b29f9723eaf5036d59748dd104f389a681"}, + {file = "botocore-1.34.92.tar.gz", hash = "sha256:d1ca4886271f184445ec737cd2e752498648cca383887c5a37b2e01c8ab94039"}, ] [package.dependencies] @@ -267,7 +269,7 @@ python-dateutil = ">=2.1,<3.0.0" urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} [package.extras] -crt = ["awscrt (==0.19.19)"] +crt = ["awscrt (==0.20.9)"] [[package]] name = "broadbean" @@ -621,63 +623,63 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, + {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, + {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, + {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, + {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, + {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, + {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, + {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, + {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, + {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, + {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, + {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, + {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, + {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, + {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, + {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, + {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, + {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, + {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, + {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, + {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, + {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, + {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, + {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, + {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, + {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, + {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, + {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, ] [package.extras] @@ -754,13 +756,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dask" -version = "2024.4.1" +version = "2024.4.2" description = "Parallel PyData with Task Scheduling" optional = false python-versions = ">=3.9" files = [ - {file = "dask-2024.4.1-py3-none-any.whl", hash = "sha256:cac5d28b9de7a7cfde46d6fbd8fa81f5654980d010b44d1dbe04dd13b5b63126"}, - {file = "dask-2024.4.1.tar.gz", hash = "sha256:6cd8eb03ddc8dc08d6ca5b167b8de559872bc51cc2b6587d0e9dc754ab19cdf0"}, + {file = "dask-2024.4.2-py3-none-any.whl", hash = "sha256:56fbe92472e3b323ab7beaf2dc8437d48066ac21aa9c2c17ac40d2b6f7b4c414"}, + {file = "dask-2024.4.2.tar.gz", hash = "sha256:3d7a516468d96e72581b84c7bb00172366f30d24c689ea4e9bd1334ab6d98f8a"}, ] [package.dependencies] @@ -778,7 +780,7 @@ array = ["numpy (>=1.21)"] complete = ["dask[array,dataframe,diagnostics,distributed]", "lz4 (>=4.3.2)", "pyarrow (>=7.0)", "pyarrow-hotfix"] dataframe = ["dask-expr (>=1.0,<1.1)", "dask[array]", "pandas (>=1.3)"] diagnostics = ["bokeh (>=2.4.2)", "jinja2 (>=2.10.3)"] -distributed = ["distributed (==2024.4.1)"] +distributed = ["distributed (==2024.4.2)"] test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist"] [[package]] @@ -890,17 +892,6 @@ files = [ {file = "docstring_parser-0.15.tar.gz", hash = "sha256:48ddc093e8b1865899956fcc03b03e66bb7240c310fac5af81814580c55bf682"}, ] -[[package]] -name = "docutils" -version = "0.21.1" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -files = [ - {file = "docutils-0.21.1-py3-none-any.whl", hash = "sha256:14c8d34a55b46c88f9f714adb29cefbdd69fb82f3fef825e59c5faab935390d8"}, - {file = "docutils-0.21.1.tar.gz", hash = "sha256:65249d8a5345bc95e0f40f280ba63c98eb24de35c6c8f5b662e3e8948adea83f"}, -] - [[package]] name = "et-xmlfile" version = "1.1.0" @@ -1013,13 +1004,13 @@ url = "pkgs/flojoy" [[package]] name = "flojoy-cloud" -version = "0.2.1" +version = "0.2.2" description = "" optional = false python-versions = "<4.0,>=3.11" files = [ - {file = "flojoy_cloud-0.2.1-py3-none-any.whl", hash = "sha256:9207b032d6facfb510692ca9f428160ba652463a66667911fd2f91a9aee1d6fb"}, - {file = "flojoy_cloud-0.2.1.tar.gz", hash = "sha256:fad39457c60a276375bb5f20bd9ecb25f4afd95b8b9c33389fd25066a88e1a3f"}, + {file = "flojoy_cloud-0.2.2-py3-none-any.whl", hash = "sha256:af40dec417ca97c69a1656c0e0fbe0341422cd3a189c15c7231bddd537021f41"}, + {file = "flojoy_cloud-0.2.2.tar.gz", hash = "sha256:5a2e8649d1926c33e8af8ed231651acd4de429f10afcdff86945bacde09eaa31"}, ] [package.dependencies] @@ -1438,13 +1429,13 @@ files = [ [[package]] name = "imageio" -version = "2.34.0" +version = "2.34.1" description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." optional = false python-versions = ">=3.8" files = [ - {file = "imageio-2.34.0-py3-none-any.whl", hash = "sha256:08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba"}, - {file = "imageio-2.34.0.tar.gz", hash = "sha256:ae9732e10acf807a22c389aef193f42215718e16bd06eed0c5bb57e1034a4d53"}, + {file = "imageio-2.34.1-py3-none-any.whl", hash = "sha256:408c1d4d62f72c9e8347e7d1ca9bc11d8673328af3913868db3b828e28b40a4c"}, + {file = "imageio-2.34.1.tar.gz", hash = "sha256:f13eb76e4922f936ac4a7fec77ce8a783e63b93543d4ea3e40793a6cabd9ac7d"}, ] [package.dependencies] @@ -1548,13 +1539,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.23.0" +version = "8.24.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.23.0-py3-none-any.whl", hash = "sha256:07232af52a5ba146dc3372c7bf52a0f890a23edf38d77caef8d53f9cdc2584c1"}, - {file = "ipython-8.23.0.tar.gz", hash = "sha256:7468edaf4f6de3e1b912e57f66c241e6fd3c7099f2ec2136e239e142e800274d"}, + {file = "ipython-8.24.0-py3-none-any.whl", hash = "sha256:d7bf2f6c4314984e3e02393213bab8703cf163ede39672ce5918c51fe253a2a3"}, + {file = "ipython-8.24.0.tar.gz", hash = "sha256:010db3f8a728a578bb641fdd06c063b9fb8e96a9464c63aec6310fbcb5e80501"}, ] [package.dependencies] @@ -1567,7 +1558,7 @@ prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" traitlets = ">=5.13.0" -typing-extensions = {version = "*", markers = "python_version < \"3.12\""} +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] @@ -1580,7 +1571,7 @@ nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test = ["pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] [[package]] @@ -3017,28 +3008,29 @@ xarray = ["xarray"] [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, + {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "plotly" -version = "5.20.0" +version = "5.21.0" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.20.0-py3-none-any.whl", hash = "sha256:837a9c8aa90f2c0a2f0d747b82544d014dc2a2bdde967b5bb1da25b53932d1a9"}, - {file = "plotly-5.20.0.tar.gz", hash = "sha256:bf901c805d22032cfa534b2ff7c5aa6b0659e037f19ec1e0cca7f585918b5c89"}, + {file = "plotly-5.21.0-py3-none-any.whl", hash = "sha256:a33f41fd5922e45b2b253f795b200d14452eb625790bb72d0a72cf1328a6abbf"}, + {file = "plotly-5.21.0.tar.gz", hash = "sha256:69243f8c165d4be26c0df1c6f0b7b258e2dfeefe032763404ad7e7fb7d7c2073"}, ] [package.dependencies] @@ -3047,13 +3039,13 @@ tenacity = ">=6.2.0" [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -3262,18 +3254,18 @@ files = [ [[package]] name = "pydantic" -version = "2.7.0" +version = "2.7.1" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, - {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.18.1" +pydantic-core = "2.18.2" typing-extensions = ">=4.6.1" [package.extras] @@ -3281,90 +3273,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.18.1" +version = "2.18.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, - {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, - {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, - {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, - {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, - {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, - {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, - {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, - {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, - {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, - {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, - {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, - {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, - {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, - {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, - {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, - {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, - {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, - {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, - {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, - {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, - {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, - {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, - {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, - {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, - {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, - {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, - {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, - {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, - {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, - {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, - {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, - {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, - {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, - {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, - {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, - {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, - {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, - {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, - {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, ] [package.dependencies] @@ -3827,99 +3819,99 @@ files = [ [[package]] name = "pyzmq" -version = "26.0.0" +version = "26.0.2" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" files = [ - {file = "pyzmq-26.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:a86409f3f8eae7af5a47babd831a119bdf552e831f04d2225a313305e8e35e7c"}, - {file = "pyzmq-26.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d36a46975925b8bf14b69fe6d4097bc96c91f94ceb954d56853a2211a5cc3433"}, - {file = "pyzmq-26.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcac700269d081ded42ed3833f9d0effe734148376204af9c0ef0fd25a3fea55"}, - {file = "pyzmq-26.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49efc420e36d2e8adc5dae41c2c1e8bb37a069e40a880cbe414a032136b194b0"}, - {file = "pyzmq-26.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02773b96ef6a17a57680c3609645785c390198be31a4505c01ce0c846f9e7d0e"}, - {file = "pyzmq-26.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ce2c53f4963a358ba91b58ccecb84fab6d5f0622230d105c2589f7556ec53cc9"}, - {file = "pyzmq-26.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06525d996afdb0da3e8b7df0b654261455f6e86c2c3574c3f00d2bd335be78eb"}, - {file = "pyzmq-26.0.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bd3537f049dc0488adb3df29a77635eaff2a8d1d3d29a09714db6e2d10caba1a"}, - {file = "pyzmq-26.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9ce158ab54994c60fdde83300dc1e447446baacbe4ec9e4e80096f9b9a125c13"}, - {file = "pyzmq-26.0.0-cp310-cp310-win32.whl", hash = "sha256:271c9178a94b009651f8ad3ff9bb9ca45778aaf66c9e325a44d81a7498fcaa59"}, - {file = "pyzmq-26.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4216eee101d104a017042f0e4af0a45875400ff3794f1a59476e210b1a9760e2"}, - {file = "pyzmq-26.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:44271793067025a07d38ad4be11f08187cce850fafd1890b42046abbcdca2fc0"}, - {file = "pyzmq-26.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:1e87178437460b6df18e761650ef080d3ad5a41813cc3df7f9fd78714fca04c0"}, - {file = "pyzmq-26.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0397c7431f3fc2bac497992d7447b036bc0d8bb3e15b158b2013201857ff2354"}, - {file = "pyzmq-26.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a5b4dc4d7a3f859026083906724ad1ae743261548b61d0d5abcf2d994122c2b"}, - {file = "pyzmq-26.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:952e85c5e86f9ba100b78b60719b76e1ff3e13bb403cb6de687bb92e7b2179e7"}, - {file = "pyzmq-26.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fdeac8612a9dca6fcad6cb43c7efb75f53ba75da981fbafa949ddcde1d5662"}, - {file = "pyzmq-26.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:39b8ed8d2e5da8b8351c6aa627601b3b52e8eb5e25cf6bcd26b6f012dec7870b"}, - {file = "pyzmq-26.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f6f618d7d7c9c37053a36e6dc5435c53e9e0c7a67e6fd00b69c209d07a8db4dc"}, - {file = "pyzmq-26.0.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72ae3078b1c47552e0e39fd81fc0472e880316897a733dbb3570819be19da48a"}, - {file = "pyzmq-26.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5d7fcc648445dbfd6ce9973ec7b4a33ee9307b7e88cf4816f4403ccbaf8de9ca"}, - {file = "pyzmq-26.0.0-cp311-cp311-win32.whl", hash = "sha256:9982799d7d7807beb1b26f1aa9a192baccb1a14c5d00eca881a42a0ae562671b"}, - {file = "pyzmq-26.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:60f91afc76a3fc5d65dfba4f6b6020c462674b5eab6cbf00dec133d79656072d"}, - {file = "pyzmq-26.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:120887d773e878136e9b33bbba656df0d4c6e2861694d07d058ec60ce1108b24"}, - {file = "pyzmq-26.0.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:469f4febd63c26b20132e54cc40048d5698123794b103758ccd21b8a45890dc3"}, - {file = "pyzmq-26.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c919895132cae5a458d5a17047fd33c9eb271f15bb3485add34429cfd7b76a71"}, - {file = "pyzmq-26.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e0e94ca9a8f23000d54e11ecd727b69fb1994baf3b6b1eedb881cdd3196ecec"}, - {file = "pyzmq-26.0.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a824b3301ddd003cdceb9b537804e751ac5922a845b19d4e50b4789d1cd28b24"}, - {file = "pyzmq-26.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af9f5b1b76753584c871c1c96db8a18650886b3adf9fc8c7d4019343eb329c28"}, - {file = "pyzmq-26.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9691a6ab55d011e83d7438f6711b93b7f8aa21ee8cf3e7ad6d6d9ea26a8f3a1f"}, - {file = "pyzmq-26.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58176e2437462568b5099acf17401be64205e175e72767a8250eef84ee9ec4f5"}, - {file = "pyzmq-26.0.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d492921b398d640a1f796306531bc6911a94ce5528b798ed14e0620abd9b948d"}, - {file = "pyzmq-26.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f85bb2c47b5fd70e3cbb280e380ab97bdf9f02e1a363cb472fe0a297ac24029d"}, - {file = "pyzmq-26.0.0-cp312-cp312-win32.whl", hash = "sha256:c2e36399f0433b14a91f956bd7ecf94799c57a6f992889d45440cb05b3de8025"}, - {file = "pyzmq-26.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:12ca1afb065e5b21a32b1e35bfcbc8762efc0f7555c166acaec36c93b52d7ccf"}, - {file = "pyzmq-26.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:f66c925f62ce28946525c32a094e346dd8da6c828d568d7ecda97f5ae36089c3"}, - {file = "pyzmq-26.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e495ff09514fc657c5fb2cba0aac082ce0494c6217230783297da9008333a8db"}, - {file = "pyzmq-26.0.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5736c9a54c27319a65ffc72dbf684538f2773237e94ba50b7f1f74f4e3cb9115"}, - {file = "pyzmq-26.0.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cd62830100b9b1adb51da4094142bd680d51daf9a0f6f3f39e1f80474eddc011"}, - {file = "pyzmq-26.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a7ee271fac41ddc0ba11f4b128ddd5f2bf0a3186d25be331ed8bfbb253536"}, - {file = "pyzmq-26.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:694625c2c22be57149e9439757ee02ee4fb6432f7054dc5008bbbc33ef388d1c"}, - {file = "pyzmq-26.0.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:90ba8f7c6f34c2c11179b293050417c14661035969ef3f8867200ea6901f9000"}, - {file = "pyzmq-26.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab2e55046263c8b24e64116e80b63cf701df747b44aadcf317aa47c8af2dfe67"}, - {file = "pyzmq-26.0.0-cp37-cp37m-win32.whl", hash = "sha256:7353d231686bbc96c458b934f134ff9165a1e9dd0a2ea8f724469e44bcc2c07a"}, - {file = "pyzmq-26.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1df2b992eabc59f078ca916e9ac8b5bd463536bf7828c13940b35b8555ed7861"}, - {file = "pyzmq-26.0.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2397364289334840c81ff1ef95a5a5ee326de01c1437cc38f7e16785a7b653d9"}, - {file = "pyzmq-26.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c952cf06edbbd2d67f627037e2c8e3187ca834d6b9a222e3a3037f80d393a345"}, - {file = "pyzmq-26.0.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55f390adb763196d75a2e8c18277b4344f8a7f94f223b5d096324c5b47c2471e"}, - {file = "pyzmq-26.0.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1da5e11862a994360319df4f425e89662563683334e1079684eb77b9a6478ae2"}, - {file = "pyzmq-26.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72340614ea23904cff824109eb025648bdf32775d87f5814d3ba6f2335a853f3"}, - {file = "pyzmq-26.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa7431d12ebb5433a92e99dc326d45eaf52a90046032bac4c558b4bdeee5dc7a"}, - {file = "pyzmq-26.0.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a2b13008a693c0ffccaeeebcc5ab5f2398cced3b5bf482ba89a38fe56b00eb10"}, - {file = "pyzmq-26.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d68284ce48617c97e675ed8a89db12a098eaa871a026999c9a10351f547f1fe"}, - {file = "pyzmq-26.0.0-cp38-cp38-win32.whl", hash = "sha256:8783857a8c8df648a70c81ea3ff53ee71e5bf18468ca5ac3414f419fe8f3bd93"}, - {file = "pyzmq-26.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:36d0f2fcbdba1fda8ff213bd17db7ddcba848aa70480ade3fe70401dce606511"}, - {file = "pyzmq-26.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:dd87df01bc8eca392f0d505924087ccafdc4885a498e68df9f09eca9fdc736f1"}, - {file = "pyzmq-26.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abc08b2e688714216870a6ab974733d4a1fcf0437d250ac8feed59c4c5c3f395"}, - {file = "pyzmq-26.0.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dd13a30454adcf2f361155ea563ec99036678131a17c6b1a3f74426212c14ddc"}, - {file = "pyzmq-26.0.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a0562054930471b386a44b0887504687c4e7adf4ba89bddc2e5959d16c371764"}, - {file = "pyzmq-26.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc7badded4b025dbc25f34b95503b71c952235e6e40de40995c0c120efb4ff6d"}, - {file = "pyzmq-26.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f971e77358384b8bcf3e9a7577cf84f97adbd6359f943e30cbff66087afcb279"}, - {file = "pyzmq-26.0.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca4ebbef3f5fbd271eafc7c22ebbb88b74232f08b0e51759113f30a8d01f6843"}, - {file = "pyzmq-26.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc98fbd4ce4ef8a0fbe97ab6d495aaa7764461e5a45f24c04f1d234e7bb80293"}, - {file = "pyzmq-26.0.0-cp39-cp39-win32.whl", hash = "sha256:a5207bc2a923118e9afb57fee679be016ea138c27d1be5747118966e2d5d9450"}, - {file = "pyzmq-26.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e0c08a6070358a2984900a4518e2dacbfaf24aac018ab086d7ac2f6069b13340"}, - {file = "pyzmq-26.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:eae3dcc185c405cf645480745c45346a1f42afce240f69a589095e41bd2b9e3d"}, - {file = "pyzmq-26.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:71a8f010e23dfd61c531084a2b72a81885017da28352540f0b7799ca8423c044"}, - {file = "pyzmq-26.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b48b7e417c56486932fb0c01fecd24916fe6bc359c03a654aa8c63fa33e3d76"}, - {file = "pyzmq-26.0.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2806942185b40a3477d9b300c6f71354dd2be37e3f61a43193c96caa51e284d1"}, - {file = "pyzmq-26.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed127aff75a3df142ae7a883c49a85b0b2f863b59fa1b8e4280335f5ebab5fd0"}, - {file = "pyzmq-26.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:903b77dd2f17286496fa3ec902bc523f4502b0c64a2892df4b021222a2ba95fe"}, - {file = "pyzmq-26.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:321a6872a9371709a62b3a4a14c1e9b5b47549371197c0c2164d2288510cd6d6"}, - {file = "pyzmq-26.0.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cac954dc83c84e9d9d65f2359d402d7e79ae094d7808d578c9e9cc2c350c5a64"}, - {file = "pyzmq-26.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac6f54c399638858e0b2a3153f23934604f3a8c9bb5a9cf865060cc658b1e096"}, - {file = "pyzmq-26.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40af30c4cd0a046029d7b5272d02a649f9b1f89fb1361bbc90ba08d55ac88273"}, - {file = "pyzmq-26.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:814245422f1c7707634397621dbcbeea7671fdc5c43d1ae592f4e0e45179e7fb"}, - {file = "pyzmq-26.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6d3d7ef786e778351e6c51b45906e16506ad98bb78b99304032cb1876dfc81d2"}, - {file = "pyzmq-26.0.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:36a85da0eab4c5337d0de7f975cca011208a59e9d0637e0c1b571764f1dd4a8f"}, - {file = "pyzmq-26.0.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d64889bfe4109f4a59a72b1d21416550465020642d6f556efd044951386bd38"}, - {file = "pyzmq-26.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80fdea3e9e34c480bfccbb910f75380196ae9d1c12880c21743c845ebe6b13aa"}, - {file = "pyzmq-26.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7129efc54dc48f566eed5422bc555ba4e472e40a1f9de328577c90ade47ccf5d"}, - {file = "pyzmq-26.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ec5147095d6065b0e3a38a1a34f7859ab46496f3d5ce71134165893e9f83674"}, - {file = "pyzmq-26.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a1cc0445038a394479ad36b7e3cf55a19ee40099c031f65de872b8ee7025e79"}, - {file = "pyzmq-26.0.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b377b520e618c30c827966c274dd62ce7e15c72ce8767fae6193b6bdd1deb502"}, - {file = "pyzmq-26.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc907b26d287e6981d1e531c8fc21a0f94fe46a17493a8322eb3c75f8b561334"}, - {file = "pyzmq-26.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:580dd4b1c2edd51f284df0209bf439899f425ed00cb803a85ddc6cf10c866688"}, - {file = "pyzmq-26.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:08db8071020181173c70cf2dad239e5e21e5b2e95f95b0ece0da39a70f5a483c"}, - {file = "pyzmq-26.0.0.tar.gz", hash = "sha256:10ff405db5cee3bbd7aa143d78b25d90356097aed7864e50f0ae644e08759fe9"}, + {file = "pyzmq-26.0.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:1a60a03b01e8c9c58932ec0cca15b1712d911c2800eb82d4281bc1ae5b6dad50"}, + {file = "pyzmq-26.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:949067079e14ea1973bd740255e0840118c163d4bce8837f539d749f145cf5c3"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e7edfa6cf96d036a403775c96afa25058d1bb940a79786a9a2fc94a783abe3"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:903cc7a84a7d4326b43755c368780800e035aa3d711deae84a533fdffa8755b0"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cb2e41af165e5f327d06fbdd79a42a4e930267fade4e9f92d17f3ccce03f3a7"}, + {file = "pyzmq-26.0.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:55353b8189adcfc4c125fc4ce59d477744118e9c0ec379dd0999c5fa120ac4f5"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f961423ff6236a752ced80057a20e623044df95924ed1009f844cde8b3a595f9"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ba77fe84fe4f5f3dc0ef681a6d366685c8ffe1c8439c1d7530997b05ac06a04b"}, + {file = "pyzmq-26.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:52589f0a745ef61b9c75c872cf91f8c1f7c0668eb3dd99d7abd639d8c0fb9ca7"}, + {file = "pyzmq-26.0.2-cp310-cp310-win32.whl", hash = "sha256:b7b6d2a46c7afe2ad03ec8faf9967090c8ceae85c4d8934d17d7cae6f9062b64"}, + {file = "pyzmq-26.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:86531e20de249d9204cc6d8b13d5a30537748c78820215161d8a3b9ea58ca111"}, + {file = "pyzmq-26.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:f26a05029ecd2bd306b941ff8cb80f7620b7901421052bc429d238305b1cbf2f"}, + {file = "pyzmq-26.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:70770e296a9cb03d955540c99360aab861cbb3cba29516abbd106a15dbd91268"}, + {file = "pyzmq-26.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2740fd7161b39e178554ebf21aa5667a1c9ef0cd2cb74298fd4ef017dae7aec4"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e3706c32dea077faa42b1c92d825b7f86c866f72532d342e0be5e64d14d858"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fa1416876194927f7723d6b7171b95e1115602967fc6bfccbc0d2d51d8ebae1"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ef9a79a48794099c57dc2df00340b5d47c5caa1792f9ddb8c7a26b1280bd575"}, + {file = "pyzmq-26.0.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1c60fcdfa3229aeee4291c5d60faed3a813b18bdadb86299c4bf49e8e51e8605"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e943c39c206b04df2eb5d71305761d7c3ca75fd49452115ea92db1b5b98dbdef"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8da0ed8a598693731c76659880a668f4748b59158f26ed283a93f7f04d47447e"}, + {file = "pyzmq-26.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bf51970b11d67096bede97cdbad0f4333f7664f4708b9b2acb352bf4faa3140"}, + {file = "pyzmq-26.0.2-cp311-cp311-win32.whl", hash = "sha256:6f8e6bd5d066be605faa9fe5ec10aa1a46ad9f18fc8646f2b9aaefc8fb575742"}, + {file = "pyzmq-26.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:6d03da3a0ae691b361edcb39530075461202f699ce05adbb15055a0e1c9bcaa4"}, + {file = "pyzmq-26.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f84e33321b68ff00b60e9dbd1a483e31ab6022c577c8de525b8e771bd274ce68"}, + {file = "pyzmq-26.0.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:44c33ebd1c62a01db7fbc24e18bdda569d6639217d13d5929e986a2b0f69070d"}, + {file = "pyzmq-26.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ac04f904b4fce4afea9cdccbb78e24d468cb610a839d5a698853e14e2a3f9ecf"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2133de5ba9adc5f481884ccb699eac9ce789708292945c05746880f95b241c0"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7753c67c570d7fc80c2dc59b90ca1196f1224e0e2e29a548980c95fe0fe27fc1"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d4e51632e6b12e65e8d9d7612446ecda2eda637a868afa7bce16270194650dd"}, + {file = "pyzmq-26.0.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d6c38806f6ecd0acf3104b8d7e76a206bcf56dadd6ce03720d2fa9d9157d5718"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:48f496bbe14686b51cec15406323ae6942851e14022efd7fc0e2ecd092c5982c"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e84a3161149c75bb7a7dc8646384186c34033e286a67fec1ad1bdedea165e7f4"}, + {file = "pyzmq-26.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dabf796c67aa9f5a4fcc956d47f0d48b5c1ed288d628cf53aa1cf08e88654343"}, + {file = "pyzmq-26.0.2-cp312-cp312-win32.whl", hash = "sha256:3eee4c676af1b109f708d80ef0cf57ecb8aaa5900d1edaf90406aea7e0e20e37"}, + {file = "pyzmq-26.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:26721fec65846b3e4450dad050d67d31b017f97e67f7e0647b5f98aa47f828cf"}, + {file = "pyzmq-26.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:653955c6c233e90de128a1b8e882abc7216f41f44218056bd519969c8c413a15"}, + {file = "pyzmq-26.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:becd8d8fb068fbb5a52096efd83a2d8e54354383f691781f53a4c26aee944542"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7a15e5465e7083c12517209c9dd24722b25e9b63c49a563922922fc03554eb35"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e8158ac8616941f874841f9fa0f6d2f1466178c2ff91ea08353fdc19de0d40c2"}, + {file = "pyzmq-26.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c6a53e28c7066ea7db86fcc0b71d78d01b818bb11d4a4341ec35059885295"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bdbc7dab0b0e9c62c97b732899c4242e3282ba803bad668e03650b59b165466e"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e74b6d5ef57bb65bf1b4a37453d8d86d88550dde3fb0f23b1f1a24e60c70af5b"}, + {file = "pyzmq-26.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ed4c6ee624ecbc77b18aeeb07bf0700d26571ab95b8f723f0d02e056b5bce438"}, + {file = "pyzmq-26.0.2-cp37-cp37m-win32.whl", hash = "sha256:8a98b3cb0484b83c19d8fb5524c8a469cd9f10e743f5904ac285d92678ee761f"}, + {file = "pyzmq-26.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:aa5f95d71b6eca9cec28aa0a2f8310ea53dea313b63db74932879ff860c1fb8d"}, + {file = "pyzmq-26.0.2-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:5ff56c76ce77b9805378a7a73032c17cbdb1a5b84faa1df03c5d3e306e5616df"}, + {file = "pyzmq-26.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bab697fc1574fee4b81da955678708567c43c813c84c91074e452bda5346c921"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0c0fed8aa9ba0488ee1cbdaa304deea92d52fab43d373297002cfcc69c0a20c5"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:606b922699fcec472ed814dda4dc3ff7c748254e0b26762a0ba21a726eb1c107"}, + {file = "pyzmq-26.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f0fd82bad4d199fa993fbf0ac586a7ac5879addbe436a35a389df7e0eb4c91"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:166c5e41045939a52c01e6f374e493d9a6a45dfe677360d3e7026e38c42e8906"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d566e859e8b8d5bca08467c093061774924b3d78a5ba290e82735b2569edc84b"}, + {file = "pyzmq-26.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:264ee0e72b72ca59279dc320deab5ae0fac0d97881aed1875ce4bde2e56ffde0"}, + {file = "pyzmq-26.0.2-cp38-cp38-win32.whl", hash = "sha256:3152bbd3a4744cbdd83dfb210ed701838b8b0c9065cef14671d6d91df12197d0"}, + {file = "pyzmq-26.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:bf77601d75ca692c179154b7e5943c286a4aaffec02c491afe05e60493ce95f2"}, + {file = "pyzmq-26.0.2-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:c770a7545b3deca2db185b59175e710a820dd4ed43619f4c02e90b0e227c6252"}, + {file = "pyzmq-26.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d47175f0a380bfd051726bc5c0054036ae4a5d8caf922c62c8a172ccd95c1a2a"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bce298c1ce077837e110367c321285dc4246b531cde1abfc27e4a5bbe2bed4d"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c40b09b7e184d6e3e1be1c8af2cc320c0f9f610d8a5df3dd866e6e6e4e32b235"}, + {file = "pyzmq-26.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d420d856bf728713874cefb911398efe69e1577835851dd297a308a78c14c249"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d792d3cab987058451e55c70c5926e93e2ceb68ca5a2334863bb903eb860c9cb"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:83ec17729cf6d3464dab98a11e98294fcd50e6b17eaabd3d841515c23f6dbd3a"}, + {file = "pyzmq-26.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47c17d5ebfa88ae90f08960c97b49917098665b8cd8be31f2c24e177bcf37a0f"}, + {file = "pyzmq-26.0.2-cp39-cp39-win32.whl", hash = "sha256:d509685d1cd1d018705a811c5f9d5bc237790936ead6d06f6558b77e16cc7235"}, + {file = "pyzmq-26.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:c7cc8cc009e8f6989a6d86c96f87dae5f5fb07d6c96916cdc7719d546152c7db"}, + {file = "pyzmq-26.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:3ada31cb879cd7532f4a85b501f4255c747d4813ab76b35c49ed510ce4865b45"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0a6ceaddc830dd3ca86cb8451cf373d1f05215368e11834538c2902ed5205139"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a967681463aa7a99eb9a62bb18229b653b45c10ff0947b31cc0837a83dfb86f"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6472a73bc115bc40a2076609a90894775abe6faf19a78375675a2f889a613071"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d6aea92bcccfe5e5524d3c70a6f16ffdae548390ddad26f4207d55c55a40593"}, + {file = "pyzmq-26.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e025f6351e49d48a5aa2f5a09293aa769b0ee7369c25bed551647234b7fa0c75"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:40bd7ebe4dbb37d27f0c56e2a844f360239343a99be422085e13e97da13f73f9"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1dd40d586ad6f53764104df6e01810fe1b4e88fd353774629a5e6fe253813f79"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2aca15e9ad8c8657b5b3d7ae3d1724dc8c1c1059c06b4b674c3aa36305f4930"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:450ec234736732eb0ebeffdb95a352450d4592f12c3e087e2a9183386d22c8bf"}, + {file = "pyzmq-26.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f43be2bebbd09360a2f23af83b243dc25ffe7b583ea8c722e6df03e03a55f02f"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:867f55e54aff254940bcec5eec068e7c0ac1e6bf360ab91479394a8bf356b0e6"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b4dbc033c5ad46f8c429bf238c25a889b8c1d86bfe23a74e1031a991cb3f0000"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6e8dd2961462e337e21092ec2da0c69d814dcb1b6e892955a37444a425e9cfb8"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35391e72df6c14a09b697c7b94384947c1dd326aca883ff98ff137acdf586c33"}, + {file = "pyzmq-26.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:1c3d3c92fa54eda94ab369ca5b8d35059987c326ba5e55326eb068862f64b1fc"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7aa61a9cc4f0523373e31fc9255bf4567185a099f85ca3598e64de484da3ab2"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee53a8191271f144cc20b12c19daa9f1546adc84a2f33839e3338039b55c373c"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac60a980f07fa988983f7bfe6404ef3f1e4303f5288a01713bc1266df6d18783"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88896b1b4817d7b2fe1ec7205c4bbe07bf5d92fb249bf2d226ddea8761996068"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:18dfffe23751edee917764ffa133d5d3fef28dfd1cf3adebef8c90bc854c74c4"}, + {file = "pyzmq-26.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:6926dd14cfe6967d3322640b6d5c3c3039db71716a5e43cca6e3b474e73e0b36"}, + {file = "pyzmq-26.0.2.tar.gz", hash = "sha256:f0f9bb370449158359bb72a3e12c658327670c0ffe6fbcd1af083152b64f9df0"}, ] [package.dependencies] @@ -3975,13 +3967,13 @@ zurichinstruments = ["zhinst-qcodes (>=0.3)"] [[package]] name = "referencing" -version = "0.34.0" +version = "0.35.0" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, - {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, + {file = "referencing-0.35.0-py3-none-any.whl", hash = "sha256:8080727b30e364e5783152903672df9b6b091c926a146a759080b62ca3126cd6"}, + {file = "referencing-0.35.0.tar.gz", hash = "sha256:191e936b0c696d0af17ad7430a3dc68e88bc11be6514f4757dc890f04ab05889"}, ] [package.dependencies] @@ -4179,53 +4171,6 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" -[[package]] -name = "rstcheck" -version = "6.2.1" -description = "Checks syntax of reStructuredText and code blocks nested within it" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rstcheck-6.2.1-py3-none-any.whl", hash = "sha256:b450943707d8ca053f5c6b9f103ee595f4926a064203e5e579172aefb3fe2c12"}, - {file = "rstcheck-6.2.1.tar.gz", hash = "sha256:e4d173950b023eb12c2b9d2348a8c62bef46612bbc7b29e1e57d37320ed0a891"}, -] - -[package.dependencies] -rstcheck-core = ">=1.1" -typer = {version = ">=0.4.1", extras = ["all"]} - -[package.extras] -dev = ["rstcheck[docs,sphinx,testing,toml,type-check]", "tox (>=3.15)"] -docs = ["m2r2 (>=0.3.2)", "sphinx (>=5.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-click (>=4.0.3)", "sphinx-rtd-theme (>=1.2)", "sphinxcontrib-spelling (>=7.3)"] -sphinx = ["sphinx (>=5.0)"] -testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] -toml = ["tomli (>=2.0)"] -type-check = ["mypy (>=1.0)"] - -[[package]] -name = "rstcheck-core" -version = "1.2.1" -description = "Checks syntax of reStructuredText and code blocks nested within it" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rstcheck-core-1.2.1.tar.gz", hash = "sha256:9b330020d912e2864f23f332c1a0569463ca3b06b8fee7b7bdd201b055f7f831"}, - {file = "rstcheck_core-1.2.1-py3-none-any.whl", hash = "sha256:1c100de418b6c9e14d9cf6558644d0ab103fdc447f891313882d02df3a3c52ba"}, -] - -[package.dependencies] -docutils = ">=0.7" -pydantic = ">=2" - -[package.extras] -dev = ["rstcheck-core[docs,sphinx,testing,toml,type-check,yaml]", "tox (>=3.15)"] -docs = ["m2r2 (>=0.3.2)", "sphinx (>=5.0,!=7.2.5)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.15)", "sphinx-rtd-theme (>=1.2)", "sphinxcontrib-apidoc (>=0.3)", "sphinxcontrib-spelling (>=7.3)"] -sphinx = ["sphinx (>=5.0)"] -testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-mock (>=3.7)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] -toml = ["tomli (>=2.0)"] -type-check = ["mypy (>=1.0)", "types-PyYAML (>=6.0.0)", "types-docutils (>=0.18)"] -yaml = ["pyyaml (>=6.0.0)"] - [[package]] name = "ruamel-yaml" version = "0.18.6" @@ -4659,13 +4604,13 @@ files = [ [[package]] name = "tifffile" -version = "2024.2.12" +version = "2024.4.24" description = "Read and write TIFF files" optional = false python-versions = ">=3.9" files = [ - {file = "tifffile-2024.2.12-py3-none-any.whl", hash = "sha256:870998f82fbc94ff7c3528884c1b0ae54863504ff51dbebea431ac3fa8fb7c21"}, - {file = "tifffile-2024.2.12.tar.gz", hash = "sha256:4920a3ec8e8e003e673d3c6531863c99eedd570d1b8b7e141c072ed78ff8030d"}, + {file = "tifffile-2024.4.24-py3-none-any.whl", hash = "sha256:8d0b982f4b01ace358835ae6c2beb5a70cb7287f5d3a2e96c318bd5befa97b1f"}, + {file = "tifffile-2024.4.24.tar.gz", hash = "sha256:e329f36ac8ff3bbe7dd04609340be26b03c4b9e9a69235fc3ab33434157c38ea"}, ] [package.dependencies] @@ -4819,18 +4764,18 @@ files = [ [[package]] name = "traitlets" -version = "5.14.2" +version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, - {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "typer" @@ -5431,4 +5376,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "4cbeb9ef3f59318e9d5ca01d662c776c37627aa04dd98157c36d26bf87b43356" +content-hash = "8c34c30422817371ebe2752058632e8451c5779fa31e1b3668da1ea09ac06343" diff --git a/pyproject.toml b/pyproject.toml index d8646857c..ae897393a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ httpx = "^0.26.0" nidaqmx = "^0.9.0" nidmm = "^1.4.7" nimodinst = "^1.4.7" -flojoy-cloud = "^0.2.1" +flojoy-cloud = "^0.2.2" bcrypt = "^4.1.2" tinymovr = "^1.6.5" robotframework = "^7.0" diff --git a/src/renderer/hooks/useTestImport.ts b/src/renderer/hooks/useTestImport.ts index deefc0a0a..d238288cb 100644 --- a/src/renderer/hooks/useTestImport.ts +++ b/src/renderer/hooks/useTestImport.ts @@ -1,6 +1,4 @@ -import { - TestDiscoverContainer, -} from "@/renderer/types/test-sequencer"; +import { TestDiscoverContainer } from "@/renderer/types/test-sequencer"; import { createNewTest, useDisplayedSequenceState, diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx index 98a2fb9bc..7495059cc 100644 --- a/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/modals/ImportTestModal.tsx @@ -61,7 +61,7 @@ export const ImportTestModal = () => { From 03216ae75fc0213fff84df6ce31c4c4dd3fb13b3 Mon Sep 17 00:00:00 2001 From: Gui Date: Mon, 29 Apr 2024 06:51:07 -0700 Subject: [PATCH 07/10] 0.4.2 (#1176) * 0.4.2 * bump flojoy & docs --- docs/package.json | 2 +- package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package.json b/docs/package.json index 3771ead3a..e61a35e9a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "blocks", "type": "module", - "version": "0.4.1", + "version": "0.4.2", "scripts": { "dev": "astro dev", "start": "astro dev", diff --git a/package.json b/package.json index 5d8b4051e..45c39a055 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "flojoy-studio", "productName": "Flojoy Studio", "description": "Joyful visual programming for Python", - "version": "0.4.1", + "version": "0.4.2", "private": true, "homepage": "./", "author": "Jack Parmer ", diff --git a/pyproject.toml b/pyproject.toml index ae897393a..06013ab45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "flojoy-studio" -version = "0.4.1" +version = "0.4.2" description = "" authors = [ "Jack Palmer ", From 2e0a104bd6f6fe69e203e2ef8df56cb83b042d69 Mon Sep 17 00:00:00 2001 From: Gui Date: Mon, 29 Apr 2024 07:58:35 -0700 Subject: [PATCH 08/10] Small deps fix (#1177) * fix: remove unused deps * chore: lock poetry deps --- .../Robot_Sequence.tjoy | 2 +- .../flojoy_requirements.txt | 1 - poetry.lock | 50 +++++++++---------- pyproject.toml | 1 - 4 files changed, 26 insertions(+), 28 deletions(-) delete mode 100644 examples/test-sequencer-robot-framework-example/flojoy_requirements.txt diff --git a/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy index 4c6042803..4fe39879c 100644 --- a/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy +++ b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy @@ -1 +1 @@ -{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"35d63b42-c0b9-4a2e-900e-e327404a8c41","groupId":"40216fc8-785a-4610-913e-90bd54f157af","path":"TestExample.robot","testName":"TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z"},{"type":"test","id":"36374991-c721-40c6-8a35-68a996fe6ed4","groupId":"d8a898b4-a012-4177-9f9a-d07c3ab5f84e","path":"TestExample.robot","testName":"TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z","minValue":2,"maxValue":4.2,"unit":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}} \ No newline at end of file +{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"35d63b42-c0b9-4a2e-900e-e327404a8c41","groupId":"40216fc8-785a-4610-913e-90bd54f157af","path":"TestExample.robot","testName":"TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z"},{"type":"test","id":"36374991-c721-40c6-8a35-68a996fe6ed4","groupId":"d8a898b4-a012-4177-9f9a-d07c3ab5f84e","path":"TestExample.robot","testName":"TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z","minValue":2,"maxValue":4.2,"unit":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":null}} diff --git a/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt b/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt deleted file mode 100644 index f1e0ac08a..000000000 --- a/examples/test-sequencer-robot-framework-example/flojoy_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -psutil==5.9.8 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 318d4d4ee..aefb37fb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,17 +235,17 @@ typecheck = ["mypy"] [[package]] name = "boto3" -version = "1.34.92" +version = "1.34.93" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.92-py3-none-any.whl", hash = "sha256:db7bbb1c6059e99b74dcf634e497b04addcac4c527ae2b2696e47c39eccc6c50"}, - {file = "boto3-1.34.92.tar.gz", hash = "sha256:684cba753d64978a486e8ea9645d53de0d4e3b4a3ab1495b26bd04b9541cea2d"}, + {file = "boto3-1.34.93-py3-none-any.whl", hash = "sha256:b59355bf4a1408563969526f314611dbeacc151cf90ecb22af295dcc4fe18def"}, + {file = "boto3-1.34.93.tar.gz", hash = "sha256:e39516e4ca21612932599819662759c04485d53ca457996a913163da11f052a4"}, ] [package.dependencies] -botocore = ">=1.34.92,<1.35.0" +botocore = ">=1.34.93,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -254,13 +254,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.92" +version = "1.34.93" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.92-py3-none-any.whl", hash = "sha256:4211a22a1f6c6935e70cbb84c2cd93b29f9723eaf5036d59748dd104f389a681"}, - {file = "botocore-1.34.92.tar.gz", hash = "sha256:d1ca4886271f184445ec737cd2e752498648cca383887c5a37b2e01c8ab94039"}, + {file = "botocore-1.34.93-py3-none-any.whl", hash = "sha256:6fbd5a53a2adc9b3d4ebd90ae0ede83a91a41d96231f8a5984051f75495f246d"}, + {file = "botocore-1.34.93.tar.gz", hash = "sha256:79d39b0b87e962991c6dd55e78ce15155099f6fb741be88b1b8a456a702cc150"}, ] [package.dependencies] @@ -785,13 +785,13 @@ test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailu [[package]] name = "dataclasses-json" -version = "0.6.4" +version = "0.6.5" description = "Easily serialize dataclasses to and from JSON." optional = false -python-versions = ">=3.7,<4.0" +python-versions = "<4.0,>=3.7" files = [ - {file = "dataclasses_json-0.6.4-py3-none-any.whl", hash = "sha256:f90578b8a3177f7552f4e1a6e535e84293cd5da421fcce0642d49c0d7bdf8df2"}, - {file = "dataclasses_json-0.6.4.tar.gz", hash = "sha256:73696ebf24936560cca79a2430cbc4f3dd23ac7bf46ed17f38e5e5e7657a6377"}, + {file = "dataclasses_json-0.6.5-py3-none-any.whl", hash = "sha256:f49c77aa3a85cac5bf5b7f65f4790ca0d2be8ef4d92c75e91ba0103072788a39"}, + {file = "dataclasses_json-0.6.5.tar.gz", hash = "sha256:1c287594d9fcea72dc42d6d3836cf14848c2dc5ce88f65ed61b36b57f515fe26"}, ] [package.dependencies] @@ -2589,30 +2589,30 @@ grpc = ["grpcio (>=1.49.0,<2.0)", "protobuf (>=4.21,<5.0)"] [[package]] name = "nidmm" -version = "1.4.7" +version = "1.4.8" description = "NI-DMM Python API" optional = false python-versions = "*" files = [ - {file = "nidmm-1.4.7-py3-none-any.whl", hash = "sha256:b6616977647dff55b93a1087355a1775fbd4b085716d6e51b65cc91ca41a7ccb"}, - {file = "nidmm-1.4.7.tar.gz", hash = "sha256:9b688403d3b74304d666ba5123a9bd0cd76bf18e9b5eae210baec2b91d8fcaa7"}, + {file = "nidmm-1.4.8-py3-none-any.whl", hash = "sha256:6c0a4da4e4b6898d3e490148643142cc56e5135d400f4957e384da10c6ca346a"}, + {file = "nidmm-1.4.8.tar.gz", hash = "sha256:17526b0d22cde02409632adf6cef739fe5605f237c19b0f679853d3fd323d1ce"}, ] [package.dependencies] hightime = ">=0.2.0" [package.extras] -grpc = ["grpcio (>=1.49.1,<2.0)", "protobuf (>=4.21,<5.0)"] +grpc = ["grpcio (>=1.59.0,<2.0)", "protobuf (>=4.21.6,<5.0)"] [[package]] name = "nimodinst" -version = "1.4.7" +version = "1.4.8" description = "NI-ModInst Python API" optional = false python-versions = "*" files = [ - {file = "nimodinst-1.4.7-py3-none-any.whl", hash = "sha256:9d950277e0227c1983bd850771cdc006af1a07760ee0556d84a1de0636bf92f8"}, - {file = "nimodinst-1.4.7.tar.gz", hash = "sha256:bd8277b69e488d357bb7dec0bb78633f3ed0633e0310993aa351a63015fc7a3b"}, + {file = "nimodinst-1.4.8-py3-none-any.whl", hash = "sha256:3a704dfd8e4a033ec483cc69cf31568889e445cb796216b823d8350da9b132c3"}, + {file = "nimodinst-1.4.8.tar.gz", hash = "sha256:21498ab3e06ebc2a643df0b60c97692046a7d5e77037bb50fe089a135e00109a"}, ] [package.dependencies] @@ -4582,13 +4582,13 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"] [[package]] name = "threadpoolctl" -version = "3.4.0" +version = "3.5.0" description = "threadpoolctl" optional = false python-versions = ">=3.8" files = [ - {file = "threadpoolctl-3.4.0-py3-none-any.whl", hash = "sha256:8f4c689a65b23e5ed825c8436a92b818aac005e0f3715f6a1664d7c7ee29d262"}, - {file = "threadpoolctl-3.4.0.tar.gz", hash = "sha256:f11b491a03661d6dd7ef692dd422ab34185d982466c49c8f98c8f716b5c93196"}, + {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, + {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, ] [[package]] @@ -4946,13 +4946,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "versioningit" -version = "3.1.0" +version = "3.1.1" description = "Versioning It with your Version In Git" optional = false python-versions = ">=3.7" files = [ - {file = "versioningit-3.1.0-py3-none-any.whl", hash = "sha256:a2f94968a37fac1192b5b6465356c64e77ae05fc7d32a7d7dddf10339ad147a0"}, - {file = "versioningit-3.1.0.tar.gz", hash = "sha256:7aac713c31a53eb367a6bbc2e8b3de8cc2b86d10d45c5101afd651446cb10fd7"}, + {file = "versioningit-3.1.1-py3-none-any.whl", hash = "sha256:3f5a855d98a2a85e909264f2eac3cce380b71f7632e7de55bc22bff9f03639ee"}, + {file = "versioningit-3.1.1.tar.gz", hash = "sha256:b0ba586e5af08b87dbe3354082910a1d0502c36202d496e1ae60ef3b41ee29c1"}, ] [package.dependencies] @@ -5376,4 +5376,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "8c34c30422817371ebe2752058632e8451c5779fa31e1b3668da1ea09ac06343" +content-hash = "2aa25d13fe76989a7c08732bdf1724e3ccbdb616ee7d22c31d8c79a6af5ecb0a" diff --git a/pyproject.toml b/pyproject.toml index 06013ab45..91f5aa664 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,6 @@ optional = true scikit-learn = "^1.3.2" [tool.poetry.group.user.dependencies] -psutil = "5.9.8" [build-system] requires = ["poetry-core"] From 09856c86b3de0a2c770f1176d56dbac3949f668c Mon Sep 17 00:00:00 2001 From: Gui Date: Mon, 29 Apr 2024 10:03:32 -0700 Subject: [PATCH 09/10] fix: missings args (#1178) --- .../Robot_Sequence.tjoy | 2 +- .../routes/test_sequencer_panel/utils/SequenceHandler.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy index 4fe39879c..5a9bb1cdd 100644 --- a/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy +++ b/examples/test-sequencer-robot-framework-example/Robot_Sequence.tjoy @@ -1 +1 @@ -{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"35d63b42-c0b9-4a2e-900e-e327404a8c41","groupId":"40216fc8-785a-4610-913e-90bd54f157af","path":"TestExample.robot","testName":"TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z"},{"type":"test","id":"36374991-c721-40c6-8a35-68a996fe6ed4","groupId":"d8a898b4-a012-4177-9f9a-d07c3ab5f84e","path":"TestExample.robot","testName":"TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-24T15:35:41.832Z","minValue":2,"maxValue":4.2,"unit":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":null}} +{"name":"Robot_Sequence","description":"Consult the code to learn more!","elems":[{"type":"test","id":"83e8caff-1e57-498d-a167-1099f24e5f16","groupId":"32ae326b-0396-4bf8-a640-020ab37d5393","path":"TestExample.robot","testName":"TestExample.TEST EXPORT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-29T15:59:13.242Z","args":["TestExample.TEST EXPORT"]},{"type":"test","id":"7bad78a8-04e6-456d-9e7a-4363d10b4699","groupId":"dfd20ed6-4052-4616-a413-f641113ed3bb","path":"TestExample.robot","testName":"TestExample.TEST ASSERT","runInParallel":false,"testType":"robotframework","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-29T15:59:13.242Z","minValue":1,"maxValue":3,"unit":"","args":["TestExample.TEST ASSERT"]}],"projectPath":"/Users/guillaumethibault/Documents/flojoy/repo/studio/examples/test-sequencer-robot-framework-example/","interpreter":{"type":"flojoy","path":null,"requirementsPath":null}} \ No newline at end of file diff --git a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts index d2924237e..d0cdf8129 100644 --- a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts +++ b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts @@ -17,6 +17,7 @@ import { } from "@/renderer/types/test-sequencer"; import { createNewTest } from "@/renderer/hooks/useTestSequencerState"; import { err, ok, Result } from "neverthrow"; +import { toast } from "sonner"; // Exposed API export type StateManager = { @@ -235,7 +236,10 @@ async function installDeps(sequence: TestSequencerProject): Promise { const success = await window.api.poetryInstallRequirementsUserGroup( sequence.projectPath + sequence.interpreter.requirementsPath, ); - return success; + if (!success) { + toast.error("Not able to installing dependencies"); + } + return true; } async function syncSequence( @@ -285,6 +289,7 @@ async function createExportableSequenceElementsFromTestSequencerElements( minValue: elem.minValue, maxValue: elem.maxValue, unit: elem.unit, + args: elem.args, }) : { ...elem, @@ -311,6 +316,7 @@ async function createTestSequencerElementsFromSequenceElements( minValue: elem.minValue, maxValue: elem.maxValue, unit: elem.unit, + args: elem.args, }) : { ...elem, From a887e6e4d0fc0053216a0fd85a66397e3626030a Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 29 Apr 2024 17:09:35 -0400 Subject: [PATCH 10/10] chore(auth): auth backend now just a middleware to retreive info and permission --- captain/middleware/auth_middleware.py | 85 ++++++++++++++++++++------- captain/routes/auth.py | 20 +++---- captain/routes/cloud.py | 85 +++++++++++++++------------ captain/routes/key.py | 8 +-- captain/services/auth/auth_service.py | 55 ++++++++++++----- captain/types/auth.py | 13 +++- src/main/store.ts | 2 +- src/types/auth.ts | 8 ++- 8 files changed, 179 insertions(+), 97 deletions(-) diff --git a/captain/middleware/auth_middleware.py b/captain/middleware/auth_middleware.py index f5c04cf39..dc20c1a24 100644 --- a/captain/middleware/auth_middleware.py +++ b/captain/middleware/auth_middleware.py @@ -1,39 +1,82 @@ +from typing import Callable, Optional from fastapi import Request, HTTPException, status -from captain.services.auth.auth_service import validate_credentials import base64 +from captain.services.auth.auth_service import get_user, has_cloud_access, has_write_access +from captain.types.auth import Auth -async def is_admin(req: Request): +def _with_verify_access(func: Callable[[str, str]]): + async def wrapper(req: Request): + exception_txt = "You are not authorized to perform this action" + studio_cookie = req.cookies.get("studio-auth") + + if not studio_cookie: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=exception_txt, + ) + + try: + credentials = base64.b64decode(studio_cookie).decode("utf-8") + username, token = credentials.split(":", 1) + authorized = has_cloud_access(username, token) + func(username, token) + + if not authorized: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=exception_txt, + ) + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=exception_txt, + ) + return wrapper + + +@_with_verify_access +async def can_write(username, token): + """ + Middleware to check if the user can modify protected resources + Example of use + @router.get("/write", dependencies=[Depends(can_write)]) + async def update(): + return "resource updated" """ - Middleware to check if the user is an admin + return has_write_access(username, token) + +@_with_verify_access +async def is_connected(username, token): + """ + Middleware to check if the user has access to the cloud Example of use - @router.get("/write", dependencies=[Depends(is_admin)]) + @router.get("/write", dependencies=[Depends(is_connected)]) async def update(): return "resource updated" + """ + return has_cloud_access(username, token) + +def retreive_user(req: Request) -> Auth: + """ + Access the information store in the current user + Should be use in tendem with the `dependencies=[Depends(is_connected)]` middleware + - Raise an HTTPException if the user is not connected """ - exception_txt = "You are not authorized to perform this action" studio_cookie = req.cookies.get("studio-auth") - if not studio_cookie: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=exception_txt, + detail="User is not connected" ) - - try: - credentials = base64.b64decode(studio_cookie).decode("utf-8") - username, password = credentials.split(":", 1) - authorized = validate_credentials(username, password) - - if not authorized: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=exception_txt, - ) - except Exception: + credentials = base64.b64decode(studio_cookie).decode("utf-8") + username, token = credentials.split(":", 1) + user = get_user(username, token) + if user is None: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=exception_txt, + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" ) + return user diff --git a/captain/routes/auth.py b/captain/routes/auth.py index 4891d6367..53d28877c 100644 --- a/captain/routes/auth.py +++ b/captain/routes/auth.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Response from captain.services.auth.auth_service import ( - validate_credentials, + save_user, get_base64_credentials, ) from captain.types.auth import Auth @@ -10,18 +10,12 @@ @router.post("/auth/login/") async def login(response: Response, auth: Auth): - if not validate_credentials(auth.username, auth.password): - response.set_cookie( - key="studio-auth", - value="", - path="/", - samesite="none", - secure=True, - ) - return "Login failed" - - encoded_credentials = get_base64_credentials(auth.username, auth.password) - + """ Login to the backend of the app + - Actual auth with password and username is done in the frontend with cloud + - Backend auth serves as a middleware to store Cloud credentials + """ + save_user(auth) + encoded_credentials = get_base64_credentials(auth.username, auth.token) response.set_cookie( key="studio-auth", value=encoded_credentials, diff --git a/captain/routes/cloud.py b/captain/routes/cloud.py index de329a92e..2be76d243 100644 --- a/captain/routes/cloud.py +++ b/captain/routes/cloud.py @@ -1,8 +1,7 @@ import json import logging import requests -from fastapi import APIRouter, Header, Response -from flojoy.env_var import get_env_var, get_flojoy_cloud_url +from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response from flojoy_cloud import test_sequencer from pydantic import BaseModel, Field from typing import Annotated, Optional @@ -11,6 +10,9 @@ import pandas as pd from functools import wraps +from captain.middleware.auth_middleware import is_connected, retreive_user +from captain.types.auth import Auth + # Utils ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -49,10 +51,10 @@ def inner(*args, **kwargs): return decorator -async def get_cloud_part_variation(part_variation_id: str): +async def get_cloud_part_variation(part_variation_id: str, user: Auth): logging.info("Querying part variation") - url = get_flojoy_cloud_url() + "partVariation/" + part_variation_id - response = requests.get(url, headers=headers_builder()) + url = user.url + "partVariation/" + part_variation_id + response = requests.get(url, headers=headers_builder(user)) res = response.json() res["partVariationId"] = part_variation_id logging.info("Part variation retrieved: %s", res) @@ -65,6 +67,8 @@ class SecretNotFound(Exception): def error_response_builder(e: Exception) -> Response: logging.error(f"Error from Flojoy Cloud: {e}") + if isinstance(e, HTTPException): + return Response(status_code=e.status_code, content=json.dumps(e.__str__)) if isinstance(e, SecretNotFound): return Response(status_code=401, content=json.dumps([])) else: @@ -72,17 +76,14 @@ def error_response_builder(e: Exception) -> Response: @temporary_cache -def headers_builder(with_workspace_id=True) -> dict: - workspace_secret = get_env_var("FLOJOY_CLOUD_WORKSPACE_SECRET") +def headers_builder(user: Auth, with_workspace_id=True) -> dict: logging.info("Querying workspace current") - if workspace_secret is None: - raise SecretNotFound headers = { "Content-Type": "application/json", - "flojoy-workspace-personal-secret": workspace_secret, + "flojoy-workspace-personal-secret": user.token, } if with_workspace_id: - url = get_flojoy_cloud_url() + "workspace/" + url = user.url + "workspace/" response = requests.get(url, headers=headers) if response.status_code != 200: logging.error(f"Failed to get workspace id {url}: {response.text}") @@ -187,10 +188,10 @@ def get_measurement(m: Measurement) -> MeasurementData: return data -async def get_part(part_id: str) -> Part: +async def get_part(part_id: str, user: Auth) -> Part: logging.info("Querying part") - url = get_flojoy_cloud_url() + "part/" + part_id - response = requests.get(url, headers=headers_builder()) + url = user.url + "part/" + part_id + response = requests.get(url, headers=headers_builder(user)) return Part(**response.json()) @@ -200,22 +201,23 @@ async def get_part(part_id: str) -> Part: router = APIRouter(tags=["cloud"]) -@router.get("/cloud/projects/") -async def get_cloud_projects(): +@router.get("/cloud/projects/", dependencies=[Depends(is_connected)]) +async def get_cloud_projects(req: Request): """ Get all projects from the Flojoy Cloud. """ try: logging.info("Querying projects") - url = get_flojoy_cloud_url() + "project/" - response = requests.get(url, headers=headers_builder()) + user = retreive_user(req) + url = user.url + "project/" + response = requests.get(url, headers=headers_builder(user)) if response.status_code != 200: return Response(status_code=response.status_code, content=json.dumps([])) projects = [Project(**project_data) for project_data in response.json()] projects_res = [] for p in projects: - part_var = await get_cloud_part_variation(p.part_variation_id) - part = await get_part(part_var.part_id) + part_var = await get_cloud_part_variation(p.part_variation_id, user) + part = await get_part(part_var.part_id, user) projects_res.append( { "label": p.name, @@ -234,16 +236,17 @@ async def get_cloud_projects(): return error_response_builder(e) -@router.get("/cloud/stations/{project_id}") -async def get_cloud_stations(project_id: str): +@router.get("/cloud/stations/{project_id}", dependencies=[Depends(is_connected)]) +async def get_cloud_stations(project_id: str, req: Request): """ Get all station of a project from the Flojoy Cloud. """ try: logging.info("Querying stations") - url = get_flojoy_cloud_url() + "station/" + user = retreive_user(req) + url = user.url + "station/" querystring = {"projectId": project_id} - response = requests.get(url, headers=headers_builder(), params=querystring) + response = requests.get(url, headers=headers_builder(user), params=querystring) if response.status_code != 200: logging.error(f"Error getting stations from Flojoy Cloud: {response.text}") return Response(status_code=response.status_code, content=json.dumps([])) @@ -256,12 +259,13 @@ async def get_cloud_stations(project_id: str): return error_response_builder(e) -@router.get("/cloud/partVariation/{part_var_id}/unit") -async def get_cloud_variant_unit(part_var_id: str): +@router.get("/cloud/partVariation/{part_var_id}/unit", dependencies=[Depends(is_connected)]) +async def get_cloud_variant_unit(part_var_id: str, req: Request): try: logging.info(f"Querying unit for part {part_var_id}") - url = f"{get_flojoy_cloud_url()}partVariation/{part_var_id}/unit" - response = requests.get(url, headers=headers_builder()) + user = retreive_user(req) + url = f"{user.url}partVariation/{part_var_id}/unit" + response = requests.get(url, headers=headers_builder(user)) if response.status_code != 200: logging.error(f"Error getting stations from Flojoy Cloud: {response.text}") return Response(status_code=response.status_code, content=json.dumps([])) @@ -272,11 +276,12 @@ async def get_cloud_variant_unit(part_var_id: str): return error_response_builder(e) -@router.post("/cloud/session/") -async def post_cloud_session(_: Response, body: Session): +@router.post("/cloud/session/", dependencies=[Depends(is_connected)]) +async def post_cloud_session(_: Response, body: Session, req: Request): try: logging.info("Posting session") - url = get_flojoy_cloud_url() + "session/" + user = retreive_user(req) + url = user.url + "session/" payload = body.model_dump(by_alias=True) payload["createdAt"] = utcnow_str() for i, m in enumerate(payload["measurements"]): @@ -284,7 +289,7 @@ async def post_cloud_session(_: Response, body: Session): m["pass"] = m.pop("pass_") m["durationMs"] = int(m.pop("completionTime") * 1000) del m["unit"] - response = requests.post(url, json=payload, headers=headers_builder()) + response = requests.post(url, json=payload, headers=headers_builder(user)) if response.status_code == 200: return Response(status_code=200, content=json.dumps(response.json())) else: @@ -296,15 +301,19 @@ async def post_cloud_session(_: Response, body: Session): return error_response_builder(e) -@router.get("/cloud/user/") -async def get_user_info(secret: Annotated[str | None, Header()]): +# Verify cloud connection ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +@router.get("/cloud/user/", dependencies=[Depends(is_connected)]) +async def get_user_info(req: Request, secret: Annotated[str | None, Header()]): try: logging.info("Querying user info") - url = get_flojoy_cloud_url() + "user/" + user = retreive_user(req) + url = user.url + "user/" headers = ( {"flojoy-workspace-personal-secret": secret} if secret - else headers_builder(with_workspace_id=False) + else headers_builder(user, with_workspace_id=False) ) response = requests.get(url, headers=headers) if response.status_code == 200: @@ -319,11 +328,11 @@ async def get_user_info(secret: Annotated[str | None, Header()]): @router.get("/cloud/health/") -async def get_cloud_health(url: Annotated[str | None, Header()]): +async def get_cloud_health(url: Annotated[str | None, Header()], req: Request): try: logging.info("Querying health") if url is None: - url = get_flojoy_cloud_url() + url = retreive_user(req).url url = url + "health/" response = requests.get(url) if response.status_code == 200: diff --git a/captain/routes/key.py b/captain/routes/key.py index ed8bd0cdf..93fbfbfd3 100644 --- a/captain/routes/key.py +++ b/captain/routes/key.py @@ -2,14 +2,14 @@ from fastapi import APIRouter, Response, status, Depends from flojoy import delete_env_var, get_credentials, get_env_var, set_env_var -from captain.middleware.auth_middleware import is_admin +from captain.middleware.auth_middleware import can_write, is_connected from captain.types.key import EnvVar from captain.utils.logger import logger router = APIRouter(tags=["env"]) -@router.post("/env/", dependencies=[Depends(is_admin)]) +@router.post("/env/", dependencies=[Depends(can_write)]) async def set_env_var_route(env_var: EnvVar): try: set_env_var(env_var.key, env_var.value) @@ -25,7 +25,7 @@ async def set_env_var_route(env_var: EnvVar): return Response(status_code=200) -@router.delete("/env/{key_name}", dependencies=[Depends(is_admin)]) +@router.delete("/env/{key_name}", dependencies=[Depends(can_write)]) async def delete_env_var_route(key_name: str): try: delete_env_var(key_name) @@ -36,7 +36,7 @@ async def delete_env_var_route(key_name: str): return Response(status_code=200) -@router.get("/env/{key_name}", response_model=EnvVar, dependencies=[Depends(is_admin)]) +@router.get("/env/{key_name}", response_model=EnvVar, dependencies=[Depends(can_write)]) async def get_env_var_by_name_route(key_name: str): value: Optional[str] = get_env_var(key_name) if value is None: diff --git a/captain/services/auth/auth_service.py b/captain/services/auth/auth_service.py index fb09e2233..be90e6e2f 100644 --- a/captain/services/auth/auth_service.py +++ b/captain/services/auth/auth_service.py @@ -1,28 +1,33 @@ import os import json +from typing import Optional import bcrypt import base64 +from captain.types.auth import Auth -def compare_pass(hashed_password: str, plain_password: str): +def compare_pass(hashed_token: str, plain_token: str): return bcrypt.checkpw( - plain_password.encode("utf-8"), hashed_password.encode("utf-8") + plain_token.encode("utf-8"), hashed_token.encode("utf-8") ) -def validate_credentials(username: str, password: str): - user = get_user(username) - - if (not user) or user.get("role") != "Admin": +def has_cloud_access(username: str, plain_token: str) -> bool: + user = get_user(username, plain_token) + if not user or user == "Local": return False + return True - if user.get("password") and (not compare_pass(user.get("password", ""), password)): - return False +def has_write_access(username: str, plain_token: str) -> bool: + user = get_user(username, plain_token) + if not user or user.permission == "Operator": + return False return True -def get_user(username: str): +def get_user(username: str, plain_token: str) -> Optional[Auth]: + """ Get the current user if it exists in the local db """ db_path = os.environ.get("LOCAL_DB_PATH", None) if not db_path: @@ -30,16 +35,34 @@ def get_user(username: str): if not os.path.exists(db_path): return None - with open(db_path, "r") as f: config = json.load(f) - users: list[dict[str, str]] = config.get("users", []) - for user in users: - if user.get("name") == username: - return user + user = config.get("user", None) + user = Auth(**user) + if not compare_pass(user.token, plain_token): + return None + if user and user.username == username: + user.token = plain_token + return user return None -def get_base64_credentials(username: str, password: str): - return base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8") +def save_user(user: Auth) -> bool: + """ Save user to local db, return True if successful """ + db_path = os.environ.get("LOCAL_DB_PATH", None) + if not db_path: + return False + + if not os.path.exists(db_path): + open(db_path, "x").close() + + with open(db_path, "rw") as f: + config = json.load(f) + config.user = user + json.dump(config, f) + return True + + +def get_base64_credentials(username: str, workspace_token: str): + return base64.b64encode(f"{username}:{workspace_token}".encode("utf-8")).decode("utf-8") diff --git a/captain/types/auth.py b/captain/types/auth.py index 2d910283d..6cc97f499 100644 --- a/captain/types/auth.py +++ b/captain/types/auth.py @@ -1,6 +1,17 @@ +from enum import StrEnum from pydantic import BaseModel +class Permission(StrEnum): + admin = "Admin" + operator = "Operator" + localUser = "Local" + + class Auth(BaseModel): username: str - password: str + permission: Permission + # If connected with cloud + token: str + url: str + diff --git a/src/main/store.ts b/src/main/store.ts index 01ab6e3e5..b52fb14f6 100644 --- a/src/main/store.ts +++ b/src/main/store.ts @@ -13,7 +13,7 @@ export const store = new Store({ users: [ { name: os.userInfo().username, - role: "Admin", + role: "Local", logged: true, }, ], diff --git a/src/types/auth.ts b/src/types/auth.ts index 89649eb64..42d3d0bac 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -1,9 +1,11 @@ -export const allRoles = ["Admin", "Operator"] as const; +export const allRoles = ["Admin", "Operator", "Local"] as const; export type Role = (typeof allRoles)[number]; export type User = { - name: string; role: Role; - password?: string; + // Field for if connected to a workspace + name: string; + token?: string; + workspace?: string; logged?: boolean; };