From 1b628dd4aaadaf793f1d6e20037f8e549864ecf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Schl=C3=A4fli?= Date: Mon, 26 Aug 2024 10:54:08 +0200 Subject: [PATCH 01/38] chore: rename sessions to quizzes and start working on new navbar component (#4181) Co-authored-by: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Co-authored-by: sjschlapbach --- .../components/sessions/EmbeddingModal.tsx | 2 +- apps/frontend-control/tsconfig.json | 1 + apps/frontend-manage/next.config.js | 29 ++++ .../src/components/common/Header.tsx | 130 +++++++++--------- .../actions/EvaluationLinkLiveQuiz.tsx | 2 +- .../courses/actions/RunningLiveQuizLink.tsx | 2 +- .../courses/actions/StartLiveQuizButton.tsx | 2 +- .../interaction/AudienceInteraction.tsx | 2 +- .../components/sessions/EmbeddingModal.tsx | 2 +- .../src/components/sessions/Session.tsx | 4 +- .../sessions/cockpit/CancelSessionModal.tsx | 2 +- .../sessions/cockpit/SessionTimeline.tsx | 2 +- .../creation/liveQuiz/LiveSessionWizard.tsx | 4 +- .../evaluation/EvaluationControlBar.tsx | 2 +- .../src/pages/analytics/index.tsx | 16 +++ apps/frontend-manage/src/pages/index.tsx | 4 +- .../{sessions => quizzes}/[id]/cockpit.tsx | 6 +- .../{sessions => quizzes}/[id]/evaluation.tsx | 0 .../{sessions => quizzes}/[id]/lecturer.tsx | 0 .../src/pages/{sessions => quizzes}/index.tsx | 4 +- apps/frontend-manage/tsconfig.json | 1 + apps/frontend-pwa/tsconfig.json | 1 + .../src/content/components/App.tsx | 2 +- .../src/content/components/URLForm.tsx | 4 +- .../cypress/e2e/F-live-quiz-workflow.cy.ts | 20 +-- .../e2e/H-practice-quiz-workflow.cy.ts | 6 +- .../e2e/K-group-activity-workflow.cy.ts | 2 +- packages/i18n/messages/de.ts | 3 + packages/i18n/messages/en.ts | 3 + 29 files changed, 156 insertions(+), 102 deletions(-) create mode 100644 apps/frontend-manage/src/pages/analytics/index.tsx rename apps/frontend-manage/src/pages/{sessions => quizzes}/[id]/cockpit.tsx (97%) rename apps/frontend-manage/src/pages/{sessions => quizzes}/[id]/evaluation.tsx (100%) rename apps/frontend-manage/src/pages/{sessions => quizzes}/[id]/lecturer.tsx (100%) rename apps/frontend-manage/src/pages/{sessions => quizzes}/index.tsx (98%) diff --git a/apps/frontend-control/src/components/sessions/EmbeddingModal.tsx b/apps/frontend-control/src/components/sessions/EmbeddingModal.tsx index fe43960e9c..717199721c 100644 --- a/apps/frontend-control/src/components/sessions/EmbeddingModal.tsx +++ b/apps/frontend-control/src/components/sessions/EmbeddingModal.tsx @@ -35,7 +35,7 @@ function LazyHMACLink({ const link = `${ process.env.NEXT_PUBLIC_MANAGE_URL - }/sessions/${sessionId}/evaluation?hmac=${sessionHMAC.data?.sessionHMAC}${ + }/quizzes/${sessionId}/evaluation?hmac=${sessionHMAC.data?.sessionHMAC}${ params ? `&${params}` : '' }` diff --git a/apps/frontend-control/tsconfig.json b/apps/frontend-control/tsconfig.json index 223493b022..44d7553101 100644 --- a/apps/frontend-control/tsconfig.json +++ b/apps/frontend-control/tsconfig.json @@ -16,6 +16,7 @@ "incremental": true, "baseUrl": ".", "paths": { + "~/*": ["src/*"], "@components/*": ["src/components/*"], "@lib/*": ["src/lib/*"], "@pages/*": ["src/pages/*"], diff --git a/apps/frontend-manage/next.config.js b/apps/frontend-manage/next.config.js index c7bfccb09f..ff9c98c2d8 100644 --- a/apps/frontend-manage/next.config.js +++ b/apps/frontend-manage/next.config.js @@ -8,6 +8,35 @@ const nextConfig = { ...getNextBaseConfig({ BLOB_STORAGE_ACCOUNT_URL: process.env.BLOB_STORAGE_ACCOUNT_URL, }), + async redirects() { + return [ + { + source: '/sessions', + destination: '/quizzes', + permanent: true, + }, + { + source: '/sessions/:id', + destination: '/quizzes/:id', + permanent: true, + }, + { + source: '/sessions/:id/cockpit', + destination: '/quizzes/:id/cockpit', + permanent: true, + }, + { + source: '/sessions/:id/evaluation', + destination: '/quizzes/:id/evaluation', + permanent: true, + }, + { + source: '/sessions/:id/lecturer', + destination: '/quizzes/:id/lecturer', + permanent: true, + }, + ] + }, } if (process.env.NODE_ENV !== 'test') { diff --git a/apps/frontend-manage/src/components/common/Header.tsx b/apps/frontend-manage/src/components/common/Header.tsx index 3d198ce1ef..f14d6a65ac 100644 --- a/apps/frontend-manage/src/components/common/Header.tsx +++ b/apps/frontend-manage/src/components/common/Header.tsx @@ -30,15 +30,15 @@ function Header({ user }: HeaderProps): React.ReactElement { const navigationItems = [ { href: '/', - label: t('manage.general.questionPool'), + label: t('manage.general.library'), active: router.pathname == '/', - cy: 'questions', + cy: 'library', }, { - href: '/sessions', - label: t('manage.general.sessions'), - active: router.pathname == '/sessions', - cy: 'sessions', + href: '/quizzes', + label: t('manage.general.quizzes'), + active: router.pathname == '/quizzes', + cy: 'quizzes', }, { href: '/courses', @@ -46,14 +46,24 @@ function Header({ user }: HeaderProps): React.ReactElement { active: router.pathname == '/courses', cy: 'courses', }, + { + href: '/analytics', + label: t('manage.general.analytics'), + active: router.pathname == '/analytics', + cy: 'analytics', + }, ] return (
- + {navigationItems.map((item) => ( { router.push(item.href) @@ -73,74 +83,64 @@ function Header({ user }: HeaderProps): React.ReactElement { /> ))} - - router.push('/migration')} - label={t('manage.general.migration')} + + + } + dropdownWidth="w-36" className={{ - label: twMerge( - 'font-bold text-base bg-left-bottom bg-gradient-to-r from-white to-white bg-[length:0%_2px] bg-no-repeat group-hover:bg-[length:100%_2px] transition-all duration-500 ease-out', - router.pathname === '/migration' && - 'text-red underline underline-offset-[0.3rem] decoration-2' + root: 'hidden md:block h-9 w-9 group', + icon: twMerge( + data?.userRunningSessions?.length !== 0 && 'text-green-600' ), - root: 'group text-white hover:bg-inherit transition-all duration-300 ease-in-out', + disabled: '!text-gray-400', }} - /> -
- - } - dropdownWidth="w-[12rem]" - className={{ - root: 'h-10 w-2 group', - icon: twMerge( - 'text-uzh-grey-80', - data?.userRunningSessions?.length !== 0 && 'text-green-600' - ), - disabled: '!text-gray-400', - dropdown: 'p-1.5 gap-0', - }} - disabled={data?.userRunningSessions?.length === 0} - > - {data?.userRunningSessions && - data?.userRunningSessions.length > 0 ? ( - data?.userRunningSessions.map((session) => { - return ( - - router.push(`/sessions/${session.id}/cockpit`) - } - className={{ title: 'text-base font-bold', root: 'p-2' }} - /> - ) - }) - ) : ( -
- )} - -
+ disabled={data?.userRunningSessions?.length === 0} + > + {data?.userRunningSessions && data?.userRunningSessions.length > 0 ? ( + data?.userRunningSessions.map((session) => { + return ( + router.push(`/quizzes/${session.id}/cockpit`)} + className={{ title: 'text-base font-bold', root: 'p-2' }} + /> + ) + }) + ) : ( +
+ )} + setShowSupportModal(true)} label="" - icon={} + icon={} className={{ - root: 'hidden md:block h-7 group-hover:text-white bg-transparent hover:bg-transparent text-white hover:text-uzh-blue-40 -mt-1', + root: 'hidden md:block h-9 group-hover:text-white bg-transparent hover:bg-transparent text-slate-700 hover:text-uzh-blue-100', }} /> } + icon={ + + } label={user?.shortname} dropdownWidth="w-[16rem]" className={{ label: - 'my-auto font-bold text-base bg-left-bottom bg-gradient-to-r from-white to-white bg-[length:0%_2px] bg-no-repeat group-hover:bg-[length:100%_2px] transition-all duration-500 ease-out', - root: 'group flex flex-row items-center gap-1 text-white hover:bg-inherit transition-all duration-300 ease-in-out', + 'text-base bg-left-bottom bg-gradient-to-r from-slate-700 to-slate-700 bg-[length:0%_2px] bg-no-repeat group-hover:bg-[length:100%_2px] transition-all duration-500 ease-out text-slate-700', + root: 'group flex flex-row items-center gap-1 text-slate-700 hover:bg-inherit transition-all duration-300 ease-in-out', dropdown: 'p-1.5 gap-0', }} data={{ cy: 'user-menu' }} diff --git a/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx b/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx index 5a7e039df9..d008277f81 100644 --- a/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx +++ b/apps/frontend-manage/src/components/courses/actions/EvaluationLinkLiveQuiz.tsx @@ -15,7 +15,7 @@ function EvaluationLinkLiveQuiz({ liveQuiz }: EvaluationLinkLiveQuizProps) {
- + {t('manage.course.runningSession')} diff --git a/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx b/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx index e5f4878cbf..527edfe46a 100644 --- a/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx +++ b/apps/frontend-manage/src/components/courses/actions/StartLiveQuizButton.tsx @@ -24,7 +24,7 @@ function StartLiveQuizButton({ liveQuiz }: StartLiveQuizButtonProps) { await startSession({ variables: { id: liveQuiz.id || '' }, }) - router.push(`/sessions/${liveQuiz.id}/cockpit`) + router.push(`/quizzes/${liveQuiz.id}/cockpit`) } catch (error) { console.log(error) } diff --git a/apps/frontend-manage/src/components/interaction/AudienceInteraction.tsx b/apps/frontend-manage/src/components/interaction/AudienceInteraction.tsx index 9b31a4c4e4..6d12e84df5 100644 --- a/apps/frontend-manage/src/components/interaction/AudienceInteraction.tsx +++ b/apps/frontend-manage/src/components/interaction/AudienceInteraction.tsx @@ -88,7 +88,7 @@ function AudienceInteraction({

{t('manage.cockpit.liveQA')}

@@ -192,7 +192,7 @@ function Session({ session }: SessionProps) { )} {SessionStatus.Completed === session.status && ( diff --git a/apps/frontend-manage/src/components/sessions/cockpit/CancelSessionModal.tsx b/apps/frontend-manage/src/components/sessions/cockpit/CancelSessionModal.tsx index a0720321fe..293c47a9dd 100644 --- a/apps/frontend-manage/src/components/sessions/cockpit/CancelSessionModal.tsx +++ b/apps/frontend-manage/src/components/sessions/cockpit/CancelSessionModal.tsx @@ -45,7 +45,7 @@ function CancelSessionModal({ diff --git a/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts b/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts index 9205a5b5e3..cd8a4d0ea0 100644 --- a/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts +++ b/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts @@ -650,7 +650,7 @@ describe('Different live-quiz workflows', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.wait(1000) @@ -677,7 +677,7 @@ describe('Different live-quiz workflows', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.wait(1000) cy.get('[data-cy="next-block-timeline"]').click() @@ -686,7 +686,7 @@ describe('Different live-quiz workflows', () => { const sessionIdEvaluation = url.split('/')[4] cy.visit( Cypress.env('URL_MANAGE') + - '/sessions/' + + '/quizzes/' + sessionIdEvaluation + '/evaluation' ) @@ -757,7 +757,7 @@ describe('Different live-quiz workflows', () => { // check that feedback is visible to lecturer and switch its status to visible cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get(`[data-cy="open-feedback-${feedback1}"]`).should('exist').click() cy.get(`[data-cy="pin-feedback-${feedback1}"]`).click() @@ -773,7 +773,7 @@ describe('Different live-quiz workflows', () => { // login to lecturer and disable moderation cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get('[data-cy="toggle-moderation"]').click() @@ -792,7 +792,7 @@ describe('Different live-quiz workflows', () => { // login to lecturer and answer second feedback const feedbackAnswer = 'Answer to feedback' cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get(`[data-cy="open-feedback-${feedback2}"]`).should('exist').click() cy.get(`[data-cy="respond-to-feedback-${feedback2}"]`).type(feedbackAnswer) @@ -806,7 +806,7 @@ describe('Different live-quiz workflows', () => { // login to lecturer and pin feedback, check lecturer display cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get(`[data-cy="open-feedback-${feedback1}"]`).should('exist').click() cy.get(`[data-cy="pin-feedback-${feedback1}"]`).click() @@ -816,7 +816,7 @@ describe('Different live-quiz workflows', () => { // delete feedback response cy.visit(Cypress.env('URL_MANAGE')) - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get(`[data-cy="open-feedback-${feedback2}"]`).should('exist').click() cy.get(`[data-cy="delete-response-${feedbackAnswer}"]`).click() @@ -829,7 +829,7 @@ describe('Different live-quiz workflows', () => { // delete feedback cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get(`[data-cy="delete-feedback-${feedback1}"]`).click() cy.get(`[data-cy="delete-feedback-${feedback1}"]`).click() @@ -841,7 +841,7 @@ describe('Different live-quiz workflows', () => { // click through blocks and end session cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName}"]`).click() cy.get('[data-cy="next-block-timeline"]').click() // open block cy.wait(1000) diff --git a/cypress/cypress/e2e/H-practice-quiz-workflow.cy.ts b/cypress/cypress/e2e/H-practice-quiz-workflow.cy.ts index b494536698..f37f1bc6f7 100644 --- a/cypress/cypress/e2e/H-practice-quiz-workflow.cy.ts +++ b/cypress/cypress/e2e/H-practice-quiz-workflow.cy.ts @@ -27,7 +27,7 @@ describe('Different practice quiz workflows', () => { it('Test creating and publishing a practice quiz', () => { // switch to question pool view - cy.get('[data-cy="questions"]').click() + cy.get('[data-cy="library"]').click() // set up question with solution cy.get('[data-cy="create-question"]').click() @@ -198,7 +198,7 @@ describe('Different practice quiz workflows', () => { it('Test scheduling and publishing functionalities of a practice quiz', () => { // create a practice quiz with availability starting in the future // switch to question pool view - cy.get('[data-cy="questions"]').click() + cy.get('[data-cy="library"]').click() // set up question with solution cy.get('[data-cy="create-question"]').click() @@ -394,7 +394,7 @@ describe('Different practice quiz workflows', () => { it('Test editing an existing practice quizs', () => { // switch back to question pool view - cy.get('[data-cy="questions"]').click() + cy.get('[data-cy="library"]').click() // create practice quiz cy.get('[data-cy="create-practice-quiz"]').click() diff --git a/cypress/cypress/e2e/K-group-activity-workflow.cy.ts b/cypress/cypress/e2e/K-group-activity-workflow.cy.ts index 20a5664f76..ea85565ffb 100644 --- a/cypress/cypress/e2e/K-group-activity-workflow.cy.ts +++ b/cypress/cypress/e2e/K-group-activity-workflow.cy.ts @@ -16,7 +16,7 @@ describe('Create and solve a group activity', () => { cy.loginLecturer() // set up question with solution - cy.get('[data-cy="questions"]').click() + cy.get('[data-cy="library"]').click() cy.get('[data-cy="create-question"]').click() cy.get('[data-cy="insert-question-title"]').click().type(questionTitle) cy.get('[data-cy="insert-question-text"]').click().type(question) diff --git a/packages/i18n/messages/de.ts b/packages/i18n/messages/de.ts index dfa60e38b0..40a8065391 100644 --- a/packages/i18n/messages/de.ts +++ b/packages/i18n/messages/de.ts @@ -691,6 +691,9 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die presentQrCode: 'QR-Code präsentieren', questionPool: 'Fragepool', sessions: 'Live-Quizzes', + library: 'Bibliothek', + quizzes: 'Quizzes', + analytics: 'Analytics', courses: 'Kurse', migration: 'Migration', generateToken: 'Login-Token generieren', diff --git a/packages/i18n/messages/en.ts b/packages/i18n/messages/en.ts index 230806b221..95f3339141 100644 --- a/packages/i18n/messages/en.ts +++ b/packages/i18n/messages/en.ts @@ -693,6 +693,9 @@ Since the KlickerUZH app is not yet available on the iOS App Store, follow these presentQrCode: 'Present QR code', questionPool: 'Question Pool', sessions: 'Live Quizzes', + library: 'Library', + quizzes: 'Quizzes', + analytics: 'Analytics', courses: 'Courses', migration: 'Migration', generateToken: 'Generate login token', From 70c203f96dfe7fa3f52dda2bef0e949586c58f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Schl=C3=A4fli?= Date: Mon, 26 Aug 2024 10:57:16 +0200 Subject: [PATCH 02/38] chore(apps/analytics): add test notebook illustrating basic computation approach for analytics (#4182) Co-authored-by: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Co-authored-by: sjschlapbach --- .gitignore | 1 + apps/analytics/poetry.lock | 355 ++++++------ apps/analytics/pyproject.toml | 6 + apps/analytics/test.ipynb | 1001 ++++++--------------------------- 4 files changed, 381 insertions(+), 982 deletions(-) diff --git a/.gitignore b/.gitignore index 16d45601c3..7f741d7386 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ packages/prisma/src/seed .turbo out/ +!out/.gitkeep diff --git a/apps/analytics/poetry.lock b/apps/analytics/poetry.lock index 3de9c334c3..b059494c73 100644 --- a/apps/analytics/poetry.lock +++ b/apps/analytics/poetry.lock @@ -306,13 +306,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] @@ -569,56 +569,63 @@ files = [ [[package]] name = "numpy" -version = "2.0.1" +version = "2.1.0" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, - {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, - {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, - {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, - {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, - {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, - {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, - {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, - {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, - {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, - {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, - {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, - {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, - {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, - {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, - {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, - {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, - {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, - {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, - {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, - {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, - {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, - {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, - {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, - {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, - {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, - {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, - {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, - {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, - {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, - {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6326ab99b52fafdcdeccf602d6286191a79fe2fda0ae90573c5814cd2b0bc1b8"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0937e54c09f7a9a68da6889362ddd2ff584c02d015ec92672c099b61555f8911"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:30014b234f07b5fec20f4146f69e13cfb1e33ee9a18a1879a0142fbb00d47673"}, + {file = "numpy-2.1.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:899da829b362ade41e1e7eccad2cf274035e1cb36ba73034946fccd4afd8606b"}, + {file = "numpy-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08801848a40aea24ce16c2ecde3b756f9ad756586fb2d13210939eb69b023f5b"}, + {file = "numpy-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:398049e237d1aae53d82a416dade04defed1a47f87d18d5bd615b6e7d7e41d1f"}, + {file = "numpy-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0abb3916a35d9090088a748636b2c06dc9a6542f99cd476979fb156a18192b84"}, + {file = "numpy-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e2350aea18d04832319aac0f887d5fcec1b36abd485d14f173e3e900b83e33"}, + {file = "numpy-2.1.0-cp310-cp310-win32.whl", hash = "sha256:f6b26e6c3b98adb648243670fddc8cab6ae17473f9dc58c51574af3e64d61211"}, + {file = "numpy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:f505264735ee074250a9c78247ee8618292091d9d1fcc023290e9ac67e8f1afa"}, + {file = "numpy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:76368c788ccb4f4782cf9c842b316140142b4cbf22ff8db82724e82fe1205dce"}, + {file = "numpy-2.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8e93a01a35be08d31ae33021e5268f157a2d60ebd643cfc15de6ab8e4722eb1"}, + {file = "numpy-2.1.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9523f8b46485db6939bd069b28b642fec86c30909cea90ef550373787f79530e"}, + {file = "numpy-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54139e0eb219f52f60656d163cbe67c31ede51d13236c950145473504fa208cb"}, + {file = "numpy-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebbf9fbdabed208d4ecd2e1dfd2c0741af2f876e7ae522c2537d404ca895c3"}, + {file = "numpy-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:378cb4f24c7d93066ee4103204f73ed046eb88f9ad5bb2275bb9fa0f6a02bd36"}, + {file = "numpy-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f699a709120b220dfe173f79c73cb2a2cab2c0b88dd59d7b49407d032b8ebd"}, + {file = "numpy-2.1.0-cp311-cp311-win32.whl", hash = "sha256:ffbd6faeb190aaf2b5e9024bac9622d2ee549b7ec89ef3a9373fa35313d44e0e"}, + {file = "numpy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0af3a5987f59d9c529c022c8c2a64805b339b7ef506509fba7d0556649b9714b"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe76d75b345dc045acdbc006adcb197cc680754afd6c259de60d358d60c93736"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f358ea9e47eb3c2d6eba121ab512dfff38a88db719c38d1e67349af210bc7529"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:dd94ce596bda40a9618324547cfaaf6650b1a24f5390350142499aa4e34e53d1"}, + {file = "numpy-2.1.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b47c551c6724960479cefd7353656498b86e7232429e3a41ab83be4da1b109e8"}, + {file = "numpy-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0756a179afa766ad7cb6f036de622e8a8f16ffdd55aa31f296c870b5679d745"}, + {file = "numpy-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24003ba8ff22ea29a8c306e61d316ac74111cebf942afbf692df65509a05f111"}, + {file = "numpy-2.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b34fa5e3b5d6dc7e0a4243fa0f81367027cb6f4a7215a17852979634b5544ee0"}, + {file = "numpy-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4f982715e65036c34897eb598d64aef15150c447be2cfc6643ec7a11af06574"}, + {file = "numpy-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c4cd94dfefbefec3f8b544f61286584292d740e6e9d4677769bc76b8f41deb02"}, + {file = "numpy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0cdef204199278f5c461a0bed6ed2e052998276e6d8ab2963d5b5c39a0500bc"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8ab81ccd753859ab89e67199b9da62c543850f819993761c1e94a75a814ed667"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442596f01913656d579309edcd179a2a2f9977d9a14ff41d042475280fc7f34e"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:848c6b5cad9898e4b9ef251b6f934fa34630371f2e916261070a4eb9092ffd33"}, + {file = "numpy-2.1.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:54c6a63e9d81efe64bfb7bcb0ec64332a87d0b87575f6009c8ba67ea6374770b"}, + {file = "numpy-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:652e92fc409e278abdd61e9505649e3938f6d04ce7ef1953f2ec598a50e7c195"}, + {file = "numpy-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab32eb9170bf8ffcbb14f11613f4a0b108d3ffee0832457c5d4808233ba8977"}, + {file = "numpy-2.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8fb49a0ba4d8f41198ae2d52118b050fd34dace4b8f3fb0ee34e23eb4ae775b1"}, + {file = "numpy-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44e44973262dc3ae79e9063a1284a73e09d01b894b534a769732ccd46c28cc62"}, + {file = "numpy-2.1.0-cp313-cp313-win32.whl", hash = "sha256:ab83adc099ec62e044b1fbb3a05499fa1e99f6d53a1dde102b2d85eff66ed324"}, + {file = "numpy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:de844aaa4815b78f6023832590d77da0e3b6805c644c33ce94a1e449f16d6ab5"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:343e3e152bf5a087511cd325e3b7ecfd5b92d369e80e74c12cd87826e263ec06"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f07fa2f15dabe91259828ce7d71b5ca9e2eb7c8c26baa822c825ce43552f4883"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5474dad8c86ee9ba9bb776f4b99ef2d41b3b8f4e0d199d4f7304728ed34d0300"}, + {file = "numpy-2.1.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1f817c71683fd1bb5cff1529a1d085a57f02ccd2ebc5cd2c566f9a01118e3b7d"}, + {file = "numpy-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a3336fbfa0d38d3deacd3fe7f3d07e13597f29c13abf4d15c3b6dc2291cbbdd"}, + {file = "numpy-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a894c51fd8c4e834f00ac742abad73fc485df1062f1b875661a3c1e1fb1c2f6"}, + {file = "numpy-2.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:9156ca1f79fc4acc226696e95bfcc2b486f165a6a59ebe22b2c1f82ab190384a"}, + {file = "numpy-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:624884b572dff8ca8f60fab591413f077471de64e376b17d291b19f56504b2bb"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15ef8b2177eeb7e37dd5ef4016f30b7659c57c2c0b57a779f1d537ff33a72c7b"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e5f0642cdf4636198a4990de7a71b693d824c56a757862230454629cf62e323d"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15976718c004466406342789f31b6673776360f3b1e3c575f25302d7e789575"}, + {file = "numpy-2.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6c1de77ded79fef664d5098a66810d4d27ca0224e9051906e634b3f7ead134c2"}, + {file = "numpy-2.1.0.tar.gz", hash = "sha256:7dc90da0081f7e1da49ec4e398ede6a8e9cc4f5ebe5f9e06b443ed889ee9aaa2"}, ] [[package]] @@ -1016,6 +1023,24 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyright" +version = "1.1.376" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.376-py3-none-any.whl", hash = "sha256:0f2473b12c15c46b3207f0eec224c3cea2bdc07cd45dd4a037687cbbca0fbeff"}, + {file = "pyright-1.1.376.tar.gz", hash = "sha256:bffd63b197cd0810395bb3245c06b01f95a85ddf6bfa0e5644ed69c841e954dd"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1080,120 +1105,120 @@ files = [ [[package]] name = "pyzmq" -version = "26.1.0" +version = "26.2.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" files = [ - {file = "pyzmq-26.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:263cf1e36862310bf5becfbc488e18d5d698941858860c5a8c079d1511b3b18e"}, - {file = "pyzmq-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d5c8b17f6e8f29138678834cf8518049e740385eb2dbf736e8f07fc6587ec682"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a95c2358fcfdef3374cb8baf57f1064d73246d55e41683aaffb6cfe6862917"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99de52b8fbdb2a8f5301ae5fc0f9e6b3ba30d1d5fc0421956967edcc6914242"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bcbfbab4e1895d58ab7da1b5ce9a327764f0366911ba5b95406c9104bceacb0"}, - {file = "pyzmq-26.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:77ce6a332c7e362cb59b63f5edf730e83590d0ab4e59c2aa5bd79419a42e3449"}, - {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ba0a31d00e8616149a5ab440d058ec2da621e05d744914774c4dde6837e1f545"}, - {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8b88641384e84a258b740801cd4dbc45c75f148ee674bec3149999adda4a8598"}, - {file = "pyzmq-26.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2fa76ebcebe555cce90f16246edc3ad83ab65bb7b3d4ce408cf6bc67740c4f88"}, - {file = "pyzmq-26.1.0-cp310-cp310-win32.whl", hash = "sha256:fbf558551cf415586e91160d69ca6416f3fce0b86175b64e4293644a7416b81b"}, - {file = "pyzmq-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a7b8aab50e5a288c9724d260feae25eda69582be84e97c012c80e1a5e7e03fb2"}, - {file = "pyzmq-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:08f74904cb066e1178c1ec706dfdb5c6c680cd7a8ed9efebeac923d84c1f13b1"}, - {file = "pyzmq-26.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:46d6800b45015f96b9d92ece229d92f2aef137d82906577d55fadeb9cf5fcb71"}, - {file = "pyzmq-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5bc2431167adc50ba42ea3e5e5f5cd70d93e18ab7b2f95e724dd8e1bd2c38120"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3bb34bebaa1b78e562931a1687ff663d298013f78f972a534f36c523311a84d"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3f6329340cef1c7ba9611bd038f2d523cea79f09f9c8f6b0553caba59ec562"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:471880c4c14e5a056a96cd224f5e71211997d40b4bf5e9fdded55dafab1f98f2"}, - {file = "pyzmq-26.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ce6f2b66799971cbae5d6547acefa7231458289e0ad481d0be0740535da38d8b"}, - {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a1f6ea5b1d6cdbb8cfa0536f0d470f12b4b41ad83625012e575f0e3ecfe97f0"}, - {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b45e6445ac95ecb7d728604bae6538f40ccf4449b132b5428c09918523abc96d"}, - {file = "pyzmq-26.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:94c4262626424683feea0f3c34951d39d49d354722db2745c42aa6bb50ecd93b"}, - {file = "pyzmq-26.1.0-cp311-cp311-win32.whl", hash = "sha256:a0f0ab9df66eb34d58205913f4540e2ad17a175b05d81b0b7197bc57d000e829"}, - {file = "pyzmq-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8efb782f5a6c450589dbab4cb0f66f3a9026286333fe8f3a084399149af52f29"}, - {file = "pyzmq-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f133d05aaf623519f45e16ab77526e1e70d4e1308e084c2fb4cedb1a0c764bbb"}, - {file = "pyzmq-26.1.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3d3146b1c3dcc8a1539e7cc094700b2be1e605a76f7c8f0979b6d3bde5ad4072"}, - {file = "pyzmq-26.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d9270fbf038bf34ffca4855bcda6e082e2c7f906b9eb8d9a8ce82691166060f7"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995301f6740a421afc863a713fe62c0aaf564708d4aa057dfdf0f0f56525294b"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7eca8b89e56fb8c6c26dd3e09bd41b24789022acf1cf13358e96f1cafd8cae3"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d4feb2e83dfe9ace6374a847e98ee9d1246ebadcc0cb765482e272c34e5820"}, - {file = "pyzmq-26.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d4fafc2eb5d83f4647331267808c7e0c5722c25a729a614dc2b90479cafa78bd"}, - {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:58c33dc0e185dd97a9ac0288b3188d1be12b756eda67490e6ed6a75cf9491d79"}, - {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:68a0a1d83d33d8367ddddb3e6bb4afbb0f92bd1dac2c72cd5e5ddc86bdafd3eb"}, - {file = "pyzmq-26.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ae7c57e22ad881af78075e0cea10a4c778e67234adc65c404391b417a4dda83"}, - {file = "pyzmq-26.1.0-cp312-cp312-win32.whl", hash = "sha256:347e84fc88cc4cb646597f6d3a7ea0998f887ee8dc31c08587e9c3fd7b5ccef3"}, - {file = "pyzmq-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:9f136a6e964830230912f75b5a116a21fe8e34128dcfd82285aa0ef07cb2c7bd"}, - {file = "pyzmq-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4b7a989c8f5a72ab1b2bbfa58105578753ae77b71ba33e7383a31ff75a504c4"}, - {file = "pyzmq-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d416f2088ac8f12daacffbc2e8918ef4d6be8568e9d7155c83b7cebed49d2322"}, - {file = "pyzmq-26.1.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:ecb6c88d7946166d783a635efc89f9a1ff11c33d680a20df9657b6902a1d133b"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:471312a7375571857a089342beccc1a63584315188560c7c0da7e0a23afd8a5c"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e6cea102ffa16b737d11932c426f1dc14b5938cf7bc12e17269559c458ac334"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec7248673ffc7104b54e4957cee38b2f3075a13442348c8d651777bf41aa45ee"}, - {file = "pyzmq-26.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0614aed6f87d550b5cecb03d795f4ddbb1544b78d02a4bd5eecf644ec98a39f6"}, - {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8746ce968be22a8a1801bf4a23e565f9687088580c3ed07af5846580dd97f76"}, - {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7688653574392d2eaeef75ddcd0b2de5b232d8730af29af56c5adf1df9ef8d6f"}, - {file = "pyzmq-26.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8d4dac7d97f15c653a5fedcafa82626bd6cee1450ccdaf84ffed7ea14f2b07a4"}, - {file = "pyzmq-26.1.0-cp313-cp313-win32.whl", hash = "sha256:ccb42ca0a4a46232d716779421bbebbcad23c08d37c980f02cc3a6bd115ad277"}, - {file = "pyzmq-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e1e5d0a25aea8b691a00d6b54b28ac514c8cc0d8646d05f7ca6cb64b97358250"}, - {file = "pyzmq-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:fc82269d24860cfa859b676d18850cbb8e312dcd7eada09e7d5b007e2f3d9eb1"}, - {file = "pyzmq-26.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:416ac51cabd54f587995c2b05421324700b22e98d3d0aa2cfaec985524d16f1d"}, - {file = "pyzmq-26.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:ff832cce719edd11266ca32bc74a626b814fff236824aa1aeaad399b69fe6eae"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393daac1bcf81b2a23e696b7b638eedc965e9e3d2112961a072b6cd8179ad2eb"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9869fa984c8670c8ab899a719eb7b516860a29bc26300a84d24d8c1b71eae3ec"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b3b8e36fd4c32c0825b4461372949ecd1585d326802b1321f8b6dc1d7e9318c"}, - {file = "pyzmq-26.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3ee647d84b83509b7271457bb428cc347037f437ead4b0b6e43b5eba35fec0aa"}, - {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:45cb1a70eb00405ce3893041099655265fabcd9c4e1e50c330026e82257892c1"}, - {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:5cca7b4adb86d7470e0fc96037771981d740f0b4cb99776d5cb59cd0e6684a73"}, - {file = "pyzmq-26.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:91d1a20bdaf3b25f3173ff44e54b1cfbc05f94c9e8133314eb2962a89e05d6e3"}, - {file = "pyzmq-26.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c0665d85535192098420428c779361b8823d3d7ec4848c6af3abb93bc5c915bf"}, - {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:96d7c1d35ee4a495df56c50c83df7af1c9688cce2e9e0edffdbf50889c167595"}, - {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b281b5ff5fcc9dcbfe941ac5c7fcd4b6c065adad12d850f95c9d6f23c2652384"}, - {file = "pyzmq-26.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5384c527a9a004445c5074f1e20db83086c8ff1682a626676229aafd9cf9f7d1"}, - {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:754c99a9840839375ee251b38ac5964c0f369306eddb56804a073b6efdc0cd88"}, - {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9bdfcb74b469b592972ed881bad57d22e2c0acc89f5e8c146782d0d90fb9f4bf"}, - {file = "pyzmq-26.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bd13f0231f4788db619347b971ca5f319c5b7ebee151afc7c14632068c6261d3"}, - {file = "pyzmq-26.1.0-cp37-cp37m-win32.whl", hash = "sha256:c5668dac86a869349828db5fc928ee3f58d450dce2c85607067d581f745e4fb1"}, - {file = "pyzmq-26.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad875277844cfaeca7fe299ddf8c8d8bfe271c3dc1caf14d454faa5cdbf2fa7a"}, - {file = "pyzmq-26.1.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:65c6e03cc0222eaf6aad57ff4ecc0a070451e23232bb48db4322cc45602cede0"}, - {file = "pyzmq-26.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:038ae4ffb63e3991f386e7fda85a9baab7d6617fe85b74a8f9cab190d73adb2b"}, - {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bdeb2c61611293f64ac1073f4bf6723b67d291905308a7de9bb2ca87464e3273"}, - {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:61dfa5ee9d7df297c859ac82b1226d8fefaf9c5113dc25c2c00ecad6feeeb04f"}, - {file = "pyzmq-26.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3292d384537b9918010769b82ab3e79fca8b23d74f56fc69a679106a3e2c2cf"}, - {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f9499c70c19ff0fbe1007043acb5ad15c1dec7d8e84ab429bca8c87138e8f85c"}, - {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d3dd5523ed258ad58fed7e364c92a9360d1af8a9371e0822bd0146bdf017ef4c"}, - {file = "pyzmq-26.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baba2fd199b098c5544ef2536b2499d2e2155392973ad32687024bd8572a7d1c"}, - {file = "pyzmq-26.1.0-cp38-cp38-win32.whl", hash = "sha256:ddbb2b386128d8eca92bd9ca74e80f73fe263bcca7aa419f5b4cbc1661e19741"}, - {file = "pyzmq-26.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:79e45a4096ec8388cdeb04a9fa5e9371583bcb826964d55b8b66cbffe7b33c86"}, - {file = "pyzmq-26.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:add52c78a12196bc0fda2de087ba6c876ea677cbda2e3eba63546b26e8bf177b"}, - {file = "pyzmq-26.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c03bd7f3339ff47de7ea9ac94a2b34580a8d4df69b50128bb6669e1191a895"}, - {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dcc37d9d708784726fafc9c5e1232de655a009dbf97946f117aefa38d5985a0f"}, - {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5a6ed52f0b9bf8dcc64cc82cce0607a3dfed1dbb7e8c6f282adfccc7be9781de"}, - {file = "pyzmq-26.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451e16ae8bea3d95649317b463c9f95cd9022641ec884e3d63fc67841ae86dfe"}, - {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:906e532c814e1d579138177a00ae835cd6becbf104d45ed9093a3aaf658f6a6a"}, - {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05bacc4f94af468cc82808ae3293390278d5f3375bb20fef21e2034bb9a505b6"}, - {file = "pyzmq-26.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:57bb2acba798dc3740e913ffadd56b1fcef96f111e66f09e2a8db3050f1f12c8"}, - {file = "pyzmq-26.1.0-cp39-cp39-win32.whl", hash = "sha256:f774841bb0e8588505002962c02da420bcfb4c5056e87a139c6e45e745c0e2e2"}, - {file = "pyzmq-26.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:359c533bedc62c56415a1f5fcfd8279bc93453afdb0803307375ecf81c962402"}, - {file = "pyzmq-26.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:7907419d150b19962138ecec81a17d4892ea440c184949dc29b358bc730caf69"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b24079a14c9596846bf7516fe75d1e2188d4a528364494859106a33d8b48be38"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59d0acd2976e1064f1b398a00e2c3e77ed0a157529779e23087d4c2fb8aaa416"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:911c43a4117915203c4cc8755e0f888e16c4676a82f61caee2f21b0c00e5b894"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10163e586cc609f5f85c9b233195554d77b1e9a0801388907441aaeb22841c5"}, - {file = "pyzmq-26.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:28a8b2abb76042f5fd7bd720f7fea48c0fd3e82e9de0a1bf2c0de3812ce44a42"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bef24d3e4ae2c985034439f449e3f9e06bf579974ce0e53d8a507a1577d5b2ab"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2cd0f4d314f4a2518e8970b6f299ae18cff7c44d4a1fc06fc713f791c3a9e3ea"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fa25a620eed2a419acc2cf10135b995f8f0ce78ad00534d729aa761e4adcef8a"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef3b048822dca6d231d8a8ba21069844ae38f5d83889b9b690bf17d2acc7d099"}, - {file = "pyzmq-26.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9a6847c92d9851b59b9f33f968c68e9e441f9a0f8fc972c5580c5cd7cbc6ee24"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9b9305004d7e4e6a824f4f19b6d8f32b3578aad6f19fc1122aaf320cbe3dc83"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:63c1d3a65acb2f9c92dce03c4e1758cc552f1ae5c78d79a44e3bb88d2fa71f3a"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d36b8fffe8b248a1b961c86fbdfa0129dfce878731d169ede7fa2631447331be"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67976d12ebfd61a3bc7d77b71a9589b4d61d0422282596cf58c62c3866916544"}, - {file = "pyzmq-26.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:998444debc8816b5d8d15f966e42751032d0f4c55300c48cc337f2b3e4f17d03"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5c88b2f13bcf55fee78ea83567b9fe079ba1a4bef8b35c376043440040f7edb"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d906d43e1592be4b25a587b7d96527cb67277542a5611e8ea9e996182fae410"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b0c9942430d731c786545da6be96d824a41a51742e3e374fedd9018ea43106"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:314d11564c00b77f6224d12eb3ddebe926c301e86b648a1835c5b28176c83eab"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:093a1a3cae2496233f14b57f4b485da01b4ff764582c854c0f42c6dd2be37f3d"}, - {file = "pyzmq-26.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c397b1b450f749a7e974d74c06d69bd22dd362142f370ef2bd32a684d6b480c"}, - {file = "pyzmq-26.1.0.tar.gz", hash = "sha256:6c5aeea71f018ebd3b9115c7cb13863dd850e98ca6b9258509de1246461a7e7f"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, ] [package.dependencies] @@ -1253,13 +1278,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -1344,4 +1369,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "82030f861eb6e5b3ec18d61953852368b49395d2abcab5b1c6aa88c555d0fe2d" +content-hash = "4982b71a45b56d2d6336dacf424bef938e744116396f7f520361c45276edd1a9" diff --git a/apps/analytics/pyproject.toml b/apps/analytics/pyproject.toml index 1a1c443da2..b4e33c687a 100644 --- a/apps/analytics/pyproject.toml +++ b/apps/analytics/pyproject.toml @@ -17,6 +17,9 @@ xlsxwriter = "^3.2.0" poethepoet = "0.27.0" ipykernel = "6.29.5" +[tool.poetry.group.dev.dependencies] +pyright = "1.1.376" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" @@ -24,3 +27,6 @@ build-backend = "poetry.core.masonry.api" [tool.poe.tasks] generate = "prisma generate" main = "doppler run --config dev -- python main.py" + +[tool.pyright] +typeCheckingMode = "strict" diff --git a/apps/analytics/test.ipynb b/apps/analytics/test.ipynb index c6bf242151..654b1aae6b 100644 --- a/apps/analytics/test.ipynb +++ b/apps/analytics/test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -28,916 +28,283 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "id=106979 score=0.0 pointsAwarded=0.0 xpAwarded=0.0 response={'choices': [1]} participant=None participantId='b99c7eb9-8874-4cf2-989e-a66e695539da' participation=Participation(id=922, isActive=False, course=None, courseId='2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2', participant=None, participantId='b99c7eb9-8874-4cf2-989e-a66e695539da', courseLeaderboard=None, courseLeaderboardId=1809, sessionLeaderboards=None, responses=None, detailResponses=None, subscriptions=None, completedMicroLearnings=[], bookmarkedElementStacks=None, createdAt=datetime.datetime(2022, 12, 17, 16, 14, 1, 891000, tzinfo=TzInfo(UTC)), updatedAt=datetime.datetime(2022, 12, 20, 18, 42, 53, 556000, tzinfo=TzInfo(UTC))) participationId=922 elementInstance=None elementInstanceId=492 createdAt=datetime.datetime(2022, 12, 20, 18, 51, 2, 551000, tzinfo=TzInfo(UTC)) updatedAt=datetime.datetime(2024, 2, 15, 9, 18, 44, 575000, tzinfo=TzInfo(UTC))\n" - ] - } - ], + "outputs": [], "source": [ - "response_details = db.questionresponsedetail.find_many(include={ \"participation\": True })\n", + "response_details = db.questionresponsedetail.find_many(\n", + " include={\n", + " \"participation\": True,\n", + " \"elementInstance\": {\"include\": {\"elementStack\": True} },\n", + " },\n", + " take=10000,\n", + ")\n", "\n", "print(response_details[0])" ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "responses = db.questionresponse.find_many(\n", + " include={\n", + " \"participation\": True,\n", + " \"elementInstance\": {\"include\": {\"elementStack\": True}},\n", + " },\n", + " take=10000,\n", + ")\n", + "\n", + "print(responses[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'id': 106979,\n", - " 'score': 0.0,\n", - " 'pointsAwarded': 0.0,\n", - " 'xpAwarded': 0.0,\n", - " 'response': {'choices': [1]},\n", - " 'participant': None,\n", - " 'participantId': 'b99c7eb9-8874-4cf2-989e-a66e695539da',\n", - " 'participation': {'id': 922,\n", - " 'isActive': False,\n", - " 'course': None,\n", - " 'courseId': '2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2',\n", - " 'participant': None,\n", - " 'participantId': 'b99c7eb9-8874-4cf2-989e-a66e695539da',\n", - " 'courseLeaderboard': None,\n", - " 'courseLeaderboardId': 1809,\n", - " 'sessionLeaderboards': None,\n", - " 'responses': None,\n", - " 'detailResponses': None,\n", - " 'subscriptions': None,\n", - " 'completedMicroLearnings': [],\n", - " 'bookmarkedElementStacks': None,\n", - " 'createdAt': datetime.datetime(2022, 12, 17, 16, 14, 1, 891000, tzinfo=TzInfo(UTC)),\n", - " 'updatedAt': datetime.datetime(2022, 12, 20, 18, 42, 53, 556000, tzinfo=TzInfo(UTC))},\n", - " 'participationId': 922,\n", - " 'elementInstance': None,\n", - " 'elementInstanceId': 492,\n", - " 'createdAt': datetime.datetime(2022, 12, 20, 18, 51, 2, 551000, tzinfo=TzInfo(UTC)),\n", - " 'updatedAt': datetime.datetime(2024, 2, 15, 9, 18, 44, 575000, tzinfo=TzInfo(UTC))}" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "def map_response_details(response_details):\n", " as_dict = response_details.dict()\n", "\n", - " return {\n", + " extended_dict = {\n", " **as_dict,\n", - " \"participation\": as_dict['participation']\n", + " \"elementStack\": as_dict['elementInstance']['elementStack'],\n", " }\n", "\n", + " return extended_dict\n", + "\n", + "\n", + "response_details_mapped = list(map(map_response_details, response_details))\n", + "\n", + "response_details_mapped[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "responses_mapped = list(map(map_response_details, responses))\n", "\n", - "response_details = list(map(map_response_details, response_details))\n", + "responses_mapped[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_response_details = pd.json_normalize(response_details_mapped, sep='_')\n", "\n", - "response_details[0]" + "print(df_response_details.columns)\n", + "print(df_response_details.head())" ] }, { "cell_type": "code", - "execution_count": 46, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Index(['id', 'score', 'pointsAwarded', 'xpAwarded', 'participant',\n", - " 'participantId', 'participationId', 'elementInstance',\n", - " 'elementInstanceId', 'createdAt', 'updatedAt', 'response_choices',\n", - " 'participation_id', 'participation_isActive', 'participation_course',\n", - " 'participation_courseId', 'participation_participant',\n", - " 'participation_participantId', 'participation_courseLeaderboard',\n", - " 'participation_courseLeaderboardId',\n", - " 'participation_sessionLeaderboards', 'participation_responses',\n", - " 'participation_detailResponses', 'participation_subscriptions',\n", - " 'participation_completedMicroLearnings',\n", - " 'participation_bookmarkedElementStacks', 'participation_createdAt',\n", - " 'participation_updatedAt', 'response_correctness', 'response_value',\n", - " 'response_viewed'],\n", - " dtype='object')\n", - " id score pointsAwarded xpAwarded participant \\\n", - "0 106979 0.0 0.0 0.0 None \n", - "1 106993 10.0 10.0 0.0 None \n", - "2 107017 10.0 10.0 0.0 None \n", - "3 107529 20.0 20.0 0.0 None \n", - "4 107865 10.0 10.0 0.0 None \n", - "\n", - " participantId participationId elementInstance \\\n", - "0 b99c7eb9-8874-4cf2-989e-a66e695539da 922 None \n", - "1 5a93fa71-88d5-4f12-b308-e6a81cbfc77e 188 None \n", - "2 edcde14c-2005-4353-90e9-d6f596270be9 38 None \n", - "3 fbe2ff7d-5984-4e42-93e7-511916d64673 492 None \n", - "4 fdbd49a6-70f6-4dd6-b365-02267679276e 767 None \n", - "\n", - " elementInstanceId createdAt ... \\\n", - "0 492 2022-12-20 18:51:02.551000+00:00 ... \n", - "1 492 2022-12-20 18:52:26.665000+00:00 ... \n", - "2 492 2022-12-20 18:54:28.070000+00:00 ... \n", - "3 492 2022-12-20 19:34:41.138000+00:00 ... \n", - "4 492 2022-12-20 19:55:39.337000+00:00 ... \n", - "\n", - " participation_responses participation_detailResponses \\\n", - "0 None None \n", - "1 None None \n", - "2 None None \n", - "3 None None \n", - "4 None None \n", - "\n", - " participation_subscriptions participation_completedMicroLearnings \\\n", - "0 None [] \n", - "1 None [] \n", - "2 None [] \n", - "3 None [] \n", - "4 None [] \n", - "\n", - " participation_bookmarkedElementStacks participation_createdAt \\\n", - "0 None 2022-12-17 16:14:01.891000+00:00 \n", - "1 None 2022-09-19 22:15:31.455000+00:00 \n", - "2 None 2022-09-19 12:15:15.363000+00:00 \n", - "3 None 2022-09-24 12:04:04.218000+00:00 \n", - "4 None 2022-10-11 10:01:18.547000+00:00 \n", - "\n", - " participation_updatedAt response_correctness response_value \\\n", - "0 2022-12-20 18:42:53.556000+00:00 NaN NaN \n", - "1 2022-10-12 17:59:28.620000+00:00 NaN NaN \n", - "2 2022-09-21 20:02:36.118000+00:00 NaN NaN \n", - "3 2022-09-26 13:52:44.602000+00:00 NaN NaN \n", - "4 2022-10-26 09:19:09.708000+00:00 NaN NaN \n", - "\n", - " response_viewed \n", - "0 NaN \n", - "1 NaN \n", - "2 NaN \n", - "3 NaN \n", - "4 NaN \n", - "\n", - "[5 rows x 31 columns]\n" - ] - } - ], + "outputs": [], "source": [ - "df = pd.json_normalize(response_details, sep='_')\n", + "df_responses = pd.json_normalize(responses_mapped, sep='_')\n", "\n", - "print(df.columns)\n", - "print(df.head())" + "print(df_responses.columns)\n", + "print(df_responses.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Response Details and Activity Analytics" ] }, { "cell_type": "code", - "execution_count": 72, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/d_/8y5nsk_n78n6lc08sxkq9w740000gn/T/ipykernel_81300/176451833.py:3: SettingWithCopyWarning: \n", - "A value is trying to be set on a copy of a slice from a DataFrame.\n", - "Try using .loc[row_indexer,col_indexer] = value instead\n", - "\n", - "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", - " df_relevant['createdAt'] = pd.to_datetime(df_relevant['createdAt'].dt.date)\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idscorepointsAwardedxpAwardedcreatedAtparticipantIdcourseId
01069790.00.00.02022-12-20b99c7eb9-8874-4cf2-989e-a66e695539da2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2
110699310.010.00.02022-12-205a93fa71-88d5-4f12-b308-e6a81cbfc77e2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2
210701710.010.00.02022-12-20edcde14c-2005-4353-90e9-d6f596270be92b302436-4fc3-4d5d-bbfb-1e13b4ee11b2
310752920.020.00.02022-12-20fbe2ff7d-5984-4e42-93e7-511916d646732b302436-4fc3-4d5d-bbfb-1e13b4ee11b2
410786510.010.00.02022-12-20fdbd49a6-70f6-4dd6-b365-02267679276e2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2
\n", - "
" - ], - "text/plain": [ - " id score pointsAwarded xpAwarded createdAt \\\n", - "0 106979 0.0 0.0 0.0 2022-12-20 \n", - "1 106993 10.0 10.0 0.0 2022-12-20 \n", - "2 107017 10.0 10.0 0.0 2022-12-20 \n", - "3 107529 20.0 20.0 0.0 2022-12-20 \n", - "4 107865 10.0 10.0 0.0 2022-12-20 \n", - "\n", - " participantId courseId \n", - "0 b99c7eb9-8874-4cf2-989e-a66e695539da 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 \n", - "1 5a93fa71-88d5-4f12-b308-e6a81cbfc77e 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 \n", - "2 edcde14c-2005-4353-90e9-d6f596270be9 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 \n", - "3 fbe2ff7d-5984-4e42-93e7-511916d64673 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 \n", - "4 fdbd49a6-70f6-4dd6-b365-02267679276e 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 " - ] - }, - "execution_count": 72, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "df_relevant = df[['id', 'score', 'pointsAwarded', 'xpAwarded', 'createdAt', 'participantId', 'participation_courseId']]\n", + "df_response_details_relevant = df_response_details[['id', 'score', 'pointsAwarded', 'xpAwarded', 'createdAt', 'participantId', 'participation_courseId']]\n", "\n", - "df_relevant['createdAt'] = pd.to_datetime(df_relevant['createdAt'].dt.date)\n", + "df_response_details_relevant['createdAt'] = pd.to_datetime(df_response_details_relevant['createdAt'].dt.date)\n", "\n", - "df_relevant = df_relevant.rename(columns={'participation_courseId': 'courseId'})\n", + "df_response_details_relevant = df_response_details_relevant.rename(columns={'participation_courseId': 'courseId'})\n", "\n", - "df_relevant.head()" + "df_response_details_relevant.head()" ] }, { "cell_type": "code", - "execution_count": 73, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idscorepointsAwardedxpAwarded
participantIdcourseIdcreatedAt
001f6f8a-a32e-43b3-953b-7f6204a97bbe2b302436-4fc3-4d5d-bbfb-1e13b4ee11b22022-12-1462380.0310.00.0
0025f331-cb85-412f-9115-323f1dfe50d3ece02221-aa68-4c6c-8d4f-dcfa03528e4f2024-03-021670.045.030.0
00686c1f-ddd2-4b6c-89f9-d2f06e688c577a000ca7-48f3-43d4-ac25-d5d1cd074afd2023-11-271595.00.090.0
2023-11-2872480.00.0340.0
2023-12-0112110.00.0100.0
\n", - "
" - ], - "text/plain": [ - " id \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-14 62 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-02 16 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-27 15 \n", - " 2023-11-28 72 \n", - " 2023-12-01 12 \n", - "\n", - " score \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-14 380.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-02 70.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-27 95.0 \n", - " 2023-11-28 480.0 \n", - " 2023-12-01 110.0 \n", - "\n", - " pointsAwarded \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-14 310.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-02 45.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-27 0.0 \n", - " 2023-11-28 0.0 \n", - " 2023-12-01 0.0 \n", - "\n", - " xpAwarded \n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-14 0.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-02 30.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-27 90.0 \n", - " 2023-11-28 340.0 \n", - " 2023-12-01 100.0 " - ] - }, - "execution_count": 73, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# group by participantId, courseId and createdAt\n", "# count the number of responses and sum the score, pointsAwarded, xpAwarded\n", "\n", - "df_relevant_daily = df_relevant.groupby(['participantId', 'courseId', 'createdAt']).agg({'id': 'count', 'score': 'sum', 'pointsAwarded': 'sum', 'xpAwarded': 'sum'})\n", + "df_response_details_relevant_daily = df_response_details_relevant.groupby(['participantId', 'courseId', 'createdAt']).agg({'id': 'count', 'score': 'sum', 'pointsAwarded': 'sum', 'xpAwarded': 'sum'})\n", "\n", "# df_relevant_daily = df_relevant_daily.reset_index()\n", "\n", "# df_relevant_daily = df_relevant_agg.sort_values(by=['participantId', 'courseId', 'createdAt'], ascending=True)\n", "\n", - "df_relevant_daily.head()" + "df_response_details_relevant_daily.head()" ] }, { "cell_type": "code", - "execution_count": 93, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "MultiIndex: 11754 entries, ('001f6f8a-a32e-43b3-953b-7f6204a97bbe', '2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2', Timestamp('2022-12-14 00:00:00')) to ('ffe51cdd-49cb-4a3a-b460-1994c22f46c8', 'f7ceeba0-ef5a-4d0b-a992-a44a1395cbf9', Timestamp('2023-06-01 00:00:00'))\n", - "Data columns (total 4 columns):\n", - " # Column Non-Null Count Dtype \n", - "--- ------ -------------- ----- \n", - " 0 id 11754 non-null int64 \n", - " 1 score 11754 non-null float64\n", - " 2 pointsAwarded 11754 non-null float64\n", - " 3 xpAwarded 11754 non-null float64\n", - "dtypes: float64(3), int64(1)\n", - "memory usage: 442.0+ KB\n" - ] - } - ], + "outputs": [], "source": [ - "df_relevant_daily.info()" + "df_response_details_relevant_daily.info()" ] }, { "cell_type": "code", - "execution_count": 89, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idscorepointsAwardedxpAwarded
participantIdcourseIdcreatedAt
001f6f8a-a32e-43b3-953b-7f6204a97bbe2b302436-4fc3-4d5d-bbfb-1e13b4ee11b22022-12-1862380.0310.00.0
0025f331-cb85-412f-9115-323f1dfe50d3ece02221-aa68-4c6c-8d4f-dcfa03528e4f2024-03-031670.045.030.0
00686c1f-ddd2-4b6c-89f9-d2f06e688c577a000ca7-48f3-43d4-ac25-d5d1cd074afd2023-12-0399685.00.0530.0
2023-12-1000.00.00.0
2023-12-171412050.00.0930.0
\n", - "
" - ], - "text/plain": [ - " id \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-18 62 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-03 16 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-12-03 99 \n", - " 2023-12-10 0 \n", - " 2023-12-17 141 \n", - "\n", - " score \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-18 380.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-03 70.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-12-03 685.0 \n", - " 2023-12-10 0.0 \n", - " 2023-12-17 2050.0 \n", - "\n", - " pointsAwarded \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-18 310.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-03 45.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-12-03 0.0 \n", - " 2023-12-10 0.0 \n", - " 2023-12-17 0.0 \n", - "\n", - " xpAwarded \n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-18 0.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-03 30.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-12-03 530.0 \n", - " 2023-12-10 0.0 \n", - " 2023-12-17 930.0 " - ] - }, - "execution_count": 89, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# create a dataframe with weekly aggregated data\n", - "df_relevant_weekly = df_relevant_daily.groupby(['participantId', 'courseId']).resample('W', level='createdAt', ).sum()\n", + "df_response_details_relevant_weekly = df_response_details_relevant_daily.groupby(['participantId', 'courseId']).resample('W', level='createdAt', ).sum()\n", "\n", - "df_relevant_weekly.head()" + "df_response_details_relevant_weekly.head()" ] }, { "cell_type": "code", - "execution_count": 94, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "MultiIndex: 12899 entries, ('001f6f8a-a32e-43b3-953b-7f6204a97bbe', '2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2', Timestamp('2022-12-18 00:00:00')) to ('ffe51cdd-49cb-4a3a-b460-1994c22f46c8', 'f7ceeba0-ef5a-4d0b-a992-a44a1395cbf9', Timestamp('2023-06-04 00:00:00'))\n", - "Data columns (total 4 columns):\n", - " # Column Non-Null Count Dtype \n", - "--- ------ -------------- ----- \n", - " 0 id 12899 non-null int64 \n", - " 1 score 12899 non-null float64\n", - " 2 pointsAwarded 12899 non-null float64\n", - " 3 xpAwarded 12899 non-null float64\n", - "dtypes: float64(3), int64(1)\n", - "memory usage: 535.6+ KB\n" - ] - } - ], + "outputs": [], "source": [ - "df_relevant_weekly.info()" + "df_response_details_relevant_weekly.info()" ] }, { "cell_type": "code", - "execution_count": 91, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idscorepointsAwardedxpAwarded
participantIdcourseIdcreatedAt
001f6f8a-a32e-43b3-953b-7f6204a97bbe2b302436-4fc3-4d5d-bbfb-1e13b4ee11b22022-12-3162380.0310.00.0
0025f331-cb85-412f-9115-323f1dfe50d3ece02221-aa68-4c6c-8d4f-dcfa03528e4f2024-03-311670.045.030.0
00686c1f-ddd2-4b6c-89f9-d2f06e688c577a000ca7-48f3-43d4-ac25-d5d1cd074afd2023-11-3087575.00.0430.0
2023-12-311532160.00.01030.0
007142eb-67f1-4f31-8eed-68a97ea466fb2b302436-4fc3-4d5d-bbfb-1e13b4ee11b22022-12-312553355.02090.00.0
\n", - "
" - ], - "text/plain": [ - " id \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 62 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-31 16 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-30 87 \n", - " 2023-12-31 153 \n", - "007142eb-67f1-4f31-8eed-68a97ea466fb 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 255 \n", - "\n", - " score \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 380.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-31 70.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-30 575.0 \n", - " 2023-12-31 2160.0 \n", - "007142eb-67f1-4f31-8eed-68a97ea466fb 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 3355.0 \n", - "\n", - " pointsAwarded \\\n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 310.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-31 45.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-30 0.0 \n", - " 2023-12-31 0.0 \n", - "007142eb-67f1-4f31-8eed-68a97ea466fb 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 2090.0 \n", - "\n", - " xpAwarded \n", - "participantId courseId createdAt \n", - "001f6f8a-a32e-43b3-953b-7f6204a97bbe 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 0.0 \n", - "0025f331-cb85-412f-9115-323f1dfe50d3 ece02221-aa68-4c6c-8d4f-dcfa03528e4f 2024-03-31 30.0 \n", - "00686c1f-ddd2-4b6c-89f9-d2f06e688c57 7a000ca7-48f3-43d4-ac25-d5d1cd074afd 2023-11-30 430.0 \n", - " 2023-12-31 1030.0 \n", - "007142eb-67f1-4f31-8eed-68a97ea466fb 2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2 2022-12-31 0.0 " - ] - }, - "execution_count": 91, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# create a dataframe with monthly aggregated data\n", - "df_relevant_monthly = df_relevant_daily.groupby(['participantId', 'courseId']).resample('ME', level='createdAt', ).sum()\n", + "df_response_details_relevant_monthly = df_response_details_relevant_daily.groupby(['participantId', 'courseId']).resample('ME', level='createdAt', ).sum()\n", "\n", - "df_relevant_monthly.head()" + "df_response_details_relevant_monthly.head()" ] }, { "cell_type": "code", - "execution_count": 95, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "MultiIndex: 4765 entries, ('001f6f8a-a32e-43b3-953b-7f6204a97bbe', '2b302436-4fc3-4d5d-bbfb-1e13b4ee11b2', Timestamp('2022-12-31 00:00:00')) to ('ffe51cdd-49cb-4a3a-b460-1994c22f46c8', 'f7ceeba0-ef5a-4d0b-a992-a44a1395cbf9', Timestamp('2023-06-30 00:00:00'))\n", - "Data columns (total 4 columns):\n", - " # Column Non-Null Count Dtype \n", - "--- ------ -------------- ----- \n", - " 0 id 4765 non-null int64 \n", - " 1 score 4765 non-null float64\n", - " 2 pointsAwarded 4765 non-null float64\n", - " 3 xpAwarded 4765 non-null float64\n", - "dtypes: float64(3), int64(1)\n", - "memory usage: 247.7+ KB\n" - ] - } - ], + "outputs": [], "source": [ - "df_relevant_monthly.info()" + "df_response_details_relevant_monthly.info()" ] }, { "cell_type": "code", - "execution_count": 118, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
idscorepointsAwardedxpAwarded
courseId
08af1f79-b5ce-4e78-bd07-e6f21d11ae616819.0895746.4769657.547724
0abf0e39-6936-4e15-a0ef-78c89add6cd2310.9677420.0000000.967742
1caf787f-98c1-4de4-8ef6-bd428ac550f3191187.7419196.6638756.632493
1e83f531-a62b-40b2-996a-f6dba986c755610.000000NaN10.000000
265b834f-8ab7-4c4f-bddf-61075d7990b397978.6863336.5303656.701031
\n", - "
" - ], - "text/plain": [ - " id score pointsAwarded \\\n", - "courseId \n", - "08af1f79-b5ce-4e78-bd07-e6f21d11ae61 681 9.089574 6.476965 \n", - "0abf0e39-6936-4e15-a0ef-78c89add6cd2 31 0.967742 0.000000 \n", - "1caf787f-98c1-4de4-8ef6-bd428ac550f3 19118 7.741919 6.663875 \n", - "1e83f531-a62b-40b2-996a-f6dba986c755 6 10.000000 NaN \n", - "265b834f-8ab7-4c4f-bddf-61075d7990b3 9797 8.686333 6.530365 \n", - "\n", - " xpAwarded \n", - "courseId \n", - "08af1f79-b5ce-4e78-bd07-e6f21d11ae61 7.547724 \n", - "0abf0e39-6936-4e15-a0ef-78c89add6cd2 0.967742 \n", - "1caf787f-98c1-4de4-8ef6-bd428ac550f3 6.632493 \n", - "1e83f531-a62b-40b2-996a-f6dba986c755 10.000000 \n", - "265b834f-8ab7-4c4f-bddf-61075d7990b3 6.701031 " - ] - }, - "execution_count": 118, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "df_relevant_mean = df_relevant.groupby(['courseId']).agg({'id': 'count', 'score': 'mean', 'pointsAwarded': 'mean', 'xpAwarded': 'mean'})\n", + "df_response_details_relevant_mean = df_response_details_relevant.groupby(['courseId']).agg({'id': 'count', 'score': 'mean', 'pointsAwarded': 'mean', 'xpAwarded': 'mean'})\n", "\n", - "df_relevant_mean.head()" + "df_response_details_relevant_mean.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_response_details_relevant_daily.to_csv('out/df_response_details_relevant_daily.csv')\n", + "df_response_details_relevant_weekly.to_csv('out/df_response_details_relevant_weekly.csv')\n", + "df_response_details_relevant_monthly.to_csv('out/df_response_details_relevant_monthly.csv')\n", + "df_response_details_relevant_mean.to_csv('out/df_response_details_relevant_mean.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Responses and Quiz Analytics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "', '.join(df_responses.columns)" ] }, { "cell_type": "code", - "execution_count": 117, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "df_relevant_daily.to_csv('out/df_relevant_daily.csv')\n", - "df_relevant_weekly.to_csv('out/df_relevant_weekly.csv')\n", - "df_relevant_monthly.to_csv('out/df_relevant_monthly.csv')\n", - "df_relevant_mean.to_csv('out/df_relevant_mean.csv')" + "df_responses_relevant = df_responses[\n", + " [\n", + " \"id\",\n", + " \"trialsCount\",\n", + " \"totalScore\",\n", + " \"totalPointsAwarded\",\n", + " \"totalXpAwarded\",\n", + " \"lastAwardedAt\",\n", + " \"lastXpAwardedAt\",\n", + " \"lastAnsweredAt\",\n", + " \"correctCount\",\n", + " \"correctCountStreak\",\n", + " \"lastCorrectAt\",\n", + " \"partialCorrectCount\",\n", + " \"lastPartialCorrectAt\",\n", + " \"wrongCount\",\n", + " \"lastWrongAt\",\n", + " \"eFactor\",\n", + " \"interval\",\n", + " \"nextDueAt\",\n", + " \"createdAt\",\n", + " \"updatedAt\",\n", + " \"participantId\",\n", + " \"participation_courseId\",\n", + " \"elementStack_type\",\n", + " \"elementStack_microLearningId\",\n", + " \"elementStack_practiceQuizId\",\n", + " \"elementStack_liveQuizId\",\n", + " \"elementStack_groupActivityId\",\n", + " ]\n", + "]\n", + "\n", + "df_responses_relevant[\"createdAt\"] = pd.to_datetime(\n", + " df_responses_relevant[\"createdAt\"].dt.date\n", + ")\n", + "\n", + "df_responses_relevant[\"updatedAt\"] = pd.to_datetime(\n", + " df_responses_relevant[\"updatedAt\"].dt.date\n", + ")\n", + "\n", + "df_responses_relevant = df_responses_relevant.rename(\n", + " columns={\"participation_courseId\": \"courseId\"}\n", + ")\n", + "\n", + "df_responses_relevant.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Cleanup" ] }, { @@ -966,7 +333,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.4" + "version": "3.12.5" } }, "nbformat": 4, From ed7e7452f7c7bb5a34a05a3f66f6ad9ab4f4de94 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:43:18 +0200 Subject: [PATCH 03/38] enhance: add python logic to compute daily participant analytics based on question response details (#4212) --- .../daily_participant_analytics.ipynb | 353 ++++++++++++++++++ apps/analytics/pyproject.toml | 2 +- .../migration.sql | 16 + .../prisma/src/prisma/schema/analytics.prisma | 23 +- 4 files changed, 382 insertions(+), 12 deletions(-) create mode 100644 apps/analytics/daily_participant_analytics.ipynb create mode 100644 packages/prisma/src/prisma/migrations/20240826113537_participant_analytics_correctness_counts/migration.sql diff --git a/apps/analytics/daily_participant_analytics.ipynb b/apps/analytics/daily_participant_analytics.ipynb new file mode 100644 index 0000000000..8bc2dbad40 --- /dev/null +++ b/apps/analytics/daily_participant_analytics.ipynb @@ -0,0 +1,353 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparation & Data Fetching" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ['DATABASE_URL'] = 'postgresql://klicker:klicker@localhost:5432/klicker-prod'\n", + "\n", + "db.connect()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch all question response detail entries for a specific day\n", + "specific_date = '2024-06-10'\n", + "date_start = specific_date + 'T00:00:00.000Z'\n", + "date_end = specific_date + 'T23:59:59.999Z'\n", + "participant_response_details = db.participant.find_many(\n", + " include={\n", + " 'detailQuestionResponses': {\n", + " 'where': {\n", + " 'createdAt': {\n", + " 'gte': date_start,\n", + " 'lte': date_end\n", + " }\n", + " },\n", + " 'include': {\n", + " 'practiceQuiz': True,\n", + " 'microLearning': True\n", + " }\n", + " },\n", + " }\n", + ")\n", + "\n", + "# Print the first 5 question response details\n", + "print(\"Found {} participants for the timespan from {} to {}\".format(len(participant_response_details), date_start, date_end))\n", + "print(participant_response_details[0])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compute Correctness Metrics for Responses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Convert the question response details to a pandas dataframe\n", + "def map_details(detail, participantId):\n", + " courseId = detail['practiceQuiz']['courseId'] if detail['practiceQuiz'] else detail['microLearning']['courseId']\n", + " return {\n", + " **detail,\n", + " 'participantId': participantId,\n", + " 'courseId': courseId\n", + " }\n", + "\n", + "def map_participants(participant):\n", + " participant_dict = participant.dict()\n", + " return list(map(lambda detail: map_details(detail, participant_dict['id']), participant_dict['detailQuestionResponses']))\n", + "\n", + "def convert_to_df(participants):\n", + " return pd.DataFrame([item for sublist in list(map(map_participants, participants)) for item in sublist])\n", + "\n", + "df_details = convert_to_df(participant_response_details)\n", + "df_details = df_details[['score', 'pointsAwarded', 'xpAwarded', 'timeSpent', 'response', 'elementInstanceId', 'participantId', 'courseId']]\n", + "print(\"Question detail responses:\", len(df_details))\n", + "print(\"Columns:\", df_details.columns)\n", + "df_details.head()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Compute correctness of the responses and add them as a separate column\n", + "# Get related element instances\n", + "element_instance_ids = df_details['elementInstanceId'].unique()\n", + "element_instances = db.elementinstance.find_many(\n", + " where={\n", + " 'id': {\n", + " 'in': element_instance_ids.tolist()\n", + " }\n", + " }\n", + ")\n", + "\n", + "# Map the element instances to the corresponding elementData.options entries and convert it to a dataframe\n", + "def map_element_instance_options(instance):\n", + " instance_dict = instance.dict()\n", + " return {\n", + " 'elementInstanceId': instance_dict['id'],\n", + " 'type': instance_dict['elementData']['type'],\n", + " 'options': instance_dict['elementData']['options'] if 'options' in instance_dict['elementData'] else None\n", + " }\n", + "\n", + "df_element_instances = pd.DataFrame(list(map(map_element_instance_options, element_instances)))\n", + "df_element_instances.head()\n", + "\n", + "# Compute the correctness for every response entry based on the element instance options (depending on the type of the element)\n", + "def compute_correctness(row):\n", + " element_instance = df_element_instances[df_element_instances['elementInstanceId'] == row['elementInstanceId']].iloc[0]\n", + " response = row['response']\n", + " options = element_instance['options']\n", + "\n", + " if element_instance['type'] == 'FLASHCARD' or element_instance['type'] == 'CONTENT':\n", + " return None\n", + "\n", + " elif element_instance['type'] == 'SC':\n", + " selected_choice = response['choices'][0]\n", + " correct_choice = next((choice['ix'] for choice in options['choices'] if choice['correct']), None)\n", + " return 'CORRECT' if selected_choice == correct_choice else 'INCORRECT'\n", + "\n", + " elif element_instance['type'] == 'MC' or element_instance['type'] == 'KPRIM':\n", + " selected_choices = response['choices']\n", + " correct_choices = [choice['ix'] for choice in options['choices'] if choice['correct']]\n", + " available_choices = len(options['choices'])\n", + " \n", + " selected_choices_array = [1 if ix in selected_choices else 0 for ix in range(available_choices)]\n", + " correct_choices_array = [1 if ix in correct_choices else 0 for ix in range(available_choices)]\n", + " hamming_distance = sum([1 for i in range(available_choices) if selected_choices_array[i] != correct_choices_array[i]])\n", + "\n", + " if element_instance['type'] == 'MC':\n", + " correctness = max(-2 * hamming_distance / available_choices + 1, 0)\n", + " if correctness == 1:\n", + " return 'CORRECT'\n", + " elif correctness == 0:\n", + " return 'INCORRECT'\n", + " else:\n", + " return 'PARTIAL'\n", + " elif element_instance['type'] == 'KPRIM':\n", + " return 'CORRECT' if hamming_distance == 0 else 'PARTIAL' if hamming_distance == 1 else 'INCORRECT'\n", + "\n", + " elif element_instance['type'] == 'NUMERICAL':\n", + " response_value = float(response['value'])\n", + " within_range = list(map(lambda range: float(range['min']) <= response_value <= float(range['max']), options['solutionRanges']))\n", + " if any(within_range):\n", + " return 'CORRECT'\n", + "\n", + " return 'INCORRECT'\n", + "\n", + " elif element_instance['type'] == 'FREE_TEXT':\n", + " raise NotImplementedError(\"Free text correctness computation not implemented yet\")\n", + "\n", + " else:\n", + " raise ValueError(\"Unknown element type: {}\".format(element_instance['type']))\n", + "\n", + "df_details['correctness'] = df_details.apply(compute_correctness, axis=1)\n", + "df_details = df_details.dropna(subset=['correctness'])\n", + "print(\"{} question response details remaining with correctness\".format(len(df_details)))\n", + "df_details.head()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Aggregate Metrics and Counts for Responses" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Aggregate the question response details for the participant and course level\n", + "df_analytics_counts = df_details.groupby(['participantId', 'courseId']).agg({\n", + " 'score': 'sum',\n", + " 'pointsAwarded': 'sum',\n", + " 'xpAwarded': 'sum',\n", + " 'timeSpent': 'sum',\n", + " 'elementInstanceId': ['count', 'nunique'] # count = trialsCount, nunique = responseCount\n", + "}).reset_index()\n", + "df_analytics_counts.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Count the 'CORRECT', 'PARTIAL', and 'INCORRECT' entries for each participantId and elementInstanceId combination\n", + "df_analytics_corr_temp = df_details.groupby(['participantId', 'elementInstanceId', 'courseId', 'correctness']).size().unstack(fill_value=0).reset_index()\n", + "\n", + "# Divide each of the correctness columns by the sum of all and rename them to meanCorrect, meanPartial, meanIncorrect\n", + "df_analytics_corr_temp['sum'] = df_analytics_corr_temp['CORRECT'] + df_analytics_corr_temp['PARTIAL'] + df_analytics_corr_temp['INCORRECT']\n", + "df_analytics_corr_temp['meanCorrect'] = df_analytics_corr_temp['CORRECT'] / df_analytics_corr_temp['sum']\n", + "df_analytics_corr_temp['meanPartial'] = df_analytics_corr_temp['PARTIAL'] / df_analytics_corr_temp['sum']\n", + "df_analytics_corr_temp['meanIncorrect'] = df_analytics_corr_temp['INCORRECT'] / df_analytics_corr_temp['sum']\n", + "\n", + "# Aggregate the correctness columns for each participantId and courseId\n", + "df_analytics_correctness = df_analytics_corr_temp.groupby(['participantId', 'courseId']).agg({\n", + " 'meanCorrect': 'sum',\n", + " 'meanPartial': 'sum',\n", + " 'meanIncorrect': 'sum'\n", + "}).reset_index().rename(columns={\n", + " 'meanCorrect': 'meanCorrectCount',\n", + " 'meanPartial': 'meanPartialCount',\n", + " 'meanIncorrect': 'meanWrongCount'\n", + "})\n", + "df_analytics_correctness.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Map the counts in the corresponding analytics dataframe to a single level\n", + "df_analytics_counts.columns = df_analytics_counts.columns.map('_'.join).str.strip('_')\n", + "df_analytics_counts = df_analytics_counts.rename(columns={\n", + " 'score_sum': 'totalScore',\n", + " 'pointsAwarded_sum': 'totalPoints',\n", + " 'xpAwarded_sum': 'totalXp',\n", + " 'timeSpent_sum': 'totalTimeSpent',\n", + " 'elementInstanceId_count': 'trialsCount',\n", + " 'elementInstanceId_nunique': 'responseCount'\n", + "})\n", + "df_analytics_counts.head()\n", + "\n", + "# Combine the analytics counts and correctness dataframes based on the unique participantId and courseId combinations\n", + "df_analytics = pd.merge(df_analytics_counts, df_analytics_correctness, on=['participantId', 'courseId'])\n", + "df_analytics.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add Computed Metrics to the Database" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create daily analytics entries for all participants\n", + "for index, row in df_analytics.iterrows():\n", + " db.participantanalytics.upsert(\n", + " where={\n", + " 'type_courseId_participantId_timestamp': {\n", + " 'type': 'DAILY',\n", + " 'courseId': row['courseId'],\n", + " 'participantId': row['participantId'],\n", + " 'timestamp': specific_date + 'T00:00:00.000Z'\n", + " }\n", + " },\n", + " data={\n", + " 'create': {\n", + " 'type': 'DAILY',\n", + " 'timestamp': specific_date + 'T00:00:00.000Z',\n", + " 'trialsCount': row['trialsCount'],\n", + " 'responseCount': row['responseCount'],\n", + " 'totalScore': row['totalScore'],\n", + " 'totalPoints': row['totalPoints'],\n", + " 'totalXp': row['totalXp'],\n", + " 'meanCorrectCount': row['meanCorrectCount'],\n", + " 'meanPartialCorrectCount': row['meanPartialCount'],\n", + " 'meanWrongCount': row['meanWrongCount'],\n", + " 'participant': {\n", + " 'connect': {\n", + " 'id': row['participantId']\n", + " }\n", + " },\n", + " 'course': {\n", + " 'connect': {\n", + " 'id': row['courseId']\n", + " }\n", + " }\n", + " },\n", + " 'update': {}\n", + " }\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-3uz8SvN3-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/apps/analytics/pyproject.toml b/apps/analytics/pyproject.toml index b4e33c687a..8b041e3a58 100644 --- a/apps/analytics/pyproject.toml +++ b/apps/analytics/pyproject.toml @@ -2,7 +2,7 @@ name = "@klicker-uzh/analytics" version = "0.0.1" description = "" -authors = ["Roland Schlaefli "] +authors = ["Roland Schlaefli ", "Julius Schlapbach "] license = "AGPL-3.0" readme = "README.md" packages = [{include = "@klicker_uzh"}] diff --git a/packages/prisma/src/prisma/migrations/20240826113537_participant_analytics_correctness_counts/migration.sql b/packages/prisma/src/prisma/migrations/20240826113537_participant_analytics_correctness_counts/migration.sql new file mode 100644 index 0000000000..c112489bbd --- /dev/null +++ b/packages/prisma/src/prisma/migrations/20240826113537_participant_analytics_correctness_counts/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `meanFirstCorrectCount` on the `ParticipantAnalytics` table. All the data in the column will be lost. + - You are about to drop the column `meanLastCorrectCount` on the `ParticipantAnalytics` table. All the data in the column will be lost. + - You are about to drop the column `collectedAchievements` on the `ParticipantAnalytics` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ParticipantAnalytics" DROP COLUMN "meanFirstCorrectCount", +DROP COLUMN "meanLastCorrectCount", +DROP COLUMN "collectedAchievements", +ADD COLUMN "firstCorrectCount" REAL, +ADD COLUMN "firstWrongCount" REAL, +ADD COLUMN "lastCorrectCount" REAL, +ADD COLUMN "lastWrongCount" REAL; diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index f97cf80092..dcd1b15f3d 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -18,7 +18,7 @@ model ParticipantAnalytics { timestamp DateTime @db.Date - // unsolvedQuestionsCount = responseCount - AggregatedAnalytics.totalElementsAvailable + // unsolvedQuestionsCount = AggregatedAnalytics.totalElementsAvailable - responseCount trialsCount Int // total number of questions attempted responseCount Int // total number of unique questions attempted @@ -27,16 +27,17 @@ model ParticipantAnalytics { totalPoints Int totalXp Int - // meanTrialsCount = meanCorrectCount + meanPartialCorrectCount + meanWrongCount - meanCorrectCount Float @db.Real // aggregated over all questions - meanPartialCorrectCount Float @db.Real // aggregated over all questions - meanWrongCount Float @db.Real // aggregated over all questions - - // divide by responseCount to get the correctness rate - meanFirstCorrectCount Float @db.Real // aggregated over all questions - meanLastCorrectCount Float @db.Real // aggregated over all questions - - collectedAchievements String[] + // responseCount = meanCorrectCount + meanPartialCorrectCount + meanWrongCount + meanCorrectCount Float @db.Real // mean over responses to same question and sum over all questions + meanPartialCorrectCount Float @db.Real // mean over responses to same question and sum over all questions + meanWrongCount Float @db.Real // mean over responses to same question and sum over all questions + + // divide by responseCount to get the correctness rate (COURSE LEVEL ONLY) + // values can be computed based on firstCorrectness and lastCorrectness on QuestionResponse + firstCorrectCount Float? @db.Real // aggregated over all questions (sum) + lastCorrectCount Float? @db.Real // aggregated over all questions (sum) + firstWrongCount Float? @db.Real // aggregated over all questions (sum) + lastWrongCount Float? @db.Real // aggregated over all questions (sum) competencyAnalytics CompetencyAnalytics[] From a1736ff46f6e7ccb43def0b292ea3df977f3e7f9 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:31:06 +0200 Subject: [PATCH 04/38] feat: add scripts to compute periodic participant learning analytics from past question response details (#4213) --- apps/analytics/src/__init__.py | 2 + apps/analytics/src/modules/__init__.py | 1 + .../modules/participant_analytics/__init__.py | 5 + .../aggregate_analytics.py | 145 ++++++++++++++ .../compute_correctness.py | 164 ++++++++++++++++ .../compute_participant_analytics.py | 30 +++ .../compute_participant_course_analytics.py | 110 +++++++++++ .../get_participant_responses.py | 86 +++++++++ .../save_participant_analytics.py | 92 +++++++++ apps/analytics/src/notebooks/__init__.py | 0 .../daily_participant_analytics.ipynb | 4 +- .../notebooks/archive}/notebook.ipynb | 0 .../{ => src/notebooks/archive}/test.ipynb | 0 .../src/notebooks/participant_analytics.ipynb | 181 ++++++++++++++++++ .../migration.sql | 36 ++++ .../prisma/src/prisma/schema/analytics.prisma | 22 ++- 16 files changed, 876 insertions(+), 2 deletions(-) create mode 100644 apps/analytics/src/__init__.py create mode 100644 apps/analytics/src/modules/__init__.py create mode 100644 apps/analytics/src/modules/participant_analytics/__init__.py create mode 100644 apps/analytics/src/modules/participant_analytics/aggregate_analytics.py create mode 100644 apps/analytics/src/modules/participant_analytics/compute_correctness.py create mode 100644 apps/analytics/src/modules/participant_analytics/compute_participant_analytics.py create mode 100644 apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py create mode 100644 apps/analytics/src/modules/participant_analytics/get_participant_responses.py create mode 100644 apps/analytics/src/modules/participant_analytics/save_participant_analytics.py create mode 100644 apps/analytics/src/notebooks/__init__.py rename apps/analytics/{ => src/notebooks/archive}/daily_participant_analytics.ipynb (98%) rename apps/analytics/{ => src/notebooks/archive}/notebook.ipynb (100%) rename apps/analytics/{ => src/notebooks/archive}/test.ipynb (100%) create mode 100644 apps/analytics/src/notebooks/participant_analytics.ipynb create mode 100644 packages/prisma/src/prisma/migrations/20240827152536_computed_at_analytics/migration.sql diff --git a/apps/analytics/src/__init__.py b/apps/analytics/src/__init__.py new file mode 100644 index 0000000000..62529e8970 --- /dev/null +++ b/apps/analytics/src/__init__.py @@ -0,0 +1,2 @@ +from .modules import * +from .notebooks import * diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py new file mode 100644 index 0000000000..9751f844cc --- /dev/null +++ b/apps/analytics/src/modules/__init__.py @@ -0,0 +1 @@ +from .participant_analytics import compute_correctness, get_participant_responses diff --git a/apps/analytics/src/modules/participant_analytics/__init__.py b/apps/analytics/src/modules/participant_analytics/__init__.py new file mode 100644 index 0000000000..1212544995 --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/__init__.py @@ -0,0 +1,5 @@ +from .compute_correctness import compute_correctness +from .get_participant_responses import get_participant_responses +from .aggregate_analytics import aggregate_analytics +from .save_participant_analytics import save_participant_analytics +from .compute_participant_course_analytics import compute_participant_course_analytics diff --git a/apps/analytics/src/modules/participant_analytics/aggregate_analytics.py b/apps/analytics/src/modules/participant_analytics/aggregate_analytics.py new file mode 100644 index 0000000000..5ff48f9a69 --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/aggregate_analytics.py @@ -0,0 +1,145 @@ +import pandas as pd + + +def aggregate_analytics(df_details, df_course_responses=None): + # Aggregate the question response details for the participant and course level + df_analytics_counts = ( + df_details.groupby(["participantId", "courseId"]) + .agg( + { + "score": "sum", + "pointsAwarded": "sum", + "xpAwarded": "sum", + "timeSpent": "sum", + "elementInstanceId": [ + "count", + "nunique", + ], # count = trialsCount, nunique = responseCount + } + ) + .reset_index() + ) + + # Count the 'CORRECT', 'PARTIAL', and 'INCORRECT' entries for each participantId and elementInstanceId combination + df_analytics_corr_temp = ( + df_details.groupby( + ["participantId", "elementInstanceId", "courseId", "correctness"] + ) + .size() + .unstack(fill_value=0) + .reset_index() + ) + + # Divide each of the correctness columns by the sum of all and rename them to meanCorrect, meanPartial, meanIncorrect + correctCount = ( + df_analytics_corr_temp["CORRECT"] + if "CORRECT" in df_analytics_corr_temp.columns + else 0 + ) + partialCount = ( + df_analytics_corr_temp["PARTIAL"] + if "PARTIAL" in df_analytics_corr_temp.columns + else 0 + ) + incorrectCount = ( + df_analytics_corr_temp["INCORRECT"] + if "INCORRECT" in df_analytics_corr_temp.columns + else 0 + ) + + df_analytics_corr_temp["sum"] = correctCount + partialCount + incorrectCount + df_analytics_corr_temp["meanCorrect"] = ( + df_analytics_corr_temp["CORRECT"] / df_analytics_corr_temp["sum"] + if "CORRECT" in df_analytics_corr_temp.columns + else 0 + ) + df_analytics_corr_temp["meanPartial"] = ( + df_analytics_corr_temp["PARTIAL"] / df_analytics_corr_temp["sum"] + if "PARTIAL" in df_analytics_corr_temp.columns + else 0 + ) + df_analytics_corr_temp["meanIncorrect"] = ( + df_analytics_corr_temp["INCORRECT"] / df_analytics_corr_temp["sum"] + if "INCORRECT" in df_analytics_corr_temp.columns + else 0 + ) + + # Aggregate the correctness columns for each participantId and courseId + df_analytics_correctness = ( + df_analytics_corr_temp.groupby(["participantId", "courseId"]) + .agg({"meanCorrect": "sum", "meanPartial": "sum", "meanIncorrect": "sum"}) + .reset_index() + .rename( + columns={ + "meanCorrect": "meanCorrectCount", + "meanPartial": "meanPartialCount", + "meanIncorrect": "meanWrongCount", + } + ) + ) + + # Map the counts in the corresponding analytics dataframe to a single level + df_analytics_counts.columns = df_analytics_counts.columns.map("_".join).str.strip( + "_" + ) + df_analytics_counts = df_analytics_counts.rename( + columns={ + "score_sum": "totalScore", + "pointsAwarded_sum": "totalPoints", + "xpAwarded_sum": "totalXp", + "timeSpent_sum": "totalTimeSpent", + "elementInstanceId_count": "trialsCount", + "elementInstanceId_nunique": "responseCount", + } + ) + + df_course_analytics = None + if df_course_responses is not None: + # Count entries where firstResponseCorrectness is 'CORRECT', 'WRONG' and lastResponseCorrectness is 'CORRECT', 'WRONG' into separate columns - grouped by participantId and courseId + df_course_analytics = ( + df_course_responses.groupby(["participantId", "courseId"]) + .agg( + { + "firstResponseCorrectness": [ + ("correct", lambda x: (x == "CORRECT").sum()), + ("wrong", lambda x: (x == "WRONG").sum()), + ], + "lastResponseCorrectness": [ + ("correct", lambda x: (x == "CORRECT").sum()), + ("wrong", lambda x: (x == "WRONG").sum()), + ], + } + ) + .reset_index() + ) + df_course_analytics.columns = df_course_analytics.columns.map( + "_".join + ).str.strip("_") + df_course_analytics = df_course_analytics.rename( + columns={ + "firstResponseCorrectness_correct": "firstCorrectCount", + "firstResponseCorrectness_wrong": "firstWrongCount", + "lastResponseCorrectness_correct": "lastCorrectCount", + "lastResponseCorrectness_wrong": "lastWrongCount", + } + ) + + # Combine the analytics counts and correctness dataframes based on the unique participantId and courseId combinations + if df_course_analytics is None: + df_analytics = pd.merge( + df_analytics_counts, + df_analytics_correctness, + on=["participantId", "courseId"], + ) + else: + df_analytics = pd.merge( + df_analytics_counts, + pd.merge( + df_analytics_correctness, + df_course_analytics, + on=["participantId", "courseId"], + ), + on=["participantId", "courseId"], + ) + + return df_analytics diff --git a/apps/analytics/src/modules/participant_analytics/compute_correctness.py b/apps/analytics/src/modules/participant_analytics/compute_correctness.py new file mode 100644 index 0000000000..b1cb4f8c9f --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/compute_correctness.py @@ -0,0 +1,164 @@ +import pandas as pd + + +def map_element_instance_options(instance): + instance_dict = instance.dict() + return { + "elementInstanceId": instance_dict["id"], + "type": instance_dict["elementData"]["type"], + "options": ( + instance_dict["elementData"]["options"] + if "options" in instance_dict["elementData"] + else None + ), + } + + +def compute_correctness_columns(df_element_instances, row): + element_instance = df_element_instances[ + df_element_instances["elementInstanceId"] == row["elementInstanceId"] + ].iloc[0] + response = row["response"] + options = element_instance["options"] + + if element_instance["type"] == "FLASHCARD" or element_instance["type"] == "CONTENT": + return None + + elif element_instance["type"] == "SC": + selected_choice = response["choices"][0] + correct_choice = next( + (choice["ix"] for choice in options["choices"] if choice["correct"]), None + ) + return "CORRECT" if selected_choice == correct_choice else "INCORRECT" + + elif element_instance["type"] == "MC" or element_instance["type"] == "KPRIM": + selected_choices = response["choices"] + correct_choices = [ + choice["ix"] for choice in options["choices"] if choice["correct"] + ] + available_choices = len(options["choices"]) + + selected_choices_array = [ + 1 if ix in selected_choices else 0 for ix in range(available_choices) + ] + correct_choices_array = [ + 1 if ix in correct_choices else 0 for ix in range(available_choices) + ] + hamming_distance = sum( + [ + 1 + for i in range(available_choices) + if selected_choices_array[i] != correct_choices_array[i] + ] + ) + + if element_instance["type"] == "MC": + correctness = max(-2 * hamming_distance / available_choices + 1, 0) + if correctness == 1: + return "CORRECT" + elif correctness == 0: + return "INCORRECT" + else: + return "PARTIAL" + elif element_instance["type"] == "KPRIM": + return ( + "CORRECT" + if hamming_distance == 0 + else "PARTIAL" if hamming_distance == 1 else "INCORRECT" + ) + + elif element_instance["type"] == "NUMERICAL": + response_value = float(response["value"]) + within_range = list( + map( + lambda range: float(range["min"]) + <= response_value + <= float(range["max"]), + options["solutionRanges"], + ) + ) + if any(within_range): + return "CORRECT" + + return "INCORRECT" + + elif element_instance["type"] == "FREE_TEXT": + response_value = response["value"] + solutions = list( + map(lambda solution: solution.strip().lower(), options["solutions"]) + ) + if response_value.strip().lower() in solutions: + return "CORRECT" + + return "INCORRECT" + + else: + raise ValueError("Unknown element type: {}".format(element_instance["type"])) + + +def compute_correctness(db, df_details, verbose=False): + if len(df_details) == 0: + print("No question response details found for the given date.") + return None, None + + df_details["course_start_date"] = pd.to_datetime(df_details["course_start_date"]) + df_details["course_end_date"] = pd.to_datetime(df_details["course_end_date"]) + df_details = df_details[ + (df_details["course_start_date"] <= df_details["createdAt"]) + & (df_details["course_end_date"] >= df_details["createdAt"]) + ] + + if verbose: + print( + "Number of question response details after course date filtering:", + len(df_details), + ) + + # Filter out the columns that are not needed + df_details = df_details[ + [ + "score", + "pointsAwarded", + "xpAwarded", + "timeSpent", + "response", + "elementInstanceId", + "participantId", + "courseId", + ] + ] + + if verbose: + print("Question detail responses:", len(df_details)) + print("Columns:", df_details.columns) + + # Compute correctness of the responses and add them as a separate column + # Get related element instances + element_instance_ids = df_details["elementInstanceId"].unique() + element_instances = db.elementinstance.find_many( + where={"id": {"in": element_instance_ids.tolist()}} + ) + + # Map the element instances to the corresponding elementData.options entries and convert it to a dataframe + df_element_instances = pd.DataFrame( + list(map(map_element_instance_options, element_instances)) + ) + + # If no element instances were found for the given element instance ids, return None + if len(df_element_instances) == 0: + print("No element instances found for the given element instance ids.") + return None, None + + # Compute the correctness for every response entry based on the element instance options (depending on the type of the element) + df_details["correctness"] = df_details.apply( + lambda x: compute_correctness_columns(df_element_instances, x), axis=1 + ) + df_details = df_details.dropna(subset=["correctness"]) + + if verbose: + print( + "Number of question response details with correctness computed (no flashcards / content elements):", + len(df_details), + ) + + return df_details, df_element_instances diff --git a/apps/analytics/src/modules/participant_analytics/compute_participant_analytics.py b/apps/analytics/src/modules/participant_analytics/compute_participant_analytics.py new file mode 100644 index 0000000000..c6ed154804 --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/compute_participant_analytics.py @@ -0,0 +1,30 @@ +from .get_participant_responses import get_participant_responses +from .compute_correctness import compute_correctness +from .aggregate_analytics import aggregate_analytics +from .save_participant_analytics import save_participant_analytics + + +def compute_participant_analytics( + db, start_date, end_date, timestamp, analytics_type="DAILY", verbose=False +): + df_details = get_participant_responses(db, start_date, end_date, verbose) + + # Compute the correctness of each question response detail + df_details, df_element_instances = compute_correctness(db, df_details, verbose) + + if df_details is None: + print(f"No participant responses found for {start_date} to {end_date}.") + del df_details + del df_element_instances + return + + # Compute participant analytics (score/xp counts and correctness statistics) + df_analytics = aggregate_analytics(df_details) + + # Save the aggreagted analytics into the database + save_participant_analytics(db, df_analytics, timestamp, analytics_type) + + # Delete the dataframes to avoid conflicts in the next iteration + del df_details + del df_element_instances + del df_analytics diff --git a/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py b/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py new file mode 100644 index 0000000000..9aed9528d1 --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py @@ -0,0 +1,110 @@ +import pandas as pd +from datetime import datetime +from .compute_correctness import compute_correctness +from .aggregate_analytics import aggregate_analytics +from .save_participant_analytics import save_participant_analytics + + +def compute_participant_course_analytics(db, df_courses, verbose=False): + # Count failure cases + courses_without_responses = 0 + + for idx, course in df_courses.iterrows(): + print( + f"Computing participant analytics for course {idx} out of {len(df_courses)}" + ) + course_id = course["id"] + course_start_date = course["startDate"] + course_end_date = course["endDate"] + course_id = course["id"] + + # Find all participants and corresponding linked responses through the participations + participations = db.participation.find_many( + where={"courseId": course_id}, + include={ + "participant": { + "include": { + "detailQuestionResponses": { + "where": { + "createdAt": { + "gte": course_start_date, + "lte": course_end_date, + } + }, + }, + "questionResponses": True, + } + } + }, + ) + + # Create a dataframe containing all detail responses + participations_dict = list(map(lambda x: x.dict(), participations)) + details_dict = list( + map( + lambda x: x["participant"]["detailQuestionResponses"], + participations_dict, + ) + ) + responses_dict = list( + map(lambda x: x["participant"]["questionResponses"], participations_dict) + ) + + details = [item for sublist in details_dict for item in sublist] + responses = [item for sublist in responses_dict for item in sublist] + if len(details) == 0 or len(responses) == 0: + courses_without_responses += 1 + print( + "No detail responses or response entries found for course {}".format( + course_id + ) + ) + continue + + # Create pandas dataframe containing all question responses and details + df_details = pd.DataFrame(details) + df_responses = pd.DataFrame(responses) + df_responses = df_responses[ + [ + "courseId", + "participantId", + "firstResponseCorrectness", + "lastResponseCorrectness", + ] + ] + + # Add the course start and end dates to the dataframe + df_details["course_start_date"] = course_start_date + df_details["course_end_date"] = course_end_date + df_details["courseId"] = course_id + + # Compute the correctness of each question response detail + df_details, df_element_instances = compute_correctness(db, df_details, verbose) + + if df_details is None: + print( + f"No participant responses found for {course_start_date} to {course_end_date}." + ) + del df_details + del df_element_instances + continue + + # Compute participant analytics (score/xp counts and correctness statistics) + df_analytics = aggregate_analytics(df_details, df_responses) + + # Save the aggreagted analytics into the database + end_curr_date = datetime.now().strftime("%Y-%m-%d") + "T23:59:59.999Z" + course_end_date_ext = course_end_date.strftime("%Y-%m-%d") + "T23:59:59.999Z" + timestamp = ( + course_end_date_ext + if course_end_date_ext < end_curr_date + else end_curr_date + ) + save_participant_analytics(db, df_analytics, timestamp, "COURSE") + + # Delete the dataframes to avoid conflicts in the next iteration + del df_details + del df_element_instances + del df_analytics + + return courses_without_responses diff --git a/apps/analytics/src/modules/participant_analytics/get_participant_responses.py b/apps/analytics/src/modules/participant_analytics/get_participant_responses.py new file mode 100644 index 0000000000..010638c8cf --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/get_participant_responses.py @@ -0,0 +1,86 @@ +import pandas as pd +from datetime import date + + +def map_details(detail, participantId): + courseId = ( + detail["practiceQuiz"]["courseId"] + if detail["practiceQuiz"] + else detail["microLearning"]["courseId"] + ) + return {**detail, "participantId": participantId, "courseId": courseId} + + +def map_participants(participant): + participant_dict = participant.dict() + return list( + map( + lambda detail: map_details(detail, participant_dict["id"]), + participant_dict["detailQuestionResponses"], + ) + ) + + +def convert_to_df(participants): + return pd.DataFrame( + [ + item + for sublist in list(map(map_participants, participants)) + for item in sublist + ] + ) + + +# Add the course start and end date to the dataframe for filtering of question response details later on +def set_course_dates(detail): + if detail["practiceQuiz"] is not None: + course = detail["practiceQuiz"]["course"] + detail["course_start_date"] = course["startDate"] + detail["course_end_date"] = course["endDate"] + elif detail["microLearning"] is not None: + course = detail["microLearning"]["course"] + detail["course_start_date"] = course["startDate"] + detail["course_end_date"] = course["endDate"] + else: + # If the instance is not part of a practice quiz or microlearning, set the start date far into the future -> no analytics should be computed + detail["course_start_date"] = date(9999, 12, 31) + detail["course_end_date"] = date(9999, 12, 31) + + return detail + + +def get_participant_responses(db, start_date, end_date, verbose=False): + participant_response_details = db.participant.find_many( + include={ + "detailQuestionResponses": { + "where": {"createdAt": {"gte": start_date, "lte": end_date}}, + "include": { + "practiceQuiz": {"include": {"course": True}}, + "microLearning": {"include": {"course": True}}, + }, + }, + } + ) + + if verbose: + # Print the first 5 question response details + print( + "Found {} participants for the timespan from {} to {}".format( + len(participant_response_details), start_date, end_date + ) + ) + print(participant_response_details[0]) + + # Convert the question response details to a pandas dataframe + df_details = convert_to_df(participant_response_details) + + # Filter out the question response details that are not within the course dates and do not consider them for the analysis + if verbose: + print( + "Number of question response details before course date filtering:", + len(df_details), + ) + + df_details = df_details.apply(set_course_dates, axis=1) + + return df_details diff --git a/apps/analytics/src/modules/participant_analytics/save_participant_analytics.py b/apps/analytics/src/modules/participant_analytics/save_participant_analytics.py new file mode 100644 index 0000000000..dd8af34f3b --- /dev/null +++ b/apps/analytics/src/modules/participant_analytics/save_participant_analytics.py @@ -0,0 +1,92 @@ +from datetime import datetime + + +def save_participant_analytics(db, df_analytics, timestamp, analytics_type="DAILY"): + computedAt = datetime.now().strftime("%Y-%m-%d") + "T00:00:00.000Z" + + # Create daily / weekly / monthly analytics entries for all participants + if analytics_type in ["DAILY", "WEEKLY", "MONTHLY"]: + for _, row in df_analytics.iterrows(): + db.participantanalytics.upsert( + where={ + "type_courseId_participantId_timestamp": { + "type": analytics_type, + "courseId": row["courseId"], + "participantId": row["participantId"], + "timestamp": timestamp, + } + }, + data={ + "create": { + "type": analytics_type, + "timestamp": timestamp, + "computedAt": computedAt, + "trialsCount": row["trialsCount"], + "responseCount": row["responseCount"], + "totalScore": row["totalScore"], + "totalPoints": row["totalPoints"], + "totalXp": row["totalXp"], + "meanCorrectCount": row["meanCorrectCount"], + "meanPartialCorrectCount": row["meanPartialCount"], + "meanWrongCount": row["meanWrongCount"], + "participant": {"connect": {"id": row["participantId"]}}, + "course": {"connect": {"id": row["courseId"]}}, + }, + "update": {}, + }, + ) + + # Create or update course-wide analytics entries (should be unique for participant / course combination) + elif analytics_type == "COURSE": + timestamp_const = "1970-01-01T00:00:00.000Z" + for _, row in df_analytics.iterrows(): + db.participantanalytics.upsert( + where={ + "type_courseId_participantId_timestamp": { + "type": analytics_type, + "courseId": row["courseId"], + "participantId": row["participantId"], + "timestamp": timestamp_const, + } + }, + data={ + "create": { + "type": "COURSE", + "timestamp": timestamp_const, + "computedAt": computedAt, + "trialsCount": row["trialsCount"], + "responseCount": row["responseCount"], + "totalScore": row["totalScore"], + "totalPoints": row["totalPoints"], + "totalXp": row["totalXp"], + "meanCorrectCount": row["meanCorrectCount"], + "meanPartialCorrectCount": row["meanPartialCount"], + "meanWrongCount": row["meanWrongCount"], + "firstCorrectCount": row["firstCorrectCount"], + "firstWrongCount": row["firstWrongCount"], + "lastCorrectCount": row["lastCorrectCount"], + "lastWrongCount": row["lastWrongCount"], + "participant": {"connect": {"id": row["participantId"]}}, + "course": {"connect": {"id": row["courseId"]}}, + }, + "update": { + "timestamp": timestamp_const, + "computedAt": computedAt, + "trialsCount": row["trialsCount"], + "responseCount": row["responseCount"], + "totalScore": row["totalScore"], + "totalPoints": row["totalPoints"], + "totalXp": row["totalXp"], + "meanCorrectCount": row["meanCorrectCount"], + "meanPartialCorrectCount": row["meanPartialCount"], + "meanWrongCount": row["meanWrongCount"], + "firstCorrectCount": row["firstCorrectCount"], + "firstWrongCount": row["firstWrongCount"], + "lastCorrectCount": row["lastCorrectCount"], + "lastWrongCount": row["lastWrongCount"], + }, + }, + ) + + else: + raise ValueError("Unknown analytics type: {}".format(analytics_type)) diff --git a/apps/analytics/src/notebooks/__init__.py b/apps/analytics/src/notebooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/analytics/daily_participant_analytics.ipynb b/apps/analytics/src/notebooks/archive/daily_participant_analytics.ipynb similarity index 98% rename from apps/analytics/daily_participant_analytics.ipynb rename to apps/analytics/src/notebooks/archive/daily_participant_analytics.ipynb index 8bc2dbad40..07aec1ea62 100644 --- a/apps/analytics/daily_participant_analytics.ipynb +++ b/apps/analytics/src/notebooks/archive/daily_participant_analytics.ipynb @@ -4,7 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Preparation & Data Fetching" + "## Preparation & Data Fetching (slightly outdated)\n", + "\n", + "New version (participant_analytics) takes into account the course start and end dates to limit the computational cost." ] }, { diff --git a/apps/analytics/notebook.ipynb b/apps/analytics/src/notebooks/archive/notebook.ipynb similarity index 100% rename from apps/analytics/notebook.ipynb rename to apps/analytics/src/notebooks/archive/notebook.ipynb diff --git a/apps/analytics/test.ipynb b/apps/analytics/src/notebooks/archive/test.ipynb similarity index 100% rename from apps/analytics/test.ipynb rename to apps/analytics/src/notebooks/archive/test.ipynb diff --git a/apps/analytics/src/notebooks/participant_analytics.ipynb b/apps/analytics/src/notebooks/participant_analytics.ipynb new file mode 100644 index 0000000000..4ccd85f5f8 --- /dev/null +++ b/apps/analytics/src/notebooks/participant_analytics.ipynb @@ -0,0 +1,181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparation & Data Fetching" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "\n", + "# set the python path correctly for module imports to work\n", + "import sys\n", + "sys.path.append('../../')\n", + "\n", + "from src.modules.participant_analytics.compute_participant_analytics import compute_participant_analytics\n", + "from src.modules.participant_analytics.compute_participant_course_analytics import compute_participant_course_analytics\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ['DATABASE_URL'] = 'postgresql://klicker:klicker@localhost:5432/klicker-prod'\n", + "\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False\n", + "\n", + "# Settings which analytics to compute\n", + "compute_daily = True\n", + "compute_weekly = True\n", + "compute_monthly = True\n", + "compute_course = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Daily / Weekly / Monthly Analytics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Print all dates between the 2022-10-23 and today\n", + "start_date = '2022-10-01'\n", + "end_date = datetime.now().strftime('%Y-%m-%d')\n", + "date_range_daily = pd.date_range(start=start_date, end=end_date, freq='D')\n", + "date_range_weekly = pd.date_range(start=start_date, end=end_date, freq='W')\n", + "date_range_monthly = pd.date_range(start=start_date, end=end_date, freq='ME')\n", + "\n", + "if compute_daily:\n", + " # Iterate over the date range and compute the participant analytics for each day\n", + " for curr_date in date_range_daily:\n", + " print(f'Computing daily participant analytics for {curr_date.strftime('%Y-%m-%d')}')\n", + " specific_date = curr_date.strftime('%Y-%m-%d')\n", + "\n", + " # Fetch all question response detail entries for a specific day\n", + " start_date = specific_date + 'T00:00:00.000Z'\n", + " end_date = specific_date + 'T23:59:59.999Z'\n", + "\n", + " # Compute participant analytics for a specific day\n", + " timestamp = start_date\n", + " compute_participant_analytics(db, start_date, end_date, timestamp, \"DAILY\", verbose)\n", + "\n", + "if compute_weekly:\n", + " # Iterate over the date range and compute the participant analytics for each week\n", + " for curr_date in date_range_weekly:\n", + " # Fetch all question response detail entries for a specific week\n", + " end_date = curr_date.strftime('%Y-%m-%d') + 'T23:59:59.999Z'\n", + " start_date = (curr_date - pd.DateOffset(days=6)).strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", + " print(f'Computing weekly participant analytics for {start_date} to {end_date}')\n", + "\n", + " # Compute participant analytics for a specific week\n", + " timestamp = end_date\n", + " compute_participant_analytics(db, start_date, end_date, timestamp, \"WEEKLY\", verbose)\n", + "\n", + "if compute_monthly:\n", + " # Iterate over the date range and compute the participant analytics for each month\n", + " for curr_date in date_range_monthly:\n", + " # Fetch all question response detail entries for a specific month\n", + " end_date = curr_date.strftime('%Y-%m-%d') + 'T23:59:59.999Z'\n", + " start_date = (curr_date - pd.offsets.MonthBegin(1)).strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", + " print(f'Computing monthly participant analytics for {start_date} to {end_date}')\n", + "\n", + " # Compute participant analytics for a specific month\n", + " timestamp = end_date\n", + " compute_participant_analytics(db, start_date, end_date, timestamp, \"MONTHLY\", verbose)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Course-Wide Participant Analytics (update daily?)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch all ongoing / past courses\n", + "if compute_course:\n", + " curr_date = '2024-08-27'\n", + " courses = db.course.find_many(\n", + " where={\n", + " # Incremental scripts can add this statement to reduce the amount of required computations\n", + " # 'endDate': {\n", + " # 'gt': datetime.now().strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", + " # }\n", + " 'startDate': {\n", + " 'lte': curr_date + 'T23:59:59.999Z'\n", + " }\n", + " }\n", + " )\n", + "\n", + " df_courses = pd.DataFrame(list(map(lambda x: x.dict(), courses)))\n", + " print(\"Found {} courses with a start date before {}\".format(len(df_courses), curr_date))\n", + "\n", + " courses_without_responses = compute_participant_course_analytics(db, df_courses, verbose)\n", + "\n", + " print(\"Found {} courses without any responses\".format(courses_without_responses))\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Disconnect from the database\n", + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-3uz8SvN3-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/packages/prisma/src/prisma/migrations/20240827152536_computed_at_analytics/migration.sql b/packages/prisma/src/prisma/migrations/20240827152536_computed_at_analytics/migration.sql new file mode 100644 index 0000000000..f4a5ba068d --- /dev/null +++ b/packages/prisma/src/prisma/migrations/20240827152536_computed_at_analytics/migration.sql @@ -0,0 +1,36 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `AggregatedAnalytics` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `AggregatedCompetencyAnalytics` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `AggregatedCourseAnalytics` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `CompetencyAnalytics` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `ParticipantAnalytics` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `ParticipantCourseAnalytics` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "AggregatedAnalytics" ADD COLUMN "computedAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "AggregatedCompetencyAnalytics" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "AggregatedCourseAnalytics" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "CompetencyAnalytics" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "ParticipantAnalytics" ADD COLUMN "computedAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- AlterTable +ALTER TABLE "ParticipantCourseAnalytics" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index dcd1b15f3d..ef1a1e8fd2 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -16,7 +16,8 @@ model ParticipantAnalytics { id Int @id @default(autoincrement()) type AnalyticsType - timestamp DateTime @db.Date + timestamp DateTime @db.Date + computedAt DateTime @db.Date @default(now()) // unsolvedQuestionsCount = AggregatedAnalytics.totalElementsAvailable - responseCount trialsCount Int // total number of questions attempted @@ -47,6 +48,9 @@ model ParticipantAnalytics { course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) courseId String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([type, courseId, participantId, timestamp]) } @@ -57,6 +61,7 @@ model AggregatedAnalytics { // all quantities are defined as the values at the end of the selected timeframe timestamp DateTime @db.Date + computedAt DateTime @db.Date @default(now()) responseCount Int participantCount Int totalScore Int @@ -69,6 +74,9 @@ model AggregatedAnalytics { course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) courseId String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([type, courseId, timestamp]) } @@ -87,6 +95,9 @@ model CompetencyAnalytics { participantAnalytics ParticipantAnalytics @relation(fields: [participantAnalyticsId], references: [id], onDelete: Cascade, onUpdate: Cascade) participantAnalyticsId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([competencyId, participantAnalyticsId]) } @@ -105,6 +116,9 @@ model AggregatedCompetencyAnalytics { aggregatedAnalytics AggregatedAnalytics @relation(fields: [aggregatedAnalyticsId], references: [id], onDelete: Cascade, onUpdate: Cascade) aggregatedAnalyticsId Int + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([competencyId, aggregatedAnalyticsId]) } @@ -123,6 +137,9 @@ model ParticipantCourseAnalytics { participant Participant @relation(fields: [participantId], references: [id], onDelete: Cascade, onUpdate: Cascade) participantId String @db.Uuid + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + @@unique([courseId, participantId]) } @@ -140,6 +157,9 @@ model AggregatedCourseAnalytics { course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) courseId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model CompetencyTree { From 71aeaa5115d4c1c99ca88d073d05478eba81386e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Schl=C3=A4fli?= Date: Mon, 11 Nov 2024 10:41:41 +0100 Subject: [PATCH 05/38] chore: improve documentation and structure of analytics service --- apps/analytics/README.md | 24 + apps/analytics/package.json | 13 + apps/analytics/poetry.lock | 812 ++++++++++++++++--------------- apps/analytics/pyproject.toml | 8 +- apps/analytics/{ => src}/main.py | 0 pnpm-lock.yaml | 164 +++---- 6 files changed, 536 insertions(+), 485 deletions(-) create mode 100644 apps/analytics/README.md create mode 100644 apps/analytics/package.json rename apps/analytics/{ => src}/main.py (100%) diff --git a/apps/analytics/README.md b/apps/analytics/README.md new file mode 100644 index 0000000000..80e0cafcbb --- /dev/null +++ b/apps/analytics/README.md @@ -0,0 +1,24 @@ +# KlickerUZH Analytics + +This service computes learning analytics for KlickerUZH, providing insights into student learning patterns and performance metrics. + +## Requirements + +- Python 3.12.x (e.g., installed through `asdf`) +- Node.js 20.x.x +- Poetry + +## Setup + +- The project uses Poetry for dependency management and environment isolation. Make sure you have Poetry installed before proceeding. Then run `poetry install` in this folder to prepare the virtual environment. +- The project uses PNPM to simplify the execution of scripts and to provide a watch mode for execution. Make sure that you have executed `pnpm install` in the repository before trying to run the commands below. +- Make sure that all `.prisma` files are available in `prisma/`. If this is not the case, run the `util/sync-schema.sh` script first. +- Make sure that a valid Python environment is used (3.12). If poetry tries to use an environment not matching specifications, the install command or script execution might fail. The Python binary to be used can be set expliticly using `poetry env use /Users/.../bin/python` (after which `poetry install` has to be run). Tools like `asdf` allow the clean management of multiple Python versions on a single machine. + +## Available Commands + +The following commands are available through PNPM: + +- `pnpm generate` - Generate the Prisma client for database access in Python +- `pnpm main` - Run the analytics service +- `pnpm dev` - Start the service in watch mode for development diff --git a/apps/analytics/package.json b/apps/analytics/package.json new file mode 100644 index 0000000000..9541005ca9 --- /dev/null +++ b/apps/analytics/package.json @@ -0,0 +1,13 @@ +{ + "name": "@klicker-uzh/analytics", + "version": "3.3.0-alpha.8", + "license": "AGPL-3.0", + "devDependencies": { + "nodemon": "~3.1.7" + }, + "scripts": { + "dev": "doppler run --config dev -- nodemon --exec 'poetry run poe main' --watch src,prisma --ext py,prisma", + "generate": "poetry run poe generate", + "main": "doppler run --config dev -- poetry run poe main" + } +} diff --git a/apps/analytics/poetry.lock b/apps/analytics/poetry.lock index b059494c73..246effdd8b 100644 --- a/apps/analytics/poetry.lock +++ b/apps/analytics/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -13,13 +13,13 @@ files = [ [[package]] name = "anyio" -version = "4.4.0" +version = "4.6.2.post1" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, + {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, + {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, ] [package.dependencies] @@ -27,9 +27,9 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "appnope" @@ -62,89 +62,89 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -194,33 +194,37 @@ test = ["pytest"] [[package]] name = "debugpy" -version = "1.8.5" +version = "1.8.8" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, - {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, - {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, - {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, - {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, - {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, - {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, - {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, - {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, - {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, - {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, - {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, - {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, - {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, - {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, - {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, - {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, - {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, - {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, - {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, - {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, - {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, + {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, + {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"}, + {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"}, + {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"}, + {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"}, + {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"}, + {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"}, + {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"}, + {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"}, + {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"}, + {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"}, + {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"}, + {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"}, + {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"}, + {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"}, + {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"}, + {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"}, + {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"}, + {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"}, + {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"}, + {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"}, + {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"}, + {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"}, + {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"}, + {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"}, + {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"}, ] [[package]] @@ -236,13 +240,13 @@ files = [ [[package]] name = "executing" -version = "2.0.1" +version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, ] [package.extras] @@ -261,13 +265,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.5" +version = "1.0.6" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, + {file = "httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f"}, + {file = "httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f"}, ] [package.dependencies] @@ -278,17 +282,17 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -303,18 +307,22 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "ipykernel" version = "6.29.5" @@ -350,13 +358,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.26.0" +version = "8.29.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.26.0-py3-none-any.whl", hash = "sha256:e6b347c27bdf9c32ee9d31ae85defc525755a1869f14057e900675b9e8d6e6ff"}, - {file = "ipython-8.26.0.tar.gz", hash = "sha256:1cec0fbba8404af13facebe83d04436a7434c7400e59f47acf467c64abd0956c"}, + {file = "ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8"}, + {file = "ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb"}, ] [package.dependencies] @@ -386,22 +394,22 @@ test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "num [[package]] name = "jedi" -version = "0.19.1" +version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [package.dependencies] -parso = ">=0.8.3,<0.9.0" +parso = ">=0.8.4,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jinja2" @@ -422,13 +430,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jupyter-client" -version = "8.6.2" +version = "8.6.3" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, - {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, ] [package.dependencies] @@ -464,71 +472,72 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout" [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] @@ -569,74 +578,77 @@ files = [ [[package]] name = "numpy" -version = "2.1.0" +version = "2.1.3" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6326ab99b52fafdcdeccf602d6286191a79fe2fda0ae90573c5814cd2b0bc1b8"}, - {file = "numpy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0937e54c09f7a9a68da6889362ddd2ff584c02d015ec92672c099b61555f8911"}, - {file = "numpy-2.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:30014b234f07b5fec20f4146f69e13cfb1e33ee9a18a1879a0142fbb00d47673"}, - {file = "numpy-2.1.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:899da829b362ade41e1e7eccad2cf274035e1cb36ba73034946fccd4afd8606b"}, - {file = "numpy-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08801848a40aea24ce16c2ecde3b756f9ad756586fb2d13210939eb69b023f5b"}, - {file = "numpy-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:398049e237d1aae53d82a416dade04defed1a47f87d18d5bd615b6e7d7e41d1f"}, - {file = "numpy-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0abb3916a35d9090088a748636b2c06dc9a6542f99cd476979fb156a18192b84"}, - {file = "numpy-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10e2350aea18d04832319aac0f887d5fcec1b36abd485d14f173e3e900b83e33"}, - {file = "numpy-2.1.0-cp310-cp310-win32.whl", hash = "sha256:f6b26e6c3b98adb648243670fddc8cab6ae17473f9dc58c51574af3e64d61211"}, - {file = "numpy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:f505264735ee074250a9c78247ee8618292091d9d1fcc023290e9ac67e8f1afa"}, - {file = "numpy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:76368c788ccb4f4782cf9c842b316140142b4cbf22ff8db82724e82fe1205dce"}, - {file = "numpy-2.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8e93a01a35be08d31ae33021e5268f157a2d60ebd643cfc15de6ab8e4722eb1"}, - {file = "numpy-2.1.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9523f8b46485db6939bd069b28b642fec86c30909cea90ef550373787f79530e"}, - {file = "numpy-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54139e0eb219f52f60656d163cbe67c31ede51d13236c950145473504fa208cb"}, - {file = "numpy-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebbf9fbdabed208d4ecd2e1dfd2c0741af2f876e7ae522c2537d404ca895c3"}, - {file = "numpy-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:378cb4f24c7d93066ee4103204f73ed046eb88f9ad5bb2275bb9fa0f6a02bd36"}, - {file = "numpy-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8f699a709120b220dfe173f79c73cb2a2cab2c0b88dd59d7b49407d032b8ebd"}, - {file = "numpy-2.1.0-cp311-cp311-win32.whl", hash = "sha256:ffbd6faeb190aaf2b5e9024bac9622d2ee549b7ec89ef3a9373fa35313d44e0e"}, - {file = "numpy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0af3a5987f59d9c529c022c8c2a64805b339b7ef506509fba7d0556649b9714b"}, - {file = "numpy-2.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe76d75b345dc045acdbc006adcb197cc680754afd6c259de60d358d60c93736"}, - {file = "numpy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f358ea9e47eb3c2d6eba121ab512dfff38a88db719c38d1e67349af210bc7529"}, - {file = "numpy-2.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:dd94ce596bda40a9618324547cfaaf6650b1a24f5390350142499aa4e34e53d1"}, - {file = "numpy-2.1.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b47c551c6724960479cefd7353656498b86e7232429e3a41ab83be4da1b109e8"}, - {file = "numpy-2.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0756a179afa766ad7cb6f036de622e8a8f16ffdd55aa31f296c870b5679d745"}, - {file = "numpy-2.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24003ba8ff22ea29a8c306e61d316ac74111cebf942afbf692df65509a05f111"}, - {file = "numpy-2.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b34fa5e3b5d6dc7e0a4243fa0f81367027cb6f4a7215a17852979634b5544ee0"}, - {file = "numpy-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4f982715e65036c34897eb598d64aef15150c447be2cfc6643ec7a11af06574"}, - {file = "numpy-2.1.0-cp312-cp312-win32.whl", hash = "sha256:c4cd94dfefbefec3f8b544f61286584292d740e6e9d4677769bc76b8f41deb02"}, - {file = "numpy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0cdef204199278f5c461a0bed6ed2e052998276e6d8ab2963d5b5c39a0500bc"}, - {file = "numpy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8ab81ccd753859ab89e67199b9da62c543850f819993761c1e94a75a814ed667"}, - {file = "numpy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442596f01913656d579309edcd179a2a2f9977d9a14ff41d042475280fc7f34e"}, - {file = "numpy-2.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:848c6b5cad9898e4b9ef251b6f934fa34630371f2e916261070a4eb9092ffd33"}, - {file = "numpy-2.1.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:54c6a63e9d81efe64bfb7bcb0ec64332a87d0b87575f6009c8ba67ea6374770b"}, - {file = "numpy-2.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:652e92fc409e278abdd61e9505649e3938f6d04ce7ef1953f2ec598a50e7c195"}, - {file = "numpy-2.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab32eb9170bf8ffcbb14f11613f4a0b108d3ffee0832457c5d4808233ba8977"}, - {file = "numpy-2.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8fb49a0ba4d8f41198ae2d52118b050fd34dace4b8f3fb0ee34e23eb4ae775b1"}, - {file = "numpy-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44e44973262dc3ae79e9063a1284a73e09d01b894b534a769732ccd46c28cc62"}, - {file = "numpy-2.1.0-cp313-cp313-win32.whl", hash = "sha256:ab83adc099ec62e044b1fbb3a05499fa1e99f6d53a1dde102b2d85eff66ed324"}, - {file = "numpy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:de844aaa4815b78f6023832590d77da0e3b6805c644c33ce94a1e449f16d6ab5"}, - {file = "numpy-2.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:343e3e152bf5a087511cd325e3b7ecfd5b92d369e80e74c12cd87826e263ec06"}, - {file = "numpy-2.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f07fa2f15dabe91259828ce7d71b5ca9e2eb7c8c26baa822c825ce43552f4883"}, - {file = "numpy-2.1.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5474dad8c86ee9ba9bb776f4b99ef2d41b3b8f4e0d199d4f7304728ed34d0300"}, - {file = "numpy-2.1.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:1f817c71683fd1bb5cff1529a1d085a57f02ccd2ebc5cd2c566f9a01118e3b7d"}, - {file = "numpy-2.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a3336fbfa0d38d3deacd3fe7f3d07e13597f29c13abf4d15c3b6dc2291cbbdd"}, - {file = "numpy-2.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a894c51fd8c4e834f00ac742abad73fc485df1062f1b875661a3c1e1fb1c2f6"}, - {file = "numpy-2.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:9156ca1f79fc4acc226696e95bfcc2b486f165a6a59ebe22b2c1f82ab190384a"}, - {file = "numpy-2.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:624884b572dff8ca8f60fab591413f077471de64e376b17d291b19f56504b2bb"}, - {file = "numpy-2.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15ef8b2177eeb7e37dd5ef4016f30b7659c57c2c0b57a779f1d537ff33a72c7b"}, - {file = "numpy-2.1.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e5f0642cdf4636198a4990de7a71b693d824c56a757862230454629cf62e323d"}, - {file = "numpy-2.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15976718c004466406342789f31b6673776360f3b1e3c575f25302d7e789575"}, - {file = "numpy-2.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6c1de77ded79fef664d5098a66810d4d27ca0224e9051906e634b3f7ead134c2"}, - {file = "numpy-2.1.0.tar.gz", hash = "sha256:7dc90da0081f7e1da49ec4e398ede6a8e9cc4f5ebe5f9e06b443ed889ee9aaa2"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, + {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, + {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, + {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, + {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, + {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, + {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, + {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, + {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, + {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, + {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, + {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, + {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, + {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, + {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, + {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, + {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, + {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, + {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, + {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, + {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, + {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, + {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, + {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, + {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, + {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, + {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, + {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, ] [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -750,19 +762,19 @@ ptyprocess = ">=0.5" [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" 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.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [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)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "poethepoet" @@ -784,13 +796,13 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "prisma" -version = "0.14.0" +version = "0.15.0" description = "Prisma Client Python is an auto-generated and fully type-safe database client" optional = false python-versions = ">=3.8.0" files = [ - {file = "prisma-0.14.0-py3-none-any.whl", hash = "sha256:778ca6d9c43295a31a0cc116bf26bb3f91f444a718b660827d8e14d050caba4b"}, - {file = "prisma-0.14.0.tar.gz", hash = "sha256:b6ffac08f5edf179e9407d2ba8121367edf5f32f14b9519e1dcaf50f31e88d9c"}, + {file = "prisma-0.15.0-py3-none-any.whl", hash = "sha256:de949cc94d3d91243615f22ff64490aa6e2d7cb81aabffce53d92bd3977c09a4"}, + {file = "prisma-0.15.0.tar.gz", hash = "sha256:5cd6402aa8322625db3fc1152040404e7fc471fe7f8fa3a314fa8a99529ca107"}, ] [package.dependencies] @@ -809,13 +821,13 @@ node = ["nodejs-bin"] [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -823,32 +835,33 @@ wcwidth = "*" [[package]] name = "psutil" -version = "6.0.0" +version = "6.1.0" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, ] [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] [[package]] name = "ptyprocess" @@ -888,18 +901,18 @@ files = [ [[package]] name = "pydantic" -version = "2.8.2" +version = "2.9.2" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" typing-extensions = [ {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, @@ -907,103 +920,104 @@ typing-extensions = [ [package.extras] email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.20.1" +version = "2.23.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, ] [package.dependencies] @@ -1071,36 +1085,40 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] name = "pywin32" -version = "306" +version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] [[package]] @@ -1267,13 +1285,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -1335,13 +1353,13 @@ files = [ [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -1369,4 +1387,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "4982b71a45b56d2d6336dacf424bef938e744116396f7f520361c45276edd1a9" +content-hash = "cc9a07ad04c87d0144e932c34865a5896db7681a8bf164998005dcbdae821ce1" diff --git a/apps/analytics/pyproject.toml b/apps/analytics/pyproject.toml index 8b041e3a58..5d8ae008b7 100644 --- a/apps/analytics/pyproject.toml +++ b/apps/analytics/pyproject.toml @@ -5,13 +5,13 @@ description = "" authors = ["Roland Schlaefli ", "Julius Schlapbach "] license = "AGPL-3.0" readme = "README.md" -packages = [{include = "@klicker_uzh"}] +package-mode = false [tool.poetry.dependencies] python = "^3.12" pandas = "2.2.2" -prisma = "0.14.0" -xlsxwriter = "^3.2.0" +prisma = "0.15.0" +xlsxwriter = "3.2.0" [tool.poetry.dev-dependencies] poethepoet = "0.27.0" @@ -26,7 +26,7 @@ build-backend = "poetry.core.masonry.api" [tool.poe.tasks] generate = "prisma generate" -main = "doppler run --config dev -- python main.py" +main = "doppler run --config dev -- python -m src.main" [tool.pyright] typeCheckingMode = "strict" diff --git a/apps/analytics/main.py b/apps/analytics/src/main.py similarity index 100% rename from apps/analytics/main.py rename to apps/analytics/src/main.py diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51d0e637f7..1653506c43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,12 @@ importers: specifier: ~2.6.0 version: 2.6.0 + apps/analytics: + devDependencies: + nodemon: + specifier: ~3.1.7 + version: 3.1.7 + apps/auth: dependencies: '@klicker-uzh/i18n': @@ -17079,7 +17085,7 @@ snapshots: '@azure/msal-common@4.5.1': dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -17176,7 +17182,7 @@ snapshots: '@babel/traverse': 7.25.4 '@babel/types': 7.25.4 convert-source-map: 1.9.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17196,7 +17202,7 @@ snapshots: '@babel/traverse': 7.25.7 '@babel/types': 7.25.8 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17216,7 +17222,7 @@ snapshots: '@babel/traverse': 7.25.4 '@babel/types': 7.25.4 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17236,7 +17242,7 @@ snapshots: '@babel/traverse': 7.25.7 '@babel/types': 7.25.8 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17349,7 +17355,7 @@ snapshots: '@babel/core': 7.21.8 '@babel/helper-compilation-targets': 7.25.7 '@babel/helper-plugin-utils': 7.24.8 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -17360,7 +17366,7 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-compilation-targets': 7.25.7 '@babel/helper-plugin-utils': 7.24.8 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -17371,7 +17377,7 @@ snapshots: '@babel/core': 7.25.8 '@babel/helper-compilation-targets': 7.25.7 '@babel/helper-plugin-utils': 7.24.8 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -19546,7 +19552,7 @@ snapshots: '@babel/parser': 7.25.8 '@babel/template': 7.25.0 '@babel/types': 7.25.4 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -19558,7 +19564,7 @@ snapshots: '@babel/parser': 7.25.8 '@babel/template': 7.25.7 '@babel/types': 7.25.8 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -19592,7 +19598,7 @@ snapshots: '@ionic/utils-subprocess': 2.1.14 '@ionic/utils-terminal': 2.3.5 commander: 9.5.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) env-paths: 2.2.1 kleur: 4.1.5 native-run: 1.7.4 @@ -19636,7 +19642,7 @@ snapshots: chalk: 4.1.2 cypress: 13.15.0 dayjs: 1.11.13 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) execa: 4.1.0 globby: 11.1.0 istanbul-lib-coverage: 3.2.2 @@ -19655,7 +19661,7 @@ snapshots: chalk: 4.1.2 cypress: 13.15.0 dayjs: 1.11.13 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) execa: 4.1.0 globby: 11.1.0 istanbul-lib-coverage: 3.2.2 @@ -19692,7 +19698,7 @@ snapshots: '@babel/preset-env': 7.25.4(@babel/core@7.25.8) babel-loader: 9.2.1(@babel/core@7.25.8)(webpack@5.94.0(@swc/core@1.3.101(@swc/helpers@0.5.13))) bluebird: 3.7.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) lodash: 4.17.21 webpack: 5.94.0(@swc/core@1.3.101(@swc/helpers@0.5.13)) transitivePeerDependencies: @@ -19704,7 +19710,7 @@ snapshots: '@babel/preset-env': 7.25.4(@babel/core@7.25.8) babel-loader: 9.2.1(@babel/core@7.25.8)(webpack@5.94.0) bluebird: 3.7.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) lodash: 4.17.21 webpack: 5.94.0 transitivePeerDependencies: @@ -20872,7 +20878,7 @@ snapshots: '@eslint/eslintrc@0.4.3': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) espree: 7.3.1 globals: 13.24.0 ignore: 4.0.6 @@ -20886,7 +20892,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -21391,7 +21397,7 @@ snapshots: '@types/js-yaml': 4.0.9 '@whatwg-node/fetch': 0.9.21 chalk: 4.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) dotenv: 16.4.5 graphql: 16.9.0 graphql-request: 6.1.0(graphql@16.9.0) @@ -21543,7 +21549,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -21551,7 +21557,7 @@ snapshots: '@humanwhocodes/config-array@0.5.0': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -21747,14 +21753,14 @@ snapshots: '@ionic/cli-framework-output@2.2.8': dependencies: '@ionic/utils-terminal': 2.3.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) tslib: 2.8.0 transitivePeerDependencies: - supports-color '@ionic/utils-array@2.1.6': dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) tslib: 2.8.0 transitivePeerDependencies: - supports-color @@ -21762,7 +21768,7 @@ snapshots: '@ionic/utils-fs@3.1.7': dependencies: '@types/fs-extra': 8.1.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) fs-extra: 9.1.0 tslib: 2.8.0 transitivePeerDependencies: @@ -21770,7 +21776,7 @@ snapshots: '@ionic/utils-object@2.1.6': dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) tslib: 2.8.0 transitivePeerDependencies: - supports-color @@ -21779,7 +21785,7 @@ snapshots: dependencies: '@ionic/utils-object': 2.1.6 '@ionic/utils-terminal': 2.3.4 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) signal-exit: 3.0.7 tree-kill: 1.2.2 tslib: 2.8.0 @@ -21788,7 +21794,7 @@ snapshots: '@ionic/utils-stream@3.1.6': dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) tslib: 2.8.0 transitivePeerDependencies: - supports-color @@ -21801,7 +21807,7 @@ snapshots: '@ionic/utils-stream': 3.1.6 '@ionic/utils-terminal': 2.3.4 cross-spawn: 7.0.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) tslib: 2.8.0 transitivePeerDependencies: - supports-color @@ -21809,7 +21815,7 @@ snapshots: '@ionic/utils-terminal@2.3.4': dependencies: '@types/slice-ansi': 4.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) signal-exit: 3.0.7 slice-ansi: 4.0.0 string-width: 4.2.3 @@ -21823,7 +21829,7 @@ snapshots: '@ionic/utils-terminal@2.3.5': dependencies: '@types/slice-ansi': 4.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) signal-exit: 3.0.7 slice-ansi: 4.0.0 string-width: 4.2.3 @@ -22107,7 +22113,7 @@ snapshots: '@microsoft/dev-tunnels-contracts@1.1.9': dependencies: buffer: 5.7.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) vscode-jsonrpc: 4.0.0 transitivePeerDependencies: - supports-color @@ -22117,7 +22123,7 @@ snapshots: '@microsoft/dev-tunnels-contracts': 1.1.9 axios: 1.7.7(debug@4.3.7) buffer: 5.7.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) vscode-jsonrpc: 4.0.0 transitivePeerDependencies: - supports-color @@ -24974,7 +24980,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@7.32.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@4.9.5) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 7.32.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -24993,7 +24999,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@7.32.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@5.6.3) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 7.32.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -25012,7 +25018,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.45.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.45.0)(typescript@5.6.3) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.45.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -25029,7 +25035,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 7.32.0 optionalDependencies: typescript: 4.9.5 @@ -25041,7 +25047,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 7.32.0 optionalDependencies: typescript: 5.6.3 @@ -25054,7 +25060,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.45.0 optionalDependencies: typescript: 5.6.3 @@ -25075,7 +25081,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@4.9.5) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@4.9.5) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 7.32.0 tsutils: 3.21.0(typescript@4.9.5) optionalDependencies: @@ -25087,7 +25093,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@7.32.0)(typescript@5.6.3) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 7.32.0 tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: @@ -25099,7 +25105,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@8.45.0)(typescript@5.6.3) - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.45.0 tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: @@ -25115,7 +25121,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -25129,7 +25135,7 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -25143,7 +25149,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -25582,13 +25588,13 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color agent-base@7.1.1: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25937,7 +25943,7 @@ snapshots: azure-functions-core-tools@4.0.6280: dependencies: chalk: 3.0.0 - extract-zip: 2.0.1 + extract-zip: 2.0.1(supports-color@8.1.1) https-proxy-agent: 5.0.0 progress: 2.0.3 rimraf: 4.4.1 @@ -27624,7 +27630,7 @@ snapshots: detect-port@1.6.1: dependencies: address: 1.2.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -27844,7 +27850,7 @@ snapshots: base64id: 2.0.0 cookie: 0.4.2 cors: 2.8.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.17.1 transitivePeerDependencies: @@ -28082,7 +28088,7 @@ snapshots: eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.45.0): dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) enhanced-resolve: 5.17.1 eslint: 8.45.0 eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.45.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.45.0))(eslint@8.45.0) @@ -28292,7 +28298,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 enquirer: 2.4.1 escape-string-regexp: 4.0.0 @@ -28341,7 +28347,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -28556,16 +28562,6 @@ snapshots: extract-files@11.0.0: {} - extract-zip@2.0.1: - dependencies: - debug: 4.3.7(supports-color@5.5.0) - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - extract-zip@2.0.1(supports-color@8.1.1): dependencies: debug: 4.3.7(supports-color@8.1.1) @@ -28794,7 +28790,7 @@ snapshots: follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) for-each@0.3.3: dependencies: @@ -29604,7 +29600,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -29651,14 +29647,14 @@ snapshots: https-proxy-agent@5.0.0: dependencies: agent-base: 6.0.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -29827,7 +29823,7 @@ snapshots: ioredis@4.28.5: dependencies: cluster-key-slot: 1.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) denque: 1.5.1 lodash.defaults: 4.2.0 lodash.flatten: 4.4.0 @@ -29844,7 +29840,7 @@ snapshots: dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -30170,7 +30166,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -30798,7 +30794,7 @@ snapshots: dependencies: chalk: 5.3.0 commander: 12.1.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) execa: 8.0.1 lilconfig: 3.1.2 listr2: 8.2.5 @@ -31029,7 +31025,7 @@ snapshots: body-parser: 1.20.3 cookie-parser: 1.4.7 cors: 2.8.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) express: 4.21.1 fast-url-parser: 1.1.3 got: 11.8.6 @@ -31811,7 +31807,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -31833,7 +31829,7 @@ snapshots: micromark@4.0.0: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.1 @@ -32000,7 +31996,7 @@ snapshots: mquery@5.0.0: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -32063,7 +32059,7 @@ snapshots: '@ionic/utils-fs': 3.1.7 '@ionic/utils-terminal': 2.3.5 bplist-parser: 0.3.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) elementtree: 0.1.7 ini: 3.0.1 plist: 3.1.0 @@ -34332,7 +34328,7 @@ snapshots: require-in-the-middle@7.4.0: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) module-details-from-path: 1.0.3 resolve: 1.22.8 transitivePeerDependencies: @@ -34411,7 +34407,7 @@ snapshots: rhea-promise@3.0.3: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) rhea: 3.0.3 tslib: 2.8.0 transitivePeerDependencies: @@ -34419,7 +34415,7 @@ snapshots: rhea@3.0.3: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -34630,7 +34626,7 @@ snapshots: dependencies: '@types/debug': 4.1.12 '@types/validator': 13.12.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) dottie: 2.0.6 inflection: 1.13.4 lodash: 4.17.21 @@ -34932,7 +34928,7 @@ snapshots: socket.io-adapter@2.5.5: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) ws: 8.17.1 transitivePeerDependencies: - bufferutil @@ -34942,7 +34938,7 @@ snapshots: socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -34951,7 +34947,7 @@ snapshots: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) engine.io: 6.5.5 socket.io-adapter: 2.5.5 socket.io-parser: 4.2.4 @@ -35047,7 +35043,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -35058,7 +35054,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -35855,7 +35851,7 @@ snapshots: cac: 6.7.14 chokidar: 3.6.0 consola: 3.2.3 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7(supports-color@8.1.1) esbuild: 0.23.1 execa: 5.1.1 joycon: 3.1.1 From 85ad9dfd8195c66a828082aa7eca8193dad93279 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Mon, 11 Nov 2024 11:28:40 +0100 Subject: [PATCH 06/38] chore: fix test suite after combination of extended tests with new live quiz URLs (#4369) --- .../src/components/sessions/Session.tsx | 2 +- .../src/content/components/URLForm.tsx | 2 +- cypress/cypress/e2e/E-course-workflow.cy.ts | 4 +- .../cypress/e2e/F-live-quiz-workflow.cy.ts | 20 ++--- packages/markdown/src/public/components.css | 1 + packages/markdown/src/public/utilities.css | 86 +++++++++---------- 6 files changed, 57 insertions(+), 58 deletions(-) diff --git a/apps/frontend-manage/src/components/sessions/Session.tsx b/apps/frontend-manage/src/components/sessions/Session.tsx index 6ee5b7f74d..e9ba259eee 100644 --- a/apps/frontend-manage/src/components/sessions/Session.tsx +++ b/apps/frontend-manage/src/components/sessions/Session.tsx @@ -193,7 +193,7 @@ function Session({ session }: SessionProps) { disabled={startingQuiz} onClick={async () => { await startSession() - router.push(`sessions/${session.id}/cockpit`) + router.push(`quizzes/${session.id}/cockpit`) }} data={{ cy: `start-session-${session.name}` }} > diff --git a/apps/office-addin/src/content/components/URLForm.tsx b/apps/office-addin/src/content/components/URLForm.tsx index ad4ae05130..f57350a6ba 100644 --- a/apps/office-addin/src/content/components/URLForm.tsx +++ b/apps/office-addin/src/content/components/URLForm.tsx @@ -47,7 +47,7 @@ export function URLForm({ slideID }: URLFormProps) { labelType="large" tooltip="Enter the embedding URL of the evaluation you want to add to this slide" className={{ root: 'w-full' }} - placeholder="https://manage.klicker.uzh.ch/sessions/12345/evaluation?hmac=xyz" + placeholder="https://manage.klicker.uzh.ch/quizzes/12345/evaluation?hmac=xyz" data={{ cy: 'url-form-input' }} /> diff --git a/cypress/cypress/e2e/E-course-workflow.cy.ts b/cypress/cypress/e2e/E-course-workflow.cy.ts index 292ee6efdb..48e2f53efa 100644 --- a/cypress/cypress/e2e/E-course-workflow.cy.ts +++ b/cypress/cypress/e2e/E-course-workflow.cy.ts @@ -513,7 +513,7 @@ describe('Test course creation and editing functionalities', () => { cy.reload() // create a question with sample solution - cy.get('[data-cy="questions"]').click() + cy.get('[data-cy="library"]').click() cy.createQuestionSC({ title: questionTitle, content: questionContent, @@ -599,7 +599,7 @@ describe('Test course creation and editing functionalities', () => { ) // check that the live quiz has been removed from the course - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.contains('[data-cy="session-block"]', liveQuizName) cy.get(`[data-cy="edit-session-${liveQuizName}"]`).click() cy.get('[data-cy="next-or-submit"]').click() diff --git a/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts b/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts index 555ec58c99..c8d67ea7ac 100644 --- a/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts +++ b/cypress/cypress/e2e/F-live-quiz-workflow.cy.ts @@ -270,7 +270,7 @@ describe('Different live-quiz workflows', () => { it('Edit the created session and check if all settings persist', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.contains('[data-cy="session-block"]', sessionName1) cy.get(`[data-cy="edit-session-${sessionName1}"]`).click() @@ -489,7 +489,7 @@ describe('Different live-quiz workflows', () => { it('Duplicate the live quiz', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.contains('[data-cy="session-block"]', sessionName1New) // duplicate the session and verify that the content is the same as for the original session @@ -523,7 +523,7 @@ describe('Different live-quiz workflows', () => { it('Cleanup: Delete the duplicated live quiz', () => { cy.loginLecturer() - cy.get(`[data-cy="sessions"]`).click() + cy.get(`[data-cy="quizzes"]`).click() cy.findByText(sessionName1Dupl).should('exist') cy.get(`[data-cy="delete-live-quiz-${sessionName1Dupl}"]`).click() cy.get(`[data-cy="confirm-delete-live-quiz"]`).click() @@ -533,7 +533,7 @@ describe('Different live-quiz workflows', () => { // ! Part 2: Live Quiz Control it('Start the created live quizzes, abort it, and restart & completes it', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.contains('[data-cy="session-block"]', sessionName1New) // start session and then abort it @@ -568,7 +568,7 @@ describe('Different live-quiz workflows', () => { it('Cleanup: Delete the created and completed live quiz', () => { cy.loginLecturer() - cy.get(`[data-cy="sessions"]`).click() + cy.get(`[data-cy="quizzes"]`).click() cy.findByText(sessionName1New).should('exist') cy.get(`[data-cy="delete-live-quiz-${sessionName1New}"]`).click() @@ -697,7 +697,7 @@ describe('Different live-quiz workflows', () => { it('Start the second block of the live quiz', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName2}"]`).click() cy.wait(1000) @@ -709,7 +709,7 @@ describe('Different live-quiz workflows', () => { it('Make feedbacks visible, respond to one and disable moderation', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName2}"]`).click() cy.wait(1000) @@ -763,7 +763,7 @@ describe('Different live-quiz workflows', () => { it('Close block and delete feedback / feedback response', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName2}"]`).click() cy.wait(1000) cy.get('[data-cy="next-block-timeline"]').click() @@ -788,7 +788,7 @@ describe('Different live-quiz workflows', () => { it('End session on lecturer cockpit', () => { cy.loginLecturer() - cy.get('[data-cy="sessions"]').click() + cy.get('[data-cy="quizzes"]').click() cy.get(`[data-cy="session-cockpit-${sessionName2}"]`).click() cy.wait(1000) cy.get('[data-cy="next-block-timeline"]').click() @@ -796,7 +796,7 @@ describe('Different live-quiz workflows', () => { it('Cleanup: Delete the live quiz used for the full cycle test', () => { cy.loginLecturer() - cy.get(`[data-cy="sessions"]`).click() + cy.get(`[data-cy="quizzes"]`).click() cy.findByText(sessionName2).should('exist') cy.get(`[data-cy="delete-live-quiz-${sessionName2}"]`).click() diff --git a/packages/markdown/src/public/components.css b/packages/markdown/src/public/components.css index e69de29bb2..8b13789179 100644 --- a/packages/markdown/src/public/components.css +++ b/packages/markdown/src/public/components.css @@ -0,0 +1 @@ + diff --git a/packages/markdown/src/public/utilities.css b/packages/markdown/src/public/utilities.css index 05776c2a66..c56dabb3e9 100644 --- a/packages/markdown/src/public/utilities.css +++ b/packages/markdown/src/public/utilities.css @@ -1,191 +1,189 @@ .absolute { - position: absolute; + position: absolute } .relative { - position: relative; + position: relative } .right-2 { - right: 0.5rem; + right: 0.5rem } .top-2 { - top: 0.5rem; + top: 0.5rem } .my-1 { margin-top: 0.25rem; - margin-bottom: 0.25rem; + margin-bottom: 0.25rem } .mb-1 { - margin-bottom: 0.25rem; + margin-bottom: 0.25rem } .line-clamp-1 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 1; + -webkit-line-clamp: 1 } .line-clamp-2 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 2; + -webkit-line-clamp: 2 } .line-clamp-3 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 3; + -webkit-line-clamp: 3 } .flex { - display: flex; + display: flex } .h-full { - height: 100%; + height: 100% } .max-h-16 { - max-height: 4rem; + max-height: 4rem } .max-h-36 { - max-height: 9rem; + max-height: 9rem } .max-h-64 { - max-height: 16rem; + max-height: 16rem } .min-h-36 { - min-height: 9rem; + min-height: 9rem } .w-auto { - width: auto; + width: auto } .w-full { - width: 100%; + width: 100% } .max-w-\[50\%\] { - max-width: 50%; + max-width: 50% } .max-w-full { - max-width: 100%; + max-width: 100% } .max-w-md { - max-width: 28rem; + max-width: 28rem } .max-w-none { - max-width: none; + max-width: none } .flex-initial { - flex: 0 1 auto; + flex: 0 1 auto } .flex-row { - flex-direction: row; + flex-direction: row } .flex-col { - flex-direction: column; + flex-direction: column } .items-start { - align-items: flex-start; + align-items: flex-start } .gap-3 { - gap: 0.75rem; + gap: 0.75rem } .rounded { - border-radius: 0.25rem; + border-radius: 0.25rem } .border { - border-width: 1px; + border-width: 1px } .bg-white { --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + background-color: rgb(255 255 255 / var(--tw-bg-opacity)) } .object-contain { -o-object-fit: contain; - object-fit: contain; + object-fit: contain } .px-4 { padding-left: 1rem; - padding-right: 1rem; + padding-right: 1rem } .py-3 { padding-top: 0.75rem; - padding-bottom: 0.75rem; + padding-bottom: 0.75rem } .text-sm { font-size: 0.875rem; - line-height: 1.25rem; + line-height: 1.25rem } .leading-6 { - line-height: 1.5rem; + line-height: 1.5rem } .text-black { --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); + color: rgb(0 0 0 / var(--tw-text-opacity)) } .text-slate-600 { --tw-text-opacity: 1; - color: rgb(71 85 105 / var(--tw-text-opacity)); + color: rgb(71 85 105 / var(--tw-text-opacity)) } .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), - 0 1px 2px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), - var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow) } @media (hover: hover) and (pointer: fine) { .hover\:bg-slate-200:hover { --tw-bg-opacity: 1; - background-color: rgb(226 232 240 / var(--tw-bg-opacity)); + background-color: rgb(226 232 240 / var(--tw-bg-opacity)) } .hover\:text-black:hover { --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); + color: rgb(0 0 0 / var(--tw-text-opacity)) } .hover\:text-white:hover { --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); + color: rgb(255 255 255 / var(--tw-text-opacity)) } } @media (min-width: 768px) { .md\:max-w-\[60\%\] { - max-width: 60%; + max-width: 60% } } From 4ca93c704f047b0822945e2fb3b42372e255eb33 Mon Sep 17 00:00:00 2001 From: sjschlapbach Date: Tue, 3 Dec 2024 14:46:34 +0100 Subject: [PATCH 07/38] fix(apps/analytics): ensure that free text questions without sample solution are handled correctly --- .../src/modules/participant_analytics/compute_correctness.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/analytics/src/modules/participant_analytics/compute_correctness.py b/apps/analytics/src/modules/participant_analytics/compute_correctness.py index b1cb4f8c9f..d684c9ee89 100644 --- a/apps/analytics/src/modules/participant_analytics/compute_correctness.py +++ b/apps/analytics/src/modules/participant_analytics/compute_correctness.py @@ -83,6 +83,11 @@ def compute_correctness_columns(df_element_instances, row): return "INCORRECT" elif element_instance["type"] == "FREE_TEXT": + # if no sample solution is specified, automatically grade as correct + if "solutions" not in options: + return "CORRECT" + + # otherwise, check if the response (ignoring capitalization) is included in the list of solutions response_value = response["value"] solutions = list( map(lambda solution: solution.strip().lower(), options["solutions"]) From 76c2300929e935b253c3fdb6df1200ea3aff3459 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Wed, 4 Dec 2024 07:11:14 +0100 Subject: [PATCH 08/38] enhance(apps/analytics): add scripts for the computation of aggregated analytics (#4385) --- apps/analytics/src/modules/__init__.py | 1 + .../modules/aggregated_analytics/__init__.py | 4 + .../aggregate_participant_analytics.py | 29 ++++ .../compute_aggregated_analytics.py | 34 ++++ .../load_participant_analytics.py | 30 ++++ .../save_aggregated_analytics.py | 108 ++++++++++++ .../src/notebooks/aggregated_analytics.ipynb | 161 ++++++++++++++++++ 7 files changed, 367 insertions(+) create mode 100644 apps/analytics/src/modules/aggregated_analytics/__init__.py create mode 100644 apps/analytics/src/modules/aggregated_analytics/aggregate_participant_analytics.py create mode 100644 apps/analytics/src/modules/aggregated_analytics/compute_aggregated_analytics.py create mode 100644 apps/analytics/src/modules/aggregated_analytics/load_participant_analytics.py create mode 100644 apps/analytics/src/modules/aggregated_analytics/save_aggregated_analytics.py create mode 100644 apps/analytics/src/notebooks/aggregated_analytics.ipynb diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py index 9751f844cc..e6d34b6e6a 100644 --- a/apps/analytics/src/modules/__init__.py +++ b/apps/analytics/src/modules/__init__.py @@ -1 +1,2 @@ from .participant_analytics import compute_correctness, get_participant_responses +from .aggregated_analytics import compute_aggregated_analytics diff --git a/apps/analytics/src/modules/aggregated_analytics/__init__.py b/apps/analytics/src/modules/aggregated_analytics/__init__.py new file mode 100644 index 0000000000..9fcb7c469c --- /dev/null +++ b/apps/analytics/src/modules/aggregated_analytics/__init__.py @@ -0,0 +1,4 @@ +from .compute_aggregated_analytics import compute_aggregated_analytics +from .load_participant_analytics import load_participant_analytics +from .aggregate_participant_analytics import aggregate_participant_analytics +from .save_aggregated_analytics import save_aggregated_analytics diff --git a/apps/analytics/src/modules/aggregated_analytics/aggregate_participant_analytics.py b/apps/analytics/src/modules/aggregated_analytics/aggregate_participant_analytics.py new file mode 100644 index 0000000000..74908714b3 --- /dev/null +++ b/apps/analytics/src/modules/aggregated_analytics/aggregate_participant_analytics.py @@ -0,0 +1,29 @@ +def aggregate_participant_analytics(df_participant_analytics, verbose=False): + # if the dataframe is empty, return None + if df_participant_analytics.empty: + if verbose: + print("No participant analytics to aggregate") + + return None + + # aggreagte all participant analytics for the specified time range and separate courses + df_aggregated_analytics = ( + df_participant_analytics.groupby("courseId") + .agg( + { + "id": "count", + "responseCount": "sum", + "totalScore": "sum", + "totalPoints": "sum", + "totalXp": "sum", + } + ) + .reset_index() + .rename( + columns={ + "id": "participantCount", + } + ) + ) + + return df_aggregated_analytics diff --git a/apps/analytics/src/modules/aggregated_analytics/compute_aggregated_analytics.py b/apps/analytics/src/modules/aggregated_analytics/compute_aggregated_analytics.py new file mode 100644 index 0000000000..87009e8230 --- /dev/null +++ b/apps/analytics/src/modules/aggregated_analytics/compute_aggregated_analytics.py @@ -0,0 +1,34 @@ +from .load_participant_analytics import load_participant_analytics +from .aggregate_participant_analytics import aggregate_participant_analytics +from .save_aggregated_analytics import save_aggregated_analytics + + +def compute_aggregated_analytics( + db, start_date, end_date, timestamp, analytics_type="DAILY", verbose=False +): + # load all participant analytics for the given timestamp and analytics time range + df_participant_analytics = load_participant_analytics( + db, timestamp, analytics_type, verbose + ) + + # aggregate all participant analytics values by course + df_aggregated_analytics = aggregate_participant_analytics( + df_participant_analytics, verbose + ) + + if df_aggregated_analytics is not None and verbose: + print("Aggregated analytics for time range:" + start_date + " to " + end_date) + print(df_aggregated_analytics.head()) + elif df_aggregated_analytics is None: + print( + "No aggregated analytics to compute for time range:" + + start_date + + " to " + + end_date + ) + + # store the computed aggregated analytics in the database + if df_aggregated_analytics is not None: + save_aggregated_analytics( + db, df_aggregated_analytics, timestamp, analytics_type + ) diff --git a/apps/analytics/src/modules/aggregated_analytics/load_participant_analytics.py b/apps/analytics/src/modules/aggregated_analytics/load_participant_analytics.py new file mode 100644 index 0000000000..6e4173f055 --- /dev/null +++ b/apps/analytics/src/modules/aggregated_analytics/load_participant_analytics.py @@ -0,0 +1,30 @@ +import pandas as pd + + +def convert_to_df(analytics): + # convert the database query result into a pandas dataframe + rows = [] + for item in analytics: + rows.append(dict(item)) + + return pd.DataFrame(rows) + + +def load_participant_analytics(db, timestamp, analytics_type, verbose=False): + participant_analytics = db.participantanalytics.find_many( + where={"timestamp": timestamp, "type": analytics_type}, + ) + + if verbose: + # Print the first participant analytics + print( + "Found {} analytics for the timespan from {} to {}".format( + len(participant_analytics), start_date, end_date + ) + ) + print(participant_analytics[0]) + + # convert the analytics to a dataframe + df_loaded_analytics = convert_to_df(participant_analytics) + + return df_loaded_analytics diff --git a/apps/analytics/src/modules/aggregated_analytics/save_aggregated_analytics.py b/apps/analytics/src/modules/aggregated_analytics/save_aggregated_analytics.py new file mode 100644 index 0000000000..f224f15392 --- /dev/null +++ b/apps/analytics/src/modules/aggregated_analytics/save_aggregated_analytics.py @@ -0,0 +1,108 @@ +from datetime import datetime + + +def save_aggregated_analytics(db, df_analytics, timestamp, analytics_type="DAILY"): + computedAt = datetime.now().strftime("%Y-%m-%d") + "T00:00:00.000Z" + + # create daily / weekly / monthly analytics entries for all participants + if analytics_type in ["DAILY", "WEEKLY", "MONTHLY"]: + for _, row in df_analytics.iterrows(): + db.aggregatedanalytics.upsert( + where={ + "type_courseId_timestamp": { + "type": analytics_type, + "courseId": row["courseId"], + "timestamp": timestamp, + } + }, + data={ + "create": { + "type": analytics_type, + "timestamp": timestamp, + "computedAt": computedAt, + "participantCount": row["participantCount"], + "responseCount": row["responseCount"], + "totalScore": row["totalScore"], + "totalPoints": row["totalPoints"], + "totalXp": row["totalXp"], + # TODO: set this value correctly for rolling updates in production code + # (cannot be computed for past learning analytics -> therefore set to invalid value) + "totalElementsAvailable": -1, + "course": {"connect": {"id": row["courseId"]}}, + }, + "update": {}, + }, + ) + + # create or update course-wide analytics entries (should be unique for participant / course combination) + elif analytics_type == "COURSE": + for _, row in df_analytics.iterrows(): + course = db.course.find_unique_or_raise( + where={"id": row["courseId"]}, + include={ + "practiceQuizzes": { + "include": { + "stacks": { + "include": {"elements": True}, + } + } + }, + "microLearnings": { + "include": { + "stacks": { + "include": {"elements": True}, + } + } + }, + }, + ) + course = dict(course) + + # add all the number of elements in all practice quizzes and microlearnings together + totalElementsAvailable = 0 + for practice_quiz in course["practiceQuizzes"]: + pq_dict = dict(practice_quiz) + for stack in pq_dict["stacks"]: + stack_dict = dict(stack) + totalElementsAvailable += len(stack_dict["elements"]) + for microlearning in course["microLearnings"]: + ml_dict = dict(microlearning) + for stack in ml_dict["stacks"]: + stack_dict = dict(stack) + totalElementsAvailable += len(stack_dict["elements"]) + + db.aggregatedanalytics.upsert( + where={ + "type_courseId_timestamp": { + "type": analytics_type, + "courseId": row["courseId"], + "timestamp": timestamp, + } + }, + data={ + "create": { + "type": analytics_type, + "timestamp": timestamp, + "computedAt": computedAt, + "participantCount": row["participantCount"], + "responseCount": row["responseCount"], + "totalScore": row["totalScore"], + "totalPoints": row["totalPoints"], + "totalXp": row["totalXp"], + "totalElementsAvailable": totalElementsAvailable, + "course": {"connect": {"id": row["courseId"]}}, + }, + "update": { + "computedAt": computedAt, + "participantCount": row["participantCount"], + "responseCount": row["responseCount"], + "totalScore": row["totalScore"], + "totalPoints": row["totalPoints"], + "totalXp": row["totalXp"], + "totalElementsAvailable": totalElementsAvailable, + }, + }, + ) + + else: + raise ValueError("Unknown analytics type: {}".format(analytics_type)) diff --git a/apps/analytics/src/notebooks/aggregated_analytics.ipynb b/apps/analytics/src/notebooks/aggregated_analytics.ipynb new file mode 100644 index 0000000000..e93422901e --- /dev/null +++ b/apps/analytics/src/notebooks/aggregated_analytics.ipynb @@ -0,0 +1,161 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Preparation\n", + "\n", + "This script computes analytics that are aggregated over all participants in a course over a specified time span. The corresponding results are stored in the `AggregatedAnalytics` database table. While the daily, weekly and monthly scripts are designed to run at the end of the corresponding period (only creation, no updates), the course analytics are once more meant to be updated on a regular basis (daily?). Minor changes to the calling logic of these functions will be required for continuous updates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "\n", + "# set the python path correctly for module imports to work\n", + "import sys\n", + "sys.path.append('../../')\n", + "\n", + "from src.modules.aggregated_analytics.compute_aggregated_analytics import compute_aggregated_analytics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ['DATABASE_URL'] = 'postgresql://klicker:klicker@localhost:5432/klicker-prod'\n", + "\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False\n", + "\n", + "# Settings which analytics to compute\n", + "compute_daily = True\n", + "compute_weekly = True\n", + "compute_monthly = True\n", + "compute_course = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Compute Aggregated Analytics on Course Level\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "start_date = \"2021-01-01\"\n", + "end_date = datetime.now().strftime(\"%Y-%m-%d\")\n", + "date_range_daily = pd.date_range(start=start_date, end=end_date, freq=\"D\")\n", + "date_range_weekly = pd.date_range(start=start_date, end=end_date, freq=\"W\")\n", + "date_range_monthly = pd.date_range(start=start_date, end=end_date, freq=\"ME\")\n", + "\n", + "if compute_daily:\n", + " # Iterate over the date range and compute the participant analytics for each day\n", + " for curr_date in date_range_daily:\n", + " # determine day start and end dates required for aggregation\n", + " specific_date = curr_date.strftime(\"%Y-%m-%d\")\n", + " day_start = specific_date + \"T00:00:00.000Z\"\n", + " day_end = specific_date + \"T23:59:59.999Z\"\n", + " print(f\"Computing daily aggregated analytics (course) for {specific_date}\")\n", + "\n", + " # compute aggregated analytics for a specific day\n", + " timestamp = day_start\n", + " compute_aggregated_analytics(\n", + " db, day_start, day_end, timestamp, \"DAILY\", verbose\n", + " )\n", + "\n", + "\n", + "if compute_weekly:\n", + " # Iterate over the date range and compute the participant analytics for each week\n", + " for curr_date in date_range_weekly:\n", + " # determine week start and end dates required for aggregation\n", + " week_end = curr_date.strftime(\"%Y-%m-%d\") + \"T23:59:59.999Z\"\n", + " week_start = (curr_date - pd.DateOffset(days=6)).strftime(\n", + " \"%Y-%m-%d\"\n", + " ) + \"T00:00:00.000Z\"\n", + " print(\n", + " f\"Computing weekly aggregated analytics (course) for {week_start } to {week_end }\"\n", + " )\n", + "\n", + " # compute aggregated analytics for a specific week\n", + " timestamp = week_end\n", + " compute_aggregated_analytics(\n", + " db, week_start, week_end, timestamp, \"WEEKLY\", verbose\n", + " )\n", + "\n", + "\n", + "if compute_monthly:\n", + " # Iterate over the date range and compute the participant analytics for each month\n", + " for curr_date in date_range_monthly:\n", + " # determine month start and end dates required for aggregation\n", + " month_end = curr_date.strftime('%Y-%m-%d') + 'T23:59:59.999Z'\n", + " month_start = (curr_date - pd.offsets.MonthBegin(1)).strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", + " print(\n", + " f\"Computing monthly aggregated analytics (course) for {month_start } to {month_end }\"\n", + " )\n", + "\n", + " # compute aggregated analytics for a specific month\n", + " timestamp = month_end\n", + " compute_aggregated_analytics(\n", + " db, month_start, month_end, timestamp, \"MONTHLY\", verbose\n", + " )\n", + "\n", + "\n", + "if compute_course:\n", + " print(\n", + " f\"Computing course-wide aggregated analytics\"\n", + " )\n", + "\n", + " # compute aggregated analytics over entire course based on corresponding participant analytics\n", + " # (a constant timestamp is used here, since the data combination has to be unique \n", + " # during querying, but only one entry per course is available by definition)\n", + " timestamp = \"1970-01-01T00:00:00.000Z\"\n", + " compute_aggregated_analytics(\n", + " db, timestamp, timestamp, timestamp, \"COURSE\", verbose\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-fkWWeYLw-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From e8f2791bd619d35cea6b933213180c8c32651b3f Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Wed, 4 Dec 2024 09:55:05 +0100 Subject: [PATCH 09/38] fix(apps/analytics): ensure that correct response data is queried for course-duration participant analytics (#4386) --- .../compute_participant_course_analytics.py | 28 +++---- .../get_participant_responses.py | 6 ++ .../src/notebooks/aggregated_analytics.ipynb | 27 +++--- .../src/notebooks/participant_analytics.ipynb | 82 ++++++++++++------- 4 files changed, 82 insertions(+), 61 deletions(-) diff --git a/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py b/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py index 9aed9528d1..31af9798a1 100644 --- a/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py +++ b/apps/analytics/src/modules/participant_analytics/compute_participant_course_analytics.py @@ -22,19 +22,15 @@ def compute_participant_course_analytics(db, df_courses, verbose=False): participations = db.participation.find_many( where={"courseId": course_id}, include={ - "participant": { - "include": { - "detailQuestionResponses": { - "where": { - "createdAt": { - "gte": course_start_date, - "lte": course_end_date, - } - }, - }, - "questionResponses": True, - } - } + "detailResponses": { + "where": { + "createdAt": { + "gte": course_start_date, + "lte": course_end_date, + } + }, + }, + "responses": True, }, ) @@ -42,13 +38,11 @@ def compute_participant_course_analytics(db, df_courses, verbose=False): participations_dict = list(map(lambda x: x.dict(), participations)) details_dict = list( map( - lambda x: x["participant"]["detailQuestionResponses"], + lambda x: x["detailResponses"], participations_dict, ) ) - responses_dict = list( - map(lambda x: x["participant"]["questionResponses"], participations_dict) - ) + responses_dict = list(map(lambda x: x["responses"], participations_dict)) details = [item for sublist in details_dict for item in sublist] responses = [item for sublist in responses_dict for item in sublist] diff --git a/apps/analytics/src/modules/participant_analytics/get_participant_responses.py b/apps/analytics/src/modules/participant_analytics/get_participant_responses.py index 010638c8cf..3c26dd51b9 100644 --- a/apps/analytics/src/modules/participant_analytics/get_participant_responses.py +++ b/apps/analytics/src/modules/participant_analytics/get_participant_responses.py @@ -83,4 +83,10 @@ def get_participant_responses(db, start_date, end_date, verbose=False): df_details = df_details.apply(set_course_dates, axis=1) + if len(df_details) > 0: + df_details = df_details[ + (df_details["createdAt"] >= df_details["course_start_date"]) + & (df_details["createdAt"] <= df_details["course_end_date"]) + ] + return df_details diff --git a/apps/analytics/src/notebooks/aggregated_analytics.ipynb b/apps/analytics/src/notebooks/aggregated_analytics.ipynb index e93422901e..32e90296f0 100644 --- a/apps/analytics/src/notebooks/aggregated_analytics.ipynb +++ b/apps/analytics/src/notebooks/aggregated_analytics.ipynb @@ -23,9 +23,12 @@ "\n", "# set the python path correctly for module imports to work\n", "import sys\n", - "sys.path.append('../../')\n", "\n", - "from src.modules.aggregated_analytics.compute_aggregated_analytics import compute_aggregated_analytics" + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.aggregated_analytics.compute_aggregated_analytics import (\n", + " compute_aggregated_analytics,\n", + ")" ] }, { @@ -37,7 +40,7 @@ "db = Prisma()\n", "\n", "# set the environment variable DATABASE_URL to the connection string of your database\n", - "os.environ['DATABASE_URL'] = 'postgresql://klicker:klicker@localhost:5432/klicker-prod'\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", "\n", "db.connect()\n", "\n", @@ -109,8 +112,10 @@ " # Iterate over the date range and compute the participant analytics for each month\n", " for curr_date in date_range_monthly:\n", " # determine month start and end dates required for aggregation\n", - " month_end = curr_date.strftime('%Y-%m-%d') + 'T23:59:59.999Z'\n", - " month_start = (curr_date - pd.offsets.MonthBegin(1)).strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", + " month_end = curr_date.strftime(\"%Y-%m-%d\") + \"T23:59:59.999Z\"\n", + " month_start = (curr_date - pd.offsets.MonthBegin(1)).strftime(\n", + " \"%Y-%m-%d\"\n", + " ) + \"T00:00:00.000Z\"\n", " print(\n", " f\"Computing monthly aggregated analytics (course) for {month_start } to {month_end }\"\n", " )\n", @@ -123,17 +128,13 @@ "\n", "\n", "if compute_course:\n", - " print(\n", - " f\"Computing course-wide aggregated analytics\"\n", - " )\n", + " print(f\"Computing course-wide aggregated analytics\")\n", "\n", " # compute aggregated analytics over entire course based on corresponding participant analytics\n", - " # (a constant timestamp is used here, since the data combination has to be unique \n", + " # (a constant timestamp is used here, since the data combination has to be unique\n", " # during querying, but only one entry per course is available by definition)\n", - " timestamp = \"1970-01-01T00:00:00.000Z\"\n", - " compute_aggregated_analytics(\n", - " db, timestamp, timestamp, timestamp, \"COURSE\", verbose\n", - " )" + " timestamp = \"1970-01-01T00:00:00.000Z\"\n", + " compute_aggregated_analytics(db, timestamp, timestamp, timestamp, \"COURSE\", verbose)" ] } ], diff --git a/apps/analytics/src/notebooks/participant_analytics.ipynb b/apps/analytics/src/notebooks/participant_analytics.ipynb index 4ccd85f5f8..98d73a8289 100644 --- a/apps/analytics/src/notebooks/participant_analytics.ipynb +++ b/apps/analytics/src/notebooks/participant_analytics.ipynb @@ -21,10 +21,15 @@ "\n", "# set the python path correctly for module imports to work\n", "import sys\n", - "sys.path.append('../../')\n", "\n", - "from src.modules.participant_analytics.compute_participant_analytics import compute_participant_analytics\n", - "from src.modules.participant_analytics.compute_participant_course_analytics import compute_participant_course_analytics\n" + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.participant_analytics.compute_participant_analytics import (\n", + " compute_participant_analytics,\n", + ")\n", + "from src.modules.participant_analytics.compute_participant_course_analytics import (\n", + " compute_participant_course_analytics,\n", + ")" ] }, { @@ -36,7 +41,7 @@ "db = Prisma()\n", "\n", "# set the environment variable DATABASE_URL to the connection string of your database\n", - "os.environ['DATABASE_URL'] = 'postgresql://klicker:klicker@localhost:5432/klicker-prod'\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", "\n", "db.connect()\n", "\n", @@ -64,49 +69,61 @@ "outputs": [], "source": [ "# Print all dates between the 2022-10-23 and today\n", - "start_date = '2022-10-01'\n", - "end_date = datetime.now().strftime('%Y-%m-%d')\n", - "date_range_daily = pd.date_range(start=start_date, end=end_date, freq='D')\n", - "date_range_weekly = pd.date_range(start=start_date, end=end_date, freq='W')\n", - "date_range_monthly = pd.date_range(start=start_date, end=end_date, freq='ME')\n", + "start_date = \"2022-10-23\"\n", + "end_date = datetime.now().strftime(\"%Y-%m-%d\")\n", + "date_range_daily = pd.date_range(start=start_date, end=end_date, freq=\"D\")\n", + "date_range_weekly = pd.date_range(start=start_date, end=end_date, freq=\"W\")\n", + "date_range_monthly = pd.date_range(start=start_date, end=end_date, freq=\"ME\")\n", "\n", "if compute_daily:\n", " # Iterate over the date range and compute the participant analytics for each day\n", " for curr_date in date_range_daily:\n", - " print(f'Computing daily participant analytics for {curr_date.strftime('%Y-%m-%d')}')\n", - " specific_date = curr_date.strftime('%Y-%m-%d')\n", + " print(\n", + " f\"Computing daily participant analytics for {curr_date.strftime('%Y-%m-%d')}\"\n", + " )\n", + " specific_date = curr_date.strftime(\"%Y-%m-%d\")\n", "\n", " # Fetch all question response detail entries for a specific day\n", - " start_date = specific_date + 'T00:00:00.000Z'\n", - " end_date = specific_date + 'T23:59:59.999Z'\n", + " start_date = specific_date + \"T00:00:00.000Z\"\n", + " end_date = specific_date + \"T23:59:59.999Z\"\n", "\n", " # Compute participant analytics for a specific day\n", " timestamp = start_date\n", - " compute_participant_analytics(db, start_date, end_date, timestamp, \"DAILY\", verbose)\n", + " compute_participant_analytics(\n", + " db, start_date, end_date, timestamp, \"DAILY\", verbose\n", + " )\n", "\n", "if compute_weekly:\n", " # Iterate over the date range and compute the participant analytics for each week\n", " for curr_date in date_range_weekly:\n", " # Fetch all question response detail entries for a specific week\n", - " end_date = curr_date.strftime('%Y-%m-%d') + 'T23:59:59.999Z'\n", - " start_date = (curr_date - pd.DateOffset(days=6)).strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", - " print(f'Computing weekly participant analytics for {start_date} to {end_date}')\n", + " end_date = curr_date.strftime(\"%Y-%m-%d\") + \"T23:59:59.999Z\"\n", + " start_date = (curr_date - pd.DateOffset(days=6)).strftime(\n", + " \"%Y-%m-%d\"\n", + " ) + \"T00:00:00.000Z\"\n", + " print(f\"Computing weekly participant analytics for {start_date} to {end_date}\")\n", "\n", " # Compute participant analytics for a specific week\n", " timestamp = end_date\n", - " compute_participant_analytics(db, start_date, end_date, timestamp, \"WEEKLY\", verbose)\n", + " compute_participant_analytics(\n", + " db, start_date, end_date, timestamp, \"WEEKLY\", verbose\n", + " )\n", "\n", "if compute_monthly:\n", " # Iterate over the date range and compute the participant analytics for each month\n", " for curr_date in date_range_monthly:\n", " # Fetch all question response detail entries for a specific month\n", - " end_date = curr_date.strftime('%Y-%m-%d') + 'T23:59:59.999Z'\n", - " start_date = (curr_date - pd.offsets.MonthBegin(1)).strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", - " print(f'Computing monthly participant analytics for {start_date} to {end_date}')\n", + " end_date = curr_date.strftime(\"%Y-%m-%d\") + \"T23:59:59.999Z\"\n", + " start_date = (curr_date - pd.offsets.MonthBegin(1)).strftime(\n", + " \"%Y-%m-%d\"\n", + " ) + \"T00:00:00.000Z\"\n", + " print(f\"Computing monthly participant analytics for {start_date} to {end_date}\")\n", "\n", " # Compute participant analytics for a specific month\n", " timestamp = end_date\n", - " compute_participant_analytics(db, start_date, end_date, timestamp, \"MONTHLY\", verbose)\n" + " compute_participant_analytics(\n", + " db, start_date, end_date, timestamp, \"MONTHLY\", verbose\n", + " )" ] }, { @@ -124,26 +141,29 @@ "source": [ "# Fetch all ongoing / past courses\n", "if compute_course:\n", - " curr_date = '2024-08-27'\n", + " curr_date = datetime.now().strftime(\"%Y-%m-%d\")\n", " courses = db.course.find_many(\n", " where={\n", " # Incremental scripts can add this statement to reduce the amount of required computations\n", " # 'endDate': {\n", " # 'gt': datetime.now().strftime('%Y-%m-%d') + 'T00:00:00.000Z'\n", " # }\n", - " 'startDate': {\n", - " 'lte': curr_date + 'T23:59:59.999Z'\n", - " }\n", + " \"startDate\": {\"lte\": curr_date + \"T23:59:59.999Z\"},\n", " }\n", " )\n", "\n", " df_courses = pd.DataFrame(list(map(lambda x: x.dict(), courses)))\n", - " print(\"Found {} courses with a start date before {}\".format(len(df_courses), curr_date))\n", + " print(\n", + " \"Found {} courses with a start date before {}\".format(\n", + " len(df_courses), curr_date\n", + " )\n", + " )\n", "\n", - " courses_without_responses = compute_participant_course_analytics(db, df_courses, verbose)\n", + " courses_without_responses = compute_participant_course_analytics(\n", + " db, df_courses, verbose\n", + " )\n", "\n", - " print(\"Found {} courses without any responses\".format(courses_without_responses))\n", - " " + " print(\"Found {} courses without any responses\".format(courses_without_responses))" ] }, { @@ -159,7 +179,7 @@ ], "metadata": { "kernelspec": { - "display_name": "analytics-3uz8SvN3-py3.12", + "display_name": "analytics-fkWWeYLw-py3.12", "language": "python", "name": "python3" }, From 4b60b7e8bf53a7ff07e281243d262a7f1c6693fa Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:07:08 +0100 Subject: [PATCH 10/38] enhance(apps/analytics): add logic for computation of participant course analytics (#4387) --- apps/analytics/src/modules/__init__.py | 6 + .../participant_course_analytics/__init__.py | 4 + .../compute_participant_activity.py | 67 ++++++++++ .../get_active_weeks.py | 42 ++++++ .../get_running_past_courses.py | 25 ++++ .../save_participant_course_analytics.py | 26 ++++ .../participant_course_analytics.ipynb | 126 ++++++++++++++++++ .../prisma/src/prisma/schema/analytics.prisma | 4 +- 8 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 apps/analytics/src/modules/participant_course_analytics/__init__.py create mode 100644 apps/analytics/src/modules/participant_course_analytics/compute_participant_activity.py create mode 100644 apps/analytics/src/modules/participant_course_analytics/get_active_weeks.py create mode 100644 apps/analytics/src/modules/participant_course_analytics/get_running_past_courses.py create mode 100644 apps/analytics/src/modules/participant_course_analytics/save_participant_course_analytics.py create mode 100644 apps/analytics/src/notebooks/participant_course_analytics.ipynb diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py index e6d34b6e6a..d2efc106fd 100644 --- a/apps/analytics/src/modules/__init__.py +++ b/apps/analytics/src/modules/__init__.py @@ -1,2 +1,8 @@ from .participant_analytics import compute_correctness, get_participant_responses from .aggregated_analytics import compute_aggregated_analytics +from .participant_course_analytics import ( + get_running_past_courses, + get_active_weeks, + compute_participant_activity, + save_participant_course_analytics, +) diff --git a/apps/analytics/src/modules/participant_course_analytics/__init__.py b/apps/analytics/src/modules/participant_course_analytics/__init__.py new file mode 100644 index 0000000000..718ca30df9 --- /dev/null +++ b/apps/analytics/src/modules/participant_course_analytics/__init__.py @@ -0,0 +1,4 @@ +from .get_running_past_courses import get_running_past_courses +from .get_active_weeks import get_active_weeks +from .compute_participant_activity import compute_participant_activity +from .save_participant_course_analytics import save_participant_course_analytics diff --git a/apps/analytics/src/modules/participant_course_analytics/compute_participant_activity.py b/apps/analytics/src/modules/participant_course_analytics/compute_participant_activity.py new file mode 100644 index 0000000000..49e94e583c --- /dev/null +++ b/apps/analytics/src/modules/participant_course_analytics/compute_participant_activity.py @@ -0,0 +1,67 @@ +import pandas as pd + + +def compute_participant_activity(db, df_activity, course_id, course_start, course_end): + # compute course duration in days + course_duration = (course_end - course_start).days + 1 + week_end_dates = pd.date_range(start=course_start, end=course_end, freq="W") + + # loop over the activity analytics tracking dataframe + for idx, row in df_activity.iterrows(): + participant_id = row["participantId"] + + # get all daily participant analytics entries for the participant + daily_analytics = db.participantanalytics.find_many( + where={ + "type": "DAILY", + "courseId": course_id, + "participantId": participant_id, + }, + ) + + # compute the mean elements answered per day based on the daily analytics response count + response_count = sum( + [dict(daily)["responseCount"] for daily in daily_analytics] + ) + df_activity.loc[idx, "meanElementsPerDay"] = response_count / course_duration + + # compute average active days per week + active_days_week = [] + + # if course lasts for less than one week, imitate a course duration of one week + if (course_duration) <= 7: + # filter the daily analytics entries for the current week + week_analytics = sum_active_days_per_week(course_end, daily_analytics) + + # add the number of active days per week to the list + active_days_week.append(len(week_analytics)) + + else: + for week_end in week_end_dates: + # filter the daily analytics entries for the current week + week_analytics = sum_active_days_per_week(week_end, daily_analytics) + + # ? first and last week might not be complete weeks - could be treated differently here for maximum precision of results + # add the number of active days per week to the list + active_days_week.append(len(week_analytics)) + + # compute the average active days per week + df_activity.loc[idx, "activeDaysPerWeek"] = sum(active_days_week) / len( + active_days_week + ) + + return df_activity + + +def sum_active_days_per_week(week_end, daily_analytics): + week_start = week_end - pd.DateOffset(days=6) + + # filter the daily analytics entries for the current week + week_analytics = list( + filter( + lambda daily: week_start <= dict(daily)["timestamp"] <= week_end, + daily_analytics, + ) + ) + + return week_analytics diff --git a/apps/analytics/src/modules/participant_course_analytics/get_active_weeks.py b/apps/analytics/src/modules/participant_course_analytics/get_active_weeks.py new file mode 100644 index 0000000000..cc0f144349 --- /dev/null +++ b/apps/analytics/src/modules/participant_course_analytics/get_active_weeks.py @@ -0,0 +1,42 @@ +import pandas as pd + + +def get_active_weeks(db, course): + course_id = course["id"] + participations = course["participations"] + + # initialize pandas dataframe to store the participant activity + df_activity = pd.DataFrame(columns=["participantId", "courseId", "activeWeeks"]) + + # iterate over all participants in the course and count the number of weekly participant analytics entries + for participation in participations: + participant_id = participation["participantId"] + weekly_analytics = db.participantanalytics.find_many( + where={ + "type": "WEEKLY", + "courseId": course_id, + "participantId": participant_id, + }, + ) + + active_weeks = len(weekly_analytics) + + # store data in the dataframe + df_activity.loc[len(df_activity)] = { + "participantId": participant_id, + "courseId": course_id, + "activeWeeks": active_weeks, + } + + if not df_activity.empty: + # compute quantiles based on active weeks + quantiles = df_activity.activeWeeks.quantile([0.25, 0.75]) + q1 = quantiles[0.25] + q3 = quantiles[0.75] + + # set activity level based on active weeks + df_activity["activityLevel"] = "MEDIUM" + df_activity.loc[df_activity.activeWeeks >= q3, "activityLevel"] = "HIGH" + df_activity.loc[df_activity.activeWeeks <= q1, "activityLevel"] = "LOW" + + return df_activity diff --git a/apps/analytics/src/modules/participant_course_analytics/get_running_past_courses.py b/apps/analytics/src/modules/participant_course_analytics/get_running_past_courses.py new file mode 100644 index 0000000000..53ff0c79ca --- /dev/null +++ b/apps/analytics/src/modules/participant_course_analytics/get_running_past_courses.py @@ -0,0 +1,25 @@ +from datetime import datetime +import pandas as pd + + +def get_running_past_courses(db): + curr_date = datetime.now().strftime("%Y-%m-%d") + courses = db.course.find_many( + where={ + # Incremental scripts can add this statement to reduce the amount of required computations + # 'endDate': { + # 'gt': datetime.now().strftime('%Y-%m-%d') + 'T00:00:00.000Z' + # } + "startDate": {"lte": curr_date + "T23:59:59.999Z"}, + }, + include={"participations": True}, + ) + + df_courses = pd.DataFrame(list(map(lambda x: x.dict(), courses))) + print( + "Found {} courses with a start date before {}".format( + len(df_courses), curr_date + ) + ) + + return df_courses diff --git a/apps/analytics/src/modules/participant_course_analytics/save_participant_course_analytics.py b/apps/analytics/src/modules/participant_course_analytics/save_participant_course_analytics.py new file mode 100644 index 0000000000..065b263a46 --- /dev/null +++ b/apps/analytics/src/modules/participant_course_analytics/save_participant_course_analytics.py @@ -0,0 +1,26 @@ +def save_participant_course_analytics(db, df_activity): + for _, row in df_activity.iterrows(): + db.participantcourseanalytics.upsert( + where={ + "courseId_participantId": { + "courseId": row["courseId"], + "participantId": row["participantId"], + } + }, + data={ + "create": { + "activeWeeks": row["activeWeeks"], + "activeDaysPerWeek": row["activeDaysPerWeek"], + "meanElementsPerDay": row["meanElementsPerDay"], + "activityLevel": row["activityLevel"], + "course": {"connect": {"id": row["courseId"]}}, + "participant": {"connect": {"id": row["participantId"]}}, + }, + "update": { + "activeWeeks": row["activeWeeks"], + "activeDaysPerWeek": row["activeDaysPerWeek"], + "meanElementsPerDay": row["meanElementsPerDay"], + "activityLevel": row["activityLevel"], + }, + }, + ) diff --git a/apps/analytics/src/notebooks/participant_course_analytics.ipynb b/apps/analytics/src/notebooks/participant_course_analytics.ipynb new file mode 100644 index 0000000000..96aa1b02f1 --- /dev/null +++ b/apps/analytics/src/notebooks/participant_course_analytics.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Preparation\n", + "\n", + "This script computes participant analytics that are aggregated over the duration of the entire course and summarize the participant's activity. The corresponding results are stored in the `ParticipantCourseAnalytics` database table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "\n", + "# set the python path correctly for module imports to work\n", + "import sys\n", + "\n", + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.participant_course_analytics.get_running_past_courses import (\n", + " get_running_past_courses,\n", + ")\n", + "from src.modules.participant_course_analytics.get_active_weeks import get_active_weeks\n", + "from src.modules.participant_course_analytics.compute_participant_activity import (\n", + " compute_participant_activity,\n", + ")\n", + "from src.modules.participant_course_analytics.save_participant_course_analytics import (\n", + " save_participant_course_analytics,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", + "\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Compute Participant Course Analytics\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# find all courses that started in the past\n", + "df_courses = get_running_past_courses(db)\n", + "\n", + "# iterate over all courses and compute the participant course analytics\n", + "for idx, course in df_courses.iterrows():\n", + " print(f\"Processing course\", idx, \"of\", len(df_courses), \"with id\", course[\"id\"])\n", + "\n", + " # compute the number of active weeks per participant and activity level\n", + " df_activity = get_active_weeks(db, course)\n", + "\n", + " # if the dataframe is empty, no participant was active in the course and the course should be skipped\n", + " if df_activity.empty:\n", + " print(\"No participant was active in the course, skipping\")\n", + " continue\n", + "\n", + " # compute the number of active days per week and mean elements per day\n", + " df_activity = compute_participant_activity(\n", + " db, df_activity, course[\"id\"], course[\"startDate\"], course[\"endDate\"]\n", + " )\n", + "\n", + " # store the computed participant course analytics\n", + " save_participant_course_analytics(db, df_activity)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-fkWWeYLw-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index ef1a1e8fd2..b22a1deacb 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -17,7 +17,7 @@ model ParticipantAnalytics { type AnalyticsType timestamp DateTime @db.Date - computedAt DateTime @db.Date @default(now()) + computedAt DateTime @default(now()) @db.Date // unsolvedQuestionsCount = AggregatedAnalytics.totalElementsAvailable - responseCount trialsCount Int // total number of questions attempted @@ -61,7 +61,7 @@ model AggregatedAnalytics { // all quantities are defined as the values at the end of the selected timeframe timestamp DateTime @db.Date - computedAt DateTime @db.Date @default(now()) + computedAt DateTime @default(now()) @db.Date responseCount Int participantCount Int totalScore Int From 4e11fa103a6dc4502b32236c3afc90391a2a5d05 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:38:46 +0100 Subject: [PATCH 11/38] enhance(apps/analytics): add logic for the computation of aggregated course analytics (#4388) --- apps/analytics/src/modules/__init__.py | 1 + .../aggregated_course_analytics/__init__.py | 1 + .../compute_weekday_activity.py | 109 ++++++++++++++++++ .../aggregated_course_analytics.ipynb | 108 +++++++++++++++++ .../migration.sql | 21 ++++ .../prisma/src/prisma/schema/analytics.prisma | 18 +-- 6 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 apps/analytics/src/modules/aggregated_course_analytics/__init__.py create mode 100644 apps/analytics/src/modules/aggregated_course_analytics/compute_weekday_activity.py create mode 100644 apps/analytics/src/notebooks/aggregated_course_analytics.ipynb create mode 100644 packages/prisma/src/prisma/migrations/20241204165711_update_daily_activity_types/migration.sql diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py index d2efc106fd..f478e9e6fa 100644 --- a/apps/analytics/src/modules/__init__.py +++ b/apps/analytics/src/modules/__init__.py @@ -6,3 +6,4 @@ compute_participant_activity, save_participant_course_analytics, ) +from .aggregated_course_analytics import compute_weekday_activity diff --git a/apps/analytics/src/modules/aggregated_course_analytics/__init__.py b/apps/analytics/src/modules/aggregated_course_analytics/__init__.py new file mode 100644 index 0000000000..837fb570f1 --- /dev/null +++ b/apps/analytics/src/modules/aggregated_course_analytics/__init__.py @@ -0,0 +1 @@ +from .compute_weekday_activity import compute_weekday_activity diff --git a/apps/analytics/src/modules/aggregated_course_analytics/compute_weekday_activity.py b/apps/analytics/src/modules/aggregated_course_analytics/compute_weekday_activity.py new file mode 100644 index 0000000000..4fd9437e0b --- /dev/null +++ b/apps/analytics/src/modules/aggregated_course_analytics/compute_weekday_activity.py @@ -0,0 +1,109 @@ +import pandas as pd +import statistics + + +def compute_weekday_activity(db, course): + course_id = course["id"] + course_start = course["startDate"].date() + course_end = course["endDate"].date() + total_course_participants = len(course["participations"]) + + # fetch all daily participant analytics entries for the course + daily_analytics = db.participantanalytics.find_many( + where={ + "type": "DAILY", + "courseId": course_id, + }, + ) + df_daily = pd.DataFrame([daily.dict() for daily in daily_analytics]) + + if df_daily.empty: + return None + + # compute date ranges with specific weekdays only + mondays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-MON", + ) + tuesdays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-TUE", + ) + wednesdays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-WED", + ) + thursdays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-THU", + ) + fridays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-FRI", + ) + saturdays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-SAT", + ) + sundays = pd.date_range( + start=course_start, + end=pd.Timestamp(course_end) + pd.tseries.offsets.DateOffset(days=1), + freq="W-SUN", + ) + + activity_monday = single_weekday_activity(mondays, df_daily) + activity_tuesday = single_weekday_activity(tuesdays, df_daily) + activity_wednesday = single_weekday_activity(wednesdays, df_daily) + activity_thursday = single_weekday_activity(thursdays, df_daily) + activity_friday = single_weekday_activity(fridays, df_daily) + activity_saturday = single_weekday_activity(saturdays, df_daily) + activity_sunday = single_weekday_activity(sundays, df_daily) + + # save the result to the database + db.aggregatedcourseanalytics.upsert( + where={"courseId": course_id}, + data={ + "create": { + "courseParticipantCount": total_course_participants, + "activityMonday": activity_monday, + "activityTuesday": activity_tuesday, + "activityWednesday": activity_wednesday, + "activityThursday": activity_thursday, + "activityFriday": activity_friday, + "activitySaturday": activity_saturday, + "activitySunday": activity_sunday, + "course": {"connect": {"id": course_id}}, + }, + "update": { + "courseParticipantCount": total_course_participants, + "activityMonday": activity_monday, + "activityTuesday": activity_tuesday, + "activityWednesday": activity_wednesday, + "activityThursday": activity_thursday, + "activityFriday": activity_friday, + "activitySaturday": activity_saturday, + "activitySunday": activity_sunday, + }, + }, + ) + + +def single_weekday_activity(weekdays, df_daily): + collector = [] + for weekday in weekdays: + df_weekday = df_daily[ + df_daily["timestamp"] == pd.Timestamp(weekday).tz_localize("UTC") + ] + + if df_weekday.empty: + collector.append(0) + + collector.append(len(df_weekday)) + + return statistics.mean(collector) if len(collector) > 0 else 0 diff --git a/apps/analytics/src/notebooks/aggregated_course_analytics.ipynb b/apps/analytics/src/notebooks/aggregated_course_analytics.ipynb new file mode 100644 index 0000000000..7539c54c22 --- /dev/null +++ b/apps/analytics/src/notebooks/aggregated_course_analytics.ipynb @@ -0,0 +1,108 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Preparation\n", + "\n", + "This script computes aggreagted course analytics containing the averaged activity of students on different weekdays of the course. The corresponding results are stored in the `AggregatedCourseAnalytics` database table." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "\n", + "# set the python path correctly for module imports to work\n", + "import sys\n", + "\n", + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.participant_course_analytics.get_running_past_courses import (\n", + " get_running_past_courses,\n", + ")\n", + "from src.modules.aggregated_course_analytics.compute_weekday_activity import (\n", + " compute_weekday_activity,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Compute Aggregated Course Analytics\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# find all courses that started in the past\n", + "df_courses = get_running_past_courses(db)\n", + "\n", + "# iterate over all courses and compute the participant course analytics\n", + "for idx, course in df_courses.iterrows():\n", + " print(f\"Processing course\", idx, \"of\", len(df_courses), \"with id\", course[\"id\"])\n", + "\n", + " # computation of activity per weekday\n", + " compute_weekday_activity(db, course)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-fkWWeYLw-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/packages/prisma/src/prisma/migrations/20241204165711_update_daily_activity_types/migration.sql b/packages/prisma/src/prisma/migrations/20241204165711_update_daily_activity_types/migration.sql new file mode 100644 index 0000000000..7366079ded --- /dev/null +++ b/packages/prisma/src/prisma/migrations/20241204165711_update_daily_activity_types/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `participantCount` on the `AggregatedCourseAnalytics` table. All the data in the column will be lost. + - A unique constraint covering the columns `[courseId]` on the table `AggregatedCourseAnalytics` will be added. If there are existing duplicate values, this will fail. + - Added the required column `courseParticipantCount` to the `AggregatedCourseAnalytics` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "AggregatedCourseAnalytics" DROP COLUMN "participantCount", +ADD COLUMN "courseParticipantCount" INTEGER NOT NULL, +ALTER COLUMN "activityMonday" SET DATA TYPE REAL, +ALTER COLUMN "activityTuesday" SET DATA TYPE REAL, +ALTER COLUMN "activityWednesday" SET DATA TYPE REAL, +ALTER COLUMN "activityThursday" SET DATA TYPE REAL, +ALTER COLUMN "activityFriday" SET DATA TYPE REAL, +ALTER COLUMN "activitySaturday" SET DATA TYPE REAL, +ALTER COLUMN "activitySunday" SET DATA TYPE REAL; + +-- CreateIndex +CREATE UNIQUE INDEX "AggregatedCourseAnalytics_courseId_key" ON "AggregatedCourseAnalytics"("courseId"); diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index b22a1deacb..dce35865b4 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -146,17 +146,17 @@ model ParticipantCourseAnalytics { model AggregatedCourseAnalytics { id Int @id @default(autoincrement()) - participantCount Int - activityMonday Int - activityTuesday Int - activityWednesday Int - activityThursday Int - activityFriday Int - activitySaturday Int - activitySunday Int + courseParticipantCount Int // total number of participants that have a participation in the course + activityMonday Float @db.Real // average number of participants that have been active on Monday + activityTuesday Float @db.Real // ... on Tuesday + activityWednesday Float @db.Real // ... on Wednesday + activityThursday Float @db.Real // ... on Thursday + activityFriday Float @db.Real // ... on Friday + activitySaturday Float @db.Real // ... on Saturday + activitySunday Float @db.Real // ... on Sunday course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) - courseId String @db.Uuid + courseId String @unique @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt From 5f402d72e02449ba1e62dce4dd0519c4a2872e30 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:43:47 +0100 Subject: [PATCH 12/38] chore(apps/analytics): add database tables for learning analytics progress and performance computations (#4389) --- .../migration.sql | 121 ++++++++++++++++++ .../prisma/src/prisma/schema/analytics.prisma | 102 +++++++++++++++ .../prisma/src/prisma/schema/course.prisma | 4 + .../prisma/src/prisma/schema/element.prisma | 9 +- .../src/prisma/schema/participant.prisma | 1 + packages/prisma/src/prisma/schema/quiz.prisma | 4 + 6 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql diff --git a/packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql b/packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql new file mode 100644 index 0000000000..2b0f6ac7a3 --- /dev/null +++ b/packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql @@ -0,0 +1,121 @@ +-- CreateEnum +CREATE TYPE "PerformanceLevel" AS ENUM ('LOW', 'MEDIUM', 'HIGH'); + +-- CreateTable +CREATE TABLE "ParticipantPerformance" ( + "id" SERIAL NOT NULL, + "firstErrorRate" REAL NOT NULL, + "firstPerformance" "PerformanceLevel" NOT NULL, + "lastErrorRate" REAL NOT NULL, + "lastPerformance" "PerformanceLevel" NOT NULL, + "totalErrorRate" REAL NOT NULL, + "totalPerformance" "PerformanceLevel" NOT NULL, + "participantId" UUID NOT NULL, + "courseId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ParticipantPerformance_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "InstancePerformance" ( + "id" SERIAL NOT NULL, + "firstErrorRate" REAL, + "firstPartialRate" REAL, + "firstCorrectRate" REAL, + "lastErrorRate" REAL, + "lastPartialRate" REAL, + "lastCorrectRate" REAL, + "totalErrorRate" REAL NOT NULL, + "totalPartialRate" REAL NOT NULL, + "totalCorrectRate" REAL NOT NULL, + "instanceId" INTEGER NOT NULL, + "courseId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "InstancePerformance_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ActivityPerformance" ( + "id" SERIAL NOT NULL, + "firstErrorRate" REAL, + "firstPartialRate" REAL, + "firstCorrectRate" REAL, + "lastErrorRate" REAL, + "lastPartialRate" REAL, + "lastCorrectRate" REAL, + "totalErrorRate" REAL NOT NULL, + "totalPartialRate" REAL NOT NULL, + "totalCorrectRate" REAL NOT NULL, + "practiceQuizId" UUID, + "microLearningId" UUID, + "courseId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ActivityPerformance_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ActivityProgress" ( + "id" SERIAL NOT NULL, + "totalCourseParticipants" INTEGER NOT NULL, + "startedCount" INTEGER NOT NULL, + "completedCount" INTEGER NOT NULL, + "repeatedCount" INTEGER, + "practiceQuizId" UUID, + "microLearningId" UUID, + "courseId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ActivityProgress_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "InstancePerformance_instanceId_key" ON "InstancePerformance"("instanceId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ActivityPerformance_practiceQuizId_key" ON "ActivityPerformance"("practiceQuizId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ActivityPerformance_microLearningId_key" ON "ActivityPerformance"("microLearningId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ActivityProgress_practiceQuizId_key" ON "ActivityProgress"("practiceQuizId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ActivityProgress_microLearningId_key" ON "ActivityProgress"("microLearningId"); + +-- AddForeignKey +ALTER TABLE "ParticipantPerformance" ADD CONSTRAINT "ParticipantPerformance_participantId_fkey" FOREIGN KEY ("participantId") REFERENCES "Participant"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ParticipantPerformance" ADD CONSTRAINT "ParticipantPerformance_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "InstancePerformance" ADD CONSTRAINT "InstancePerformance_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "ElementInstance"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "InstancePerformance" ADD CONSTRAINT "InstancePerformance_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ActivityPerformance" ADD CONSTRAINT "ActivityPerformance_practiceQuizId_fkey" FOREIGN KEY ("practiceQuizId") REFERENCES "PracticeQuiz"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ActivityPerformance" ADD CONSTRAINT "ActivityPerformance_microLearningId_fkey" FOREIGN KEY ("microLearningId") REFERENCES "MicroLearning"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ActivityPerformance" ADD CONSTRAINT "ActivityPerformance_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ActivityProgress" ADD CONSTRAINT "ActivityProgress_practiceQuizId_fkey" FOREIGN KEY ("practiceQuizId") REFERENCES "PracticeQuiz"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ActivityProgress" ADD CONSTRAINT "ActivityProgress_microLearningId_fkey" FOREIGN KEY ("microLearningId") REFERENCES "MicroLearning"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ActivityProgress" ADD CONSTRAINT "ActivityProgress_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index dce35865b4..e3c90011d8 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -11,6 +11,12 @@ enum ActivityLevel { HIGH } +enum PerformanceLevel { + LOW + MEDIUM + HIGH +} + // All metrics are based on logged in participant activity only model ParticipantAnalytics { id Int @id @default(autoincrement()) @@ -162,6 +168,102 @@ model AggregatedCourseAnalytics { updatedAt DateTime @updatedAt } +model ParticipantPerformance { + id Int @id @default(autoincrement()) + + firstErrorRate Float @db.Real // fraction of first responses that were incorrect (in the corresponding course) + firstPerformance PerformanceLevel // performance level based on quantiles of first error rate distributions over all participants + + lastErrorRate Float @db.Real // fraction of last responses that were incorrect + lastPerformance PerformanceLevel + + totalErrorRate Float @db.Real // fraction of all responses that were incorrect + totalPerformance PerformanceLevel + + participant Participant @relation(fields: [participantId], references: [id], onDelete: Cascade, onUpdate: Cascade) + participantId String @db.Uuid + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) + courseId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model InstancePerformance { + id Int @id @default(autoincrement()) + + firstErrorRate Float? @db.Real // fraction of wrong answers to this instance (first attempt, PQ only) + firstPartialRate Float? @db.Real // fraction of partially correct answers to this instance (first attempt, PQ only) + firstCorrectRate Float? @db.Real // fraction of correct answers to this instance (first attempt, PQ only) + + lastErrorRate Float? @db.Real // ... (last attempt, PQ only) + lastPartialRate Float? @db.Real // ... (last attempt, PQ only) + lastCorrectRate Float? @db.Real // ... (last attempt, PQ only) + + totalErrorRate Float @db.Real // ... (all attempts) + totalPartialRate Float @db.Real // ... (all attempts) + totalCorrectRate Float @db.Real // ... (all attempts) + + instance ElementInstance @relation(fields: [instanceId], references: [id], onDelete: Cascade, onUpdate: Cascade) + instanceId Int @unique + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) + courseId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ActivityPerformance { + id Int @id @default(autoincrement()) + + firstErrorRate Float? @db.Real // fraction of wrong answers to instances in this activity (first attempt, PQ only) + firstPartialRate Float? @db.Real // fraction of partially correct answers to instances in this activity (first attempt, PQ only) + firstCorrectRate Float? @db.Real // fraction of correct answers to instances in this activity (first attempt, PQ only) + + lastErrorRate Float? @db.Real // ... (last attempt, PQ only) + lastPartialRate Float? @db.Real // ... (last attempt, PQ only) + lastCorrectRate Float? @db.Real // ... (last attempt, PQ only) + + totalErrorRate Float @db.Real // ... (all attempts) + totalPartialRate Float @db.Real // ... (all attempts) + totalCorrectRate Float @db.Real // ... (all attempts) + + practiceQuiz PracticeQuiz? @relation(fields: [practiceQuizId], references: [id], onDelete: Cascade, onUpdate: Cascade) + practiceQuizId String? @unique @db.Uuid + + microLearning MicroLearning? @relation(fields: [microLearningId], references: [id], onDelete: Cascade, onUpdate: Cascade) + microLearningId String? @unique @db.Uuid + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) + courseId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ActivityProgress { + id Int @id @default(autoincrement()) + + totalCourseParticipants Int + startedCount Int + completedCount Int + repeatedCount Int? + + practiceQuiz PracticeQuiz? @relation(fields: [practiceQuizId], references: [id], onDelete: Cascade, onUpdate: Cascade) + practiceQuizId String? @unique @db.Uuid + + microLearning MicroLearning? @relation(fields: [microLearningId], references: [id], onDelete: Cascade, onUpdate: Cascade) + microLearningId String? @unique @db.Uuid + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade, onUpdate: Cascade) + courseId String @db.Uuid + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model CompetencyTree { id Int @id @default(autoincrement()) diff --git a/packages/prisma/src/prisma/schema/course.prisma b/packages/prisma/src/prisma/schema/course.prisma index 7c1ae29c14..4fc2228b90 100644 --- a/packages/prisma/src/prisma/schema/course.prisma +++ b/packages/prisma/src/prisma/schema/course.prisma @@ -39,6 +39,10 @@ model Course { aggregatedAnalytics AggregatedAnalytics[] aggregatedCourseAnalytics AggregatedCourseAnalytics[] participantCourseAnalytics ParticipantCourseAnalytics[] + participantPerformances ParticipantPerformance[] + instancePerformances InstancePerformance[] + activityPerformances ActivityPerformance[] + activityProgresses ActivityProgress[] responses QuestionResponse[] groupAssignmentPoolEntries GroupAssignmentPoolEntry[] diff --git a/packages/prisma/src/prisma/schema/element.prisma b/packages/prisma/src/prisma/schema/element.prisma index ca0abcc9a8..56a0865217 100644 --- a/packages/prisma/src/prisma/schema/element.prisma +++ b/packages/prisma/src/prisma/schema/element.prisma @@ -87,10 +87,11 @@ model ElementInstance { /// [PrismaElementResults] anonymousResults Json // contains the collection of gathered results by anonymous participants - responses QuestionResponse[] - detailResponses QuestionResponseDetail[] - feedbacks ElementFeedback[] - instanceStatistics InstanceStatistics? + responses QuestionResponse[] + detailResponses QuestionResponseDetail[] + feedbacks ElementFeedback[] + instanceStatistics InstanceStatistics? + instancePerformance InstancePerformance? element Element @relation(fields: [elementId], references: [id], onDelete: Cascade, onUpdate: Cascade) elementId Int diff --git a/packages/prisma/src/prisma/schema/participant.prisma b/packages/prisma/src/prisma/schema/participant.prisma index 37e0a22006..246a1acaa5 100644 --- a/packages/prisma/src/prisma/schema/participant.prisma +++ b/packages/prisma/src/prisma/schema/participant.prisma @@ -48,6 +48,7 @@ model Participant { titles Title[] participantAnalytics ParticipantAnalytics[] participantCourseAnalytics ParticipantCourseAnalytics[] + coursePerformances ParticipantPerformance[] groupAssignmentPoolEntries GroupAssignmentPoolEntry[] messages GroupMessage[] diff --git a/packages/prisma/src/prisma/schema/quiz.prisma b/packages/prisma/src/prisma/schema/quiz.prisma index 07d264e7dd..de5d835cee 100644 --- a/packages/prisma/src/prisma/schema/quiz.prisma +++ b/packages/prisma/src/prisma/schema/quiz.prisma @@ -31,6 +31,8 @@ model PracticeQuiz { responses QuestionResponse[] responseDetails QuestionResponseDetail[] + performance ActivityPerformance? + progress ActivityProgress? startedCount Int @default(0) // >= completedCount (at least one answer given to quiz) completedCount Int @default(0) // >= repeatedCount (every instance answered at least once) @@ -213,6 +215,8 @@ model MicroLearning { responses QuestionResponse[] responseDetails QuestionResponseDetail[] + performance ActivityPerformance? + progress ActivityProgress? startedCount Int @default(0) // >= completedCount (at least one answer given to quiz) completedCount Int @default(0) // (every instance answered at least once) From cb471c3f16659b4e763d028815d6b8398718c799 Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:17:04 +0100 Subject: [PATCH 13/38] enhance(apps/analytics): add computation logic for participant course performance (#4390) --- apps/analytics/src/modules/__init__.py | 5 + .../participant_performance/__init__.py | 3 + .../compute_performance_levels.py | 39 ++++++ .../compute_response_error_rates.py | 54 ++++++++ .../save_participant_performance.py | 30 +++++ .../notebooks/participant_performance.ipynb | 126 ++++++++++++++++++ .../migration.sql | 3 + .../prisma/src/prisma/schema/analytics.prisma | 2 + 8 files changed, 262 insertions(+) create mode 100644 apps/analytics/src/modules/participant_performance/__init__.py create mode 100644 apps/analytics/src/modules/participant_performance/compute_performance_levels.py create mode 100644 apps/analytics/src/modules/participant_performance/compute_response_error_rates.py create mode 100644 apps/analytics/src/modules/participant_performance/save_participant_performance.py create mode 100644 apps/analytics/src/notebooks/participant_performance.ipynb rename packages/prisma/src/prisma/migrations/{20241205143213_learning_analytics_performance_progress => 20241205154359_learning_analytics_performance_progress}/migration.sql (97%) diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py index f478e9e6fa..3435c1fc55 100644 --- a/apps/analytics/src/modules/__init__.py +++ b/apps/analytics/src/modules/__init__.py @@ -7,3 +7,8 @@ save_participant_course_analytics, ) from .aggregated_course_analytics import compute_weekday_activity +from .participant_performance import ( + compute_response_error_rates, + compute_performance_levels, + save_participant_performance, +) diff --git a/apps/analytics/src/modules/participant_performance/__init__.py b/apps/analytics/src/modules/participant_performance/__init__.py new file mode 100644 index 0000000000..17911cd421 --- /dev/null +++ b/apps/analytics/src/modules/participant_performance/__init__.py @@ -0,0 +1,3 @@ +from .compute_response_error_rates import compute_response_error_rates +from .compute_performance_levels import compute_performance_levels +from .save_participant_performance import save_participant_performance diff --git a/apps/analytics/src/modules/participant_performance/compute_performance_levels.py b/apps/analytics/src/modules/participant_performance/compute_performance_levels.py new file mode 100644 index 0000000000..c4a7f64bbf --- /dev/null +++ b/apps/analytics/src/modules/participant_performance/compute_performance_levels.py @@ -0,0 +1,39 @@ +def compute_performance_levels(df_performance): + # set the performance levels based on the quantiles + first_qs = df_performance.firstErrorRate.quantile([0.25, 0.75]) + last_qs = df_performance.lastErrorRate.quantile([0.25, 0.75]) + total_qs = df_performance.totalErrorRate.quantile([0.25, 0.75]) + + first_q1 = first_qs[0.25] + first_q3 = first_qs[0.75] + last_q1 = last_qs[0.25] + last_q3 = last_qs[0.75] + total_q1 = total_qs[0.25] + total_q3 = total_qs[0.75] + + # set the performance levels based on the quantiles (inverse logic compared to activity - higher error rate is worse) + df_performance["firstPerformance"] = "MEDIUM" + df_performance.loc[ + df_performance.firstErrorRate <= first_q1, "firstPerformance" + ] = "HIGH" + df_performance.loc[ + df_performance.firstErrorRate >= first_q3, "firstPerformance" + ] = "LOW" + + df_performance["lastPerformance"] = "MEDIUM" + df_performance.loc[df_performance.lastErrorRate <= last_q1, "lastPerformance"] = ( + "HIGH" + ) + df_performance.loc[df_performance.lastErrorRate >= last_q3, "lastPerformance"] = ( + "LOW" + ) + + df_performance["totalPerformance"] = "MEDIUM" + df_performance.loc[ + df_performance.totalErrorRate <= total_q1, "totalPerformance" + ] = "HIGH" + df_performance.loc[ + df_performance.totalErrorRate >= total_q3, "totalPerformance" + ] = "LOW" + + return df_performance diff --git a/apps/analytics/src/modules/participant_performance/compute_response_error_rates.py b/apps/analytics/src/modules/participant_performance/compute_response_error_rates.py new file mode 100644 index 0000000000..edc40d61df --- /dev/null +++ b/apps/analytics/src/modules/participant_performance/compute_response_error_rates.py @@ -0,0 +1,54 @@ +def compute_response_error_rates(df_responses): + # compute the error rate for each response itself + df_responses["responseErrorRate"] = ( + df_responses["wrongCount"] / df_responses["trialsCount"] + ) + + # compute the total number of responses, number of wrong first and last responses, + # total number of wrong responses, and the average total error rate + df_response_count = ( + df_responses.groupby("participantId").size().reset_index(name="responseCount") + ) + df_first_response_wrong_count = ( + df_responses[df_responses["firstResponseCorrectness"] == "WRONG"] + .groupby("participantId") + .size() + .reset_index(name="wrongFirstResponseCount") + ) + df_last_response_wrong_count = ( + df_responses[df_responses["lastResponseCorrectness"] == "WRONG"] + .groupby("participantId") + .size() + .reset_index(name="wrongLastResponseCount") + ) + df_total_error_rate = ( + df_responses[["participantId", "responseErrorRate"]] + .groupby("participantId") + .agg("mean") + .reset_index() + .rename( + columns={ + "responseErrorRate": "totalErrorRate", + } + ) + ) + + # combine the dataframes into a single one + df_performance = ( + df_response_count.merge( + df_first_response_wrong_count, on="participantId", how="left" + ) + .merge(df_last_response_wrong_count, on="participantId", how="left") + .merge(df_total_error_rate, on="participantId", how="left") + .fillna(0) + ) + + # compute the first and last error rates + df_performance["firstErrorRate"] = ( + df_performance["wrongFirstResponseCount"] / df_performance["responseCount"] + ) + df_performance["lastErrorRate"] = ( + df_performance["wrongLastResponseCount"] / df_performance["responseCount"] + ) + + return df_performance diff --git a/apps/analytics/src/modules/participant_performance/save_participant_performance.py b/apps/analytics/src/modules/participant_performance/save_participant_performance.py new file mode 100644 index 0000000000..95d4705a06 --- /dev/null +++ b/apps/analytics/src/modules/participant_performance/save_participant_performance.py @@ -0,0 +1,30 @@ +def save_participant_performance(db, df_performance, course_id): + for _, row in df_performance.iterrows(): + db.participantperformance.upsert( + where={ + "participantId_courseId": { + "participantId": row["participantId"], + "courseId": course_id, + } + }, + data={ + "create": { + "firstErrorRate": row["firstErrorRate"], + "firstPerformance": row["firstPerformance"], + "lastErrorRate": row["lastErrorRate"], + "lastPerformance": row["lastPerformance"], + "totalErrorRate": row["totalErrorRate"], + "totalPerformance": row["totalPerformance"], + "participant": {"connect": {"id": row["participantId"]}}, + "course": {"connect": {"id": course_id}}, + }, + "update": { + "firstErrorRate": row["firstErrorRate"], + "firstPerformance": row["firstPerformance"], + "lastErrorRate": row["lastErrorRate"], + "lastPerformance": row["lastPerformance"], + "totalErrorRate": row["totalErrorRate"], + "totalPerformance": row["totalPerformance"], + }, + }, + ) diff --git a/apps/analytics/src/notebooks/participant_performance.ipynb b/apps/analytics/src/notebooks/participant_performance.ipynb new file mode 100644 index 0000000000..7cb68989b2 --- /dev/null +++ b/apps/analytics/src/notebooks/participant_performance.ipynb @@ -0,0 +1,126 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "import sys\n", + "\n", + "# set the python path correctly for module imports to work\n", + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.participant_course_analytics.get_running_past_courses import (\n", + " get_running_past_courses,\n", + ")\n", + "from src.modules.participant_performance.compute_response_error_rates import (\n", + " compute_response_error_rates,\n", + ")\n", + "from src.modules.participant_performance.compute_performance_levels import (\n", + " compute_performance_levels,\n", + ")\n", + "from src.modules.participant_performance.save_participant_performance import (\n", + " save_participant_performance,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", + "\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compute Participant Performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch all courses from the database\n", + "df_courses = get_running_past_courses(db)\n", + "\n", + "# Iterate over the course and fetch all question responses linked to it\n", + "for idx, course in df_courses.iterrows():\n", + " course_id = course[\"id\"]\n", + " print(f\"Processing course\", idx, \"of\", len(df_courses), \"with id\", course_id)\n", + "\n", + " # fetch all question responses linked to this course\n", + " question_responses = db.questionresponse.find_many(where={\"courseId\": course_id})\n", + " df_responses = pd.DataFrame(list(map(lambda x: x.dict(), question_responses)))\n", + "\n", + " # if no responses are linked to the course, skip the iteration\n", + " if df_responses.empty:\n", + " print(\"No responses linked to course\", course_id)\n", + " continue\n", + "\n", + " df_performance = compute_response_error_rates(df_responses)\n", + " df_performance = compute_performance_levels(df_performance)\n", + "\n", + " # store computed performance analytics in the corresponding database table\n", + " save_participant_performance(db, df_performance, course_id)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Disconnect from the database\n", + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-fkWWeYLw-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql b/packages/prisma/src/prisma/migrations/20241205154359_learning_analytics_performance_progress/migration.sql similarity index 97% rename from packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql rename to packages/prisma/src/prisma/migrations/20241205154359_learning_analytics_performance_progress/migration.sql index 2b0f6ac7a3..9fdc277283 100644 --- a/packages/prisma/src/prisma/migrations/20241205143213_learning_analytics_performance_progress/migration.sql +++ b/packages/prisma/src/prisma/migrations/20241205154359_learning_analytics_performance_progress/migration.sql @@ -75,6 +75,9 @@ CREATE TABLE "ActivityProgress" ( CONSTRAINT "ActivityProgress_pkey" PRIMARY KEY ("id") ); +-- CreateIndex +CREATE UNIQUE INDEX "ParticipantPerformance_participantId_courseId_key" ON "ParticipantPerformance"("participantId", "courseId"); + -- CreateIndex CREATE UNIQUE INDEX "InstancePerformance_instanceId_key" ON "InstancePerformance"("instanceId"); diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index e3c90011d8..ed27694f09 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -188,6 +188,8 @@ model ParticipantPerformance { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@unique([participantId, courseId]) } model InstancePerformance { From a04be0a2adcc8e9b800987963e89c54d703d17dd Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:41:56 +0100 Subject: [PATCH 14/38] enhance(apps/analytics): add computation logic for instance and activity performance analytics (#4391) --- apps/analytics/src/modules/__init__.py | 20 +-- .../instance_activity_performance/__init__.py | 5 + .../agg_activity_performance.py | 6 + .../compute_instance_performance.py | 110 +++++++++++++ .../get_course_activities.py | 14 ++ .../save_activity_performance.py | 41 +++++ .../save_instance_performances.py | 43 ++++++ .../instance_activity_performance.ipynb | 144 ++++++++++++++++++ .../migration.sql | 1 + .../prisma/src/prisma/schema/analytics.prisma | 2 + 10 files changed, 372 insertions(+), 14 deletions(-) create mode 100644 apps/analytics/src/modules/instance_activity_performance/__init__.py create mode 100644 apps/analytics/src/modules/instance_activity_performance/agg_activity_performance.py create mode 100644 apps/analytics/src/modules/instance_activity_performance/compute_instance_performance.py create mode 100644 apps/analytics/src/modules/instance_activity_performance/get_course_activities.py create mode 100644 apps/analytics/src/modules/instance_activity_performance/save_activity_performance.py create mode 100644 apps/analytics/src/modules/instance_activity_performance/save_instance_performances.py create mode 100644 apps/analytics/src/notebooks/instance_activity_performance.ipynb rename packages/prisma/src/prisma/migrations/{20241205154359_learning_analytics_performance_progress => 20241205172606_learning_analytics_performance_progress}/migration.sql (99%) diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py index 3435c1fc55..d22993a147 100644 --- a/apps/analytics/src/modules/__init__.py +++ b/apps/analytics/src/modules/__init__.py @@ -1,14 +1,6 @@ -from .participant_analytics import compute_correctness, get_participant_responses -from .aggregated_analytics import compute_aggregated_analytics -from .participant_course_analytics import ( - get_running_past_courses, - get_active_weeks, - compute_participant_activity, - save_participant_course_analytics, -) -from .aggregated_course_analytics import compute_weekday_activity -from .participant_performance import ( - compute_response_error_rates, - compute_performance_levels, - save_participant_performance, -) +from .participant_analytics import * +from .aggregated_analytics import * +from .participant_course_analytics import * +from .aggregated_course_analytics import * +from .participant_performance import * +from .instance_activity_performance import * diff --git a/apps/analytics/src/modules/instance_activity_performance/__init__.py b/apps/analytics/src/modules/instance_activity_performance/__init__.py new file mode 100644 index 0000000000..788742b6b8 --- /dev/null +++ b/apps/analytics/src/modules/instance_activity_performance/__init__.py @@ -0,0 +1,5 @@ +from .get_course_activities import get_course_activities +from .compute_instance_performance import compute_instance_performance +from .agg_activity_performance import agg_activity_performance +from .save_instance_performances import save_instance_performances +from .save_activity_performance import save_activity_performance diff --git a/apps/analytics/src/modules/instance_activity_performance/agg_activity_performance.py b/apps/analytics/src/modules/instance_activity_performance/agg_activity_performance.py new file mode 100644 index 0000000000..b09f707957 --- /dev/null +++ b/apps/analytics/src/modules/instance_activity_performance/agg_activity_performance.py @@ -0,0 +1,6 @@ +def agg_activity_performance(df_instance_performance): + activity_performance = df_instance_performance.mean() + activity_performance.drop("instanceId", inplace=True) + activity_performance.to_dict() + + return activity_performance diff --git a/apps/analytics/src/modules/instance_activity_performance/compute_instance_performance.py b/apps/analytics/src/modules/instance_activity_performance/compute_instance_performance.py new file mode 100644 index 0000000000..b287515a20 --- /dev/null +++ b/apps/analytics/src/modules/instance_activity_performance/compute_instance_performance.py @@ -0,0 +1,110 @@ +import pandas as pd + + +def compute_instance_performance(db, activity, total_only=False): + # initialize dataframes for performance tracking + df_instance_performance = pd.DataFrame( + columns=[ + "instanceId", + "responseCount", + "firstErrorRate", + "firstPartialRate", + "firstCorrectRate", + "lastErrorRate", + "lastPartialRate", + "lastCorrectRate", + "totalErrorRate", + "totalPartialRate", + "totalCorrectRate", + ] + ) + + for stack in activity["stacks"]: + for instance in stack["elements"]: + df_responses = pd.DataFrame(instance["responses"]) + + if df_responses.empty: + continue + + # count number of responses + num_responses = len(df_responses) + + if not total_only: + # compute correctness rates for first and last response + first_error_rate = ( + df_responses["firstResponseCorrectness"] + .value_counts() + .get("WRONG", 0) + / num_responses + ) + first_partial_rate = ( + df_responses["firstResponseCorrectness"] + .value_counts() + .get("PARTIAL", 0) + / num_responses + ) + first_correct_rate = ( + df_responses["firstResponseCorrectness"] + .value_counts() + .get("CORRECT", 0) + / num_responses + ) + last_error_rate = ( + df_responses["lastResponseCorrectness"] + .value_counts() + .get("WRONG", 0) + / num_responses + ) + last_partial_rate = ( + df_responses["lastResponseCorrectness"] + .value_counts() + .get("PARTIAL", 0) + / num_responses + ) + last_correct_rate = ( + df_responses["lastResponseCorrectness"] + .value_counts() + .get("CORRECT", 0) + / num_responses + ) + + # compute total correctness rates + df_responses["responseErrorRate"] = ( + df_responses["wrongCount"] / df_responses["trialsCount"] + ) + df_responses["responsePartialRate"] = ( + df_responses["partialCorrectCount"] / df_responses["trialsCount"] + ) + df_responses["responseCorrectRate"] = ( + df_responses["correctCount"] / df_responses["trialsCount"] + ) + total_error_rate = df_responses["responseErrorRate"].mean() + total_partial_rate = df_responses["responsePartialRate"].mean() + total_correct_rate = df_responses["responseCorrectRate"].mean() + + # append instance values to dataframe + instance_performance = { + "instanceId": instance["id"], + "responseCount": num_responses, + "totalErrorRate": total_error_rate, + "totalPartialRate": total_partial_rate, + "totalCorrectRate": total_correct_rate, + } + + if not total_only: + instance_performance.update( + { + "firstErrorRate": first_error_rate, + "firstPartialRate": first_partial_rate, + "firstCorrectRate": first_correct_rate, + "lastErrorRate": last_error_rate, + "lastPartialRate": last_partial_rate, + "lastCorrectRate": last_correct_rate, + } + ) + + df_instance_performance.loc[len(df_instance_performance)] = ( + instance_performance + ) + + return df_instance_performance diff --git a/apps/analytics/src/modules/instance_activity_performance/get_course_activities.py b/apps/analytics/src/modules/instance_activity_performance/get_course_activities.py new file mode 100644 index 0000000000..f904c0232b --- /dev/null +++ b/apps/analytics/src/modules/instance_activity_performance/get_course_activities.py @@ -0,0 +1,14 @@ +def get_course_activities(db, course_id): + pqs = db.practicequiz.find_many( + where={"courseId": course_id}, + include={"stacks": {"include": {"elements": {"include": {"responses": True}}}}}, + ) + pqs = list(map(lambda x: x.dict(), pqs)) + + mls = db.microlearning.find_many( + where={"courseId": course_id}, + include={"stacks": {"include": {"elements": {"include": {"responses": True}}}}}, + ) + mls = list(map(lambda x: x.dict(), mls)) + + return pqs, mls diff --git a/apps/analytics/src/modules/instance_activity_performance/save_activity_performance.py b/apps/analytics/src/modules/instance_activity_performance/save_activity_performance.py new file mode 100644 index 0000000000..86df7d9f1c --- /dev/null +++ b/apps/analytics/src/modules/instance_activity_performance/save_activity_performance.py @@ -0,0 +1,41 @@ +def save_activity_performance( + db, activity_performance, course_id, practice_quiz_id=None, microlearning_id=None +): + values = { + "totalErrorRate": activity_performance.totalErrorRate, + "totalPartialRate": activity_performance.totalPartialRate, + "totalCorrectRate": activity_performance.totalCorrectRate, + } + + if practice_quiz_id is not None: + values.update( + { + "firstErrorRate": activity_performance.firstErrorRate, + "firstPartialRate": activity_performance.firstPartialRate, + "firstCorrectRate": activity_performance.firstCorrectRate, + "lastErrorRate": activity_performance.lastErrorRate, + "lastPartialRate": activity_performance.lastPartialRate, + "lastCorrectRate": activity_performance.lastCorrectRate, + } + ) + + create_values = values.copy() + create_values["practiceQuiz"] = {"connect": {"id": practice_quiz_id}} + create_values["course"] = {"connect": {"id": course_id}} + where_clause = {"practiceQuizId": practice_quiz_id} + + elif microlearning_id is not None: + create_values = values.copy() + create_values["microLearning"] = {"connect": {"id": microlearning_id}} + create_values["course"] = {"connect": {"id": course_id}} + where_clause = {"microLearningId": microlearning_id} + + else: + raise ValueError( + "Either practice_quiz_id or microlearning_id must be provided for activity performance creation/update" + ) + + db.activityperformance.upsert( + where=where_clause, + data={"create": create_values, "update": values}, + ) diff --git a/apps/analytics/src/modules/instance_activity_performance/save_instance_performances.py b/apps/analytics/src/modules/instance_activity_performance/save_instance_performances.py new file mode 100644 index 0000000000..5a3910524a --- /dev/null +++ b/apps/analytics/src/modules/instance_activity_performance/save_instance_performances.py @@ -0,0 +1,43 @@ +def save_instance_performances( + db, df_instance_performance, course_id, total_only=False +): + for _, row in df_instance_performance.iterrows(): + # extract values from dataframe + values = { + "responseCount": row["responseCount"], + "totalErrorRate": row["totalErrorRate"], + "totalPartialRate": row["totalPartialRate"], + "totalCorrectRate": row["totalCorrectRate"], + } + + # only define first and last response rates if applicable + if not total_only: + values.update( + { + "firstErrorRate": row["firstErrorRate"], + "firstPartialRate": row["firstPartialRate"], + "firstCorrectRate": row["firstCorrectRate"], + "lastErrorRate": row["lastErrorRate"], + "lastPartialRate": row["lastPartialRate"], + "lastCorrectRate": row["lastCorrectRate"], + } + ) + + # add relational links during creation + create_values = values.copy() + create_values.update( + { + "instance": {"connect": {"id": row["instanceId"]}}, + "course": {"connect": {"id": course_id}}, + } + ) + + db.instanceperformance.upsert( + where={ + "instanceId": row["instanceId"], + }, + data={ + "create": create_values, + "update": values, + }, + ) diff --git a/apps/analytics/src/notebooks/instance_activity_performance.ipynb b/apps/analytics/src/notebooks/instance_activity_performance.ipynb new file mode 100644 index 0000000000..dd3a77f83c --- /dev/null +++ b/apps/analytics/src/notebooks/instance_activity_performance.ipynb @@ -0,0 +1,144 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "import sys\n", + "\n", + "# set the python path correctly for module imports to work\n", + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.participant_course_analytics.get_running_past_courses import (\n", + " get_running_past_courses,\n", + ")\n", + "from src.modules.instance_activity_performance.get_course_activities import get_course_activities\n", + "from src.modules.instance_activity_performance.compute_instance_performance import compute_instance_performance\n", + "from src.modules.instance_activity_performance.agg_activity_performance import agg_activity_performance\n", + "from src.modules.instance_activity_performance.save_instance_performances import save_instance_performances\n", + "from src.modules.instance_activity_performance.save_activity_performance import save_activity_performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", + "\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compute Participant Performance" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch all courses from the database\n", + "df_courses = get_running_past_courses(db)\n", + "\n", + "# Iterate over the course and fetch all question responses linked to it\n", + "for idx, course in df_courses.iterrows():\n", + " course_id = course[\"id\"]\n", + " print(f\"Processing course\", idx, \"of\", len(df_courses), \"with id\", course_id)\n", + "\n", + " # fetch all practice quizzes and microlearnings linked to the course\n", + " pqs, mls = get_course_activities(db, course_id)\n", + "\n", + " for quiz in pqs:\n", + " # compute instance performances\n", + " df_instance_performance = compute_instance_performance(db, quiz)\n", + "\n", + " # if no instances with values were found, skip the activity\n", + " if df_instance_performance.empty:\n", + " continue\n", + "\n", + " # compute the activity performance by aggregating the all instance performances\n", + " activity_performance = agg_activity_performance(df_instance_performance)\n", + "\n", + " # save instance performance data\n", + " save_instance_performances(db, df_instance_performance, course_id)\n", + "\n", + " # save activity performance data\n", + " save_activity_performance(db, activity_performance, course_id, practice_quiz_id=quiz[\"id\"])\n", + "\n", + " for ml in mls:\n", + " # compute instance performances\n", + " df_instance_performance = compute_instance_performance(db, ml, total_only=True)\n", + "\n", + " # if no instances with values were found, skip the activity\n", + " if df_instance_performance.empty:\n", + " continue\n", + "\n", + " # compute the activity performance by aggregating the all instance performances\n", + " activity_performance = agg_activity_performance(df_instance_performance)\n", + "\n", + " # save instance performance data\n", + " save_instance_performances(db, df_instance_performance, course_id, total_only=True)\n", + "\n", + " # save activity performance data\n", + " save_activity_performance(db, activity_performance, course_id, microlearning_id=ml[\"id\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Disconnect from the database\n", + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-fkWWeYLw-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/packages/prisma/src/prisma/migrations/20241205154359_learning_analytics_performance_progress/migration.sql b/packages/prisma/src/prisma/migrations/20241205172606_learning_analytics_performance_progress/migration.sql similarity index 99% rename from packages/prisma/src/prisma/migrations/20241205154359_learning_analytics_performance_progress/migration.sql rename to packages/prisma/src/prisma/migrations/20241205172606_learning_analytics_performance_progress/migration.sql index 9fdc277283..599a5aa80c 100644 --- a/packages/prisma/src/prisma/migrations/20241205154359_learning_analytics_performance_progress/migration.sql +++ b/packages/prisma/src/prisma/migrations/20241205172606_learning_analytics_performance_progress/migration.sql @@ -21,6 +21,7 @@ CREATE TABLE "ParticipantPerformance" ( -- CreateTable CREATE TABLE "InstancePerformance" ( "id" SERIAL NOT NULL, + "responseCount" INTEGER NOT NULL, "firstErrorRate" REAL, "firstPartialRate" REAL, "firstCorrectRate" REAL, diff --git a/packages/prisma/src/prisma/schema/analytics.prisma b/packages/prisma/src/prisma/schema/analytics.prisma index ed27694f09..dbf0f5683b 100644 --- a/packages/prisma/src/prisma/schema/analytics.prisma +++ b/packages/prisma/src/prisma/schema/analytics.prisma @@ -195,6 +195,8 @@ model ParticipantPerformance { model InstancePerformance { id Int @id @default(autoincrement()) + responseCount Int // total number of responses to this instance + firstErrorRate Float? @db.Real // fraction of wrong answers to this instance (first attempt, PQ only) firstPartialRate Float? @db.Real // fraction of partially correct answers to this instance (first attempt, PQ only) firstCorrectRate Float? @db.Real // fraction of correct answers to this instance (first attempt, PQ only) From 28d64f67f952aa009e0f879a8bba5466f68e15ff Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:23:31 +0100 Subject: [PATCH 15/38] enhance(apps/analytics): add computation logic for activity progress (#4392) --- apps/analytics/src/modules/__init__.py | 1 + .../src/modules/activity_progress/__init__.py | 4 + .../compute_progress_counts.py | 41 +++++ .../get_course_progress_activities.py | 14 ++ .../save_microlearning_progress.py | 21 +++ .../save_practice_quiz_progress.py | 23 +++ .../src/notebooks/activity_progress.ipynb | 147 ++++++++++++++++++ .../instance_activity_performance.ipynb | 32 +++- 8 files changed, 275 insertions(+), 8 deletions(-) create mode 100644 apps/analytics/src/modules/activity_progress/__init__.py create mode 100644 apps/analytics/src/modules/activity_progress/compute_progress_counts.py create mode 100644 apps/analytics/src/modules/activity_progress/get_course_progress_activities.py create mode 100644 apps/analytics/src/modules/activity_progress/save_microlearning_progress.py create mode 100644 apps/analytics/src/modules/activity_progress/save_practice_quiz_progress.py create mode 100644 apps/analytics/src/notebooks/activity_progress.ipynb diff --git a/apps/analytics/src/modules/__init__.py b/apps/analytics/src/modules/__init__.py index d22993a147..d7730fcebb 100644 --- a/apps/analytics/src/modules/__init__.py +++ b/apps/analytics/src/modules/__init__.py @@ -4,3 +4,4 @@ from .aggregated_course_analytics import * from .participant_performance import * from .instance_activity_performance import * +from .activity_progress import * diff --git a/apps/analytics/src/modules/activity_progress/__init__.py b/apps/analytics/src/modules/activity_progress/__init__.py new file mode 100644 index 0000000000..f178da34cc --- /dev/null +++ b/apps/analytics/src/modules/activity_progress/__init__.py @@ -0,0 +1,4 @@ +from .get_course_progress_activities import get_course_progress_activities +from .compute_progress_counts import compute_progress_counts +from .save_practice_quiz_progress import save_practice_quiz_progress +from .save_microlearning_progress import save_microlearning_progress diff --git a/apps/analytics/src/modules/activity_progress/compute_progress_counts.py b/apps/analytics/src/modules/activity_progress/compute_progress_counts.py new file mode 100644 index 0000000000..4126bcc1b8 --- /dev/null +++ b/apps/analytics/src/modules/activity_progress/compute_progress_counts.py @@ -0,0 +1,41 @@ +import pandas as pd + + +def compute_progress_counts(activity): + started_count = 0 + completed_count = 0 + repeated_count = 0 + + if len(activity["responses"]) != 0: + # count number of elements in activity stacks + num_elements = 0 + for stack in activity["stacks"]: + num_elements += len(stack["elements"]) + + # group the activity responses by participant and count them + df_responses = pd.DataFrame(activity["responses"]) + df_statistics = ( + df_responses[["id", "trialsCount", "participantId"]] + .groupby("participantId") + .agg({"id": "count", "trialsCount": "min"}) + .rename(columns={"id": "count", "trialsCount": "min_trials"}) + ) + + # compute number of participants that have started the activity + started_count = len(df_statistics[df_statistics["count"] <= num_elements]) + + # compute number of participants that have completed the activity + completed_count = len(df_statistics[df_statistics["count"] == num_elements]) + + # count the number of participants that have repeated the activity (completed and min_trials >= 2) + repeated_count = len( + df_statistics[ + (df_statistics["count"] == num_elements) + & (df_statistics["min_trials"] >= 2) + ] + ) + + else: + print("No responses found for activity", activity["id"]) + + return started_count, completed_count, repeated_count diff --git a/apps/analytics/src/modules/activity_progress/get_course_progress_activities.py b/apps/analytics/src/modules/activity_progress/get_course_progress_activities.py new file mode 100644 index 0000000000..9dfd34a2b0 --- /dev/null +++ b/apps/analytics/src/modules/activity_progress/get_course_progress_activities.py @@ -0,0 +1,14 @@ +def get_course_progress_activities(db, course_id): + pqs = db.practicequiz.find_many( + where={"courseId": course_id}, + include={"stacks": {"include": {"elements": True}}, "responses": True}, + ) + pqs = list(map(lambda x: x.dict(), pqs)) + + mls = db.microlearning.find_many( + where={"courseId": course_id}, + include={"stacks": {"include": {"elements": True}}, "responses": True}, + ) + mls = list(map(lambda x: x.dict(), mls)) + + return pqs, mls diff --git a/apps/analytics/src/modules/activity_progress/save_microlearning_progress.py b/apps/analytics/src/modules/activity_progress/save_microlearning_progress.py new file mode 100644 index 0000000000..e4fd274d8f --- /dev/null +++ b/apps/analytics/src/modules/activity_progress/save_microlearning_progress.py @@ -0,0 +1,21 @@ +def save_microlearning_progress( + db, + course_participants, + started_count, + completed_count, + course_id, + ml_id, +): + values = { + "totalCourseParticipants": course_participants, + "startedCount": started_count, + "completedCount": completed_count, + } + creation_values = values.copy() + creation_values["course"] = {"connect": {"id": course_id}} + creation_values["microLearning"] = {"connect": {"id": ml_id}} + + db.activityprogress.upsert( + where={"microLearningId": ml_id}, + data={"create": creation_values, "update": values}, + ) diff --git a/apps/analytics/src/modules/activity_progress/save_practice_quiz_progress.py b/apps/analytics/src/modules/activity_progress/save_practice_quiz_progress.py new file mode 100644 index 0000000000..1cafd7a31d --- /dev/null +++ b/apps/analytics/src/modules/activity_progress/save_practice_quiz_progress.py @@ -0,0 +1,23 @@ +def save_practice_quiz_progress( + db, + course_participants, + started_count, + completed_count, + repeated_count, + course_id, + quiz_id, +): + values = { + "totalCourseParticipants": course_participants, + "startedCount": started_count, + "completedCount": completed_count, + "repeatedCount": repeated_count, + } + creation_values = values.copy() + creation_values["course"] = {"connect": {"id": course_id}} + creation_values["practiceQuiz"] = {"connect": {"id": quiz_id}} + + db.activityprogress.upsert( + where={"practiceQuizId": quiz_id}, + data={"create": creation_values, "update": values}, + ) diff --git a/apps/analytics/src/notebooks/activity_progress.ipynb b/apps/analytics/src/notebooks/activity_progress.ipynb new file mode 100644 index 0000000000..9413120834 --- /dev/null +++ b/apps/analytics/src/notebooks/activity_progress.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preparation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from datetime import datetime\n", + "from prisma import Prisma\n", + "import pandas as pd\n", + "import sys\n", + "\n", + "# set the python path correctly for module imports to work\n", + "sys.path.append(\"../../\")\n", + "\n", + "from src.modules.participant_course_analytics.get_running_past_courses import (\n", + " get_running_past_courses,\n", + ")\n", + "from src.modules.activity_progress.get_course_progress_activities import (\n", + " get_course_progress_activities,\n", + ")\n", + "from src.modules.activity_progress.compute_progress_counts import (\n", + " compute_progress_counts,\n", + ")\n", + "from src.modules.activity_progress.save_practice_quiz_progress import (\n", + " save_practice_quiz_progress,\n", + ")\n", + "from src.modules.activity_progress.save_microlearning_progress import (\n", + " save_microlearning_progress,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "db = Prisma()\n", + "\n", + "# set the environment variable DATABASE_URL to the connection string of your database\n", + "os.environ[\"DATABASE_URL\"] = \"postgresql://klicker:klicker@localhost:5432/klicker-prod\"\n", + "\n", + "db.connect()\n", + "\n", + "# Script settings\n", + "verbose = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compute Activity Progress" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch all courses from the database\n", + "df_courses = get_running_past_courses(db)\n", + "\n", + "# Iterate over the course and fetch all question responses linked to it\n", + "for idx, course in df_courses.iterrows():\n", + " course_id = course[\"id\"]\n", + " print(f\"Processing course\", idx, \"of\", len(df_courses), \"with id\", course_id)\n", + "\n", + " # extract number of participants\n", + " course_participants = len(course[\"participations\"])\n", + "\n", + " # fetch all practice quizzes and microlearnings linked to the course\n", + " pqs, mls = get_course_progress_activities(db, course_id)\n", + "\n", + " for quiz in pqs:\n", + " started_count, completed_count, repeated_count = compute_progress_counts(quiz)\n", + "\n", + " # store results in database table\n", + " save_practice_quiz_progress(\n", + " db,\n", + " course_participants,\n", + " started_count,\n", + " completed_count,\n", + " repeated_count,\n", + " course_id,\n", + " quiz[\"id\"],\n", + " )\n", + "\n", + " for ml in mls:\n", + " started_count, completed_count, repeated_count = compute_progress_counts(ml)\n", + "\n", + " # store results in database table\n", + " save_microlearning_progress(\n", + " db,\n", + " course_participants,\n", + " started_count,\n", + " completed_count,\n", + " course_id,\n", + " ml[\"id\"],\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Disconnect from the database\n", + "db.disconnect()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "analytics-fkWWeYLw-py3.12", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/apps/analytics/src/notebooks/instance_activity_performance.ipynb b/apps/analytics/src/notebooks/instance_activity_performance.ipynb index dd3a77f83c..8dd4417006 100644 --- a/apps/analytics/src/notebooks/instance_activity_performance.ipynb +++ b/apps/analytics/src/notebooks/instance_activity_performance.ipynb @@ -26,11 +26,21 @@ "from src.modules.participant_course_analytics.get_running_past_courses import (\n", " get_running_past_courses,\n", ")\n", - "from src.modules.instance_activity_performance.get_course_activities import get_course_activities\n", - "from src.modules.instance_activity_performance.compute_instance_performance import compute_instance_performance\n", - "from src.modules.instance_activity_performance.agg_activity_performance import agg_activity_performance\n", - "from src.modules.instance_activity_performance.save_instance_performances import save_instance_performances\n", - "from src.modules.instance_activity_performance.save_activity_performance import save_activity_performance" + "from src.modules.instance_activity_performance.get_course_activities import (\n", + " get_course_activities,\n", + ")\n", + "from src.modules.instance_activity_performance.compute_instance_performance import (\n", + " compute_instance_performance,\n", + ")\n", + "from src.modules.instance_activity_performance.agg_activity_performance import (\n", + " agg_activity_performance,\n", + ")\n", + "from src.modules.instance_activity_performance.save_instance_performances import (\n", + " save_instance_performances,\n", + ")\n", + "from src.modules.instance_activity_performance.save_activity_performance import (\n", + " save_activity_performance,\n", + ")" ] }, { @@ -89,7 +99,9 @@ " save_instance_performances(db, df_instance_performance, course_id)\n", "\n", " # save activity performance data\n", - " save_activity_performance(db, activity_performance, course_id, practice_quiz_id=quiz[\"id\"])\n", + " save_activity_performance(\n", + " db, activity_performance, course_id, practice_quiz_id=quiz[\"id\"]\n", + " )\n", "\n", " for ml in mls:\n", " # compute instance performances\n", @@ -103,10 +115,14 @@ " activity_performance = agg_activity_performance(df_instance_performance)\n", "\n", " # save instance performance data\n", - " save_instance_performances(db, df_instance_performance, course_id, total_only=True)\n", + " save_instance_performances(\n", + " db, df_instance_performance, course_id, total_only=True\n", + " )\n", "\n", " # save activity performance data\n", - " save_activity_performance(db, activity_performance, course_id, microlearning_id=ml[\"id\"])" + " save_activity_performance(\n", + " db, activity_performance, course_id, microlearning_id=ml[\"id\"]\n", + " )" ] }, { From b8a134aec969f2f369a345ba71ca8930164350ff Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:42:34 +0100 Subject: [PATCH 16/38] enhance: add first version of activity dashboard with daily and weekly student activity charts (#4393) --- .../activity/ActivityAnalyticsNavigation.tsx | 16 + .../activity/ActivityTimeSeriesPlot.tsx | 85 +++++ .../analytics/activity/DailyActivityPlot.tsx | 84 +++++ .../overview/ActivityDashboardLabel.tsx | 16 + .../overview/AnalyticsCourseLabel.tsx | 34 ++ .../overview/AnalyticsNavigation.tsx | 35 ++ .../overview/CourseDashboardList.tsx | 42 +++ .../analytics/overview/DashboardButtons.tsx | 48 +++ .../overview/PerformanceDashboardLabel.tsx | 16 + .../analytics/overview/QuizDashboardLabel.tsx | 16 + .../pages/analytics/[courseId]/activity.tsx | 111 +++++++ .../analytics/[courseId]/performance.tsx | 45 +++ .../pages/analytics/[courseId]/quizzes.tsx | 45 +++ .../src/pages/analytics/index.tsx | 28 +- .../src/pages/courses/index.tsx | 10 +- .../ops/QGetCourseActivityAnalytics.graphql | 23 ++ packages/graphql/src/ops.schema.json | 305 ++++++++++++++++++ packages/graphql/src/ops.ts | 40 +++ packages/graphql/src/public/client.json | 1 + packages/graphql/src/public/schema.graphql | 24 ++ packages/graphql/src/public/server.json | 1 + packages/graphql/src/schema/analytics.ts | 70 ++++ packages/graphql/src/schema/query.ts | 14 +- packages/graphql/src/services/analytics.ts | 50 +++ packages/graphql/src/services/courses.ts | 11 +- packages/i18n/messages/de.ts | 25 ++ packages/i18n/messages/en.ts | 25 ++ .../migration.sql | 0 .../prisma/src/prisma/schema/course.prisma | 2 +- packages/shared-components/src/Loader.tsx | 5 +- 30 files changed, 1207 insertions(+), 20 deletions(-) create mode 100644 apps/frontend-manage/src/components/analytics/activity/ActivityAnalyticsNavigation.tsx create mode 100644 apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx create mode 100644 apps/frontend-manage/src/components/analytics/activity/DailyActivityPlot.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/ActivityDashboardLabel.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/AnalyticsCourseLabel.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/AnalyticsNavigation.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/CourseDashboardList.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/DashboardButtons.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/PerformanceDashboardLabel.tsx create mode 100644 apps/frontend-manage/src/components/analytics/overview/QuizDashboardLabel.tsx create mode 100644 apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx create mode 100644 apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx create mode 100644 apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx create mode 100644 packages/graphql/src/graphql/ops/QGetCourseActivityAnalytics.graphql create mode 100644 packages/graphql/src/services/analytics.ts rename packages/prisma/src/prisma/migrations/{20241205172606_learning_analytics_performance_progress => 20241206141120_learning_analytics_performance_progress}/migration.sql (100%) diff --git a/apps/frontend-manage/src/components/analytics/activity/ActivityAnalyticsNavigation.tsx b/apps/frontend-manage/src/components/analytics/activity/ActivityAnalyticsNavigation.tsx new file mode 100644 index 0000000000..df24fdd7d3 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/activity/ActivityAnalyticsNavigation.tsx @@ -0,0 +1,16 @@ +import AnalyticsNavigation from '../overview/AnalyticsNavigation' +import PerformanceDashboardLabel from '../overview/PerformanceDashboardLabel' +import QuizDashboardLabel from '../overview/QuizDashboardLabel' + +function ActivityAnalyticsNavigation({ courseId }: { courseId: string }) { + return ( + } + hrefRight={`/analytics/${courseId}/performance`} + labelRight={} + /> + ) +} + +export default ActivityAnalyticsNavigation diff --git a/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx b/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx new file mode 100644 index 0000000000..c8d8b187a6 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx @@ -0,0 +1,85 @@ +import { ParticipantActivityTimestamp } from '@klicker-uzh/graphql/dist/ops' +import { H2 } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import { + CartesianGrid, + Label, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' + +function ActivityTimeSeriesPlot({ + title, + activity, + courseParticipants, +}: { + title: string + activity: ParticipantActivityTimestamp[] + courseParticipants: number +}) { + const t = useTranslations() + + return ( +
+

{title}

+ + ({ + ...item, + activeParticipants: + (item.activeParticipants / courseParticipants) * 100, + }))} + > + + { + const date = new Date(value) + return date + .toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) + .replace(/\//g, '-') + }} + /> + + + { + const date = new Date(value) + return `${t('shared.generic.date')}: ${date + .toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) + .replace(/\//g, '-')}` + }} + formatter={(value) => [ + `${(value as number).toFixed(2)} %`, + t('manage.analytics.activeStudents'), + ]} + contentStyle={{ + borderRadius: '8px', + padding: '8px', + }} + /> + + + +
+ ) +} + +export default ActivityTimeSeriesPlot diff --git a/apps/frontend-manage/src/components/analytics/activity/DailyActivityPlot.tsx b/apps/frontend-manage/src/components/analytics/activity/DailyActivityPlot.tsx new file mode 100644 index 0000000000..e575bb69f9 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/activity/DailyActivityPlot.tsx @@ -0,0 +1,84 @@ +import { WeekdayActivityAnalytics } from '@klicker-uzh/graphql/dist/ops' +import { H2 } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import { + Bar, + BarChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts' + +function DailyActivityPlot({ + courseParticipants, + activeDays, +}: { + courseParticipants: number + activeDays: WeekdayActivityAnalytics +}) { + const t = useTranslations() + const barChartData = [ + { + weekday: t('shared.generic.monday'), + value: (activeDays.monday / courseParticipants) * 100, + }, + { + weekday: t('shared.generic.tuesday'), + value: (activeDays.tuesday / courseParticipants) * 100, + }, + { + weekday: t('shared.generic.wednesday'), + value: (activeDays.wednesday / courseParticipants) * 100, + }, + { + weekday: t('shared.generic.thursday'), + value: (activeDays.thursday / courseParticipants) * 100, + }, + { + weekday: t('shared.generic.friday'), + value: (activeDays.friday / courseParticipants) * 100, + }, + { + weekday: t('shared.generic.saturday'), + value: (activeDays.saturday / courseParticipants) * 100, + }, + { + weekday: t('shared.generic.sunday'), + value: (activeDays.sunday / courseParticipants) * 100, + }, + ] + + return ( +
+

{t('manage.analytics.dailyActivity')}

+ + + `${value.toFixed(0)}%`} + label={{ + value: t('manage.analytics.percentageOfStudents'), + dy: 12, + }} + height={40} + /> + + [ + `${(value as number).toFixed(2)} %`, + t('manage.analytics.activeStudents'), + ]} + contentStyle={{ + borderRadius: '8px', + padding: '8px', + }} + /> + + + +
+ ) +} + +export default DailyActivityPlot diff --git a/apps/frontend-manage/src/components/analytics/overview/ActivityDashboardLabel.tsx b/apps/frontend-manage/src/components/analytics/overview/ActivityDashboardLabel.tsx new file mode 100644 index 0000000000..b65d020933 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/ActivityDashboardLabel.tsx @@ -0,0 +1,16 @@ +import { faChartLine } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useTranslations } from 'next-intl' + +function ActivityDashboardLabel() { + const t = useTranslations() + + return ( + <> + +
{t('manage.analytics.activityDashboard')}
+ + ) +} + +export default ActivityDashboardLabel diff --git a/apps/frontend-manage/src/components/analytics/overview/AnalyticsCourseLabel.tsx b/apps/frontend-manage/src/components/analytics/overview/AnalyticsCourseLabel.tsx new file mode 100644 index 0000000000..54cc439442 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/AnalyticsCourseLabel.tsx @@ -0,0 +1,34 @@ +import { faClock } from '@fortawesome/free-regular-svg-icons' +import { faCheck, faPlay } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Course } from '@klicker-uzh/graphql/dist/ops' +import { H4 } from '@uzh-bf/design-system' +import dayjs from 'dayjs' + +function AnalyticsCourseLabel({ + course, +}: { + course: Pick +}) { + const isPast = course.endDate + ? dayjs(course.endDate).isBefore(dayjs()) + : false + const isFuture = course.startDate + ? dayjs(course.startDate).isAfter(dayjs()) + : false + + return ( +
+ {isPast ? ( + + ) : isFuture ? ( + + ) : ( + + )} +

{course.name}

+
+ ) +} + +export default AnalyticsCourseLabel diff --git a/apps/frontend-manage/src/components/analytics/overview/AnalyticsNavigation.tsx b/apps/frontend-manage/src/components/analytics/overview/AnalyticsNavigation.tsx new file mode 100644 index 0000000000..0f8193ce50 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/AnalyticsNavigation.tsx @@ -0,0 +1,35 @@ +import { + faChevronLeft, + faChevronRight, +} from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import Link from 'next/link' + +interface AnalyticsNavigationProps { + hrefLeft: string + labelLeft: React.ReactNode + hrefRight: string + labelRight: React.ReactNode +} + +function AnalyticsNavigation({ + hrefLeft, + labelLeft, + hrefRight, + labelRight, +}: AnalyticsNavigationProps) { + return ( +
+ + +
{labelLeft}
+ + +
{labelRight}
+ + +
+ ) +} + +export default AnalyticsNavigation diff --git a/apps/frontend-manage/src/components/analytics/overview/CourseDashboardList.tsx b/apps/frontend-manage/src/components/analytics/overview/CourseDashboardList.tsx new file mode 100644 index 0000000000..25e6f920e5 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/CourseDashboardList.tsx @@ -0,0 +1,42 @@ +import { Course } from '@klicker-uzh/graphql/dist/ops' +import { H3 } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import AnalyticsCourseLabel from './AnalyticsCourseLabel' +import DashboardButtons from './DashboardButtons' + +function CourseDashboardList({ + courses, +}: { + courses?: Pick[] | null +}) { + const t = useTranslations() + + return ( +
+
+

{t('manage.analytics.selectAnalyticsDashboard')}:

+
+
+ {courses?.map((course) => ( + + ))} +
+
+ {courses?.map((course) => ( + + ))} +
+
+
+
+ ) +} + +export default CourseDashboardList diff --git a/apps/frontend-manage/src/components/analytics/overview/DashboardButtons.tsx b/apps/frontend-manage/src/components/analytics/overview/DashboardButtons.tsx new file mode 100644 index 0000000000..d69ce3a358 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/DashboardButtons.tsx @@ -0,0 +1,48 @@ +import { Course } from '@klicker-uzh/graphql/dist/ops' +import { Button } from '@uzh-bf/design-system' +import { useRouter } from 'next/router' +import ActivityDashboardLabel from './ActivityDashboardLabel' +import PerformanceDashboardLabel from './PerformanceDashboardLabel' +import QuizDashboardLabel from './QuizDashboardLabel' + +function DashboardButtons({ course }: { course: Pick }) { + const router = useRouter() + + return ( +
+ {[ + { + href: `/analytics/${course.id}/activity`, + label: , + cy: `activity-dashboard-button-${course.name}`, + }, + { + href: `/analytics/${course.id}/performance`, + label: , + cy: `performance-dashboard-button-${course.name}`, + }, + { + href: `/analytics/${course.id}/quizzes`, + label: , + cy: `quiz-dashboard-button-${course.name}`, + }, + ].map((button, ix) => ( + + ))} +
+ ) +} + +export default DashboardButtons diff --git a/apps/frontend-manage/src/components/analytics/overview/PerformanceDashboardLabel.tsx b/apps/frontend-manage/src/components/analytics/overview/PerformanceDashboardLabel.tsx new file mode 100644 index 0000000000..b6bea203e7 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/PerformanceDashboardLabel.tsx @@ -0,0 +1,16 @@ +import { faChartSimple } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useTranslations } from 'next-intl' + +function PerformanceDashboardLabel() { + const t = useTranslations() + + return ( + <> + +
{t('manage.analytics.performanceDashboard')}
+ + ) +} + +export default PerformanceDashboardLabel diff --git a/apps/frontend-manage/src/components/analytics/overview/QuizDashboardLabel.tsx b/apps/frontend-manage/src/components/analytics/overview/QuizDashboardLabel.tsx new file mode 100644 index 0000000000..364a56211c --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/overview/QuizDashboardLabel.tsx @@ -0,0 +1,16 @@ +import { faChartPie } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useTranslations } from 'next-intl' + +function QuizDashboardLabel() { + const t = useTranslations() + + return ( + <> + +
{t('manage.analytics.quizDashboard')}
+ + ) +} + +export default QuizDashboardLabel diff --git a/apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx b/apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx new file mode 100644 index 0000000000..16f4381469 --- /dev/null +++ b/apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx @@ -0,0 +1,111 @@ +import { useQuery } from '@apollo/client' +import { GetCourseActivityAnalyticsDocument } from '@klicker-uzh/graphql/dist/ops' +import Loader from '@klicker-uzh/shared-components/src/Loader' +import { H1, UserNotification } from '@uzh-bf/design-system' +import { GetStaticPropsContext } from 'next' +import { useTranslations } from 'next-intl' +import { useRouter } from 'next/router' +import ActivityAnalyticsNavigation from '~/components/analytics/activity/ActivityAnalyticsNavigation' +import ActivityTimeSeriesPlot from '~/components/analytics/activity/ActivityTimeSeriesPlot' +import DailyActivityPlot from '~/components/analytics/activity/DailyActivityPlot' +import Layout from '~/components/Layout' + +function ActivityDashboard() { + const t = useTranslations() + const router = useRouter() + const courseId = router.query.courseId + + const { data, loading, error } = useQuery( + GetCourseActivityAnalyticsDocument, + { + variables: { courseId: courseId as string }, + skip: !courseId, + } + ) + const course = data?.getCourseActivityAnalytics + + // TODO: extract to separate component with variable names / navigation + // loading state + if (loading || !courseId) { + return ( + + +
+ {t('manage.analytics.analyticsLoadingWait')} + +
+
+ ) + } + + // TODO: extract to separate component for re-use + // error state + if (course === null || typeof course === 'undefined' || error) { + return ( + + +

{t('manage.analytics.activityDashboard')}

+ +
+ ) + } + + return ( + + +
+

+ {t('manage.analytics.activityDashboard')}: {course.name} +

+
+ {t('manage.analytics.totalParticipants', { + number: course.totalParticipants, + })} +
+
+
+ +
+
+ +
+
+ +
+
+
+
+ ) +} + +export async function getStaticProps({ locale }: GetStaticPropsContext) { + return { + props: { + messages: (await import(`@klicker-uzh/i18n/messages/${locale}`)).default, + }, + } +} + +export function getStaticPaths() { + return { + paths: [], + fallback: 'blocking', + } +} + +export default ActivityDashboard diff --git a/apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx b/apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx new file mode 100644 index 0000000000..03a8c45021 --- /dev/null +++ b/apps/frontend-manage/src/pages/analytics/[courseId]/performance.tsx @@ -0,0 +1,45 @@ +import { H1, H3 } from '@uzh-bf/design-system' +import { GetStaticPropsContext } from 'next' +import { useTranslations } from 'next-intl' +import { useRouter } from 'next/router' +import ActivityDashboardLabel from '~/components/analytics/overview/ActivityDashboardLabel' +import AnalyticsNavigation from '~/components/analytics/overview/AnalyticsNavigation' +import QuizDashboardLabel from '~/components/analytics/overview/QuizDashboardLabel' +import Layout from '~/components/Layout' + +function PerformanceDashboard() { + const t = useTranslations() + const router = useRouter() + + return ( + + } + hrefRight={`/analytics/${router.query.courseId}/quizzes`} + labelRight={} + /> +
+

{t('manage.analytics.performanceDashboard')}

+

Coursename Placeholder

+
+
+ ) +} + +export async function getStaticProps({ locale }: GetStaticPropsContext) { + return { + props: { + messages: (await import(`@klicker-uzh/i18n/messages/${locale}`)).default, + }, + } +} + +export function getStaticPaths() { + return { + paths: [], + fallback: 'blocking', + } +} + +export default PerformanceDashboard diff --git a/apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx b/apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx new file mode 100644 index 0000000000..ccc4e5f1ba --- /dev/null +++ b/apps/frontend-manage/src/pages/analytics/[courseId]/quizzes.tsx @@ -0,0 +1,45 @@ +import { H1, H3 } from '@uzh-bf/design-system' +import { GetStaticPropsContext } from 'next' +import { useTranslations } from 'next-intl' +import { useRouter } from 'next/router' +import ActivityDashboardLabel from '~/components/analytics/overview/ActivityDashboardLabel' +import AnalyticsNavigation from '~/components/analytics/overview/AnalyticsNavigation' +import PerformanceDashboardLabel from '~/components/analytics/overview/PerformanceDashboardLabel' +import Layout from '~/components/Layout' + +function QuizDashboard() { + const t = useTranslations() + const router = useRouter() + + return ( + + } + hrefRight={`/analytics/${router.query.courseId}/activity`} + labelRight={} + /> +
+

{t('manage.analytics.quizDashboard')}

+

Coursename Placeholder

+
+
+ ) +} + +export async function getStaticProps({ locale }: GetStaticPropsContext) { + return { + props: { + messages: (await import(`@klicker-uzh/i18n/messages/${locale}`)).default, + }, + } +} + +export function getStaticPaths() { + return { + paths: [], + fallback: 'blocking', + } +} + +export default QuizDashboard diff --git a/apps/frontend-manage/src/pages/analytics/index.tsx b/apps/frontend-manage/src/pages/analytics/index.tsx index b3f0d8d294..6b3939824c 100644 --- a/apps/frontend-manage/src/pages/analytics/index.tsx +++ b/apps/frontend-manage/src/pages/analytics/index.tsx @@ -1,8 +1,34 @@ +import { useQuery } from '@apollo/client' +import { GetUserCoursesDocument } from '@klicker-uzh/graphql/dist/ops' +import Loader from '@klicker-uzh/shared-components/src/Loader' import { GetStaticPropsContext } from 'next' +import { useTranslations } from 'next-intl' +import { useRouter } from 'next/router' +import CourseDashboardList from '~/components/analytics/overview/CourseDashboardList' import Layout from '~/components/Layout' function Analytics() { - return Analytics + const t = useTranslations() + const router = useRouter() + const { loading: loadingCourses, data: dataCourses } = useQuery( + GetUserCoursesDocument + ) + + if (loadingCourses) { + return ( + + + + ) + } + + const courses = dataCourses?.userCourses + + return ( + + + + ) } export async function getStaticProps({ locale }: GetStaticPropsContext) { diff --git a/apps/frontend-manage/src/pages/courses/index.tsx b/apps/frontend-manage/src/pages/courses/index.tsx index fefa41031d..f1714dffa2 100644 --- a/apps/frontend-manage/src/pages/courses/index.tsx +++ b/apps/frontend-manage/src/pages/courses/index.tsx @@ -58,13 +58,9 @@ function CourseSelectionPage() { ) } - const courses = dataCourses?.userCourses - ?.filter((course) => { - return showArchive ? true : !course.isArchived - }) - .sort((a, b) => { - return dayjs(b.endDate).diff(dayjs(a.endDate)) - }) + const courses = dataCourses?.userCourses?.filter((course) => { + return showArchive ? true : !course.isArchived + }) return ( diff --git a/packages/graphql/src/graphql/ops/QGetCourseActivityAnalytics.graphql b/packages/graphql/src/graphql/ops/QGetCourseActivityAnalytics.graphql new file mode 100644 index 0000000000..50e3010305 --- /dev/null +++ b/packages/graphql/src/graphql/ops/QGetCourseActivityAnalytics.graphql @@ -0,0 +1,23 @@ +query GetCourseActivityAnalytics($courseId: String!) { + getCourseActivityAnalytics(courseId: $courseId) { + name + totalParticipants + dailyActivity { + date + activeParticipants + } + weeklyActivity { + date + activeParticipants + } + activeDays { + monday + tuesday + wednesday + thursday + friday + saturday + sunday + } + } +} diff --git a/packages/graphql/src/ops.schema.json b/packages/graphql/src/ops.schema.json index 4831180362..284d297eb8 100644 --- a/packages/graphql/src/ops.schema.json +++ b/packages/graphql/src/ops.schema.json @@ -3249,6 +3249,114 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "CourseActivityAnalytics", + "description": null, + "isOneOf": null, + "fields": [ + { + "name": "activeDays", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "WeekdayActivityAnalytics", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dailyActivity", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ParticipantActivityTimestamp", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalParticipants", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "weeklyActivity", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ParticipantActivityTimestamp", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "CourseSummary", @@ -17153,6 +17261,50 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "ParticipantActivityTimestamp", + "description": null, + "isOneOf": null, + "fields": [ + { + "name": "activeParticipants", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "date", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Date", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "ParticipantGroup", @@ -18438,6 +18590,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "getCourseActivityAnalytics", + "description": null, + "args": [ + { + "name": "courseId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CourseActivityAnalytics", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "getCourseGroups", "description": null, @@ -21661,6 +21842,130 @@ ], "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "WeekdayActivityAnalytics", + "description": null, + "isOneOf": null, + "fields": [ + { + "name": "friday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "monday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "saturday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sunday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "thursday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tuesday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "wednesday", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "__Directive", diff --git a/packages/graphql/src/ops.ts b/packages/graphql/src/ops.ts index cd3c060ee9..f6c7950877 100644 --- a/packages/graphql/src/ops.ts +++ b/packages/graphql/src/ops.ts @@ -316,6 +316,15 @@ export type Course = { updatedAt?: Maybe; }; +export type CourseActivityAnalytics = { + __typename?: 'CourseActivityAnalytics'; + activeDays: WeekdayActivityAnalytics; + dailyActivity: Array; + name: Scalars['String']['output']; + totalParticipants: Scalars['Int']['output']; + weeklyActivity: Array; +}; + export type CourseSummary = { __typename?: 'CourseSummary'; numOfGroupActivities: Scalars['Int']['output']; @@ -1887,6 +1896,12 @@ export type ParticipantAchievementInstance = { id: Scalars['Int']['output']; }; +export type ParticipantActivityTimestamp = { + __typename?: 'ParticipantActivityTimestamp'; + activeParticipants: Scalars['Int']['output']; + date: Scalars['Date']['output']; +}; + export type ParticipantGroup = { __typename?: 'ParticipantGroup'; averageMemberScore: Scalars['Int']['output']; @@ -1985,6 +2000,7 @@ export type Query = { getActiveUserCourses?: Maybe>; getBookmarkedElementStacks?: Maybe>; getBookmarksPracticeQuiz?: Maybe>; + getCourseActivityAnalytics?: Maybe; getCourseGroups?: Maybe; getCourseOverviewData?: Maybe; getCourseRunningLiveQuizzes?: Maybe>; @@ -2103,6 +2119,11 @@ export type QueryGetBookmarksPracticeQuizArgs = { }; +export type QueryGetCourseActivityAnalyticsArgs = { + courseId: Scalars['String']['input']; +}; + + export type QueryGetCourseGroupsArgs = { courseId: Scalars['String']['input']; }; @@ -2510,6 +2531,17 @@ export enum UserLoginScope { SessionExec = 'SESSION_EXEC' } +export type WeekdayActivityAnalytics = { + __typename?: 'WeekdayActivityAnalytics'; + friday: Scalars['Float']['output']; + monday: Scalars['Float']['output']; + saturday: Scalars['Float']['output']; + sunday: Scalars['Float']['output']; + thursday: Scalars['Float']['output']; + tuesday: Scalars['Float']['output']; + wednesday: Scalars['Float']['output']; +}; + export type ElementDataFragment = { __typename?: 'ElementInstance', elementData: { __typename: 'ChoicesElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number, options: { __typename?: 'ChoiceQuestionOptions', hasSampleSolution?: boolean | null, displayMode: ElementDisplayMode, choices: Array<{ __typename?: 'Choice', ix: number, correct?: boolean | null, feedback?: string | null, value: string }> } } | { __typename: 'ContentElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'FlashcardElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'FreeTextElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number, options: { __typename?: 'FreeTextQuestionOptions', hasSampleSolution?: boolean | null, solutions?: Array | null, restrictions?: { __typename?: 'FreeTextRestrictions', maxLength?: number | null } | null } } | { __typename: 'NumericalElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number, options: { __typename?: 'NumericalQuestionOptions', hasSampleSolution?: boolean | null, accuracy?: number | null, placeholder?: string | null, unit?: string | null, restrictions?: { __typename?: 'NumericalRestrictions', min?: number | null, max?: number | null } | null, solutionRanges?: Array<{ __typename?: 'NumericalSolutionRange', min?: number | null, max?: number | null }> | null } } }; export type ElementDataInfoFragment = { __typename?: 'ElementInstance', elementData: { __typename: 'ChoicesElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'ContentElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'FlashcardElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'FreeTextElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'NumericalElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } }; @@ -3498,6 +3530,13 @@ export type GetControlLiveQuizQueryVariables = Exact<{ export type GetControlLiveQuizQuery = { __typename?: 'Query', controlLiveQuiz?: { __typename?: 'LiveQuiz', id: string, name: string, displayName: string, course?: { __typename?: 'Course', id: string, displayName: string } | null, blocks?: Array<{ __typename?: 'ElementBlock', id: number, order: number, status: ElementBlockStatus, expiresAt?: any | null, timeLimit?: number | null, randomSelection?: number | null, execution?: number | null, elements?: Array<{ __typename?: 'ElementInstance', id: number, elementData: { __typename: 'ChoicesElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'ContentElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'FlashcardElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'FreeTextElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } | { __typename: 'NumericalElementData', id: string, elementId: number, name: string, type: ElementType, content: string, explanation?: string | null, pointsMultiplier: number } }> | null }> | null, activeBlock?: { __typename?: 'ElementBlock', id: number, order: number } | null } | null }; +export type GetCourseActivityAnalyticsQueryVariables = Exact<{ + courseId: Scalars['String']['input']; +}>; + + +export type GetCourseActivityAnalyticsQuery = { __typename?: 'Query', getCourseActivityAnalytics?: { __typename?: 'CourseActivityAnalytics', name: string, totalParticipants: number, dailyActivity: Array<{ __typename?: 'ParticipantActivityTimestamp', date: any, activeParticipants: number }>, weeklyActivity: Array<{ __typename?: 'ParticipantActivityTimestamp', date: any, activeParticipants: number }>, activeDays: { __typename?: 'WeekdayActivityAnalytics', monday: number, tuesday: number, wednesday: number, thursday: number, friday: number, saturday: number, sunday: number } } | null }; + export type GetCourseGroupActivitiesQueryVariables = Exact<{ courseId: Scalars['String']['input']; }>; @@ -4023,6 +4062,7 @@ export const GetCockpitQuizDocument = {"kind":"Document","definitions":[{"kind": export const GetControlCourseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetControlCourse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"controlCourse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"liveQuizzes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetControlCoursesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetControlCourses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"controlCourses"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode; export const GetControlLiveQuizDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetControlLiveQuiz"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"controlLiveQuiz"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"blocks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"timeLimit"}},{"kind":"Field","name":{"kind":"Name","value":"randomSelection"}},{"kind":"Field","name":{"kind":"Name","value":"execution"}},{"kind":"Field","name":{"kind":"Name","value":"elements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ElementDataInfo"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"activeBlock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ElementDataInfo"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ElementInstance"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"elementData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ChoicesElementData"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"elementId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"explanation"}},{"kind":"Field","name":{"kind":"Name","value":"pointsMultiplier"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NumericalElementData"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"elementId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"explanation"}},{"kind":"Field","name":{"kind":"Name","value":"pointsMultiplier"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FreeTextElementData"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"elementId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"explanation"}},{"kind":"Field","name":{"kind":"Name","value":"pointsMultiplier"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FlashcardElementData"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"elementId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"explanation"}},{"kind":"Field","name":{"kind":"Name","value":"pointsMultiplier"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ContentElementData"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"elementId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"explanation"}},{"kind":"Field","name":{"kind":"Name","value":"pointsMultiplier"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetCourseActivityAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCourseActivityAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getCourseActivityAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"courseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"totalParticipants"}},{"kind":"Field","name":{"kind":"Name","value":"dailyActivity"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"activeParticipants"}}]}},{"kind":"Field","name":{"kind":"Name","value":"weeklyActivity"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"activeParticipants"}}]}},{"kind":"Field","name":{"kind":"Name","value":"activeDays"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"monday"}},{"kind":"Field","name":{"kind":"Name","value":"tuesday"}},{"kind":"Field","name":{"kind":"Name","value":"wednesday"}},{"kind":"Field","name":{"kind":"Name","value":"thursday"}},{"kind":"Field","name":{"kind":"Name","value":"friday"}},{"kind":"Field","name":{"kind":"Name","value":"saturday"}},{"kind":"Field","name":{"kind":"Name","value":"sunday"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetCourseGroupActivitiesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCourseGroupActivities"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupActivities"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"courseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"scheduledStartAt"}},{"kind":"Field","name":{"kind":"Name","value":"scheduledEndAt"}}]}}]}}]} as unknown as DocumentNode; export const GetCourseGroupsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCourseGroups"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getCourseGroups"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"courseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"participantGroups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"averageMemberScore"}},{"kind":"Field","name":{"kind":"Name","value":"groupActivityScore"}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupAssignmentPoolEntries"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"participant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetCourseOverviewDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCourseOverviewData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getCourseOverviewData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"courseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inRandomGroupPool"}},{"kind":"Field","name":{"kind":"Name","value":"participant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"xp"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"participantGroups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"participation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}},{"kind":"Field","name":{"kind":"Name","value":"course"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isGamificationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"groupDeadlineDate"}},{"kind":"Field","name":{"kind":"Name","value":"isGroupDeadlinePassed"}},{"kind":"Field","name":{"kind":"Name","value":"isGroupCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"maxGroupSize"}},{"kind":"Field","name":{"kind":"Name","value":"preferredGroupSize"}},{"kind":"Field","name":{"kind":"Name","value":"awards"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"participant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"participantGroup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"leaderboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"participantId"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"isSelf"}},{"kind":"Field","name":{"kind":"Name","value":"rank"}},{"kind":"Field","name":{"kind":"Name","value":"level"}}]}},{"kind":"Field","name":{"kind":"Name","value":"leaderboardStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"participantCount"}},{"kind":"Field","name":{"kind":"Name","value":"averageScore"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupLeaderboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"rank"}},{"kind":"Field","name":{"kind":"Name","value":"isMember"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupLeaderboardStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"participantCount"}},{"kind":"Field","name":{"kind":"Name","value":"averageScore"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"participantGroups"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"courseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"courseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"averageMemberScore"}},{"kind":"Field","name":{"kind":"Name","value":"groupActivityScore"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"messages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"participant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"participants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"isSelf"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"rank"}}]}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/graphql/src/public/client.json b/packages/graphql/src/public/client.json index 1cbc79371e..7d84daac86 100644 --- a/packages/graphql/src/public/client.json +++ b/packages/graphql/src/public/client.json @@ -114,6 +114,7 @@ "GetControlCourse": "e75fbff1c5cb7ff5e8ba8dea58c1cf225065c420b007433ad34477dd8c75c42b", "GetControlCourses": "f62c7d2cb59eb42a077e6d527c90f4838a2888cb9a9f60259979114b6a3b759f", "GetControlLiveQuiz": "8f20969b7ca79ad364ba7d6f86bb726ac727fd7f6eaf2591ab6e2671d2c36c9d", + "GetCourseActivityAnalytics": "09008680cb5f6f1c378c6b67a0c7a2f7a700bb2355c9ac81adff1e7e72e74c06", "GetCourseGroupActivities": "01799c6afc7a0e5eccede415c803d033cfb972507499f6cd3bb4e493c2e9d83a", "GetCourseGroups": "b273f652a6ee76b9eee6d99f4f64b277480502f674f6145d7d0b706d53f25e6e", "GetCourseOverviewData": "fe16bc8ac7ebe8c4ea42314a6b209843a13a077bc3ebd9e3845e4d8f0472fc77", diff --git a/packages/graphql/src/public/schema.graphql b/packages/graphql/src/public/schema.graphql index 48b4843250..9f605fa85d 100644 --- a/packages/graphql/src/public/schema.graphql +++ b/packages/graphql/src/public/schema.graphql @@ -273,6 +273,14 @@ type Course { updatedAt: Date } +type CourseActivityAnalytics { + activeDays: WeekdayActivityAnalytics! + dailyActivity: [ParticipantActivityTimestamp!]! + name: String! + totalParticipants: Int! + weeklyActivity: [ParticipantActivityTimestamp!]! +} + type CourseSummary { numOfGroupActivities: Int! numOfLeaderboardEntries: Int! @@ -1133,6 +1141,11 @@ type ParticipantAchievementInstance { id: Int! } +type ParticipantActivityTimestamp { + activeParticipants: Int! + date: Date! +} + type ParticipantGroup { averageMemberScore: Int! code: Int! @@ -1223,6 +1236,7 @@ type Query { getActiveUserCourses: [Course!] getBookmarkedElementStacks(courseId: String!): [ElementStack!] getBookmarksPracticeQuiz(courseId: String!, quizId: String): [Int!] + getCourseActivityAnalytics(courseId: String!): CourseActivityAnalytics getCourseGroups(courseId: String!): Course getCourseOverviewData(courseId: String!): ParticipantLearningData getCourseRunningLiveQuizzes(courseId: String!): [LiveQuiz!] @@ -1449,4 +1463,14 @@ enum UserLoginScope { OTP READ_ONLY SESSION_EXEC +} + +type WeekdayActivityAnalytics { + friday: Float! + monday: Float! + saturday: Float! + sunday: Float! + thursday: Float! + tuesday: Float! + wednesday: Float! } \ No newline at end of file diff --git a/packages/graphql/src/public/server.json b/packages/graphql/src/public/server.json index f0e4478661..dba6e8fa8d 100644 --- a/packages/graphql/src/public/server.json +++ b/packages/graphql/src/public/server.json @@ -114,6 +114,7 @@ "e75fbff1c5cb7ff5e8ba8dea58c1cf225065c420b007433ad34477dd8c75c42b": "query GetControlCourse($courseId: String!) {\n controlCourse(id: $courseId) {\n id\n name\n liveQuizzes {\n id\n name\n status\n __typename\n }\n __typename\n }\n}", "f62c7d2cb59eb42a077e6d527c90f4838a2888cb9a9f60259979114b6a3b759f": "query GetControlCourses {\n controlCourses {\n id\n name\n isArchived\n displayName\n description\n __typename\n }\n}", "8f20969b7ca79ad364ba7d6f86bb726ac727fd7f6eaf2591ab6e2671d2c36c9d": "fragment ElementDataInfo on ElementInstance {\n elementData {\n ... on ChoicesElementData {\n __typename\n id\n elementId\n name\n type\n content\n explanation\n pointsMultiplier\n }\n ... on NumericalElementData {\n __typename\n id\n elementId\n name\n type\n content\n explanation\n pointsMultiplier\n }\n ... on FreeTextElementData {\n __typename\n id\n elementId\n name\n type\n content\n explanation\n pointsMultiplier\n }\n ... on FlashcardElementData {\n __typename\n id\n elementId\n name\n type\n content\n explanation\n pointsMultiplier\n }\n ... on ContentElementData {\n __typename\n id\n elementId\n name\n type\n content\n explanation\n pointsMultiplier\n }\n __typename\n }\n __typename\n}\nquery GetControlLiveQuiz($id: String!) {\n controlLiveQuiz(id: $id) {\n id\n name\n displayName\n course {\n id\n displayName\n __typename\n }\n blocks {\n id\n order\n status\n expiresAt\n timeLimit\n randomSelection\n execution\n elements {\n id\n ...ElementDataInfo\n __typename\n }\n __typename\n }\n activeBlock {\n id\n order\n __typename\n }\n __typename\n }\n}", + "09008680cb5f6f1c378c6b67a0c7a2f7a700bb2355c9ac81adff1e7e72e74c06": "query GetCourseActivityAnalytics($courseId: String!) {\n getCourseActivityAnalytics(courseId: $courseId) {\n name\n totalParticipants\n dailyActivity {\n date\n activeParticipants\n __typename\n }\n weeklyActivity {\n date\n activeParticipants\n __typename\n }\n activeDays {\n monday\n tuesday\n wednesday\n thursday\n friday\n saturday\n sunday\n __typename\n }\n __typename\n }\n}", "01799c6afc7a0e5eccede415c803d033cfb972507499f6cd3bb4e493c2e9d83a": "query GetCourseGroupActivities($courseId: String!) {\n groupActivities(courseId: $courseId) {\n id\n displayName\n status\n description\n scheduledStartAt\n scheduledEndAt\n __typename\n }\n}", "b273f652a6ee76b9eee6d99f4f64b277480502f674f6145d7d0b706d53f25e6e": "query GetCourseGroups($courseId: String!) {\n getCourseGroups(courseId: $courseId) {\n participantGroups {\n id\n name\n code\n averageMemberScore\n groupActivityScore\n participants {\n id\n username\n score\n email\n avatar\n __typename\n }\n __typename\n }\n groupAssignmentPoolEntries {\n id\n participant {\n id\n username\n email\n avatar\n __typename\n }\n __typename\n }\n __typename\n }\n}", "fe16bc8ac7ebe8c4ea42314a6b209843a13a077bc3ebd9e3845e4d8f0472fc77": "query GetCourseOverviewData($courseId: String!) {\n getCourseOverviewData(courseId: $courseId) {\n id\n inRandomGroupPool\n participant {\n id\n avatar\n username\n xp\n level\n participantGroups {\n id\n __typename\n }\n __typename\n }\n participation {\n id\n isActive\n __typename\n }\n course {\n id\n displayName\n color\n description\n isGamificationEnabled\n groupDeadlineDate\n isGroupDeadlinePassed\n isGroupCreationEnabled\n maxGroupSize\n preferredGroupSize\n awards {\n id\n order\n type\n displayName\n description\n participant {\n id\n username\n avatar\n __typename\n }\n participantGroup {\n id\n name\n __typename\n }\n __typename\n }\n __typename\n }\n leaderboard {\n id\n participantId\n username\n avatar\n score\n isSelf\n rank\n level\n __typename\n }\n leaderboardStatistics {\n participantCount\n averageScore\n __typename\n }\n groupLeaderboard {\n id\n name\n score\n rank\n isMember\n __typename\n }\n groupLeaderboardStatistics {\n participantCount\n averageScore\n __typename\n }\n __typename\n }\n participantGroups(courseId: $courseId) {\n id\n name\n code\n averageMemberScore\n groupActivityScore\n score\n messages {\n id\n content\n participant {\n id\n username\n avatar\n __typename\n }\n createdAt\n updatedAt\n __typename\n }\n participants {\n id\n username\n avatar\n score\n isSelf\n level\n rank\n __typename\n }\n __typename\n }\n}", diff --git a/packages/graphql/src/schema/analytics.ts b/packages/graphql/src/schema/analytics.ts index 3d3e41cb30..ccd0e2c910 100644 --- a/packages/graphql/src/schema/analytics.ts +++ b/packages/graphql/src/schema/analytics.ts @@ -12,3 +12,73 @@ export const ElementFeedback = builder.objectType(ElementFeedbackRef, { elementInstanceId: t.exposeInt('elementInstanceId'), }), }) + +interface IParticipantActivityTimestamp { + date: Date + activeParticipants: number +} +export const ParticipantActivityTimestampRef = + builder.objectRef( + 'ParticipantActivityTimestamp' + ) +export const ParticipantActivityTimestamp = builder.objectType( + ParticipantActivityTimestampRef, + { + fields: (t) => ({ + date: t.expose('date', { type: 'Date' }), + activeParticipants: t.exposeInt('activeParticipants'), + }), + } +) + +interface IWeekdayActivityAnalytics { + monday: number + tuesday: number + wednesday: number + thursday: number + friday: number + saturday: number + sunday: number +} +export const WeekdayActivityAnalyticsRef = + builder.objectRef('WeekdayActivityAnalytics') +export const WeekdayActivityAnalytics = builder.objectType( + WeekdayActivityAnalyticsRef, + { + fields: (t) => ({ + monday: t.exposeFloat('monday'), + tuesday: t.exposeFloat('tuesday'), + wednesday: t.exposeFloat('wednesday'), + thursday: t.exposeFloat('thursday'), + friday: t.exposeFloat('friday'), + saturday: t.exposeFloat('saturday'), + sunday: t.exposeFloat('sunday'), + }), + } +) + +interface ICourseActivityAnalytics { + name: string + totalParticipants: number + dailyActivity: IParticipantActivityTimestamp[] + weeklyActivity: IParticipantActivityTimestamp[] + activeDays: IWeekdayActivityAnalytics +} +export const CourseActivityAnalyticsRef = + builder.objectRef('CourseActivityAnalytics') +export const CourseActivityAnalytics = builder.objectType( + CourseActivityAnalyticsRef, + { + fields: (t) => ({ + name: t.exposeString('name'), + totalParticipants: t.exposeInt('totalParticipants'), + dailyActivity: t.expose('dailyActivity', { + type: [ParticipantActivityTimestamp], + }), + weeklyActivity: t.expose('weeklyActivity', { + type: [ParticipantActivityTimestamp], + }), + activeDays: t.expose('activeDays', { type: WeekdayActivityAnalytics }), + }), + } +) diff --git a/packages/graphql/src/schema/query.ts b/packages/graphql/src/schema/query.ts index c120b34d07..29ea2eff73 100644 --- a/packages/graphql/src/schema/query.ts +++ b/packages/graphql/src/schema/query.ts @@ -1,6 +1,7 @@ import * as DB from '@klicker-uzh/prisma' import builder from '../builder.js' import * as AccountService from '../services/accounts.js' +import * as AnalyticsService from '../services/analytics.js' import * as CourseService from '../services/courses.js' import * as FeedbackService from '../services/feedbacks.js' import * as GroupService from '../services/groups.js' @@ -10,7 +11,7 @@ import * as ParticipantService from '../services/participants.js' import * as PracticeQuizService from '../services/practiceQuizzes.js' import * as QuestionService from '../services/questions.js' import * as StacksService from '../services/stacks.js' -import { ElementFeedback } from './analytics.js' +import { CourseActivityAnalytics, ElementFeedback } from './analytics.js' import { Course, CourseSummary, @@ -741,6 +742,17 @@ export const Query = builder.queryType({ return PracticeQuizService.getBookmarksPracticeQuiz(args, ctx) }, }), + + getCourseActivityAnalytics: asUser.field({ + nullable: true, + type: CourseActivityAnalytics, + args: { + courseId: t.arg.string({ required: true }), + }, + resolve(_, args, ctx) { + return AnalyticsService.getCourseActivityAnalytics(args, ctx) + }, + }), } }, }) diff --git a/packages/graphql/src/services/analytics.ts b/packages/graphql/src/services/analytics.ts new file mode 100644 index 0000000000..7bbebb18a7 --- /dev/null +++ b/packages/graphql/src/services/analytics.ts @@ -0,0 +1,50 @@ +import { ContextWithUser } from 'src/lib/context.js' + +export async function getCourseActivityAnalytics( + { courseId }: { courseId: string }, + ctx: ContextWithUser +) { + const course = await ctx.prisma.course.findUnique({ + where: { id: courseId, ownerId: ctx.user.sub }, + include: { + participations: true, + aggregatedAnalytics: { + orderBy: { timestamp: 'asc' }, + }, + aggregatedCourseAnalytics: true, + }, + }) + + if (!course) { + return null + } + + const dailyActivity = course.aggregatedAnalytics + .filter((analytics) => analytics.type === 'DAILY') + .map((analytics) => ({ + date: analytics.timestamp, + activeParticipants: analytics.participantCount, + })) + const weeklyActivity = course.aggregatedAnalytics + .filter((analytics) => analytics.type === 'WEEKLY') + .map((analytics) => ({ + date: analytics.timestamp, + activeParticipants: analytics.participantCount, + })) + + return { + name: course.name, + totalParticipants: course.participations.length, + dailyActivity, + weeklyActivity, + activeDays: { + monday: course.aggregatedCourseAnalytics?.activityMonday ?? 0, + tuesday: course.aggregatedCourseAnalytics?.activityTuesday ?? 0, + wednesday: course.aggregatedCourseAnalytics?.activityWednesday ?? 0, + thursday: course.aggregatedCourseAnalytics?.activityThursday ?? 0, + friday: course.aggregatedCourseAnalytics?.activityFriday ?? 0, + saturday: course.aggregatedCourseAnalytics?.activitySaturday ?? 0, + sunday: course.aggregatedCourseAnalytics?.activitySunday ?? 0, + }, + } +} diff --git a/packages/graphql/src/services/courses.ts b/packages/graphql/src/services/courses.ts index 019bb7c3e3..ff2ba6d52e 100644 --- a/packages/graphql/src/services/courses.ts +++ b/packages/graphql/src/services/courses.ts @@ -554,24 +554,19 @@ export async function getUserCourses(ctx: ContextWithUser) { include: { courses: { orderBy: { - createdAt: 'desc', + endDate: 'desc', }, }, }, }) // sort courses by archived or not - const archivedSortedCourses = + const filteredCourses = userCourses?.courses.sort((a, b) => { return a.isArchived === b.isArchived ? 0 : a.isArchived ? 1 : -1 }) ?? [] - // sort courses by start date descending - const startDateSortedCourses = archivedSortedCourses.sort((a, b) => { - return a.startDate > b.startDate ? -1 : a.startDate < b.startDate ? 1 : 0 - }) - - return startDateSortedCourses + return filteredCourses } export async function getActiveUserCourses(ctx: ContextWithUser) { diff --git a/packages/i18n/messages/de.ts b/packages/i18n/messages/de.ts index 3475f5189b..ec70c5f870 100644 --- a/packages/i18n/messages/de.ts +++ b/packages/i18n/messages/de.ts @@ -63,6 +63,8 @@ export default { installButton: 'Jetzt installieren', }, generic: { + date: 'Datum', + percentage: 'Prozent', groupMessages: 'Gruppennachrichten', preferred: 'bevorzugt', groupSize: 'Gruppengrösse', @@ -214,6 +216,14 @@ export default { forgotPassword: 'Passwort vergessen?', archived: 'Archiviert', ended: 'Beendet', + learningAnalytics: 'Learning Analytics', + monday: 'Montag', + tuesday: 'Dienstag', + wednesday: 'Mittwoch', + thursday: 'Donnerstag', + friday: 'Freitag', + saturday: 'Samstag', + sunday: 'Sonntag', }, contentInput: { boldStyle: @@ -1812,6 +1822,21 @@ Da die KlickerUZH-App noch nicht im iOS-App-Store verfügbar ist, folgen Sie die gradingAlreadyFinalized: 'Die Bewertung wurde bereits abgeschlossen und kann nicht mehr geändert werden. Wählen Sie eine Abgabe aus, um sich die eingegebene Bewertung anzusehen.', }, + analytics: { + selectAnalyticsDashboard: 'Bitte wählen Sie ein Analyse-Dashboard aus', + activityDashboard: 'Aktivitäts-Dashboard', + performanceDashboard: 'Leistungs-Dashboard', + quizDashboard: 'Quiz-Dashboard', + analyticsLoadingWait: 'Lade Analyse-Daten. Bitte warten...', + analyticsLoadingFailed: + 'Beim Laden der Analyse-Daten ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut oder kontaktieren Sie den Support.', + weeklyStudentActivity: 'Wöchentliche Aktivität der Studierenden', + dailyStudentActivity: 'Tägliche Aktivität der Studierenden', + totalParticipants: 'Kurs-Teilnehmende: {number}', + dailyActivity: 'Tagesabhängige Aktivität', + activeStudents: 'Aktive Studierende', + percentageOfStudents: 'Prozentuale Verteilung der Studierenden', + }, }, control: { login: { diff --git a/packages/i18n/messages/en.ts b/packages/i18n/messages/en.ts index e961475341..52668ca888 100644 --- a/packages/i18n/messages/en.ts +++ b/packages/i18n/messages/en.ts @@ -63,6 +63,8 @@ export default { installButton: 'Install Now', }, generic: { + date: 'Date', + percentage: 'Percentage', groupMessages: 'Group Messages', preferred: 'preferred', groupSize: 'Group Size', @@ -214,6 +216,14 @@ export default { forgotPassword: 'Forgot password?', archived: 'Archived', ended: 'Ended', + learningAnalytics: 'Learning Analytics', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + sunday: 'Sunday', }, contentInput: { boldStyle: @@ -1800,6 +1810,21 @@ Since the KlickerUZH app is not yet available on the iOS App Store, follow these gradingAlreadyFinalized: 'Grading has already been finalized and cannot be changed anymore. Select a submission to view the entered grading.', }, + analytics: { + selectAnalyticsDashboard: 'Please select an analytics dashboard', + activityDashboard: 'Activity Dashboard', + performanceDashboard: 'Performance Dashboard', + quizDashboard: 'Quiz Dashboard', + analyticsLoadingWait: 'Loading analytics data. Please wait...', + analyticsLoadingFailed: + 'An error occurred while loading the analytics data. Please try again later or contact the support.', + weeklyStudentActivity: 'Weekly Student Activity', + dailyStudentActivity: 'Daily Student Activity', + totalParticipants: 'Course participants: {number}', + dailyActivity: 'Daily Activity', + activeStudents: 'Active students', + percentageOfStudents: 'Percentage of students', + }, }, control: { login: { diff --git a/packages/prisma/src/prisma/migrations/20241205172606_learning_analytics_performance_progress/migration.sql b/packages/prisma/src/prisma/migrations/20241206141120_learning_analytics_performance_progress/migration.sql similarity index 100% rename from packages/prisma/src/prisma/migrations/20241205172606_learning_analytics_performance_progress/migration.sql rename to packages/prisma/src/prisma/migrations/20241206141120_learning_analytics_performance_progress/migration.sql diff --git a/packages/prisma/src/prisma/schema/course.prisma b/packages/prisma/src/prisma/schema/course.prisma index 4fc2228b90..ffe85b0eba 100644 --- a/packages/prisma/src/prisma/schema/course.prisma +++ b/packages/prisma/src/prisma/schema/course.prisma @@ -37,7 +37,7 @@ model Course { liveQuizzes LiveQuiz[] participantAnalytics ParticipantAnalytics[] aggregatedAnalytics AggregatedAnalytics[] - aggregatedCourseAnalytics AggregatedCourseAnalytics[] + aggregatedCourseAnalytics AggregatedCourseAnalytics? participantCourseAnalytics ParticipantCourseAnalytics[] participantPerformances ParticipantPerformance[] instancePerformances InstancePerformance[] diff --git a/packages/shared-components/src/Loader.tsx b/packages/shared-components/src/Loader.tsx index ea51127729..2db5b96e0d 100644 --- a/packages/shared-components/src/Loader.tsx +++ b/packages/shared-components/src/Loader.tsx @@ -1,10 +1,11 @@ import { faSpinner } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import React from 'react' +import { twMerge } from 'tailwind-merge' -function Loader() { +function Loader({ basic }: { basic?: boolean }) { return ( -
+
) From a799972b9e8eeed54620ca78bc4938297b987ddf Mon Sep 17 00:00:00 2001 From: Julius Schlapbach <80708107+sjschlapbach@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:47:17 +0100 Subject: [PATCH 17/38] enhance(apps/analytics): add course data comparison for weekly student activity chart (#4394) --- .../activity/ActivityTimeSeriesPlot.tsx | 115 ++++++++---------- .../activity/DailyActivityTimeSeries.tsx | 40 ++++++ .../activity/SuspendedCourseComparison.tsx | 70 +++++++++++ .../activity/WeeklyActivityTimeSeries.tsx | 84 +++++++++++++ .../pages/analytics/[courseId]/activity.tsx | 10 +- .../ops/QGetCourseWeeklyActivity.graphql | 9 ++ packages/graphql/src/ops.schema.json | 77 ++++++++++++ packages/graphql/src/ops.ts | 20 +++ packages/graphql/src/public/client.json | 1 + packages/graphql/src/public/schema.graphql | 6 + packages/graphql/src/public/server.json | 1 + packages/graphql/src/schema/analytics.ts | 18 +++ packages/graphql/src/schema/query.ts | 17 ++- packages/graphql/src/services/analytics.ts | 31 +++++ packages/i18n/messages/de.ts | 5 + packages/i18n/messages/en.ts | 5 + 16 files changed, 441 insertions(+), 68 deletions(-) create mode 100644 apps/frontend-manage/src/components/analytics/activity/DailyActivityTimeSeries.tsx create mode 100644 apps/frontend-manage/src/components/analytics/activity/SuspendedCourseComparison.tsx create mode 100644 apps/frontend-manage/src/components/analytics/activity/WeeklyActivityTimeSeries.tsx create mode 100644 packages/graphql/src/graphql/ops/QGetCourseWeeklyActivity.graphql diff --git a/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx b/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx index c8d8b187a6..9c1b36bb08 100644 --- a/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx +++ b/apps/frontend-manage/src/components/analytics/activity/ActivityTimeSeriesPlot.tsx @@ -1,9 +1,8 @@ -import { ParticipantActivityTimestamp } from '@klicker-uzh/graphql/dist/ops' -import { H2 } from '@uzh-bf/design-system' import { useTranslations } from 'next-intl' import { CartesianGrid, Label, + Legend, Line, LineChart, ResponsiveContainer, @@ -13,72 +12,64 @@ import { } from 'recharts' function ActivityTimeSeriesPlot({ - title, - activity, - courseParticipants, + singleCourse = true, + currentCourse, + comparisonCourse, + activityData, }: { - title: string - activity: ParticipantActivityTimestamp[] - courseParticipants: number + singleCourse?: boolean + currentCourse?: string + comparisonCourse?: string + activityData: { date: string; activeParticipants: number }[] }) { const t = useTranslations() return ( -
-

{title}

- - ({ - ...item, - activeParticipants: - (item.activeParticipants / courseParticipants) * 100, - }))} - > - - { - const date = new Date(value) - return date - .toLocaleDateString('en-GB', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - }) - .replace(/\//g, '-') - }} + + + + + + - -
+ + { + return singleCourse + ? `${t('shared.generic.date')}: ${value}` + : value + }} + formatter={(value) => [ + `${(value as number).toFixed(2)} %`, + t('manage.analytics.activeStudents'), + ]} + contentStyle={{ + borderRadius: '8px', + padding: '8px', + }} + /> + + + {!singleCourse && } + + ) } diff --git a/apps/frontend-manage/src/components/analytics/activity/DailyActivityTimeSeries.tsx b/apps/frontend-manage/src/components/analytics/activity/DailyActivityTimeSeries.tsx new file mode 100644 index 0000000000..074dd8772c --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/activity/DailyActivityTimeSeries.tsx @@ -0,0 +1,40 @@ +import { ParticipantActivityTimestamp } from '@klicker-uzh/graphql/dist/ops' +import { H2 } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import ActivityTimeSeriesPlot from './ActivityTimeSeriesPlot' + +function DailyActivityTimeSeries({ + activity, + courseParticipants, +}: { + activity: ParticipantActivityTimestamp[] + courseParticipants: number +}) { + const t = useTranslations() + + return ( +
+

{t('manage.analytics.dailyStudentActivity')}

+
+ { + const date = new Date(item.date) + return { + date: date + .toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }) + .replace(/\//g, '-'), + activeParticipants: + (item.activeParticipants / courseParticipants) * 100, + } + })} + /> +
+
+ ) +} + +export default DailyActivityTimeSeries diff --git a/apps/frontend-manage/src/components/analytics/activity/SuspendedCourseComparison.tsx b/apps/frontend-manage/src/components/analytics/activity/SuspendedCourseComparison.tsx new file mode 100644 index 0000000000..0632b09f99 --- /dev/null +++ b/apps/frontend-manage/src/components/analytics/activity/SuspendedCourseComparison.tsx @@ -0,0 +1,70 @@ +import { useSuspenseQuery } from '@apollo/client' +import { GetUserCoursesDocument } from '@klicker-uzh/graphql/dist/ops' +import { Checkbox, H3, Select } from '@uzh-bf/design-system' +import { useTranslations } from 'next-intl' +import { useRouter } from 'next/router' +import { useState } from 'react' + +function SuspendedCourseComparison({ + courseComparison, + setCourseComparison, +}: { + courseComparison: { id: string; name: string } | undefined + setCourseComparison: ( + course: { id: string; name: string } | undefined + ) => void +}) { + const t = useTranslations() + const router = useRouter() + const [showCourseDropdown, setShowCourseDropdown] = useState(false) + + const { data } = useSuspenseQuery(GetUserCoursesDocument) + const courses = + data.userCourses + ?.filter((course) => course.id !== router.query.courseId) + .map((course) => ({ + label: course.name, + value: course.id, + })) ?? [] + + return ( +
+
+ + setShowCourseDropdown((prev) => { + if (prev) { + setCourseComparison(undefined) + } + return !prev + }) + } + className={{ root: 'border-black' }} + /> +

+ {t('manage.analytics.courseComparison')} +

+
+ {showCourseDropdown ? ( +
+
{t('manage.analytics.courseComparisonDescription')}
+ - setCourseComparison({ - id: newValue, - name: - courses.find((course) => course.value === newValue)?.label ?? - '', - }) - } - placeholder={t('manage.analytics.selectCourse')} - /> +
+