From 3cdb782ef9a7c948067a0eba37b3ff12428ed61c Mon Sep 17 00:00:00 2001 From: Sal Tijerina Date: Wed, 14 Aug 2024 17:50:47 -0500 Subject: [PATCH] feat: Tapis v3 Redesign (#1184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * task/DES-2702: Tapis v3 Auth (#1174) * add tapipy; Tapis v3 OAuth token and flow * add docstrings to test methods * update service account to use Tapis * remove future imports * uncomment agave imports * add TapisOAuthToken model migration * fix bugs * remove attrdict for agavepy * pylint: ignore test files * fix ignore unit test regex * fix login url * formatting; remove AgaveOAuthToken references * add test settings for tapisv3 * fix auth/views_unit_test.py * fix backends unit test * formatting * task/DES-2654: Tapis v2/v3 apps model (#1158) * v3 apps model * make appId required field * add AppBundle model * updated model admin * update models and add migration * rework app tray models * squash appitem and appbundle * un-squash migrations * remove faulty app description migration * Use an inline admin form to manage app variants within bundles (#1164) Co-authored-by: Sal Tijerina * model adjustments * model adjustments; add category pop migration --------- Co-authored-by: Jake Rosenberg * fix fixture import * skip tapisv2 tests; add todov3 notes; fix fixture * remove unnecessary commented code --------- Co-authored-by: Jake Rosenberg * Add Google Oauth secrets. * task/DES-2709: Tapis v3 Apps Views (#1177) * add apps views * add AppDescriptionView * TapisTokenRefreshMiddleware: redirect to login if expired * formatting * fix bugs; add AuthenticatedApiView class * update apps call for v3 * separate linting from unit tests in workflow * formatting * formatting * add uuidv4 * tests * fix uuid import * tests * fix formatting * set allowJS to true in tsconfig * add uuid types * rename docker tags and volumes to not conflict with main branch * fix volumes * add tags, description, is_simcenter; some reorg * remove broken source mapping * squash migrations * split websockets; clean up nginx.debug.conf * change staticfiles dirs to align with deployed config; nginx: /var, django: /srv * formatting * task/DES-2710: v3 Jobs Views & Workspace UI updates (#1182) * add apps views * add AppDescriptionView * TapisTokenRefreshMiddleware: redirect to login if expired * formatting * fix bugs; add AuthenticatedApiView class * update apps call for v3 * separate linting from unit tests in workflow * formatting * formatting * add uuidv4 * tests * fix uuid import * tests * fix formatting * set allowJS to true in tsconfig * add uuid types * rename docker tags and volumes to not conflict with main branch * fix volumes * add tags, description, is_simcenter; some reorg * remove broken source mapping * squash migrations * split websockets; clean up nginx.debug.conf * change staticfiles dirs to align with deployed config; nginx: /var, django: /srv * formatting * add jobs views; update workspace UI * fix client linting and tests * fix some server side tests * add getUseJobs hook * add job detail spinner * add usePostJobs * fix webhooks tests * server side linting and tests * formatting * fix unit test * add CSRF_TRUSTED_ORIGINS setting * add m1 makefile targetsg * Use Django to serve static files in debug mode (#1186) * Use Django to serve static files in debug mode * clean up volumes; remove local nginx static and media locations * add back /data/media mount * add media back to nginx; same locations as main --------- Co-authored-by: Sal Tijerina * task/DES-2708: Build and serve React assets on deploy (#1173) * Build and serve React assets * fix template string * Add custom render block for react assets * re-add agavepy to requirements for legacy migration purposes (#1187) * update dockerfile to copy static assets to correct location * add V3 file operation module and file listing util (#1189) * Dockerfile build targets and cleanup (#1190) * clean up unused files; add development docker build target; fix workspace react-assets ref * fix data depot react-assets ref * docker-compose fix; combine python-base and builder-base steps (#1191) * docker-compose fix; combine python-base and builder-base steps * revert rabbitmq images * parity between intel/m1 rabbit versions * Revert "parity between intel/m1 rabbit versions" This reverts commit 9f5b709d01c8f8999135085875424a1789bef5ce. * add newline * revert .gitignore changes --------- Co-authored-by: Jake Rosenberg * gitignore compiled react assets * TV3/DES-2706: Add view decorator using Tapis JWT auth (#1192) * add view decorator using Tapis JWT auth * Update designsafe/apps/api/decorators.py Co-authored-by: Nathan Franklin * Add method decorator on project instance view --------- Co-authored-by: Nathan Franklin * use wsgi for django server locally with working staticfiles (#1200) * Quick: fix tapis client import in filemeta * hotfix/update build info in README.md (#1227) * Remove obsolete version param * Update README for building development images * Allow JWT-authed requests for filemeta routes (#1229) * Milestone: Project creation/publication works end-to-end (#1236) * Get project creation, updates, and type changes working end-to-end * projects creation/curation/publication works end-to-end * import error fix * remove print/console.log statements * DES-2684: NEES Detail View (#1219) * formatting/type errors * tweak for pi array check, updated placeholder text * initial work minus file listing for nees publication details * reworking top level data styling * updated citation modal title * first pass at adding file listing * fix: app card design bugs DES-2741 through DES-2745 (#1223) * fix: DES-2471, DES-2472, DES-2473 bugs (#1221) * fix: DES-2744 hover and focus card UI (#1224) * hotfix: des 2745 global font icon size tweaks (#1225) * refactor: des-2745 rename DS font icons stylesheet * fix: des-2745 load DS font icons + allow customize * chore: des-2745 obvious app-card font icon space * fix: des-2745 use rem not em for icon–title space * fix: des-2745 DS font icons not found * feat: des-2745 re-align certain DS font icons * feat: des-2745 re-position certain DS font icons * chore: des-2745 use (proper) `::before` syntax * fix: des-2745 title alignment after icon resize * fix: DES-2766 app with 1 variant needs diff list (#1232) * fix: DES-2766 app with 1 variant needs diff list * fix: DES-2766 more padding (designer request) * formatting * add nees listing breadcrumbs * add breadcrumbs * fix breadcrumbs --------- Co-authored-by: Jake Rosenberg Co-authored-by: Wesley B <62723358+wesleyboar@users.noreply.github.com> * task/DES-2767: fix file metadata route path issue (#1235) * Normalize path to be consistent * Remove some unneeded fixgtures --------- Co-authored-by: Jake Rosenberg * use async task for publication * task/DES-2705: Update post-login task to use TAPIS V3 to configure the two systems (#1183) * Add encryption and system access util from CEP * Add pycryptodome package * Update login to use tapisv3 to configure the two systems * Fix linting * Fix linting/black issues * Fix lockfile * Refactor to have parameter path and also use AGAVE_WORKING_SYSTEM via keyservice * Update test settings for +AGAVE_WORKING_SYSTEM * Add retry --------- Co-authored-by: Jake Rosenberg Co-authored-by: Sal Tijerina * Working search bars in data files/published areas (#1240) * working search bars in data files/published areas * update test settings * Working file operations/Data Depot buttons. (#1242) * working search bars in data files/published areas * Working V3 move/copy modals * Get toolbar buttons working and enabled/disabled correctly depending on context. * Migrate listings/breadcrumbs to Common for use in apps (#1243) * migrate file listing table and breadcrumbs to common component module for use in Apps * pass pipeline without tests * Working pipeline for amend/revise (#1248) * fix search filters for hyb sim/field recon * formatting * Refinements to Data Depot UI and pub ingest (#1256) * Add placeholder/error UI for listings; add Data Diagram for pubs * improve line wrapping in Data Diagram for entities with long names * refinements to publication ingest and display * task/WG-258: allow jwt access for project metadata updating (#1255) * Move project fixture higher up to use in view testing * Allow jwt access for updating project metadata --------- Co-authored-by: Jake Rosenberg * task/DES-2629: v3 Apps Form (#1185) * add apps views * add AppDescriptionView * TapisTokenRefreshMiddleware: redirect to login if expired * formatting * fix bugs; add AuthenticatedApiView class * update apps call for v3 * separate linting from unit tests in workflow * formatting * formatting * add uuidv4 * tests * fix uuid import * tests * fix formatting * set allowJS to true in tsconfig * add uuid types * rename docker tags and volumes to not conflict with main branch * fix volumes * add tags, description, is_simcenter; some reorg * remove broken source mapping * squash migrations * split websockets; clean up nginx.debug.conf * change staticfiles dirs to align with deployed config; nginx: /var, django: /srv * formatting * add jobs views; update workspace UI * fix client linting and tests * fix some server side tests * add getUseJobs hook * add job detail spinner * add usePostJobs * fix webhooks tests * server side linting and tests * formatting * fix unit test * add useGetApps hook; app routing * move tapis types to types file * add spinner; cleanup * add CSRF_TRUSTED_ORIGINS setting * add m1 makefile targetsg * formatting * wip: appswizard and layout * wip: apps routing; prefetch app listing data * remove await/defer * fix m1 django command * formatting; linting; 1s stale time * use wsgi for django server locally with working staticfiles * wip: cleanup * remove /applications sub path for tools & apps url * fix backend response for private apps * install zod; tanstack form * add workspace utils; apps submission form * cleanup unused styles * wip * install zod; tanstack form * add workspace utils; apps submission form * cleanup unused styles * remove version from docker compose files * data loads * working back * state persists * clean up * remove unused dependencies * working reset * working submission * update antd; add SystemsPushKeysModal * add ssh keys manager; system push keys endpoint * push keys backend * reset push keys form on success * reorg apps view components * set defaultSystem in app response * better param handling; fix apps view settins * add systems hooks and views * use prefetching to speed up load time * fix submit status * add app icon * add job submit messages * Task/DES-2656 apps sidebar (#1202) * -Adds accordion ant sidebar -Need to update to allow more than one category to be open at once -Needs styles * Change from Panels to Menu Ant components * -Adds ant menu and submenus with children rather than with item prop passed to menu * Adds unit test to app AppSideNav * - Fixed unit test * WIP ant d Menu * WIP * side nav submenus * - Adds styles per mockup - Adds WIP collapse icon and state to manage it - Adds working subnav of application bundles * - Removes unnecessary toggle because antd menu handles appropriately - Removes broken expand icon logic * fix unbundled item render; formatting --------- Co-authored-by: Sal Tijerina * Task/des 2631 apps breadcrumb (#1188) * Add AppsBreadcrumb component and integrate it into WorkspaceBaseLayout * Refactor AppsBreadcrumb and AppsSideNav components * Add appsListingJson import and modify breadcrumb rendering * Refactor path modification logic in WorkspaceBaseLayout.tsx * Update subproject commit hash * Fix archiveSystemDir formatting in JobsView * Update SITE_ID values in site configuration files * Update subproject commit hash * Fix formatting issues in code * Update app names and formatting * Update SITE_ID values in site configuration files * Remove unused code and imports * Add Tapis operations to datafiles handlers * Remove unused React assets from base template * Update React and database configurations * Update datafiles handlers and workspace views * Replace Tapis operations with Agave operations * Update import statement for service_account * Fix formatting in data_depot.j2 template * Fix formatting issues in data_depot.j2 template * Fix formatting and add missing newline at end of file * retrigger checks * Refactor JobsDetailModal component * Add AppsBreadcrumb component and update layout styles * Refactor AppsBreadcrumb module CSS and fix function formatting * Remove unused import statement in WorkspaceBaseLayout.tsx * Fix modifiedPath for Job Status * Refactor path modification in WorkspaceBaseLayout.tsx * Add app data and loading state to AppsBreadcrumb component * updated AppsBreadcrumb * update to Job Status label * refactor breadcrumb code from WorkspaceBaseLayout into AppsBreadcrumb * disable app form for testing on this branch * Refactor AppsBreadcrumb component to improve breadcrumb rendering; no link for last item * Refactor AppsBreadcrumb component to improve breadcrumb rendering and remove last item link; change separator from / to > * Refactor AppsBreadcrumb component to improve breadcrumb rendering and remove last item link; change separator from / to > * remove suspense query * revert reversion of suspense * remove portal dir --------- Co-authored-by: Sal Tijerina * linting * linting and refactor breadcrumb * linting * layout adjustments * more styling * fix user guide link * fix tests; fix tags and labels for description list * Bug DES-2762: Adding key to avoid optimization from Antd (#1233) Co-authored-by: Sal Tijerina * fix linting * add typing * add more typing * working Edit button * fix type * fix username render * more linting fixes * more typing * finish typing * linting * fix content hash * Tasks/DES-2630: App Form styling (#1241) * App Form Styling according to design (first version) * Lint fix for FormField * More lint fixes * Common Component Button and font increase * Fix typescript error * Fix type error --------- Co-authored-by: Sal Tijerina * fix accessor error * fix workspace prod template * fix is_bundled computed value; fix missing label * Support html apps (#1244) * support html apps * linting * task/DES-2758, DES-2747: Tapis v3 Job Notifications (#1245) * fix job sumbit error render * fix tapipy error propogation * job status notifications * fix state redirect on notification update by removing angular code from workspace * only render data_depot events * propogate error for ApiException * add message to notification events for bell dropdown * DES-2749: Handle changes to queue (#1246) Co-authored-by: Sal Tijerina * fix common exports * Bug/DES-2778: Handle number validation in App Form (#1250) * Bug/DES-2778: Fix validation for number input in App Form * Fix merge issue * Lint fix * fix app variant href * fix persistent previous app state in step * fix: allow compute max run time when no queue in VM case * fix toasts for data_depot events * more explicitly ignore jobs events * fix errant root: declaration * default open app * Bug/DES-2786: Use key to make sure app instance is created on navigation (#1251) * Bug/DES-2786: Use key to make sure app instance is created on navigation Fix state issues * Remove un-necessary field init * App Form step and summary rendering fixes (#1252) Initial working version * task/des-2657--job-history-listing (#1239) * Add support for displaying node count and cores per node in JobsListing component * Add support for displaying archive system directory in JobsListing component * interactive session modal; styling * wip: notification badge * linting * add todo notes --------- Co-authored-by: Sal Tijerina * disable notifs in job status nav * task/DES-2779: Jobs Listing Notification Badge & Update Job Listing Upon Notification (#1253) * update notification badge and listing when job updates; enable job listing buttons * linting * linting * fix app ordering * remove unused import * tasks/DES-2655: Select file modal in Apps Form (#1254) * Create SelectModal for file selection * Handle public vs private * Use isMyData to check if this is private or public --------- Co-authored-by: Sal Tijerina * task/DES-2713 job history modal (#1234) * - Adds View Details modal for job status listings - Adds working output link button (may need to check I'm directing to the correct path) - Does not add a functioning delete button - Adds modal with the design based of current DS Job Detail Modal * -Adds closing/cancel action - Doesn't handle state correctly for closing - Buttons and links don't work yet * - Adds state handling for open/close modal * Adds linting changes * Adds linting * Uses PrimaryButton component * Task/des 2713 job history modal adjustments (#1257) * url nav for job modal; working buttons * fix data files links --------- Co-authored-by: Sal Tijerina * render job detail on top of listing * DES-2629: Use portal names to build list. (#1258) * fix Tools & Applications link * DES-2789: Only validate current steps on continue button (#1260) * Apps form styling fixes (#1262) * DES-2818: add missing license prompt * fix submit button; submission details css; standard button width; standard font family * job status header; small job action buttons * larger summary item label width * adjust spacing * revert helvetica default * fix button size on job detail modal * use base Button for link type * linting * Remove htmltype submit on continue button (#1263) * task/DES-2755 + DES-2756: Allocations backend + hooks (#1247) * Allocations backend w/ TAS hookup * Add Allocations elasticsearch model + add model to elasticsearch settings * Add allocation type * Add useGetAllocations hook * Add TAS env variables to django settings * Add UserAllocations model * Use UserAllocations model in view * Update designsafe/apps/workspace/api/views.py Co-authored-by: Jake Rosenberg * Pylint errors fixes * Create util function for grabbing user allocations and creating/updating their cached allocations * Add async allocations cache update to login flow * Roll back elasticsearch implementation * Pylint fixes for allocations model * fix unit test * Roll back elasticsearch changes to IndexedApp * Resolve pylint errors on allocations model * Fix client linting errors * Fix poetry black reformatting checks * task/DES-2820: Allocations in App Form (#1264) * use allocations in app form * use dynamic user model; fix task * fix linting * fix type error * fix test * linting --------- Co-authored-by: Garrett Edmonds Co-authored-by: Sal Tijerina Co-authored-by: Jake Rosenberg Co-authored-by: Garrett Edmonds --------- Co-authored-by: sophia-massie <96220951+sophia-massie@users.noreply.github.com> Co-authored-by: Van Go <35277477+van-go@users.noreply.github.com> Co-authored-by: Chandra Y Co-authored-by: Jake Rosenberg Co-authored-by: Garrett Edmonds <43251554+edmondsgarrett@users.noreply.github.com> Co-authored-by: Garrett Edmonds Co-authored-by: Garrett Edmonds * fix cache_allocations task; linting * Fixes for publication session items (#1270) * Fixes for project/publication testing session items * layout fixes and explanation for pipeline select * fixes for file tags and operations on published files * use filetype-specific icons * various pipeline fixes * add Best Practices modal and fix ingest for legacy simulations * add version changes modal * linting/type fixes * task/DES-2823: Fix interactive modal (#1266) * update placeholder job listing message * fix interactive modal state * Task/DES-2801 [UI] Apps Nav Bar Spacing (#1271) * - Adds spacing styles and header to AppsSideNav - Removes unused style with comment * - Adds border to sider nav menu * - Linting * - Linting * - Removes AppNavLink component - Removes display block from Title div so that justify content style applies - Change font for title to match design - Change color of right nav border to match design * -Change item height of nav bar - Removes css module for AppsSideNav --------- Co-authored-by: Sal Tijerina * docs: tapis v3 redesign readme step №3 (#1273) * docs: Tapis v3 changes to "First time setup" №3 * docs: Do not use `[!NOTE]` syntax * Bug/layout sider fix (#1274) * - Moves app sider styles to workspace specificity to avoid changing Data Depot sider style * - Linting * DES-2819: Select Modal Design Updates (Part 1 of 2) (#1276) * DES-2655: Select Modal Design work * Adjust modal body to new global styles * Address review comments * fix lint * Fix isRequired for nested fields (#1277) * Task/des 2849 cite this data (#1272) * commit to main * Add useCitationMetrics hook for fetching citation metrics in publications module * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Add MetricsModal to project modals and update Datacite API endpoints * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor ProjectCitation module and styles, and add DownloadCitation component * Refactor ProjectCitation module and styles, and add DownloadCitation component * chore: Refactor ProjectCitation module, add DOI link to PublishedCitation * Refactor ProjectCitation module, add DOI link to PublishedCitation * chore: Update Docker images and volumes configuration for development environment * Refactor ProjectPreview module, fix useState declaration * Refactor useState declaration in ProjectPreview module * Refactor ProjectCitation module, optimize DOI retrieval and usage * feat: Optimize DOI retrieval and usage in ProjectCitation module * Refactor ProjectCitation module, optimize DOI retrieval and usage * Refactor ProjectCitation module, remove commented out code for MetricsModal * Update index.ts --------- Co-authored-by: Jake Rosenberg * Apps form testing session fixes (#1279) * fix SystemsPushKeysModal: error handling; onSuccess callback; layout; info text * show missing allocation message * rm unuseful comment * open View User Guide in new tab * point notifications to job history; remove apps and jobs from dashboard * task/DES-2726: configure cloud data on login (#1265) * Configure cloud.data on login * Update test settings and todo comments * Rename setting * Remove listing check in from view Checkin and configuring occurs only in the task now * Move task to an onboarding queue --------- Co-authored-by: Sal Tijerina * Add modals for publication feedback/archive download (#1275) * Add feedback and project download modals * link formatting * form/modal overflow fixes * Bug: Handle zod optional fields for undefined or empty values (#1281) * include App Overview link in breadcrumbs (#1283) * task/DES-2759 : Apps side nav styling updates (#1280) * Add shortLabel for rendering in app side nav - add shortLabel to AppVariant model * Add shortLabel for rendering in app side nav - add shortLabel to relevant app views * Add shortLabel for rendering in app side nav - add shortLabel to frontend app types * Optionally render shortLabel for apps in side nav + update hover and selected colors for side nav menu items * Remove unused import * Linting fixes * include app notes.shortLabel in _get_public_apps response * fix submenu open key * update apps side nav border colors and weight (#1282) * Modify main themeConfig for design's #cbdded hover & select menu item color * Client linting fix * Change new appVariant field name to 'short_label' * Update references to 'shortLabel' to renamed 'short_label for AppVariant model * Linting fixes for new migration * Update designsafe/apps/workspace/api/views.py --------- Co-authored-by: Garrett Edmonds Co-authored-by: Sal Tijerina * Bug DES-2876: Select Modal bug fixes (#1284) * Select Modal fixes * Update usePathDisplayName.ts * Fix from merge issue - getPathName * Breadcrumb use system label * Bug/download citation not working in next (#1285) * commit to main * Add useCitationMetrics hook for fetching citation metrics in publications module * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Add MetricsModal to project modals and update Datacite API endpoints * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor ProjectCitation module and styles, and add DownloadCitation component * Refactor ProjectCitation module and styles, and add DownloadCitation component * chore: Refactor ProjectCitation module, add DOI link to PublishedCitation * Refactor ProjectCitation module, add DOI link to PublishedCitation * chore: Update Docker images and volumes configuration for development environment * Refactor ProjectPreview module, fix useState declaration * Refactor useState declaration in ProjectPreview module * Refactor ProjectCitation module, optimize DOI retrieval and usage * feat: Optimize DOI retrieval and usage in ProjectCitation module * Refactor ProjectCitation module, optimize DOI retrieval and usage * Refactor ProjectCitation module, remove commented out code for MetricsModal * Refactor DOI retrieval and usage in ProjectCitation module * test commit * Update index.ts * fix app version href * Bugs/DES-2828 and DES-2829: Select Modal system selection and validation bugs (#1287) * Retain system select based on input and validation fix * Fix test failure * update default DS allocation prefix * also accept DesignSafe-DCV * fix allocation check on non-batch exec system * full length workspace content; apps side nav scrolls (#1290) * quick: fix hidden overflow on app side nav * task/DES-1989 - Redirect guest users when accessing non-published project (#1292) * If an unauthenticated user now attempts to view a project, they will now be prompted to log in. Some styling changes done to make the JSX messages look like the Django error messages produced in file listings. * Ran a formatter * Minor change to make sure projectID and data were defined when used. * One last linting change. * fixes to pipeline publish operations (#1294) * scope .o-site__body changes to workspace * revert changes to alert styling * - Uses selectedKey property in Menu to correctly (#1296) select/deselect active Menu Item based on route * Workspace fixes from 6/12 testing session (#1295) * unread notifs: only count unique jobs * disable JobActionButton after success; make cancel button red * fix scrolling on long app form * expand app side nav to 250px * disable app form while submitting * fix notifications fetching; highlight newest notification * remove DesignSafe-DCV as default allocation * fix next url param redirect; update login redirect setting to reflect production * use formatDateTimeFromValue util; do not render date for null values * fix eventTypes array parsing * fix notification bell event links * Update apps placeholder text * Hide "Edit" button for step when that step is active * fix style override on ant btn disabled state * fix long app form scroll * allow small buffer for long app param labels * no horizontal scrolling * add right margin to user guide link to avoid scrollbar ui conflict * Task/des 2862 update form validation messages (#1297) * commit to main * Add useCitationMetrics hook for fetching citation metrics in publications module * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Add MetricsModal to project modals and update Datacite API endpoints * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * add Total Request values to table * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update MetricsModal component * Refactor useCitationMetrics hook and update MetricsModal component * Refactor MetricsModal component and add Total Request values to table * Refactor ProjectPreview module, remove DownloadCitation component * update variable names * format write * Refactor ProjectPreview module, update MetricsModal props * refactor: Add custom error messages to form validation rules * refactor: Update form validation error messages * Remove file from the pull request * refactor: Update form validation error messages * refactor: Update form validation error messages * refactor: Update form validation error messages * Delete client/modules/datafiles/src/projects/modals/MetricsModal.tsx * refactor: Update form validation error messages * task/WG-294: improve links from DS projects to hazmapper maps (#1291) * Fix links from DS projects to hazmapper maps * Fix linting issues * TV3: disable move/rename on files with metadata associations. (#1293) * disable move/rename on files with metadata associations * remove logging * Task/des 2666 metrics modal (#1237) * commit to main * Add useCitationMetrics hook for fetching citation metrics in publications module * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Add MetricsModal to project modals and update Datacite API endpoints * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * add Total Request values to table * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update MetricsModal component * Refactor useCitationMetrics hook and update MetricsModal component * Refactor MetricsModal component and add Total Request values to table * Refactor ProjectPreview module, remove DownloadCitation component * update variable names * format write * Refactor ProjectPreview module, update MetricsModal props * Update docker-compose-dev.all.debug.yml * Update docker-compose-dev.all.debug.yml --------- Co-authored-by: Jake Rosenberg * pipeline bugfixes; redirect to project after publishing (#1298) * close dropdown menus after selection * replace registration link with redirect to TAM * accept jwt auth for license view (#1300) * DES-2896: Do not block continue to next step on invalid fields (#1301) * styling for job status nav when active and hover (#1303) * Bugs/DES-2907: Exclude allocations at portal level (#1302) * Bug/DES-2907: Allow exclusions from allocations at portal level * Fix formatting * Use env for settings * add default to ALLOCATIONS_TO_EXCLUDE * task/DES-2911: Render `jobAttributes` `notes.label` as form field label if it exists (#1304) * Render job attributes notes.label if it exists * linting * linting * Task/des 2905 cite this data type other (#1299) * Add citation download options and metrics to ProjectPreview and PublishedDetailLayout * Refactor Download Citation link in ProjectPreview and PublishedDetailLayout * Refactor useCitationMetrics hook and remove unused error variable * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and update ProjectPreview module and styles * Refactor useCitationMetrics hook and remove MetricsModal from ProjectPreview module * Refactor useCitationMetrics hook and remove unused error variable * Refactor import path for MetricsModal in ProjectCitation and PublishedDetailLayout * DES-2909 and DES-2914 - bug fixes (#1306) Co-authored-by: Garrett Edmonds <43251554+edmondsgarrett@users.noreply.github.com> * Bugs/DES-2888: Select Modal - regex fix to detect storage system and file selection modes (#1308) * Regex fix and file selection modes * Lint and formatting fixes * Bug/DES-2835,2855: Ribbon Buttons and Amend text (#1311) * chore: Refactor RenameModal to handle file extensions correctly * feat: Add step to change metadata in curation directory before publishing * Fixes: Data Depot testing session 2024-06-17 (#1310) * data depot fixes- dropdown usability and error UI for forms * pipeline display fixes * fixes for Trash button * working downloads for private data * disable downloads from frontera.work * don't indicate success on submission of async file transfer * DES-2931, DES-2932, DES-2929: Project edit modal fixes (#1314) * allow editing of type None; fix guest member input on type Other * enforce URL formatting on referenced data/related works * Show tags in Select Files listing for type Other (#1312) * Bug/DES-2923: Remove Download Data from My Projects (#1318) * chore: Refactor PipelineSelectForPublish component - Add line break for clarity in the process description - Include instructions for submitting a ticket with project number, dataset name(s), and author order * feat: Add line break for clarity in the process description and include instructions for submitting a ticket with project number, dataset name(s), and author order * feat: Add hybrid simulation steps to project type modal * chore: Refactor PipelineSelectForPublish component * Refactor * chore: Refactor DownloadCitation component to include preview option * remove previous ticket files * bug/DES-2922: word/spelling/typo issues (#1313) * chore: Refactor PipelineSelectForPublish component - Add line break for clarity in the process description - Include instructions for submitting a ticket with project number, dataset name(s), and author order * feat: Add line break for clarity in the process description and include instructions for submitting a ticket with project number, dataset name(s), and author order * set mydata acls for other and mask (#1315) * feat: DES-2903 add id to app listing headings (#1316) * DES-2940 - handle env var with labels (#1319) * feat: DES-2806 cms breadcrumbs (#1268) (#1320) * feat: DES-2903 add id to app listing headings * feat: DES-2631 cms breadcrumbs - added link state mixin - added link state styles - moved CSS variables - added CSS variables - added CMS breadcrumbs template - included CMS breadcrumbs template - changed DjangoCMS breadcrumb template * fix: DES-2631 font size & distance from nav * fix: DES-2809 lighter 2nd-level breadcrumb text * fix: DES-2809 only add breadcrumbs on app page * feat: DES-2809 load CSS sooner * refactor: DES-2806 no new breadcrumb classes I.e. skin Bootstrap's. * chore: DES-2806 remove outdated vars Added in DES-2806, but then not used after later commits for DES-2806. * fix: DES-2806 incorrect margin ref. thus spacing * fix: DES-2806 font size should match feat/Tapis-v3 * feat: DES-2806 move bs3 breadcrumb style to .css * fix: DES-2806 core-styles update & match workspace * fix: DES-2806 match workspace top padding * feat: DES-2806 improve cms focus ui * fix: DES-2806 match workspace colors * fix: DES-2806 use `.breadcrumb-item` not `li` Bootstrap uses `.breadcrumb-item`. * Revert "fix: DES-2806 use `.breadcrumb-item` not `li`" This reverts commit b58cc963a26fde8e092af686ed04775b2364a2e8. Bootstrap 4 uses `.breadcrumb-item`, but not Bootstrap 3. * chore(cms): update core-styles breadcrumb * chore(cms): use core-styles breadcrumb at v2.27.0 * Bug/des 2990 Fix fr protected data options (#1321) * show protected data options when changing project type to FR * improve variable naming * formatting * fix prop typing * Persist HazMapper maps when changing project type (#1323) * bug/DES-3000: Handle DOI not provided in useCitationMetrics (#1325) * feat: Handle DOI not provided in useCitationMetrics * feat: Add cache page decorator to PublicationDetailView and PublicationDataCiteView * feat: Add cache page decorator to PublicationDetailView and PublicationDataCiteView * Bug/DES-2982: text/typo/wording/spelling issues (#1329) * Fix typo in curation office hours link * chore: Fix typo in Publish/ Amend/ Version * Pipeline fixes and Fedora representation for publications (#1332) * Pipeline fixes and Fedora representation for publications * formatting * task/DES-2924: Add metadata and move/copy/download buttons to preview modal (#1334) * Add metadata and move/copy/download buttons to preview modal * formatting * Data Files/Projects styling fixes (#1336) * Scope scroll overflow behavior in modals to Tools & Applications (#1338) * Scope overflow scroll in modal to Tools & Applications * clean up diff * Task/DES-2984: Use a single form for creating and updating categories (#1341) * use a single form for both creating and editing categories * set smooth scrolling; remove error dialog when switching edit mode * fix type error * successCallback typo fix in preview/move modals * Bugs/DES-2872: [AppForm] Handle axios request errors without crashing client (#1322) * DES-2872: AppViews error handling * Add key * Task/WA-199: Uniform margin/padding for HTML app content. (#1342) * Add uniform padding to HTML app container * add top margin to app HTML * Include all job submission values in job detail modal (#1333) * Job Details Modal * Fix label and remove unnecessary env variables * Fix unit test error * Switch to antd description instead of core's descriptionlist * Filter display items with no data --------- Co-authored-by: Garrett Edmonds <43251554+edmondsgarrett@users.noreply.github.com> Co-authored-by: Sal Tijerina * Update file metadata on move/copy/rename (#1344) * [AppForm] Use bundle_icon field for app icon in App Form header (#1345) * [SelectModal] New features added - back button and first row (#1340) * [SelectModal] Back button and related features * Fix lint error * Disable selection for root if it is first row * Update SelectModal.tsx --------- Co-authored-by: Sal Tijerina * add search bars to NEES layouts; add links to files in experiments (#1347) * task/DES-2935: Notifications fine tuning (#1346) * hover pointer; duration 5s; onClick to job history * only fetch notifications once - subsequent notifications will come through as webhooks * Bug/des-2986: description error messages (#1328) * Refactor form validation rules for project description length * Refactor form validation rules for project description length * Handle `memoryMb` value change on queue change (#1350) * update docker compose command in makefile; show execution dir while running (#1351) * split datacite metrics and events hooks; fix caching (#1349) * reset 'download datset' modal on close * add dropdown with help links (#1355) * [Apps] Translate Early Termination TAPIS Failure Status to Success if interactive (#1358) * [Job History] Implement expected functionality of "Reuse Inputs" (#1357) * [Job History] Implement expected functionality of "Reuse Inputs" * Fix build error * Address review comments - shared component, suspense * Fix lint error --------- Co-authored-by: Sal Tijerina * add category to breadcrumb (#1360) * task/DES-3020: Fix scroll in HTML apps (#1361) * fix html app overflow * remove unnecessary css * Task/DES-2601: DOI logging for file/publication operations (#1356) * log DOIs when published files are downloaded/copied/previewed * re-add logging when entity dropdown is opened * Add instructions for bulk data transfer into projects (#1365) * Bugs/DES-3033 [Job Details Modal] Do not show hidden fields and show fields with empty strings (#1364) * DES-3033: Handle hidden parameters and inputs * more consolidion * task/DES-2936 - Hide Outputs section in form for interactive jobs (#1359) * Outputs form files are hidden from interactive job submission, still needs to be hidden from side panel. * Hid Outputs field from Job Submission Summary sidebar. * Refactored to bring app definition in as prop, to prevent accidentally double-loading the app and to make the code more readable. --------- Co-authored-by: Sal Tijerina * Task/WG-316 Data Depot disable menu hazmapper (#1352) * Disabled menu on preview modal and on listing for hazmapper files * - Disables Copy file option in preview PreviewModal - Adds display of response in preview modal for testing * - Adds functionality to open hazmapper map in hazmapper in PreviewContent - Exports Hazmapper base urls - Handles cancel of preview modal once the hazmapper map opens in new tab * - Adds handleCancel prop to PreviewContent in PreviewModal * - Front end linting * - Fixing merge issues * - Linting * - Addressing error Expected a 'break' statement before 'default' no-fallthrough - Addresses lexical delcaration error in Hazmapper case block * - Linting * - Linting error * - Combines file menu logic in PreviewModal --------- Co-authored-by: Sal Tijerina * task/DES-2939 - New App Notes Field "hideQueue" (#1326) * Adding hideQueue flag for apps to hide the queue selection from users. Should only be used with Compress/Extract for now. * Can now hide queue from job subission summary * As suggested, Queue and Allocation now hide independently of each other, and the fields are hidden from the Summary if the app definition requires it. * Improve handling of error responses for projects/publications (#1367) * improve error boundaries on projects/pubs * remove logging * task/DES-2938 & DES-2941: App form updates (#1366) * Add user_guide_link to app entry model * Render user_guide_link from app model in app form header, and move app form header to capture html apps * Move Suspense one more level up from Header * Fix element arrangement + clean up unused vars * linting * linting * linting * move user_guide_link to AppListingEntry model; send value to client side; ignore blank entry * minor comment adjustment; capitalize Queue in job detail modal * fix app form vertical scroll * add max_length to charfield --------- Co-authored-by: Sal Tijerina * formatting * update to Usage Breakdown * Revert "update to Usage Breakdown" This reverts commit fe80f0dfef1e12a0e658a848eef1bc898cb8cbfd. * Bug/des 2528 update datacite metrics (#1371) * chore: Fix MetricsModalBody data display issue * chore: Fix MetricsModalBody data display issue * Revert "update to Usage Breakdown" This reverts commit fe80f0dfef1e12a0e658a848eef1bc898cb8cbfd. * chore: Update MetricsModalBody to display most recent year by default * chore: Update MetricsModalBody to display most recent year by default * chore: Update MetricsModalBody to display most recent year by default * chore: Refactor MetricsModalBody to improve data display and filtering * update missing allocation message (#1372) * Task/DES-2707: add script to migrate project systems to v3 tenant (#1180) * Add tapipy * Use tapipy 1.6.3 due to bug in 1.6.2 * Add command to migrate project systems * Log any issues * Fix pylint issues * Format with 'black' code formatter * Fix linting issues after black formatting changes * Update v3 system using any users who only exist in v2 system roles * Fix linting issues * Refactor how community data is handled --------- Co-authored-by: Sal Tijerina * disable injection of path information into anchor elements on error pages (#1374) * fix ingest script for projects * Quick: Fix `check_or_configure_system_and_user_directory` `retry()`; Update `` wording. (#1376) * fix retry call; update wording on push keys modal * use bind=True * fix tombstone ingest and add tombstone UI (#1378) * hide move button on public systems; hide copy button for unauth'd users (#1379) * DES-2626: NCO TTC Updates (#1368) * next batch of nco ttc grants page updates * removing category filter, adding subjects as keywords to details modal * updating button styles * fixing bug in abstract details modal * adding pi name to the text field search * fix deprecated external_resource_secrets setting * log DOI when downloading project archives (#1382) * Quick: Maintenance toggle with staff bypass (#1381) * Add setting to toggle maintenance page display * refactor logic * don't show page for requests coming from TACC VPN * move VPN IP prefix to settings * fix last-updated dates on projects after migrating (#1385) * list all app versions (#1386) * Fedora: Ingest on publish (#1387) * add Fedora ingest step to publication pipeline * update test settings * fix webhook settings for local dev * formatting * always use www in prod redirect uri (#1388) * get nco dashboard working for unauth'd users (#1389) * NCO fix * Fix/prod redirect uri (#1391) * always use www in prod redirect uri * add www replacement in tapis_oath call * Update quarters in MetricsModal to match correct time periods * Revert "Update quarters in MetricsModal to match correct time periods" This reverts commit 5db4818e88dd014bb31a477dccad35cec5dfbd26. * Update quarters in MetricsModal to match correct time periods (#1393) * Use Zod refine for regex (#1392) * curation fixes for entity ordering/file trashing (#1395) * task/WP-647: Remove portal name filter for jobs by default (#1394) * remove portalname filter by default for jobs * remove old test description from setfacl job --------- Co-authored-by: Garrett Edmonds <43251554+edmondsgarrett@users.noreply.github.com> * Now apps will list simcenter apps after popular apps, but before the rest of the apps. (#1397) Co-authored-by: Sal Tijerina --------- Co-authored-by: Jake Rosenberg Co-authored-by: Nathan Franklin Co-authored-by: Sarah Gray Co-authored-by: Wesley B <62723358+wesleyboar@users.noreply.github.com> Co-authored-by: sophia-massie <96220951+sophia-massie@users.noreply.github.com> Co-authored-by: Van Go <35277477+van-go@users.noreply.github.com> Co-authored-by: Chandra Y Co-authored-by: Garrett Edmonds <43251554+edmondsgarrett@users.noreply.github.com> Co-authored-by: Garrett Edmonds Co-authored-by: Garrett Edmonds Co-authored-by: fnets --- .dockerignore | 6 +- .docs/source/designsafe.apps.auth.rst | 8 - .flake8 | 5 +- .github/workflows/main.yml | 74 +- .gitignore | 2 + .pylintrc | 2 +- Makefile | 18 +- README.md | 54 +- bin/build_client.sh | 5 - bin/dumpdata.sh | 7 - bin/loaddata.sh | 10 - bin/mysql.sh | 1 - bin/run-celery-debug.sh | 2 +- bin/run-celery-dev.sh | 6 - bin/run-celery.sh | 2 +- bin/run-django.sh | 3 - bin/run-flower.sh | 6 - bin/run-tests.sh | 18 - bin/run-uwsgi.sh | 3 - .../DatafilesBreadcrumb.module.css | 0 .../DatafilesBreadcrumb.tsx | 51 +- .../FileListingTable.module.css | 0 .../FileListingTable/FileListingTable.tsx | 216 ++ .../FileListingTableCheckbox.tsx | 0 .../datafiles/FileTypeIcon/FileTypeIcon.tsx | 62 + .../src/datafiles/fileUtils.ts | 9 + .../_common_components/src/datafiles/index.ts | 4 + .../modules/_common_components/src/index.ts | 5 +- .../src/lib/Button/Button.module.css | 3 + .../src/lib/Button/Button.tsx | 49 + .../src/lib/Button/index.ts | 1 + .../src/lib/Icon/Icon.module.css | 0 .../_common_components/src/lib/Icon/Icon.tsx | 11 + .../_common_components/src/lib/Icon/index.ts | 1 + .../src/lib/Spinner/Spinner.module.css | 5 + .../src/lib/Spinner/Spinner.tsx | 7 + .../src/lib/Spinner/index.ts | 1 + .../src/lib/common-components.module.css | 7 - .../src/lib/common-components.spec.tsx | 10 - .../src/lib/common-components.tsx | 14 - .../modules/_common_components/vite.config.ts | 1 + client/modules/_hooks/src/datafiles/index.ts | 2 + .../_hooks/src/datafiles/nees/index.ts | 3 + .../src/datafiles/nees/useNeesDetails.ts | 112 + .../src/datafiles/nees/useNeesListing.ts | 13 +- .../_hooks/src/datafiles/projects/index.ts | 5 + .../_hooks/src/datafiles/projects/types.ts | 14 + .../projects/useChangeProjectType.ts | 37 + .../projects/useCheckFilesForAssociation.ts | 46 + .../src/datafiles/projects/useCreateEntity.ts | 32 + .../datafiles/projects/useCreateProject.ts | 27 + .../src/datafiles/projects/useDeleteEntity.ts | 21 + .../src/datafiles/publications/index.ts | 8 + .../datafiles/publications/useAmendProject.ts | 21 + .../publications/useCreateFeedbackTicket.ts | 37 + .../publications/useDataciteEvents.ts | 44 + .../publications/useDataciteMetrics.ts | 46 + .../datafiles/publications/useDoiContext.ts | 20 + .../publications/usePublicationDetail.ts | 5 +- .../publications/usePublishProject.ts | 28 + .../publications/usePublishedListing.ts | 15 +- .../publications/useVersionProject.ts | 34 + .../_hooks/src/datafiles/useFileCopy.ts | 9 +- .../_hooks/src/datafiles/useFileDetail.ts | 32 + .../_hooks/src/datafiles/useFileListing.ts | 5 +- .../datafiles/useFileListingRouteParams.ts | 2 +- .../_hooks/src/datafiles/useFileMove.ts | 34 + .../_hooks/src/datafiles/useFilePreview.ts | 10 +- .../_hooks/src/datafiles/useNewFolder.ts | 11 +- .../src/datafiles/usePathDisplayName.ts | 19 +- .../modules/_hooks/src/datafiles/useRename.ts | 9 +- .../modules/_hooks/src/datafiles/useTrash.ts | 9 +- .../_hooks/src/datafiles/useUploadFile.ts | 11 +- .../_hooks/src/datafiles/useUploadFolder.ts | 11 +- client/modules/_hooks/src/index.ts | 4 +- .../modules/_hooks/src/notifications/index.ts | 2 + .../src/notifications/useNotifications.ts | 105 + .../src/notifications/useNotifyContext.ts | 9 + client/modules/_hooks/src/systems/index.ts | 6 + client/modules/_hooks/src/systems/types.ts | 73 + .../_hooks/src/systems/useGetSystems.ts | 42 + .../modules/_hooks/src/systems/usePushKeys.ts | 25 + .../_hooks/src/useAuthenticatedUser.ts | 15 +- client/modules/_hooks/src/workspace/index.ts | 21 +- client/modules/_hooks/src/workspace/types.ts | 201 ++ .../_hooks/src/workspace/useAppsListing.ts | 51 +- .../_hooks/src/workspace/useGetAllocations.ts | 40 + .../_hooks/src/workspace/useGetApps.ts | 62 + .../_hooks/src/workspace/useGetJobs.ts | 83 + .../_hooks/src/workspace/useJobsListing.ts | 55 + .../_hooks/src/workspace/usePostJobs.ts | 84 + .../workspace/allocations-listing.json | 34 + .../src/fixtures/workspace/apps-listing.json | 146 - .../fixtures/workspace/apps-tray-listing.json | 61 + .../fixtures/workspace/systems-listing.json | 1036 +++++++ client/modules/_test-fixtures/src/handlers.ts | 12 +- client/modules/_test-fixtures/src/index.ts | 1 + .../src/AddFileFolder/AddFileFolder.tsx | 255 +- .../DatafilesHelpDropdown.module.css | 10 + .../DatafilesHelpDropdown.tsx | 130 + .../DatafilesModal/CopyModal/CopyModal.tsx | 21 +- .../src/DatafilesModal/DatafilesModal.tsx | 4 + .../DownloadModal/DownloadModal.tsx | 75 + .../src/DatafilesModal/DownloadModal/index.ts | 1 + .../MoveModal/MoveModal.module.css | 41 + .../DatafilesModal/MoveModal/MoveModal.tsx | 285 ++ .../src/DatafilesModal/MoveModal/index.ts | 1 + .../NewFolderModal/NewFolderModal.tsx | 14 +- .../PreviewModal/PreviewContent.tsx | 49 +- .../PreviewModal/PreviewMetadata.tsx | 62 + .../PreviewModal/PreviewModal.module.css | 15 + .../PreviewModal/PreviewModal.tsx | 103 +- .../RenameModal/RenameModal.tsx | 96 +- .../UploadFileModal/UploadFileModal.tsx | 10 +- .../src/DatafilesSideNav/DatafilesSideNav.tsx | 71 +- .../src/DatafilesToolbar/DatafilesToolbar.tsx | 121 +- .../src/DatafilesToolbar/TrashButton.tsx | 44 +- .../datafiles/src/FileListing/FileListing.tsx | 99 +- client/modules/datafiles/src/index.ts | 2 +- .../datafiles/src/nees/NeesDetails.module.css | 34 + .../datafiles/src/nees/NeesDetails.tsx | 445 +++ .../datafiles/src/nees/NeesListing.tsx | 2 +- client/modules/datafiles/src/nees/index.ts | 1 + .../projects/BaseProjectDetails.module.css | 2 +- .../src/projects/BaseProjectDetails.tsx | 129 +- .../src/projects/EmptyProjectFileListing.tsx | 24 + .../ProjectCitation.module.css | 3 + .../ProjectCitation/ProjectCitation.tsx | 146 +- .../ProjectCurationFileListing.tsx | 126 +- .../datafiles/src/projects/ProjectListing.tsx | 35 +- .../ProjectMetrics/ProjectMetrics.module.css | 0 .../ProjectMetrics/ProjectMetrics.tsx | 29 + .../ProjectPipeline/PipelineOrderAuthors.tsx | 8 +- .../PipelineOtherSelectFiles.tsx | 56 +- .../PipelineProofreadCategories.tsx | 54 +- .../PipelineProofreadProjectStep.tsx | 2 +- .../PipelineProofreadPublications.tsx | 52 +- .../ProjectPipeline/PipelinePublishModal.tsx | 157 +- .../PipelineSelectForPublish.tsx | 135 +- .../ProjectPipeline/PipelineSelectLicense.tsx | 18 +- .../ProjectPipeline/ProjectPipeline.tsx | 37 +- .../ProjectPreview/ProjectPreview.module.css | 4 + .../ProjectPreview/ProjectPreview.tsx | 301 +- .../ProjectTitleHeader/ProjectTitleHeader.tsx | 1 + .../ProjectTree/ProjectTree.module.css | 20 +- .../src/projects/ProjectTree/ProjectTree.tsx | 77 +- .../src/projects/PublishableEntityButton.tsx | 123 + .../src/projects/PublishedEntityDetails.tsx | 40 +- .../src/projects/SubEntityDetails.tsx | 166 + .../datafiles/src/projects/constants.ts | 4 +- .../src/projects/forms/BaseProjectForm.tsx | 281 +- .../src/projects/forms/CreateProjectForm.tsx | 152 + .../projects/forms/ProjectCategoryForm.tsx | 221 +- .../forms/ProjectCategoryFormHelp.tsx | 140 + .../projects/forms/ProjectFormDropdowns.ts | 3 +- .../projects/forms/PublishableEntityForm.tsx | 282 +- .../src/projects/forms/_fields/DateInput.tsx | 3 +- .../projects/forms/_fields/DropdownSelect.tsx | 15 +- .../forms/_fields/HazardEventsInput.tsx | 6 +- .../forms/_fields/ReferencedDataInput.tsx | 13 +- .../forms/_fields/RelatedWorkInput.tsx | 13 +- .../src/projects/forms/_fields/UserSelect.tsx | 17 +- .../modules/datafiles/src/projects/index.ts | 6 + .../modals/BaseProjectCreateModal.tsx | 65 + .../modals/BaseProjectUpdateModal.tsx | 54 +- .../modals/ChangeProjectTypeModal.tsx | 30 +- .../projects/modals/ManageCategoryModal.tsx | 138 +- .../modals/ManagePublishableEntityModal.tsx | 147 +- .../src/projects/modals/MetricsModal.tsx | 474 +++ .../modals/PipelineEditCategoryModal.tsx | 93 + .../modals/ProjectBestPracticesModal.tsx | 71 + .../modals/ProjectDataTransferModal.tsx | 54 + .../src/projects/modals/ProjectInfoModal.tsx | 40 + .../ProjectInfoStepper/ExperimentalSteps.tsx | 12 +- .../ProjectInfoStepper/FieldReconSteps.tsx | 4 +- .../ProjectInfoStepper/SimulationSteps.tsx | 12 +- .../src/projects/modals/RelateDataModal.tsx | 17 +- .../projects/modals/VersionChangesModal.tsx | 63 + .../datafiles/src/projects/modals/index.ts | 4 + .../modules/datafiles/src/projects/utils.ts | 56 + .../PublicationSearchSidebar.tsx | 47 +- .../PublicationSearchToolbar.tsx | 7 +- .../PublishedListing/PublishedListing.tsx | 106 +- .../datafiles/src/publications/index.ts | 1 + .../modals/DownloadDatasetModal.tsx | 345 +++ .../modals/SubmitFeedbackModal.tsx | 144 + .../AppsBreadcrumb/AppsBreadcrumb.module.css | 9 + .../src/AppsBreadcrumb/AppsBreadcrumb.tsx | 117 + .../src/AppsSideNav/AppSideNav.spec.tsx | 19 + .../workspace/src/AppsSideNav/AppsSideNav.tsx | 142 + .../AppsSubmissionDetails.module.css | 12 + .../AppsSubmissionDetails.tsx | 270 ++ .../src/AppsSubmissionForm/AppIcon.module.css | 5 + .../src/AppsSubmissionForm/AppIcon.tsx | 9 + .../AppsSubmissionForm/AppsSubmissionForm.tsx | 628 ++++ .../src/AppsWizard/AppsFormSchema.ts | 566 ++++ .../src/AppsWizard/AppsWizard.module.css | 0 .../workspace/src/AppsWizard/AppsWizard.tsx | 87 + .../workspace/src/AppsWizard/FormField.tsx | 151 + .../workspace/src/AppsWizard/Steps.tsx | 61 + .../InteractiveSessionModal.module.css | 19 + .../InteractiveSessionModal.tsx | 42 + .../src/InteractiveSessionModal/index.ts | 1 + .../src/JobStatusNav/JobStatusNav.module.css | 21 + .../src/JobStatusNav/JobStatusNav.spec.tsx | 15 + .../src/JobStatusNav/JobStatusNav.tsx | 43 + .../JobsDetailModal.module.css | 160 + .../src/JobsDetailModal/JobsDetailModal.tsx | 329 ++ .../src/JobsListing/JobsListing.module.css | 10 + .../workspace/src/JobsListing/JobsListing.tsx | 250 ++ .../JobsListingTable.module.css | 7 + .../JobsListingTable/JobsListingTable.tsx} | 103 +- .../JobsListingTableCheckbox.tsx | 29 + .../JobsReuseInputsButton.tsx | 31 + .../src/SelectModal/SelectModal.module.css | 76 + .../workspace/src/SelectModal/SelectModal.tsx | 552 ++++ .../SelectModal/SelectModalProjectListing.tsx | 57 + .../SystemsPushKeysModal.tsx | 144 + .../src/Toast/Notifications.module.css | 3 + client/modules/workspace/src/Toast/Toast.tsx | 59 + client/modules/workspace/src/Toast/index.tsx | 1 + client/modules/workspace/src/constants.ts | 19 + client/modules/workspace/src/index.ts | 12 +- .../workspace/src/lib/workspace.module.css | 7 - .../workspace/src/lib/workspace.spec.tsx | 10 - .../modules/workspace/src/lib/workspace.tsx | 25 - client/modules/workspace/src/utils/apps.ts | 474 +++ client/modules/workspace/src/utils/index.ts | 5 + client/modules/workspace/src/utils/jobs.ts | 395 +++ .../workspace/src/utils/notifications.ts | 40 + client/modules/workspace/src/utils/systems.ts | 18 + .../modules/workspace/src/utils/timeFormat.ts | 55 + .../workspace/src/utils/truncateMiddle.ts | 29 + client/package-lock.json | 849 +++-- client/package.json | 12 +- client/project.json | 2 +- client/{index.html => react-assets.html} | 0 client/src/datafiles/datafilesRouter.tsx | 7 +- .../datafiles/layouts/DataFilesBaseLayout.tsx | 58 +- .../datafiles/layouts/FileListingLayout.tsx | 7 +- .../layouts/nees/NeesDetailLayout.tsx | 46 +- .../layouts/nees/NeesListingLayout.tsx | 37 +- .../projects/ProjectCurationLayout.tsx | 83 +- .../layouts/projects/ProjectDetailLayout.tsx | 44 +- .../projects/ProjectPipelineSelectLayout.tsx | 45 +- .../layouts/projects/ProjectPreviewLayout.tsx | 23 +- .../layouts/projects/ProjectWorkdirLayout.tsx | 54 +- .../PublishedDetailLayout.module.css | 3 + .../published/PublishedDetailLayout.tsx | 116 +- .../PublishedEntityListingLayout.tsx | 31 +- .../published/PublishedFileListingLayout.tsx | 9 +- .../PublishedListingLayout.module.css | 5 + .../published/PublishedListingLayout.tsx | 15 +- client/src/main.tsx | 7 +- client/src/styles.css | 41 +- client/src/styles/modal.css | 61 + client/src/workspace/Workspace.module.css | 1 - client/src/workspace/Workspace.spec.tsx | 15 - client/src/workspace/Workspace.tsx | 14 - .../layouts/AppsPlaceholderLayout.tsx | 20 + .../src/workspace/layouts/AppsViewLayout.tsx | 91 + .../workspace/layouts/JobsListingLayout.tsx | 29 + .../layouts/WorkspaceBaseLayout.spec.tsx | 10 + .../workspace/layouts/WorkspaceBaseLayout.tsx | 78 + .../src/workspace/layouts/layout.module.css | 22 + client/src/workspace/workspaceRouter.tsx | 42 +- client/tsconfig.json | 2 +- client/vite.config.ts | 6 + conf/bootstrap-config.json | 434 --- conf/docker/Dockerfile | 76 +- conf/docker/colorize_logs.awk | 120 - conf/docker/docker-compose-build.yml | 20 - .../docker-compose-dev.all.debug.m1.yml | 55 +- conf/docker/docker-compose-dev.all.debug.yml | 53 +- conf/docker/docker-compose-dev.all.yml | 131 - conf/docker/docker-compose-dev.yml | 190 +- conf/docker/docker-compose.backup.yml | 65 - conf/docker/docker-compose.yml | 5 +- conf/elasticsearch/elasticsearch.sample.yml | 15 - conf/env_files/designsafe.sample.env | 39 +- conf/env_files/mysql.sample.env | 8 - conf/mysql.sample.cnf | 22 - conf/newrelic.ini | 206 -- conf/nginx/error.html | 75 - conf/nginx/nginx.conf | 126 - conf/nginx/nginx.debug.conf | 77 +- conf/rabbitmq.sample.conf | 16 - conf/redis.sample.conf | 1292 -------- conf/supervisor.conf | 17 - conf/uwsgi/uwsgi_websocket.ini | 14 - designsafe/LoginTest.py | 117 +- .../apps/accounts/fixtures/user-data.json | 64 - designsafe/apps/accounts/tasks.py | 16 +- .../designsafe/apps/accounts/register.html | 4 +- designsafe/apps/accounts/tests.py | 2 +- designsafe/apps/accounts/views.py | 15 +- designsafe/apps/api/agave/__init__.py | 33 +- designsafe/apps/api/datafiles/handlers.py | 17 +- .../datafiles/operations/shared_operations.py | 2 +- ...gave_operations.py => tapis_operations.py} | 314 +- .../operations/transfer_operations.py | 10 +- designsafe/apps/api/datafiles/views.py | 6 +- designsafe/apps/api/decorators.py | 47 + ...e_filemeta_table_from_tavpisv2_metadata.py | 6 +- designsafe/apps/api/filemeta/models.py | 3 +- designsafe/apps/api/filemeta/tasks.py | 70 + designsafe/apps/api/filemeta/tests.py | 34 +- designsafe/apps/api/filemeta/views.py | 18 +- .../api/fixtures/agave-oauth-token-data.json | 28 - designsafe/apps/api/fixtures/user-data.json | 69 - designsafe/apps/api/licenses/views.py | 21 +- .../fixtures/agave-oauth-token-data.json | 28 - .../api/notifications/fixtures/user-data.json | 69 - designsafe/apps/api/notifications/tests.py | 137 +- designsafe/apps/api/notifications/urls.py | 4 - .../apps/api/notifications/views/api.py | 132 +- .../apps/api/notifications/views/webhooks.py | 67 - designsafe/apps/api/projects/tests.py | 12 +- designsafe/apps/api/projects_v2/conftest.py | 65 + .../api/projects_v2/management/__init__.py | 0 .../management/commands/__init__.py | 0 ...te_projects_tapis_systems_from_v2_to_v3.py | 272 ++ .../migration_utils/file_obj_ingest.py | 5 +- .../migration_utils/graph_constructor.py | 46 +- .../migration_utils/project_db_ingest.py | 42 +- .../migration_utils/publication_transforms.py | 48 +- .../operations/_tests/publish_unit_test.py | 50 - .../operations/datacite_operations.py | 1 + .../operations/graph_operations.py | 17 + .../operations/project_archive_operations.py | 7 + .../operations/project_meta_operations.py | 40 + .../operations/project_publish_operations.py | 237 +- .../operations/project_system_operations.py | 216 ++ .../schema_models/_field_models.py | 26 +- .../api/projects_v2/schema_models/base.py | 61 + .../projects_v2/schema_models/experimental.py | 73 +- .../projects_v2/schema_models/field_recon.py | 159 +- .../projects_v2/schema_models/hybrid_sim.py | 101 +- .../projects_v2/schema_models/simulation.py | 64 + designsafe/apps/api/projects_v2/tasks.py | 65 + .../projects_v2/tests/schema_integration.py | 2 +- designsafe/apps/api/projects_v2/urls.py | 1 + designsafe/apps/api/projects_v2/views.py | 130 +- .../apps/api/projects_v2/views_unit_test.py | 65 + .../apps/api/publications/operations.py | 9 +- designsafe/apps/api/publications/urls.py | 6 +- designsafe/apps/api/publications/views.py | 31 +- .../apps/api/publications_v2/elasticsearch.py | 29 + .../operations/fedora_graph_operations.py | 382 +++ designsafe/apps/api/publications_v2/tasks.py | 11 + designsafe/apps/api/publications_v2/urls.py | 6 + designsafe/apps/api/publications_v2/views.py | 292 +- .../apps/api/search/searchmanager/cms.py | 3 +- designsafe/apps/api/systems/__init__.py | 0 .../apps/api/systems/ssh_keys_manager.py | 146 + designsafe/apps/api/systems/urls.py | 8 + designsafe/apps/api/systems/utils.py | 82 + designsafe/apps/api/systems/views.py | 56 + designsafe/apps/api/tests.py | 4 +- designsafe/apps/api/urls.py | 2 + designsafe/apps/api/users/utils.py | 37 +- designsafe/apps/api/users/views.py | 25 +- designsafe/apps/api/views.py | 137 +- designsafe/apps/applications/views.py | 4 +- designsafe/apps/auth/README.md | 16 +- designsafe/apps/auth/backends.py | 219 +- designsafe/apps/auth/backends_unit_test.py | 97 + designsafe/apps/auth/context_processors.py | 14 - designsafe/apps/auth/middleware.py | 95 +- ..._tapisoauthtoken_delete_agaveoauthtoken.py | 44 + designsafe/apps/auth/models.py | 163 +- designsafe/apps/auth/models_unit_test.py | 46 + designsafe/apps/auth/tasks.py | 142 +- .../templates/designsafe/apps/auth/login.html | 4 +- designsafe/apps/auth/urls.py | 43 +- designsafe/apps/auth/views.py | 283 +- designsafe/apps/auth/views_unit_test.py | 94 + .../box_integration/fixtures/user-data.json | 69 - .../apps/dashboard/fixtures/user-data.json | 20 - designsafe/apps/data/models/agave/files.py | 2 +- designsafe/apps/data/models/elasticsearch.py | 11 +- designsafe/apps/data/tasks.py | 11 +- .../apps/data/templates/data/data_depot.j2 | 128 +- designsafe/apps/data/views/base.py | 86 +- designsafe/apps/data/views/mixins.py | 4 +- designsafe/apps/nco/api_urls.py | 12 +- designsafe/apps/nco/managers/projects.py | 5 +- designsafe/apps/nco/managers/ttc_grants.py | 38 +- .../designsafe/apps/nco/ttc_grants.j2 | 15 +- designsafe/apps/nco/views/__init__.py | 20 +- designsafe/apps/nco/views/api.py | 34 +- designsafe/apps/notifications/views.py | 101 - designsafe/apps/projects/managers/base.py | 3 +- designsafe/apps/projects/managers/datacite.py | 1 - .../apps/projects/models/elasticsearch.py | 11 +- designsafe/apps/rapid/fixtures/user-data.json | 64 - designsafe/apps/search/views.py | 4 +- .../apps/token_access/fixtures/user-data.json | 85 - designsafe/apps/webhooks/__init__.py | 0 designsafe/apps/webhooks/apps.py | 12 + .../apps/webhooks/fixtures/job_event.json | 23 + .../apps/webhooks/fixtures/job_failed.json | 75 + .../apps/webhooks/fixtures/job_running.json | 75 + .../apps/webhooks/fixtures/job_staging.json | 75 + designsafe/apps/webhooks/unit_test.py | 183 ++ designsafe/apps/webhooks/urls.py | 16 + designsafe/apps/webhooks/views.py | 202 ++ designsafe/apps/workspace/admin.py | 1 + designsafe/apps/workspace/api/__init__.py | 0 .../workspace/api/tas_to_tacc_resources.json | 77 + designsafe/apps/workspace/api/tasks.py | 14 + designsafe/apps/workspace/api/urls.py | 22 + designsafe/apps/workspace/api/utils.py | 33 + designsafe/apps/workspace/api/views.py | 923 ++++++ designsafe/apps/workspace/apps.py | 11 +- .../fixtures/agave-oauth-token-data.json | 28 - .../apps/workspace/fixtures/user-data.json | 46 - .../migrations/0013_userallocations.py | 37 + .../migrations/0014_appvariant_short_label.py | 21 + .../0015_alter_apptraycategory_priority.py | 20 + .../0016_applistingentry_user_guide_link.py | 19 + designsafe/apps/workspace/models/__init__.py | 27 - .../apps/workspace/models/allocations.py | 14 + .../apps/workspace/models/app_entries.py | 19 +- .../apps/workspace/models/elasticsearch.py | 11 +- designsafe/apps/workspace/tasks.py | 230 -- .../designsafe/apps/workspace/index.j2 | 24 +- designsafe/apps/workspace/tests.py | 74 +- designsafe/apps/workspace/urls.py | 22 +- designsafe/apps/workspace/views.py | 297 +- designsafe/asgi.py | 1 - designsafe/conftest.py | 78 +- designsafe/fixtures/auth.json | 35 + .../tapis/auth/create-tokens-response.json | 20 + designsafe/fixtures/user-data.json | 103 + designsafe/libs/elasticsearch/docs.py | 2 +- designsafe/libs/elasticsearch/docs/base.py | 10 +- designsafe/libs/elasticsearch/docs/files.py | 14 +- .../elasticsearch/docs/publication_legacy.py | 10 +- .../libs/elasticsearch/docs/publications.py | 6 +- designsafe/libs/elasticsearch/indices.py | 2 +- designsafe/libs/elasticsearch/utils.py | 26 +- designsafe/libs/fedora/fedora_operations.py | 9 +- designsafe/libs/mongo/load_ttc_grants.py | 24 +- designsafe/libs/tapis/serializers.py | 41 + designsafe/middleware.py | 27 +- designsafe/settings/celery_settings.py | 2 + designsafe/settings/common_settings.py | 59 +- designsafe/settings/elasticsearch_settings.py | 5 + .../settings/external_resource_settings.py | 2 + designsafe/settings/test_settings.py | 37 +- designsafe/sitemaps.py | 25 +- .../dashboard/dashboard.component.html | 50 +- .../modals/ttc-abstract-modal.template.html | 9 +- .../nco-ttc-grants.component.js | 68 +- .../nco-ttc-grants.template.html | 35 +- .../nco/styles/nco_ttc_grants_styles.css | 6 +- .../notification-badge.component.html | 5 +- .../controllers/notifications.js | 13 +- .../providers/notifications-provider.js | 11 +- .../static/scripts/notifications/app.js | 14 +- designsafe/static/styles/main.css | 2 +- designsafe/static/styles/ng-designsafe.css | 5 + designsafe/static/styles/variables.css | 4 +- .../vendor/bootstrap-ds/css/bootstrap.css | 4 +- designsafe/templates/403.html | 2 +- designsafe/templates/404.html | 2 +- designsafe/templates/500.html | 2 +- designsafe/templates/base.j2 | 2 + designsafe/templates/includes/header.html | 10 +- designsafe/templates/maintenance.html | 18 + designsafe/urls.py | 32 +- designsafe/utils/__init__.py | 0 designsafe/utils/encryption.py | 104 + designsafe/utils/system_access.py | 45 + designsafe/webhooks.py | 12 - docker-compose-dev.yml | 182 -- poetry.lock | 2718 +++++++++-------- portal | 1 - pyproject.toml | 31 +- requirements.txt | 168 - 481 files changed, 24327 insertions(+), 9312 deletions(-) delete mode 100644 bin/build_client.sh delete mode 100755 bin/dumpdata.sh delete mode 100755 bin/loaddata.sh delete mode 100755 bin/mysql.sh delete mode 100755 bin/run-celery-dev.sh delete mode 100755 bin/run-django.sh delete mode 100755 bin/run-flower.sh delete mode 100755 bin/run-tests.sh delete mode 100755 bin/run-uwsgi.sh rename client/modules/{datafiles/src => _common_components/src/datafiles}/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css (100%) rename client/modules/{datafiles/src => _common_components/src/datafiles}/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx (64%) rename client/modules/{datafiles/src/FileListing => _common_components/src/datafiles}/FileListingTable/FileListingTable.module.css (100%) create mode 100644 client/modules/_common_components/src/datafiles/FileListingTable/FileListingTable.tsx rename client/modules/{datafiles/src/FileListing => _common_components/src/datafiles}/FileListingTable/FileListingTableCheckbox.tsx (100%) create mode 100644 client/modules/_common_components/src/datafiles/FileTypeIcon/FileTypeIcon.tsx create mode 100644 client/modules/_common_components/src/datafiles/fileUtils.ts create mode 100644 client/modules/_common_components/src/datafiles/index.ts create mode 100644 client/modules/_common_components/src/lib/Button/Button.module.css create mode 100644 client/modules/_common_components/src/lib/Button/Button.tsx create mode 100644 client/modules/_common_components/src/lib/Button/index.ts create mode 100644 client/modules/_common_components/src/lib/Icon/Icon.module.css create mode 100644 client/modules/_common_components/src/lib/Icon/Icon.tsx create mode 100644 client/modules/_common_components/src/lib/Icon/index.ts create mode 100644 client/modules/_common_components/src/lib/Spinner/Spinner.module.css create mode 100644 client/modules/_common_components/src/lib/Spinner/Spinner.tsx create mode 100644 client/modules/_common_components/src/lib/Spinner/index.ts delete mode 100644 client/modules/_common_components/src/lib/common-components.module.css delete mode 100644 client/modules/_common_components/src/lib/common-components.spec.tsx delete mode 100644 client/modules/_common_components/src/lib/common-components.tsx create mode 100644 client/modules/_hooks/src/datafiles/nees/useNeesDetails.ts create mode 100644 client/modules/_hooks/src/datafiles/projects/useChangeProjectType.ts create mode 100644 client/modules/_hooks/src/datafiles/projects/useCheckFilesForAssociation.ts create mode 100644 client/modules/_hooks/src/datafiles/projects/useCreateEntity.ts create mode 100644 client/modules/_hooks/src/datafiles/projects/useCreateProject.ts create mode 100644 client/modules/_hooks/src/datafiles/projects/useDeleteEntity.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/useAmendProject.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/useCreateFeedbackTicket.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/useDataciteEvents.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/useDataciteMetrics.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/useDoiContext.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/usePublishProject.ts create mode 100644 client/modules/_hooks/src/datafiles/publications/useVersionProject.ts create mode 100644 client/modules/_hooks/src/datafiles/useFileDetail.ts create mode 100644 client/modules/_hooks/src/datafiles/useFileMove.ts create mode 100644 client/modules/_hooks/src/notifications/index.ts create mode 100644 client/modules/_hooks/src/notifications/useNotifications.ts create mode 100644 client/modules/_hooks/src/notifications/useNotifyContext.ts create mode 100644 client/modules/_hooks/src/systems/index.ts create mode 100644 client/modules/_hooks/src/systems/types.ts create mode 100644 client/modules/_hooks/src/systems/useGetSystems.ts create mode 100644 client/modules/_hooks/src/systems/usePushKeys.ts create mode 100644 client/modules/_hooks/src/workspace/types.ts create mode 100644 client/modules/_hooks/src/workspace/useGetAllocations.ts create mode 100644 client/modules/_hooks/src/workspace/useGetApps.ts create mode 100644 client/modules/_hooks/src/workspace/useGetJobs.ts create mode 100644 client/modules/_hooks/src/workspace/useJobsListing.ts create mode 100644 client/modules/_hooks/src/workspace/usePostJobs.ts create mode 100644 client/modules/_test-fixtures/src/fixtures/workspace/allocations-listing.json delete mode 100644 client/modules/_test-fixtures/src/fixtures/workspace/apps-listing.json create mode 100644 client/modules/_test-fixtures/src/fixtures/workspace/apps-tray-listing.json create mode 100644 client/modules/_test-fixtures/src/fixtures/workspace/systems-listing.json create mode 100644 client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.module.css create mode 100644 client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/DownloadModal/DownloadModal.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/DownloadModal/index.ts create mode 100644 client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.module.css create mode 100644 client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.tsx create mode 100644 client/modules/datafiles/src/DatafilesModal/MoveModal/index.ts create mode 100644 client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewMetadata.tsx create mode 100644 client/modules/datafiles/src/nees/NeesDetails.module.css create mode 100644 client/modules/datafiles/src/nees/NeesDetails.tsx create mode 100644 client/modules/datafiles/src/projects/EmptyProjectFileListing.tsx create mode 100644 client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.module.css create mode 100644 client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.module.css create mode 100644 client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.tsx create mode 100644 client/modules/datafiles/src/projects/PublishableEntityButton.tsx create mode 100644 client/modules/datafiles/src/projects/SubEntityDetails.tsx create mode 100644 client/modules/datafiles/src/projects/forms/CreateProjectForm.tsx create mode 100644 client/modules/datafiles/src/projects/forms/ProjectCategoryFormHelp.tsx create mode 100644 client/modules/datafiles/src/projects/modals/BaseProjectCreateModal.tsx create mode 100644 client/modules/datafiles/src/projects/modals/MetricsModal.tsx create mode 100644 client/modules/datafiles/src/projects/modals/PipelineEditCategoryModal.tsx create mode 100644 client/modules/datafiles/src/projects/modals/ProjectBestPracticesModal.tsx create mode 100644 client/modules/datafiles/src/projects/modals/ProjectDataTransferModal.tsx create mode 100644 client/modules/datafiles/src/projects/modals/ProjectInfoModal.tsx create mode 100644 client/modules/datafiles/src/projects/modals/VersionChangesModal.tsx create mode 100644 client/modules/datafiles/src/projects/utils.ts create mode 100644 client/modules/datafiles/src/publications/modals/DownloadDatasetModal.tsx create mode 100644 client/modules/datafiles/src/publications/modals/SubmitFeedbackModal.tsx create mode 100644 client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.module.css create mode 100644 client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.tsx create mode 100644 client/modules/workspace/src/AppsSideNav/AppSideNav.spec.tsx create mode 100644 client/modules/workspace/src/AppsSideNav/AppsSideNav.tsx create mode 100644 client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.module.css create mode 100644 client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx create mode 100644 client/modules/workspace/src/AppsSubmissionForm/AppIcon.module.css create mode 100644 client/modules/workspace/src/AppsSubmissionForm/AppIcon.tsx create mode 100644 client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx create mode 100644 client/modules/workspace/src/AppsWizard/AppsFormSchema.ts create mode 100644 client/modules/workspace/src/AppsWizard/AppsWizard.module.css create mode 100644 client/modules/workspace/src/AppsWizard/AppsWizard.tsx create mode 100644 client/modules/workspace/src/AppsWizard/FormField.tsx create mode 100644 client/modules/workspace/src/AppsWizard/Steps.tsx create mode 100644 client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.module.css create mode 100644 client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx create mode 100644 client/modules/workspace/src/InteractiveSessionModal/index.ts create mode 100644 client/modules/workspace/src/JobStatusNav/JobStatusNav.module.css create mode 100644 client/modules/workspace/src/JobStatusNav/JobStatusNav.spec.tsx create mode 100644 client/modules/workspace/src/JobStatusNav/JobStatusNav.tsx create mode 100644 client/modules/workspace/src/JobsDetailModal/JobsDetailModal.module.css create mode 100644 client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx create mode 100644 client/modules/workspace/src/JobsListing/JobsListing.module.css create mode 100644 client/modules/workspace/src/JobsListing/JobsListing.tsx create mode 100644 client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.module.css rename client/modules/{datafiles/src/FileListing/FileListingTable/FileListingTable.tsx => workspace/src/JobsListing/JobsListingTable/JobsListingTable.tsx} (59%) create mode 100644 client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTableCheckbox.tsx create mode 100644 client/modules/workspace/src/JobsReuseInputsButton/JobsReuseInputsButton.tsx create mode 100644 client/modules/workspace/src/SelectModal/SelectModal.module.css create mode 100644 client/modules/workspace/src/SelectModal/SelectModal.tsx create mode 100644 client/modules/workspace/src/SelectModal/SelectModalProjectListing.tsx create mode 100644 client/modules/workspace/src/SystemsPushKeysModal/SystemsPushKeysModal.tsx create mode 100644 client/modules/workspace/src/Toast/Notifications.module.css create mode 100644 client/modules/workspace/src/Toast/Toast.tsx create mode 100644 client/modules/workspace/src/Toast/index.tsx create mode 100644 client/modules/workspace/src/constants.ts delete mode 100644 client/modules/workspace/src/lib/workspace.module.css delete mode 100644 client/modules/workspace/src/lib/workspace.spec.tsx delete mode 100644 client/modules/workspace/src/lib/workspace.tsx create mode 100644 client/modules/workspace/src/utils/apps.ts create mode 100644 client/modules/workspace/src/utils/index.ts create mode 100644 client/modules/workspace/src/utils/jobs.ts create mode 100644 client/modules/workspace/src/utils/notifications.ts create mode 100644 client/modules/workspace/src/utils/systems.ts create mode 100644 client/modules/workspace/src/utils/timeFormat.ts create mode 100644 client/modules/workspace/src/utils/truncateMiddle.ts rename client/{index.html => react-assets.html} (100%) create mode 100644 client/src/datafiles/layouts/published/PublishedDetailLayout.module.css create mode 100644 client/src/datafiles/layouts/published/PublishedListingLayout.module.css create mode 100644 client/src/styles/modal.css delete mode 100644 client/src/workspace/Workspace.module.css delete mode 100644 client/src/workspace/Workspace.spec.tsx delete mode 100644 client/src/workspace/Workspace.tsx create mode 100644 client/src/workspace/layouts/AppsPlaceholderLayout.tsx create mode 100644 client/src/workspace/layouts/AppsViewLayout.tsx create mode 100644 client/src/workspace/layouts/JobsListingLayout.tsx create mode 100644 client/src/workspace/layouts/WorkspaceBaseLayout.spec.tsx create mode 100644 client/src/workspace/layouts/WorkspaceBaseLayout.tsx create mode 100644 client/src/workspace/layouts/layout.module.css delete mode 100644 conf/bootstrap-config.json delete mode 100644 conf/docker/colorize_logs.awk delete mode 100644 conf/docker/docker-compose-build.yml delete mode 100644 conf/docker/docker-compose-dev.all.yml delete mode 100644 conf/docker/docker-compose.backup.yml delete mode 100644 conf/elasticsearch/elasticsearch.sample.yml delete mode 100644 conf/env_files/mysql.sample.env delete mode 100644 conf/mysql.sample.cnf delete mode 100644 conf/newrelic.ini delete mode 100644 conf/nginx/error.html delete mode 100644 conf/nginx/nginx.conf delete mode 100644 conf/rabbitmq.sample.conf delete mode 100644 conf/redis.sample.conf delete mode 100644 conf/supervisor.conf delete mode 100644 conf/uwsgi/uwsgi_websocket.ini delete mode 100644 designsafe/apps/accounts/fixtures/user-data.json rename designsafe/apps/api/datafiles/operations/{agave_operations.py => tapis_operations.py} (65%) create mode 100644 designsafe/apps/api/filemeta/tasks.py delete mode 100644 designsafe/apps/api/fixtures/agave-oauth-token-data.json delete mode 100644 designsafe/apps/api/fixtures/user-data.json delete mode 100644 designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json delete mode 100644 designsafe/apps/api/notifications/fixtures/user-data.json delete mode 100644 designsafe/apps/api/notifications/views/webhooks.py create mode 100644 designsafe/apps/api/projects_v2/conftest.py create mode 100644 designsafe/apps/api/projects_v2/management/__init__.py create mode 100644 designsafe/apps/api/projects_v2/management/commands/__init__.py create mode 100644 designsafe/apps/api/projects_v2/management/commands/migrate_projects_tapis_systems_from_v2_to_v3.py create mode 100644 designsafe/apps/api/projects_v2/tasks.py create mode 100644 designsafe/apps/api/projects_v2/views_unit_test.py create mode 100644 designsafe/apps/api/publications_v2/elasticsearch.py create mode 100644 designsafe/apps/api/publications_v2/operations/fedora_graph_operations.py create mode 100644 designsafe/apps/api/publications_v2/tasks.py create mode 100644 designsafe/apps/api/systems/__init__.py create mode 100644 designsafe/apps/api/systems/ssh_keys_manager.py create mode 100644 designsafe/apps/api/systems/urls.py create mode 100644 designsafe/apps/api/systems/utils.py create mode 100644 designsafe/apps/api/systems/views.py create mode 100644 designsafe/apps/auth/backends_unit_test.py delete mode 100644 designsafe/apps/auth/context_processors.py create mode 100644 designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py create mode 100644 designsafe/apps/auth/models_unit_test.py create mode 100644 designsafe/apps/auth/views_unit_test.py delete mode 100644 designsafe/apps/box_integration/fixtures/user-data.json delete mode 100644 designsafe/apps/dashboard/fixtures/user-data.json delete mode 100644 designsafe/apps/rapid/fixtures/user-data.json delete mode 100644 designsafe/apps/token_access/fixtures/user-data.json create mode 100644 designsafe/apps/webhooks/__init__.py create mode 100644 designsafe/apps/webhooks/apps.py create mode 100644 designsafe/apps/webhooks/fixtures/job_event.json create mode 100644 designsafe/apps/webhooks/fixtures/job_failed.json create mode 100644 designsafe/apps/webhooks/fixtures/job_running.json create mode 100644 designsafe/apps/webhooks/fixtures/job_staging.json create mode 100644 designsafe/apps/webhooks/unit_test.py create mode 100644 designsafe/apps/webhooks/urls.py create mode 100644 designsafe/apps/webhooks/views.py create mode 100644 designsafe/apps/workspace/api/__init__.py create mode 100644 designsafe/apps/workspace/api/tas_to_tacc_resources.json create mode 100644 designsafe/apps/workspace/api/tasks.py create mode 100644 designsafe/apps/workspace/api/urls.py create mode 100644 designsafe/apps/workspace/api/utils.py create mode 100644 designsafe/apps/workspace/api/views.py delete mode 100644 designsafe/apps/workspace/fixtures/agave-oauth-token-data.json delete mode 100644 designsafe/apps/workspace/fixtures/user-data.json create mode 100644 designsafe/apps/workspace/migrations/0013_userallocations.py create mode 100644 designsafe/apps/workspace/migrations/0014_appvariant_short_label.py create mode 100644 designsafe/apps/workspace/migrations/0015_alter_apptraycategory_priority.py create mode 100644 designsafe/apps/workspace/migrations/0016_applistingentry_user_guide_link.py create mode 100644 designsafe/apps/workspace/models/allocations.py delete mode 100644 designsafe/apps/workspace/tasks.py create mode 100644 designsafe/fixtures/auth.json create mode 100644 designsafe/fixtures/tapis/auth/create-tokens-response.json create mode 100644 designsafe/fixtures/user-data.json create mode 100644 designsafe/libs/tapis/serializers.py create mode 100644 designsafe/templates/maintenance.html create mode 100644 designsafe/utils/__init__.py create mode 100644 designsafe/utils/encryption.py create mode 100644 designsafe/utils/system_access.py delete mode 100644 designsafe/webhooks.py delete mode 100644 docker-compose-dev.yml delete mode 160000 portal delete mode 100644 requirements.txt diff --git a/.dockerignore b/.dockerignore index 37a0a977b4..0de5cabe87 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,4 +8,8 @@ coverage .docs .github .pytest_cache -data \ No newline at end of file +data + +client/node_modules +client/.nx +client/dist diff --git a/.docs/source/designsafe.apps.auth.rst b/.docs/source/designsafe.apps.auth.rst index 0d872b14a2..ccdbdd0f2a 100644 --- a/.docs/source/designsafe.apps.auth.rst +++ b/.docs/source/designsafe.apps.auth.rst @@ -28,14 +28,6 @@ designsafe.apps.auth.backends module :undoc-members: :show-inheritance: -designsafe.apps.auth.context_processors module ----------------------------------------------- - -.. automodule:: designsafe.apps.auth.context_processors - :members: - :undoc-members: - :show-inheritance: - designsafe.apps.auth.middleware module -------------------------------------- diff --git a/.flake8 b/.flake8 index d5604d56e3..3c81c28f5d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,10 @@ [flake8] # E501: line is too long. # H101: Use TODO(NAME) -ignore = E501, H101 +# W503: line break before binary operator. Ingore as black will break this rule. +ignore = E501, H101, W503 exclude = __pycache__, tests.py, migrations + +extend-ignore = W503 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aa17acbf10..75fa1b30e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,9 @@ name: CI -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch +# Controls when the action will run. Triggers the workflow on pushes to main or on pull request events on: push: - branches: [ master ] + branches: [ main ] pull_request: branches: [ '**' ] @@ -17,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Fetch base and install Poetry - run: | + run: | git fetch origin ${{github.base_ref}} pipx install poetry @@ -34,6 +33,30 @@ jobs: - run: | poetry install + - name: Run Server-side unit tests and generate coverage report + run: | + poetry run pytest --cov-config=.coveragerc --cov=designsafe --cov-report=xml -ra designsafe + + Server_Side_Linting: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Fetch base and install Poetry + run: | + git fetch origin ${{github.base_ref}} + pipx install poetry + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'poetry' + + - name: Install Python Packages + run: | + poetry install + - name: Run Server-side linting with pytest # Only run on new files for now-- for all changes, filter is ACMRTUXB # Check manage.py to prevent a crash if no files are selected. @@ -44,16 +67,12 @@ jobs: run: | poetry run black $(git diff --name-only --diff-filter=A origin/${{github.base_ref}} | grep -E "(.py$)") manage.py --check - - name: Run Server-side unit tests and generate coverage report - run: | - poetry run pytest --cov-config=.coveragerc --cov=designsafe --cov-report=xml -ra designsafe - Client_Side_Unit_Tests: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Setup Node.js for use with actions - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 16.x cache: npm @@ -64,12 +83,12 @@ jobs: React_NX_unit_tests: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js for use with actions - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 20 cache: npm @@ -78,14 +97,35 @@ jobs: working-directory: client - uses: nrwl/nx-set-shas@v3 - # Check linting/formatting of workspace files. - - run: npx nx format:check - working-directory: client - # Lint/test/build any apps and libs that have been impacted by the diff. - - run: npx nx affected --target=lint --parallel=3 - working-directory: client + # Test/build any apps and libs that have been impacted by the diff. - run: npx nx affected --target=test --parallel=3 --ci --code-coverage working-directory: client - run: npx nx affected --target=build --parallel=3 working-directory: client + + React_NX_linting: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js for use with actions + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + working-directory: client + + - uses: nrwl/nx-set-shas@v3 + + # Check linting/formatting of workspace files. + - run: npx nx format:check + working-directory: client + + # Lint any apps and libs that have been impacted by the diff. + - run: npx nx affected --target=lint --parallel=3 + working-directory: client diff --git a/.gitignore b/.gitignore index 8bd0b28d09..dbcc6364ed 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ settings.json designsafe/apps/rapid/static/designsafe/apps/rapid/build/bundle.* designsafe/apps/geo/static/designsafe/apps/geo/build/bundle.* designsafe/static/build/ +designsafe/static/react-assets/ +designsafe/templates/react-assets.html # designsafe/static/styles/base.* /static diff --git a/.pylintrc b/.pylintrc index 5a4da2b364..d8f0f80226 100644 --- a/.pylintrc +++ b/.pylintrc @@ -583,7 +583,7 @@ ignored-checks-for-mixins=no-member, # List of class names for which member attributes should not be checked (useful # for classes with dynamically set attributes). This supports the use of # qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace,Tapis # Show a hint with possible names when a member name was not found. The aspect # of finding the hint is based on edit distance. diff --git a/Makefile b/Makefile index 98bd8815d6..0104e5c8b2 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,23 @@ .PHONY: build build: - docker-compose -f ./conf/docker/docker-compose.yml build + docker compose -f ./conf/docker/docker-compose.yml build + +.PHONY: build-dev +build-dev: + docker compose -f ./conf/docker/docker-compose-dev.yml build .PHONY: start start: - docker-compose -f ./conf/docker/docker-compose-dev.all.debug.yml up + docker compose -f ./conf/docker/docker-compose-dev.all.debug.yml up .PHONY: stop stop: - docker-compose -f ./conf/docker/docker-compose-dev.all.debug.yml down + docker compose -f ./conf/docker/docker-compose-dev.all.debug.yml down + +.PHONY: start-m1 +start-m1: + docker compose -f ./conf/docker/docker-compose-dev.all.debug.m1.yml up + +.PHONY: stop-m1 +stop-m1: + docker compose -f ./conf/docker/docker-compose-dev.all.debug.m1.yml down diff --git a/README.md b/README.md index 7e939da5f1..b2e91f8b7a 100644 --- a/README.md +++ b/README.md @@ -40,34 +40,37 @@ If you are on a Mac or a Windows machine, the recommended method is to install - `AGAVE_*`: should be set to enable Agave API integration (authentication, etc.) - `RT_*`: should be set to enable ticketing - Make copies of [rabbitmq.sample.env](conf/env_files/rabbitmq.sample.env) and [mysql.sample.env](conf/env_files/mysql.sample.env), - then rename them to `rabbitmq.env` and `mysql.env`. - - Make copies of [mysql.sample.cnf](conf/mysql.sample.cnf), [redis.sample.conf](conf/redis.sample.conf), - and [rabbitmq.sample.conf](conf/rabbitmq.sample.conf), then rename them to `mysql.cnf`, `redis.conf`, and `rabbitmq.conf`. + Make a copy of [rabbitmq.sample.env](conf/env_files/rabbitmq.sample.env) + then rename it to `rabbitmq.env`. Make a copy of [external_resource_secrets.sample.py](designsafe/settings/external_resource_secrets.sample.py) and rename it to `external_resource_secrets.py`. -3. Build the containers and frontend package +3. Build the containers and frontend packages - ``` - $ make build - ``` - or - ``` - $ docker-compose -f conf/docker/docker-compose.yml build - ``` + 1. Containers: + ```sh + make build-dev + ``` + or + ```sh + docker-compose -f conf/docker/docker-compose-dev.yml build + ``` - These lines install the node packages required for DesignSafe, - and build the frontend package. - ``` - $ npm ci - $ npm run build - ``` + 2. Angular Frontend + static assets: + ```sh + npm ci + docker run -v `pwd`:`pwd` -w `pwd` -it node:16 /bin/bash -c "npm run build" + ``` - If you are working with the frontend code and want it to automatically update, - use `npm run dev` rather than `npm run build` to have it build upon saving the file. + **Note:** If you are working with the frontend code and want it to automatically update, use `npm run dev` rather than `npm run build` to have it build upon saving the file. + + 3. React Frontend (in another terminal): + ```sh + cd client + npm ci + npm run start + ``` 4. Start local containers @@ -82,7 +85,7 @@ If you are on a Mac or a Windows machine, the recommended method is to install ``` $ docker exec -it des_django bash $ ./manage.py migrate - $ ./manage.py collectstatic -i demo + $ ./manage.py collectstatic --ignore demo --no-input $ ./manage.py createsuperuser ``` @@ -225,8 +228,8 @@ $ docker-compose -f conf/docker/docker-compose-dev.all.debug.yml up $ npm run dev ``` -When using this compose file, your Agave Client should be configured with a `callback_url` -of `http://$DOCKER_HOST_IP:8000/auth/agave/callback/`. +When using this compose file, your Tapis Client should be configured with a `callback_url` +of `http://$DOCKER_HOST_IP:8000/auth/tapis/callback/`. For developing some services, e.g. Box.com integration, https support is required. To enable an Nginx http proxy run using the [`docker-compose-http.yml`](docker-compose-http.yml) @@ -238,9 +241,6 @@ $ docker-compose -f docker-compose-http.yml build $ docker-compose -f docker-compose-http.yml up ``` -When using this compose file, your Agave Client should be configured with a `callback_url` -of `https://$DOCKER_HOST_IP/auth/agave/callback/`. - ### Agave filesystem setup 1. Delete all of the old metadata objects using this command: diff --git a/bin/build_client.sh b/bin/build_client.sh deleted file mode 100644 index 90421c977e..0000000000 --- a/bin/build_client.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -x -if [ "$1" == false ] ; then - cd /srv/www/designsafe - npm ci && npm run build -fi diff --git a/bin/dumpdata.sh b/bin/dumpdata.sh deleted file mode 100755 index f9bc38c80c..0000000000 --- a/bin/dumpdata.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -python /portal/manage.py dumpdata \ - --natural-foreign --natural-primary \ - --exclude=cmsplugin_cascade.Segmentation \ - --exclude=admin.logentry \ - --exclude=cms.pageusergroup > /datadump/datadump-`date +%Y%m%d`.json diff --git a/bin/loaddata.sh b/bin/loaddata.sh deleted file mode 100755 index 828939f56e..0000000000 --- a/bin/loaddata.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -DATE=`date +%Y%m%d` -echo "Flushing current database..." -python /portal/manage.py flush --no-initial-data -echo "Loading data from file datadump-`date +%Y%m%d`.json..." -python /portal/manage.py loaddata /datadump/datadump-${DATE}.json -echo "Copying db.sqlite3 out of container..." -cp db.sqlite3 /datadump/db-${DATE}.sqlite3 -echo "Done!" diff --git a/bin/mysql.sh b/bin/mysql.sh deleted file mode 100755 index f1f641af19..0000000000 --- a/bin/mysql.sh +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env bash diff --git a/bin/run-celery-debug.sh b/bin/run-celery-debug.sh index 55602a0982..3e2d894794 100755 --- a/bin/run-celery-debug.sh +++ b/bin/run-celery-debug.sh @@ -5,4 +5,4 @@ # celery -A designsafe beat -l info --pidfile= --schedule=/tmp/celerybeat-schedule & celery -A designsafe worker -l info --autoscale=15,5 -Q indexing,files -n designsafe_worker01 & -celery -A designsafe worker -l info --autoscale=10,3 -Q default,api -n designsafe_worker02 \ No newline at end of file +celery -A designsafe worker -l info --autoscale=10,3 -Q default,api,onboarding -n designsafe_worker02 diff --git a/bin/run-celery-dev.sh b/bin/run-celery-dev.sh deleted file mode 100755 index dc7f4678e5..0000000000 --- a/bin/run-celery-dev.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -# Run Celery as the DesignSafe Community Account -celery -A designsafe beat -l info --pidfile= --schedule=/tmp/celerybeat-schedule & -celery -A designsafe worker -l info --autoscale=15,5 -Q indexing,files & -celery -A designsafe worker -l info --autoscale=10,3 -Q default,api" \ No newline at end of file diff --git a/bin/run-celery.sh b/bin/run-celery.sh index 092e3eb671..1544da1f74 100755 --- a/bin/run-celery.sh +++ b/bin/run-celery.sh @@ -3,4 +3,4 @@ # Run Celery as the DesignSafe Community Account celery -A designsafe beat -l info --pidfile= --schedule=/tmp/celerybeat-schedule & celery -A designsafe worker -l info --autoscale=15,5 -Q indexing,files -n designsafe_worker01 & -celery -A designsafe worker -l info --autoscale=10,3 -Q default,api -n designsafe_worker02 \ No newline at end of file +celery -A designsafe worker -l info --autoscale=10,3 -Q default,api,onboarding -n designsafe_worker02 diff --git a/bin/run-django.sh b/bin/run-django.sh deleted file mode 100755 index 25fca63107..0000000000 --- a/bin/run-django.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# run django dev server as designsafe community account -python -m debugpy --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000 diff --git a/bin/run-flower.sh b/bin/run-flower.sh deleted file mode 100755 index f869d64714..0000000000 --- a/bin/run-flower.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -## -# Run Flower monitor UI -# -su tg458981 -c "flower -A designsafe proj --broker=$FLOWER_BROKER --broker_api=$FLOWER_BROKER_API" diff --git a/bin/run-tests.sh b/bin/run-tests.sh deleted file mode 100755 index 5ad58bcf9b..0000000000 --- a/bin/run-tests.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -### -# Run front-end tests -### - -# Start Xvfb -test -e /tmp/.X99-lock -/usr/bin/Xvfb :99 & -xvfb=$! - -export DISPLAY=:99.0 -export CHROME_BIN=/usr/bin/chromium-browser - -/portal/node_modules/.bin/karma start /portal/karma.conf.js --single-run - -kill -TERM $xvfb -wait $xvfb diff --git a/bin/run-uwsgi.sh b/bin/run-uwsgi.sh deleted file mode 100755 index 8b10fecd74..0000000000 --- a/bin/run-uwsgi.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -/usr/local/bin/uwsgi --ini /portal/conf/uwsgi_websocket.ini diff --git a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css b/client/modules/_common_components/src/datafiles/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css similarity index 100% rename from client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css rename to client/modules/_common_components/src/datafiles/DatafilesBreadcrumb/DatafilesBreadcrumb.module.css diff --git a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx b/client/modules/_common_components/src/datafiles/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx similarity index 64% rename from client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx rename to client/modules/_common_components/src/datafiles/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx index dda77dfcb4..50c7a922db 100644 --- a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx +++ b/client/modules/_common_components/src/datafiles/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx @@ -6,27 +6,18 @@ import { getSystemRootDisplayName, useAuthenticatedUser } from '@client/hooks'; function getPathRoutes( baseRoute: string, path: string = '', - systemRoot: string = '', - systemRootAlias?: string + relativeTo: string = '' ) { - const pathComponents = decodeURIComponent(path.replace(systemRoot, '')) + const pathComponents = path + .replace(relativeTo, '') .split('/') .filter((p) => !!p); - - const systemRootBreadcrumb = { - path: `${baseRoute}/${systemRoot}`, - title: systemRootAlias ?? 'Data Files', - }; - - return [ - systemRootBreadcrumb, - ...pathComponents.map((comp, i) => ({ - title: comp, - path: `${baseRoute}/${systemRoot}${encodeURIComponent( - '/' + pathComponents.slice(0, i + 1).join('/') - )}`, - })), - ]; + return pathComponents.map((comp, i) => ({ + title: comp, + path: `${baseRoute}/${encodeURIComponent(relativeTo)}${encodeURIComponent( + '/' + pathComponents.slice(0, i + 1).join('/') + )}`, + })); } export const DatafilesBreadcrumb: React.FC< @@ -36,7 +27,6 @@ export const DatafilesBreadcrumb: React.FC< baseRoute: string; systemRoot: string; systemRootAlias?: string; - skipBreadcrumbs?: number; // Number of path elements to skip when generating breadcrumbs } & BreadcrumbProps > = ({ initialBreadcrumbs, @@ -44,14 +34,11 @@ export const DatafilesBreadcrumb: React.FC< baseRoute, systemRoot, systemRootAlias, - skipBreadcrumbs, ...props }) => { const breadcrumbItems = [ ...initialBreadcrumbs, - ...getPathRoutes(baseRoute, path, systemRoot, systemRootAlias).slice( - skipBreadcrumbs ?? 0 - ), + ...getPathRoutes(baseRoute, path, systemRoot), ]; return ( @@ -77,6 +64,7 @@ export const BaseFileListingBreadcrumb: React.FC< path: string; systemRootAlias?: string; initialBreadcrumbs?: { title: string; path: string }[]; + systemLabel?: string; } & BreadcrumbProps > = ({ api, @@ -84,18 +72,25 @@ export const BaseFileListingBreadcrumb: React.FC< path, systemRootAlias, initialBreadcrumbs = [], + systemLabel, ...props }) => { const { user } = useAuthenticatedUser(); - + const rootAlias = + systemRootAlias || getSystemRootDisplayName(api, system, systemLabel); + const systemRoot = isUserHomeSystem(system) ? '/' + user?.username : ''; return ( diff --git a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.module.css b/client/modules/_common_components/src/datafiles/FileListingTable/FileListingTable.module.css similarity index 100% rename from client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.module.css rename to client/modules/_common_components/src/datafiles/FileListingTable/FileListingTable.module.css diff --git a/client/modules/_common_components/src/datafiles/FileListingTable/FileListingTable.tsx b/client/modules/_common_components/src/datafiles/FileListingTable/FileListingTable.tsx new file mode 100644 index 0000000000..668b752d88 --- /dev/null +++ b/client/modules/_common_components/src/datafiles/FileListingTable/FileListingTable.tsx @@ -0,0 +1,216 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styles from './FileListingTable.module.css'; +import { Alert, Table, TableColumnType, TableProps } from 'antd'; +import { + useFileListing, + TFileListing, + useSelectedFiles, + useDoiContext, +} from '@client/hooks'; +import { FileListingTableCheckbox } from './FileListingTableCheckbox'; +import parse from 'html-react-parser'; + +type TableRef = { + nativeElement: HTMLDivElement; + scrollTo: (config: { index?: number; key?: React.Key; top?: number }) => void; +}; + +export type TFileListingColumns = (TableColumnType & { + dataIndex: keyof TFileListing; +})[]; + +export const FileListingTable: React.FC< + { + api: string; + system?: string; + path?: string; + scheme?: string; + columns: TFileListingColumns; + filterFn?: (listing: TFileListing[]) => TFileListing[]; + disabled?: boolean; + className?: string; + emptyListingDisplay?: React.ReactNode; + noSelection?: boolean; + searchTerm?: string | null; + currentDisplayPath?: TFileListing | undefined; + } & Omit +> = ({ + api, + system, + path = '', + scheme = 'private', + filterFn, + columns, + disabled = false, + className, + emptyListingDisplay, + searchTerm = '', + noSelection, + currentDisplayPath = null, + ...props +}) => { + const limit = 100; + const [scrollElement, setScrollElement] = useState( + undefined + ); + + /* FETCH FILE LISTINGS */ + const { + data, + isLoading, + error, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useFileListing({ + api, + system: system ?? '-', + path: path ?? '', + scheme, + disabled, + searchTerm, + pageSize: limit, + }); + + const combinedListing = useMemo(() => { + const cl: TFileListing[] = []; + data?.pages.forEach((page) => cl.push(...page.listing)); + if (filterFn) { + return filterFn(cl); + } + if (currentDisplayPath) { + return [currentDisplayPath, ...cl]; + } + + return cl; + }, [data, filterFn, currentDisplayPath]); + + /* HANDLE FILE SELECTION */ + const doi = useDoiContext(); + const { selectedFiles, setSelectedFiles } = useSelectedFiles( + api, + system ?? '-', + path + ); + const onSelectionChange = useCallback( + (_: React.Key[], selection: TFileListing[]) => { + setSelectedFiles(doi ? selection.map((s) => ({ ...s, doi })) : selection); + }, + [setSelectedFiles, doi] + ); + const selectedRowKeys = useMemo( + () => selectedFiles.map((s) => s.path), + [selectedFiles] + ); + + /* HANDLE INFINITE SCROLL */ + const scrollRefCallback = useCallback( + (node: TableRef) => { + if (node !== null) { + const lastRow = node.nativeElement.querySelectorAll( + '.ant-table-row:last-child' + )[0]; + setScrollElement(lastRow); + } + }, + [setScrollElement] + ); + useEffect(() => { + // Set and clean up scroll event listener on the table ref. + const observer = new IntersectionObserver((entries) => { + // Fetch the next page when the final listing item enters the viewport. + entries.forEach((entry) => { + if ( + entry.isIntersecting && + hasNextPage && + !(isFetchingNextPage || isLoading) + ) { + fetchNextPage(); + } + }); + }); + scrollElement && observer.observe(scrollElement); + + return () => { + observer.disconnect(); + }; + }, [ + scrollElement, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + isLoading, + ]); + + /* RENDER THE TABLE */ + return ( + 0 ? 'table--pull-spinner-bottom' : '' + } ${className ?? ''}`} + rowSelection={ + noSelection + ? undefined + : { + type: 'checkbox', + onChange: onSelectionChange, + selectedRowKeys, + renderCell: (checked, _rc, _idx, node) => ( + + ), + } + } + scroll={{ y: '100%', x: '1000px' }} // set to undefined to disable sticky header + columns={columns} + rowKey={(record) => record.path} + dataSource={combinedListing} + pagination={false} + loading={isLoading || isFetchingNextPage} + locale={{ + emptyText: + isLoading || isFetchingNextPage ? ( +
 
+ ) : ( + <> + {error && ( + + {parse(error.response?.data.message ?? '')} + {system?.includes('project') && ( +
+ + If this is a newly created project, it may take a + few minutes for file system permissions to + propagate. + +
+ )} + + } + /> + )} + {!error && ( + + )} + + ), + }} + {...props} + > + placeholder +
+ ); +}; diff --git a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTableCheckbox.tsx b/client/modules/_common_components/src/datafiles/FileListingTable/FileListingTableCheckbox.tsx similarity index 100% rename from client/modules/datafiles/src/FileListing/FileListingTable/FileListingTableCheckbox.tsx rename to client/modules/_common_components/src/datafiles/FileListingTable/FileListingTableCheckbox.tsx diff --git a/client/modules/_common_components/src/datafiles/FileTypeIcon/FileTypeIcon.tsx b/client/modules/_common_components/src/datafiles/FileTypeIcon/FileTypeIcon.tsx new file mode 100644 index 0000000000..cf017b3339 --- /dev/null +++ b/client/modules/_common_components/src/datafiles/FileTypeIcon/FileTypeIcon.tsx @@ -0,0 +1,62 @@ +function icon(name: string, type?: string) { + if (type === 'dir' || type === 'folder') { + return 'fa-folder'; + } + const ext = (name.split('.').pop() ?? '').toLowerCase(); + + switch (ext) { + case 'zip': + case 'tar': + case 'gz': + case 'bz2': + return 'fa-file-archive-o'; + case 'png': + case 'jpg': + case 'jpeg': + case 'gif': + case 'tif': + case 'tiff': + return 'fa-file-image-o'; + case 'pdf': + return 'fa-file-pdf-o'; + case 'doc': + case 'docx': + return 'fa-file-word-o'; + case 'xls': + case 'xlsx': + return 'fa-file-excel-o'; + case 'ppt': + case 'pptx': + return 'fa-file-powerpoint-o'; + case 'ogg': + case 'webm': + case 'mp4': + return 'fa-file-video-o'; + case 'mp3': + case 'wav': + return 'fa-file-audio-o'; + case 'txt': + case 'out': + case 'err': + return 'fa-file-text-o'; + case 'tcl': + case 'sh': + case 'json': + return 'fa-file-code-o'; + case 'geojson': + case 'kml': + case 'kmz': + return 'fa-map-o'; + default: + return 'fa-file-o'; + } +} + +export const FileTypeIcon: React.FC<{ name: string; type?: string }> = ({ + name, + type, +}) => { + const iconClassName = icon(name, type); + const className = `fa ${iconClassName}`; + return ; +}; diff --git a/client/modules/_common_components/src/datafiles/fileUtils.ts b/client/modules/_common_components/src/datafiles/fileUtils.ts new file mode 100644 index 0000000000..be932895a8 --- /dev/null +++ b/client/modules/_common_components/src/datafiles/fileUtils.ts @@ -0,0 +1,9 @@ +export function toBytes(bytes?: number) { + if (bytes === 0) return '0 bytes'; + if (!bytes) return '-'; + const units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']; + const orderOfMagnitude = Math.floor(Math.log(bytes) / Math.log(1024)); + const precision = orderOfMagnitude === 0 ? 0 : 1; + const bytesInUnits = bytes / Math.pow(1024, orderOfMagnitude); + return `${bytesInUnits.toFixed(precision)} ${units[orderOfMagnitude]}`; +} diff --git a/client/modules/_common_components/src/datafiles/index.ts b/client/modules/_common_components/src/datafiles/index.ts new file mode 100644 index 0000000000..c0114b5b1f --- /dev/null +++ b/client/modules/_common_components/src/datafiles/index.ts @@ -0,0 +1,4 @@ +export * from './FileListingTable/FileListingTable'; +export * from './DatafilesBreadcrumb/DatafilesBreadcrumb'; +export * from './FileTypeIcon/FileTypeIcon'; +export { toBytes } from './fileUtils'; diff --git a/client/modules/_common_components/src/index.ts b/client/modules/_common_components/src/index.ts index e84894599d..f93d99586e 100644 --- a/client/modules/_common_components/src/index.ts +++ b/client/modules/_common_components/src/index.ts @@ -1 +1,4 @@ -export * from './lib/common-components'; +export * from './datafiles'; +export { PrimaryButton, SecondaryButton } from './lib/Button'; +export { Icon } from './lib/Icon'; +export { Spinner } from './lib/Spinner'; diff --git a/client/modules/_common_components/src/lib/Button/Button.module.css b/client/modules/_common_components/src/lib/Button/Button.module.css new file mode 100644 index 0000000000..85dc4e27fc --- /dev/null +++ b/client/modules/_common_components/src/lib/Button/Button.module.css @@ -0,0 +1,3 @@ +.root { + min-width: 100px; +} diff --git a/client/modules/_common_components/src/lib/Button/Button.tsx b/client/modules/_common_components/src/lib/Button/Button.tsx new file mode 100644 index 0000000000..1ada660abe --- /dev/null +++ b/client/modules/_common_components/src/lib/Button/Button.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Button, ButtonProps, ConfigProvider, ThemeConfig } from 'antd'; +import styles from './Button.module.css'; + +const secondaryTheme: ThemeConfig = { + components: { + Button: { + defaultActiveBg: '#f4f4f4', + defaultActiveColor: '#222', + defaultActiveBorderColor: '#026', + defaultBg: '#f4f4f4', + defaultBorderColor: '#222222', + defaultColor: '#222222', + defaultHoverBg: '#aac7ff', + }, + }, +}; + +export const SecondaryButton: React.FC = (props) => { + return ( + + - - )} + ); }; diff --git a/client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.module.css b/client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.module.css new file mode 100644 index 0000000000..4816f75235 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.module.css @@ -0,0 +1,10 @@ +.datafilesHelp div { + text-align: center; + line-height: 20px; +} + +.datafilesHelp a { + text-decoration: none; + color: unset; + white-space: nowrap; +} diff --git a/client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.tsx b/client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.tsx new file mode 100644 index 0000000000..8ee66a2a83 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesHelpDropdown/DatafilesHelpDropdown.tsx @@ -0,0 +1,130 @@ +import { Button, Dropdown } from 'antd'; +import React from 'react'; +import styles from './DatafilesHelpDropdown.module.css'; + +const items = [ + { + label: ( + +
Curation Tutorials
+
+ ), + key: '1', + }, + { + label: ( + +
Curation Guidelines
+
+ ), + key: '2', + }, + { + label: ( + +
+ Learn About the
+ Data Depot +
+
+ ), + key: '3', + }, + { + label: ( + +
Data Transfer Guide
+
+ ), + key: '4', + }, + { + label: ( + +
Curation FAQ
+
+ ), + key: '5', + }, + { + label: ( + +
+ How to Acknowledge
+ DesignSafe-CI +
+
+ ), + key: '6', + }, + { + label: ( + +
Data Usage Agreement
+
+ ), + key: '7', + }, + { + label: ( + +
FAQ
+
+ ), + key: '8', + }, +]; + +export const DatafilesHelpDropdown: React.FC = () => { + return ( + + + + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx index 95fae57d4d..5afa8d33a1 100644 --- a/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx @@ -2,16 +2,17 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { TModalChildren } from '../DatafilesModal'; import { Button, Modal, Select, Table } from 'antd'; import { + TFileListing, useAuthenticatedUser, useFileCopy, usePathDisplayName, - useSelectedFiles, } from '@client/hooks'; import { FileListingTable, + FileTypeIcon, TFileListingColumns, -} from '../../FileListing/FileListingTable/FileListingTable'; -import { BaseFileListingBreadcrumb } from '../../DatafilesBreadcrumb/DatafilesBreadcrumb'; +} from '@client/common-components'; +import { BaseFileListingBreadcrumb } from '@client/common-components'; import styles from './CopyModal.module.css'; import { toBytes } from '../../FileListing/FileListing'; import { CopyModalProjectListing } from './CopyModalProjectListing'; @@ -20,6 +21,12 @@ const SelectedFilesColumns: TFileListingColumns = [ { title: 'Files/Folders to Copy', dataIndex: 'name', + render: (value, record) => ( + + +   {value} + + ), }, { title: , @@ -106,14 +113,14 @@ export const CopyModal: React.FC<{ api: string; system: string; path: string; + selectedFiles: TFileListing[]; children: TModalChildren; -}> = ({ api, system, path, children }) => { +}> = ({ api, system, path, selectedFiles, children }) => { const [isModalOpen, setIsModalOpen] = useState(false); const showModal = () => setIsModalOpen(true); const handleClose = () => setIsModalOpen(false); - const { selectedFiles } = useSelectedFiles(api, system, path); const { user } = useAuthenticatedUser(); const defaultDestParams = useMemo( @@ -190,6 +197,7 @@ export const CopyModal: React.FC<{ mutate({ src: { api, system, path: encodeURIComponent(f.path) }, dest: { api: destApi, system: destSystem, path: dPath }, + doi: f.doi, }) ); handleClose(); @@ -256,7 +264,7 @@ export const CopyModal: React.FC<{ f.type === 'dir') } scroll={undefined} + emptyListingDisplay="No folders to display." /> diff --git a/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx b/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx index 0a250a2ee3..30458b5ca0 100644 --- a/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/DatafilesModal.tsx @@ -5,6 +5,8 @@ import { RenameModal } from './RenameModal'; import { NewFolderModal } from './NewFolderModal'; import { UploadFileModal } from './UploadFileModal'; import { UploadFolderModal } from './UploadFolderModal'; +import { MoveModal } from './MoveModal'; +import { DownloadModal } from './DownloadModal'; export type TModalChildren = (props: { onClick: React.MouseEventHandler; @@ -18,5 +20,7 @@ DatafilesModal.Rename = RenameModal; DatafilesModal.NewFolder = NewFolderModal; DatafilesModal.UploadFile = UploadFileModal; DatafilesModal.UploadFolder = UploadFolderModal; +DatafilesModal.Move = MoveModal; +DatafilesModal.Download = DownloadModal; export default DatafilesModal; diff --git a/client/modules/datafiles/src/DatafilesModal/DownloadModal/DownloadModal.tsx b/client/modules/datafiles/src/DatafilesModal/DownloadModal/DownloadModal.tsx new file mode 100644 index 0000000000..6be4e00da0 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/DownloadModal/DownloadModal.tsx @@ -0,0 +1,75 @@ +import { Modal } from 'antd'; +import React, { useState } from 'react'; +import { TModalChildren } from '../DatafilesModal'; +import { TFileListing, apiClient } from '@client/hooks'; + +export const DownloadModal: React.FC<{ + api: string; + system: string; + scheme?: string; + selectedFiles: TFileListing[]; + children: TModalChildren; +}> = ({ api, system, scheme, selectedFiles, children }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const doiArray = selectedFiles.filter((f) => f.doi).map((f) => f.doi); + + const doiString = [...new Set(doiArray)].join(','); + + const showModal = () => { + setIsModalOpen(true); + }; + + const zipUrl = `/api/datafiles/${api}/${ + scheme ?? 'public' + }/download/${system}/?doi=${doiString}`; + + const handleDownload = () => { + apiClient + .put(zipUrl, { paths: selectedFiles.map((f) => f.path) }) + .then((resp) => { + const link = document.createElement('a'); + link.style.display = 'none'; + link.setAttribute('href', resp.data.href); + link.setAttribute('download', 'null'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }) + .catch((e) => { + console.log(e); + if (e.response.status === 413) { + showModal(); + } + }); + }; + + const handleCancel = () => { + setIsModalOpen(false); + }; + + return ( + <> + {React.createElement(children, { onClick: handleDownload })} + Data Transfer Help} + onCancel={handleCancel} + cancelButtonProps={{ hidden: true }} + onOk={handleCancel} + > +

+ The data set that you are attempting to download is too large for a + direct download. Direct downloads are supported for up to 2 gigabytes + of data at a time. Alternative approaches for transferring large + amounts of data are provided in the Large Data Transfer Methods + section of the Data Transfer Guide ( + + https://www.designsafe-ci.org/rw/user-guides/data-transfer-guide/ + + ). +

+
+ + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/DownloadModal/index.ts b/client/modules/datafiles/src/DatafilesModal/DownloadModal/index.ts new file mode 100644 index 0000000000..11db946700 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/DownloadModal/index.ts @@ -0,0 +1 @@ +export * from './DownloadModal'; diff --git a/client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.module.css b/client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.module.css new file mode 100644 index 0000000000..5ceac66cca --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.module.css @@ -0,0 +1,41 @@ +.copyModalContent { + display: flex; + max-height: 60vh; + min-height: 400px; + gap: 50px; +} + +.copyModalContent td { + vertical-align: middle; +} + +.srcFilesSection { + flex: 1; + overflow: auto; + border: 1px solid #707070; +} + +.srcFilesTable { + height: 100%; +} + +.modalRightPanel { + display: flex; + flex: 1; + flex-direction: column; +} + +.destFilesSection { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + border: 1px solid #707070; +} + +.destFilesTableContainer { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; +} diff --git a/client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.tsx b/client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.tsx new file mode 100644 index 0000000000..1bad6efdd3 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/MoveModal/MoveModal.tsx @@ -0,0 +1,285 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { TModalChildren } from '../DatafilesModal'; +import { Alert, Button, Modal, Table } from 'antd'; +import { + TFileListing, + useCheckFilesForAssociation, + useFileMove, + usePathDisplayName, +} from '@client/hooks'; + +import { + BaseFileListingBreadcrumb, + FileTypeIcon, +} from '@client/common-components'; +import styles from './MoveModal.module.css'; +import { toBytes } from '../../FileListing/FileListing'; +import { + FileListingTable, + TFileListingColumns, +} from '@client/common-components'; +import { useParams } from 'react-router-dom'; + +const SelectedFilesColumns: TFileListingColumns = [ + { + title: 'Files/Folders to Move', + dataIndex: 'name', + render: (value, record) => ( + + +   {value} + + ), + }, + { + title: , + dataIndex: 'length', + render: (value) => toBytes(value), + }, +]; + +const DestHeaderTitle: React.FC<{ + api: string; + system: string; + path: string; + projectId?: string; +}> = ({ api, system, path, projectId }) => { + const getPathName = usePathDisplayName(); + return ( + + +    + + {projectId || getPathName(api, system, path)} + + ); +}; + +function getDestFilesColumns( + api: string, + system: string, + path: string, + mutationCallback: (path: string) => void, + navCallback: (path: string) => void, + projectId?: string, + disabled?: boolean +): TFileListingColumns { + return [ + { + title: ( + + ), + dataIndex: 'name', + ellipsis: true, + + render: (data, record) => ( + + ), + }, + { + dataIndex: 'path', + align: 'end', + title: ( + + ), + render: (_, record) => ( + + ), + }, + ]; +} + +export const MoveModal: React.FC<{ + api: string; + system: string; + path: string; + selectedFiles: TFileListing[]; + successCallback?: CallableFunction; + children: TModalChildren; +}> = ({ api, system, path, selectedFiles, successCallback, children }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const showModal = () => setIsModalOpen(true); + const handleClose = () => setIsModalOpen(false); + + let { projectId } = useParams(); + if (!projectId) projectId = ''; + + const hasAssociations = useCheckFilesForAssociation( + projectId, + selectedFiles.map((f) => f.path) + ); + + const defaultDestParams = useMemo( + () => ({ + destApi: api, + destSystem: system, + destPath: path, + destProjectId: projectId, + }), + [api, system, path, projectId] + ); + + const [dest, setDest] = useState<{ + destApi: string; + destSystem: string; + destPath: string; + destProjectId?: string; + }>(defaultDestParams); + const { destApi, destSystem, destPath } = dest; + useEffect(() => setDest(defaultDestParams), [isModalOpen, defaultDestParams]); + + const navCallback = useCallback( + (path: string) => { + const newPath = path.split('/').slice(-1)[0]; + setDest({ ...dest, destPath: newPath }); + }, + [dest] + ); + const { mutate } = useFileMove(); + + const mutateCallback = useCallback( + (dPath: string) => { + selectedFiles.forEach((f) => + mutate( + { + src: { api, system, path: encodeURIComponent(f.path) }, + dest: { api: destApi, system: destSystem, path: dPath }, + }, + { onSuccess: () => successCallback && successCallback() } + ) + ); + handleClose(); + }, + [selectedFiles, mutate, destApi, destSystem, successCallback, api, system] + ); + + const DestFilesColumns = useMemo( + () => + getDestFilesColumns( + destApi, + destSystem, + destPath, + (dPath: string) => mutateCallback(dPath), + navCallback, + dest.destProjectId, + hasAssociations + ), + [ + navCallback, + destApi, + destSystem, + destPath, + dest.destProjectId, + mutateCallback, + hasAssociations, + ] + ); + + return ( + <> + {React.createElement(children, { onClick: showModal })} + Move Files} + footer={null} + > + {hasAssociations && ( + + This file or folder cannot be moved until its tags or associated + entities have been removed using the Curation Directory tab. + + } + /> + )} +
+
+ record.path} + scroll={{ y: '100%' }} + /> + +
+
+ { + return ( + + ); + }} + /> +
+ + listing.filter( + (f) => + f.type === 'dir' && + !selectedFiles.map((sf) => sf.path).includes(f.path) + ) + } + emptyListingDisplay="No folders to display." + scroll={undefined} + /> +
+
+
+ + + + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/MoveModal/index.ts b/client/modules/datafiles/src/DatafilesModal/MoveModal/index.ts new file mode 100644 index 0000000000..4200e93362 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/MoveModal/index.ts @@ -0,0 +1 @@ +export * from './MoveModal'; diff --git a/client/modules/datafiles/src/DatafilesModal/NewFolderModal/NewFolderModal.tsx b/client/modules/datafiles/src/DatafilesModal/NewFolderModal/NewFolderModal.tsx index 2ab487fb7b..d98a98effc 100644 --- a/client/modules/datafiles/src/DatafilesModal/NewFolderModal/NewFolderModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/NewFolderModal/NewFolderModal.tsx @@ -16,21 +16,19 @@ export const NewFolderModalBody: React.FC<{ const handleNewFolderFinish = async (values: { newFolder: string }) => { const newFolder = values.newFolder; - try { - await mutate({ + mutate( + { src: { api, system, path, dirName: newFolder, }, - }); + }, + { onSuccess: () => handleCancel() } + ); - handleCancel(); // Close the modal after creating new folder - } catch (error) { - console.error('Error during form submission:', error); - // Handle error if needed - } + // Close the modal after creating new folder }; const validateNewFolder = (_: unknown, value: string) => { diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx index 41a61ae3aa..b3989d9a8e 100644 --- a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewContent.tsx @@ -1,5 +1,6 @@ import { useConsumePostit, TPreviewFileType } from '@client/hooks'; -import { Spin } from 'antd'; +import { Alert, Spin } from 'antd'; +import { HAZMAPPER_BASE_URL_MAP } from '../../projects/utils'; import React, { useState } from 'react'; import styles from './PreviewModal.module.css'; @@ -10,18 +11,32 @@ export const PreviewSpinner: React.FC = () => ( export type TPreviewContent = React.FC<{ href: string; fileType: TPreviewFileType; + handleCancel: () => void; }>; -export const PreviewContent: TPreviewContent = ({ href, fileType }) => { +export const PreviewContent: TPreviewContent = ({ + href, + fileType, + handleCancel, +}) => { const [iframeLoading, setIframeLoading] = useState(true); const { data: PostitData, isLoading: isConsumingPostit } = useConsumePostit({ href, responseType: fileType === 'video' ? 'blob' : 'text', queryOptions: { - enabled: (!!href && fileType === 'text') || fileType === 'video', + enabled: + (!!href && fileType === 'text') || + fileType === 'video' || + fileType === 'hazmapper', }, }); + if (isConsumingPostit && fileType === 'hazmapper') + return ( + <> +

Opening in Hazmapper ...

+ + ); if (isConsumingPostit) return ; switch (fileType) { @@ -68,7 +83,33 @@ export const PreviewContent: TPreviewContent = ({ href, fileType }) => { > ); + case 'hazmapper': + { + if (!PostitData) return; + const body = JSON.parse(PostitData as string); + let baseUrl = + HAZMAPPER_BASE_URL_MAP[ + body.deployment as keyof typeof HAZMAPPER_BASE_URL_MAP + ]; + if (!baseUrl) { + console.error( + `Invalid deployment type: ${body.deployment}. Falling back to local` + ); + baseUrl = HAZMAPPER_BASE_URL_MAP['local']; + } + window.open(`${baseUrl}/project/${body.uuid}`, '_blank'); + handleCancel(); + } + break; default: - return Error.; + return ( + + ); } }; diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewMetadata.tsx b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewMetadata.tsx new file mode 100644 index 0000000000..672d117163 --- /dev/null +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewMetadata.tsx @@ -0,0 +1,62 @@ +import { Collapse, Table, TableProps } from 'antd'; +import React from 'react'; +import styles from './PreviewModal.module.css'; +import { TFileListing, useFileDetail } from '@client/hooks'; +import { toBytes } from '../../FileListing/FileListing'; + +const tableColumns: TableProps['columns'] = [ + { dataIndex: 'key', render: (value) => {value}, width: 200 }, + { dataIndex: 'value' }, +]; + +export const PreviewMetadata: React.FC<{ + selectedFile: TFileListing; + fileMeta: Record; +}> = ({ selectedFile, fileMeta }) => { + const { data: fileListingMeta } = useFileDetail( + 'tapis', + selectedFile.system, + 'private', + selectedFile.path + ); + + const baseListingMeta = [ + { key: 'File Name', value: fileListingMeta?.name }, + { key: 'File Path', value: fileListingMeta?.path }, + { key: 'File Size', value: toBytes(fileListingMeta?.length) }, + { + key: 'Last Modified', + value: + fileListingMeta?.lastModified && + new Date(fileListingMeta.lastModified).toLocaleString(), + }, + ]; + + const fullListingMeta = [ + ...baseListingMeta, + ...Object.keys(fileMeta).map((k) => ({ key: k, value: fileMeta[k] })), + ]; + + return ( + + File Metadata + + ), + children: ( +
+ ), + }, + ]} + /> + ); +}; diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css index 1e5a1b0a3a..9449f9c4a0 100644 --- a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.module.css @@ -27,3 +27,18 @@ .previewContainer img { width: 100%; } + +.metadataCollapse { + background-color: 'black'; +} + +.metadataCollapse :global(.ant-collapse-content-box) { + padding: 0px !important; +} +.metadataCollapse :global(.ant-table-thead) { + display: none; +} + +.metadataCollapse :global(.ant-table-tbody) tr:nth-child(odd) td { + background-color: white; +} diff --git a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx index 90349ddc82..bc6b10c144 100644 --- a/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/PreviewModal/PreviewModal.tsx @@ -1,19 +1,28 @@ import { useQueryClient } from '@tanstack/react-query'; -import { useFilePreview } from '@client/hooks'; +import { + TFileListing, + useAuthenticatedUser, + useFileListingRouteParams, + useFilePreview, +} from '@client/hooks'; import { Button, Modal } from 'antd'; import React, { useCallback, useState } from 'react'; import styles from './PreviewModal.module.css'; import { TModalChildren } from '../DatafilesModal'; import { PreviewSpinner, PreviewContent } from './PreviewContent'; +import { PreviewMetadata } from './PreviewMetadata'; +import { CopyModal } from '../CopyModal'; +import { DownloadModal } from '../DownloadModal'; +import { MoveModal } from '../MoveModal'; export const PreviewModalBody: React.FC<{ isOpen: boolean; api: string; - system: string; scheme?: string; - path: string; + + selectedFile: TFileListing; handleCancel: () => void; -}> = ({ isOpen, api, system, scheme, path, handleCancel }) => { +}> = ({ isOpen, api, scheme, selectedFile, handleCancel }) => { /* Typically modals are rendered in the same component as the button that manages the open/closed state. The modal body is exported separately for file previews, since @@ -22,23 +31,32 @@ export const PreviewModalBody: React.FC<{ const queryClient = useQueryClient(); const { data, isLoading } = useFilePreview({ api, - system, + system: selectedFile.system, scheme, - path, + path: selectedFile.path, + doi: selectedFile.doi, queryOptions: { enabled: isOpen }, }); + const { path: listingPath } = useFileListingRouteParams(); const handleClose = useCallback(() => { // Flush queries on close to prevent stale postits being read from cache. queryClient.removeQueries({ queryKey: ['datafiles', 'preview'] }); handleCancel(); }, [handleCancel, queryClient]); + const { user } = useAuthenticatedUser(); + const isReadOnly = [ + 'designsafe.storage.published', + 'designsafe.storage.community', + 'nees.public', + ].includes(selectedFile.system); + if (!isOpen) return null; return ( File Preview: {path}} + title={

File Preview: {selectedFile.path.split('/').slice(-1)}

} width="60%" open={isOpen} footer={() => ( @@ -48,12 +66,74 @@ export const PreviewModalBody: React.FC<{ )} onCancel={handleClose} > + +
+ {!selectedFile.path.endsWith('.hazmapper') && ( + <> + {!isReadOnly && api === 'tapis' && ( + + {({ onClick }) => ( + + )} + + )} + + {user && ( + + {({ onClick }) => ( + + )} + + )} + + {({ onClick }) => ( + + )} + + + )} +
{isLoading && } {data && isOpen && ( )}
@@ -63,16 +143,14 @@ export const PreviewModalBody: React.FC<{ type TPreviewModal = React.FC<{ api: string; - system: string; scheme?: string; - path: string; + selectedFile: TFileListing; children: TModalChildren; }>; export const PreviewModal: TPreviewModal = ({ api, - system, scheme, - path, + selectedFile, children, }) => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -91,9 +169,8 @@ export const PreviewModal: TPreviewModal = ({ {isModalOpen && ( diff --git a/client/modules/datafiles/src/DatafilesModal/RenameModal/RenameModal.tsx b/client/modules/datafiles/src/DatafilesModal/RenameModal/RenameModal.tsx index 5adeb94938..34d8a10451 100644 --- a/client/modules/datafiles/src/DatafilesModal/RenameModal/RenameModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/RenameModal/RenameModal.tsx @@ -1,7 +1,12 @@ -import { Button, Modal, Form, Input } from 'antd'; -import { useSelectedFiles, useRename } from '@client/hooks'; +import { Button, Modal, Form, Input, Alert } from 'antd'; +import { + useSelectedFiles, + useRename, + useCheckFilesForAssociation, +} from '@client/hooks'; import React, { useState } from 'react'; import { TModalChildren } from '../DatafilesModal'; +import { useParams } from 'react-router-dom'; export const RenameModalBody: React.FC<{ isOpen: boolean; @@ -17,17 +22,34 @@ export const RenameModalBody: React.FC<{ const { mutate } = useRename(); + let { projectId } = useParams(); + if (!projectId) projectId = ''; + + const hasAssociations = useCheckFilesForAssociation( + projectId, + selectedFiles.map((f) => f.path) + ); + const handleRenameFinish = async (values: { newName: string }) => { + const originalName = selectedFiles[0].name; const newName = values.newName; + const extension = originalName.includes('.') + ? originalName.substring(originalName.lastIndexOf('.')) + : ''; + + const fullName = newName.endsWith(extension) + ? newName + : newName + extension; + try { await mutate({ src: { api, system, path, - name: selectedFiles[0].name, - newName: newName, + name: originalName, + newName: fullName, }, }); @@ -61,34 +83,56 @@ export const RenameModalBody: React.FC<{ return ( Rename {selectedFilesName[0].name}} + title={

Rename {selectedFilesName[0]?.name}

} width="60%" open={isOpen} + destroyOnClose footer={null} // Remove the footer from here onCancel={handleCancel} > -
- + This file or folder cannot be renamed until its tags or associated + entities have been removed using the Curation Directory tab. + + } + /> + )} + {isOpen && ( + - - - -
- -
- + + + + +
+ +
+ + )}
); }; diff --git a/client/modules/datafiles/src/DatafilesModal/UploadFileModal/UploadFileModal.tsx b/client/modules/datafiles/src/DatafilesModal/UploadFileModal/UploadFileModal.tsx index 054b40d22f..f7d56b3cb3 100644 --- a/client/modules/datafiles/src/DatafilesModal/UploadFileModal/UploadFileModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/UploadFileModal/UploadFileModal.tsx @@ -16,7 +16,7 @@ export const UploadFileModalBody: React.FC<{ path: string; handleCancel: () => void; }> = ({ isOpen, api, system, scheme, path, handleCancel }) => { - const { mutate } = useUploadFile(); + const { mutateAsync } = useUploadFile(); const [fileList, setFileList] = useState([]); const [uploading, setUploading] = useState(false); @@ -36,7 +36,7 @@ export const UploadFileModalBody: React.FC<{ formData.append('file_name', fileList[i].name); formData.append('webkit_relative_path', ''); - await mutate({ + await mutateAsync({ api, system, scheme: 'private', // Optional @@ -47,6 +47,7 @@ export const UploadFileModalBody: React.FC<{ // All files uploaded successfully, close the modal setUploading(false); + setFileList([]); handleCancel(); } catch (error) { console.error('Error during form submission:', error); @@ -82,7 +83,10 @@ export const UploadFileModalBody: React.FC<{ width="60%" open={isOpen} footer={null} // Remove the footer from here - onCancel={handleCancel} + onCancel={() => { + handleCancel(); + handleReset(); + }} >
diff --git a/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx b/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx index 7ddba05d1e..f9412bed83 100644 --- a/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx +++ b/client/modules/datafiles/src/DatafilesSideNav/DatafilesSideNav.tsx @@ -2,16 +2,18 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; import styles from './DatafilesSideNav.module.css'; import { useAuthenticatedUser } from '@client/hooks'; +import { Tooltip } from 'antd'; -const DataFilesNavLink: React.FC> = ({ - to, - children, -}) => { +const DataFilesNavLink: React.FC< + React.PropsWithChildren<{ to: string; tooltip?: string }> +> = ({ to, tooltip, children }) => { return (
  • - -
    {children}
    -
    + + +
    {children}
    +
    +
  • ); }; @@ -28,37 +30,70 @@ export const DatafilesSideNav: React.FC = () => { > {user && ( <> - + My Data - + + HPC Work - My Projects - - Shared with Me + + My Projects
    - Box.com - Dropbox.com - Google Drive + + Box.com + + + Dropbox.com + + + Google Drive +
    )} - + Published
    - + Published (NEES) - + Community Data diff --git a/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx b/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx index 31b6341d67..8b08bbc73f 100644 --- a/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx +++ b/client/modules/datafiles/src/DatafilesToolbar/DatafilesToolbar.tsx @@ -3,16 +3,20 @@ import styles from './DatafilesToolbar.module.css'; import { useAuthenticatedUser, useFileListingRouteParams, + useProjectDetail, useSelectedFiles, + useSelectedFilesForSystem, } from '@client/hooks'; import DatafilesModal from '../DatafilesModal/DatafilesModal'; import TrashButton from './TrashButton'; import { Button, ButtonProps, ConfigProvider, ThemeConfig } from 'antd'; +import { useMatches, useParams } from 'react-router-dom'; const toolbarTheme: ThemeConfig = { components: { Button: { colorPrimaryHover: 'rgba(0, 0, 0, 0.88)', + motionDurationMid: '0', }, }, }; @@ -27,22 +31,83 @@ const ToolbarButton: React.FC = (props) => { export const DatafilesToolbar: React.FC<{ searchInput?: React.ReactNode }> = ({ searchInput, }) => { - const { api, system, scheme, path } = useFileListingRouteParams(); - const { selectedFiles } = useSelectedFiles(api, system, path); + const routeParams = useFileListingRouteParams(); + const { scheme } = routeParams; + let { api, system, path } = routeParams; + const { neesid } = useParams(); + let { projectId } = useParams(); + const { user } = useAuthenticatedUser(); + const matches = useMatches(); + const isProjects = matches.find((m) => m.id === 'project'); + const isPublished = matches.find((m) => m.id === 'published'); + const isEntityListing = matches.find((m) => m.id === 'entity-listing'); + const isNees = matches.find((m) => m.id === 'nees'); + + const isReadOnly = + isPublished || isNees || system === 'designsafe.storage.community'; + + if (!isProjects) projectId = ''; + const { data } = useProjectDetail(projectId ?? ''); + if (projectId) { + system = `project-${data?.baseProject.uuid}`; + api = 'tapis'; + } + if (isPublished) { + system = 'designsafe.storage.published'; + api = 'tapis'; + } + if (isNees) { + system = 'nees.public'; + api = 'tapis'; + } + if (isNees && !path) { + path = `/${neesid}`; + } + + /* + Project landing pages have multiple selectable listings, so use the + useSelectedFilesForSystem hook to capture every selection on the page. + */ + const { selectedFiles: listingSelectedFiles } = useSelectedFiles( + api, + system, + path + ); + const publicationSelectedFiles = useSelectedFilesForSystem('tapis', system); + const selectedFiles = isEntityListing + ? publicationSelectedFiles + : listingSelectedFiles; + const rules = useMemo( function () { // Rules for which toolbar buttons are active for a given selection. return { canPreview: selectedFiles.length === 1 && selectedFiles[0].type === 'file', - canRename: user && selectedFiles.length === 1, - canCopy: user && selectedFiles.length >= 1, - canTrash: user && selectedFiles.length >= 1, + canRename: + user && + selectedFiles.length === 1 && + !isReadOnly && + !selectedFiles[0].path.endsWith('.hazmapper'), + canCopy: + user && + selectedFiles.length >= 1 && + !selectedFiles[0].path.endsWith('.hazmapper'), + canTrash: + user && + selectedFiles.length >= 1 && + !isReadOnly && + !selectedFiles[0].path.endsWith('.hazmapper'), + // Disable downloads from frontera.work until we have a non-flaky mount on ds-download. + canDownload: + selectedFiles.length >= 1 && + system !== 'designsafe.storage.frontera.work' && + !selectedFiles[0].path.endsWith('.hazmapper'), }; }, - [selectedFiles, user] + [selectedFiles, isReadOnly, user, system] ); return ( @@ -62,11 +127,28 @@ export const DatafilesToolbar: React.FC<{ searchInput?: React.ReactNode }> = ({ )} - + {({ onClick }) => ( + + + Move + + )} + + {({ onClick }) => ( = ({ )} - + {({ onClick }) => ( = ({ Trash + + {({ onClick }) => ( + + + Download + + )} +
    ); diff --git a/client/modules/datafiles/src/DatafilesToolbar/TrashButton.tsx b/client/modules/datafiles/src/DatafilesToolbar/TrashButton.tsx index 1ddd578cf0..fdbfa6e8c5 100644 --- a/client/modules/datafiles/src/DatafilesToolbar/TrashButton.tsx +++ b/client/modules/datafiles/src/DatafilesToolbar/TrashButton.tsx @@ -1,6 +1,12 @@ import React, { useCallback } from 'react'; -import { useAuthenticatedUser, useTrash } from '@client/hooks'; +import { + useAuthenticatedUser, + useCheckFilesForAssociation, + useNotifyContext, + useTrash, +} from '@client/hooks'; import { Button, ButtonProps, ConfigProvider } from 'antd'; +import { useParams } from 'react-router-dom'; interface TrashButtonProps extends ButtonProps { api: string; @@ -25,11 +31,38 @@ const TrashButton: React.FC> = React.memo( [selectedFiles, mutate, api, system] ); + let { projectId } = useParams(); + if (!projectId) projectId = ''; + + const hasAssociations = useCheckFilesForAssociation( + projectId, + selectedFiles.map((f) => f.path) + ); + + const { notifyApi } = useNotifyContext(); + const handleTrashClick = () => { // const trashPath = path === 'myData' ? '${user.username}/.Trash' : '.Trash'; + + if (hasAssociations) { + notifyApi?.open({ + type: 'error', + message: 'Cannot Trash File(s)', + duration: 10, + description: ( +
    + The selected file(s) are associated to one or more categories. + Please remove category associations before proceeding. +
    + ), + placement: 'bottomLeft', + }); + return; + } + const userUsername: string | undefined = user?.username; let trashPath: string; - if (typeof userUsername === 'string') { + if (typeof userUsername === 'string' && !system.startsWith('project-')) { trashPath = userUsername + '/.Trash'; updateFilesPath(trashPath); } else { @@ -42,7 +75,12 @@ const TrashButton: React.FC> = React.memo( return ( - ), + + + )} +
    + {(fileTags ?? []) + .filter((tag) => tag.path === record.path) + .map((tag) => ( + + {tag.tagName} + + ))} + + ), }, { title: 'Size', @@ -94,7 +116,7 @@ export const FileListing: React.FC< render: (d) => new Date(d).toLocaleString(), }, ], - [setPreviewModalState, baseRoute] + [setPreviewModalState, baseRoute, fileTags, doi] ); return ( @@ -105,14 +127,15 @@ export const FileListing: React.FC< scheme={scheme} path={path} columns={columns} + emptyListingDisplay={emptyListingDisplay} {...tableProps} /> - {previewModalState.path && ( + {previewModalState.path && previewModalState.selectedFile && ( setPreviewModalState({ isOpen: false })} /> )} diff --git a/client/modules/datafiles/src/index.ts b/client/modules/datafiles/src/index.ts index 7b7de2123f..ff75d61eb3 100644 --- a/client/modules/datafiles/src/index.ts +++ b/client/modules/datafiles/src/index.ts @@ -1,11 +1,11 @@ export * from './lib/datafiles'; export * from './DatafilesSideNav/DatafilesSideNav'; +export * from './DatafilesHelpDropdown/DatafilesHelpDropdown'; export * from './AddFileFolder/AddFileFolder'; export * from './FileListing/FileListing'; export { default as DatafilesModal } from './DatafilesModal/DatafilesModal'; export * from './DatafilesToolbar/DatafilesToolbar'; -export * from './DatafilesBreadcrumb/DatafilesBreadcrumb'; export * from './nees'; export * from './projects'; diff --git a/client/modules/datafiles/src/nees/NeesDetails.module.css b/client/modules/datafiles/src/nees/NeesDetails.module.css new file mode 100644 index 0000000000..eefa204c32 --- /dev/null +++ b/client/modules/datafiles/src/nees/NeesDetails.module.css @@ -0,0 +1,34 @@ +.line-clamped { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + overflow: hidden; +} + +.line-unclamped { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; +} + +th { + vertical-align: top; +} + +.nees-th { + width: 25%; +} + +.nees-td { + width: 75%; +} + +.nees-mini-table { + width: 100%; + margin-bottom: 0; +} + +.nees-divider { + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/client/modules/datafiles/src/nees/NeesDetails.tsx b/client/modules/datafiles/src/nees/NeesDetails.tsx new file mode 100644 index 0000000000..2ceef5f2ba --- /dev/null +++ b/client/modules/datafiles/src/nees/NeesDetails.tsx @@ -0,0 +1,445 @@ +import { useNeesDetails } from '@client/hooks'; +import { Tabs, Button, Divider, Modal, Flex } from 'antd'; +import React, { useEffect, useState, useCallback } from 'react'; +import { Link, useParams } from 'react-router-dom'; + +import styles from './NeesDetails.module.css'; +import { DatafilesBreadcrumb } from '@client/common-components'; +import { FileListing } from '../FileListing/FileListing'; + +export const DescriptionExpander: React.FC = ({ + children, +}) => { + const [expanderRef, setExpanderRef] = useState(null); + const [expanded, setExpanded] = useState(false); + const [expandable, setExpandable] = useState(false); + + const expanderRefCallback = useCallback( + (node: HTMLElement) => { + if (node !== null) setExpanderRef(node); + }, + [setExpanderRef] + ); + + useEffect(() => { + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + setExpandable(entry.target.scrollHeight > entry.target.clientHeight); + } + }); + expanderRef && ro.observe(expanderRef); + return () => { + ro.disconnect(); + }; + }, [setExpandable, expanderRef]); + + return ( +
    + + {children} + + {(expandable || expanded) && ( + + )} +
    + ); +}; + +export const NeesDetails: React.FC<{ neesId: string }> = ({ neesId }) => { + const { data } = useNeesDetails(neesId); + const neesProjectData = data?.metadata.project; + const neesExperiments = data?.metadata.experiments; + const numDOIs = neesExperiments?.filter((exp) => !!exp.doi).length || 0; + const routeParams = useParams(); + const path = routeParams.path ?? data?.path; + + const [activeTab, setActiveTab] = useState('files'); + useEffect(() => setActiveTab('files'), [path]); + + const neesCitations = neesExperiments + ?.filter((exp) => !!exp.doi) + .map((u) => { + const authors = u.creators + ?.map((a) => a.lastName + ', ' + a.firstName) + .join('; '); + const doi = u.doi; + const doiUrl = 'https://doi.org/' + doi; + const year = u.endDate + ? u.endDate.split('T')[0].split('-')[0] + : u.startDate.split('T')[0].split('-')[0]; + + return ( +
    + {authors}, ({year}), "{u.title}", DesignSafe-CI [publisher], doi:{' '} + {doi} +
    + {doiUrl} + +
    + ); + }); + + const doiList = () => { + Modal.info({ + title: 'DOIs', + content: neesCitations, + width: 600, + }); + }; + + const experimentsList = neesExperiments?.map((exp) => { + return ( +
    + +
    {exp.name}
    +
    +
    + + + + + + + + + + + + + + {exp.doi ? ( + + + + + ) : ( + + )} + {exp.doi ? ( + + + + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Title{exp.title}
    Creators + {exp.creators + ? exp.creators?.map((c) => ( +
    + {c.firstName} {c.lastName} +
    + )) + : 'No Creators Listed'} +
    DOI{exp.doi}
    Citation + {exp.creators + ?.map( + (author) => author.lastName + ', ' + author.firstName + ) + .join('; ')} + , ( + {exp.endDate + ? exp.endDate.split('T')[0].split('-')[0] + : exp.startDate.split('T')[0].split('-')[0]} + ), "{exp.title}", DesignSafe-CI [publisher], doi:{' '} + {exp.doi} +
    Type{exp.type}
    Description + {exp.description ? ( + + {exp.description} + + ) : ( + 'No Description' + )} +
    Start Date{exp.startDate}
    End Date{exp.endDate ? exp.endDate : 'No End Date'}
    Equipment + + + {exp.equipment ? ( + + + + + + + ) : ( + + + + )} + + + {exp.equipment?.map((eq) => ( + + + + + + + ))} + +
    EquipmentComponentEquipment ClassFacility
    No Equipment Listed
    {eq.equipment}{eq.component}{eq.equipmentClass}{eq.facility}
    +
    Material + {exp.material + ? exp.material?.map((mat) => ( +
    +
    {mat.component}:
    +
    + {mat.materials?.map((mats) => ( +
    {mats}
    + ))} +
    +
    +
    + )) + : 'No Materials Listed '} +
    Files + setActiveTab('files')} + > + {' '} + {exp.name} + +
    + + + + + ); + }); + + const neesFiles = ( + <> + { + return ( + + {obj.title} + + ); + }} + /> + + + ); + + return ( + <> +
    +

    + {neesProjectData?.name}: {neesProjectData?.title} +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    PIs + {neesProjectData?.pis + ? neesProjectData?.pis.map((u) => ( +
    + {u.firstName} {u.lastName} +
    + )) + : 'No PIs Listed'} +
    +
    + + + + + +
    Organizations + {neesProjectData?.organization + ? neesProjectData?.organization.map((u) => ( +
    + {u.name} {u.state}, {u.country} +
    + )) + : 'No Organizations Listed'} +
    +
    + + + +
    + + + + + +
    NEES ID{neesProjectData?.name}
    +
    + + + + + +
    Sponsors + {neesProjectData?.sponsor + ? neesProjectData?.sponsor?.map((u) => ( +
    + + {u.name} + +
    + )) + : 'No Sponsors Listed'} +
    +
    + + + +
    + + + + + + + +
    Project TypeNEES
    +
    + + + + + + + +
    Start Date + {neesProjectData?.startDate + ? neesProjectData?.startDate + : 'No Start Date'} +
    +
    + + + +
    + + {numDOIs > 0 ? ( + + + + + ) : ( + + )} + +
    DOIs + +
    + + + Description: + + {neesProjectData?.description} + +
    +
    + setActiveTab(activeKey)} + type="card" + items={[ + { + key: 'files', + label: 'Files', + children: neesFiles, + }, + { + key: 'experiments', + label: 'Experiments', + children: experimentsList, + }, + ]} + /> +
    + + ); +}; diff --git a/client/modules/datafiles/src/nees/NeesListing.tsx b/client/modules/datafiles/src/nees/NeesListing.tsx index 6e8002d946..200a88842e 100644 --- a/client/modules/datafiles/src/nees/NeesListing.tsx +++ b/client/modules/datafiles/src/nees/NeesListing.tsx @@ -57,7 +57,7 @@ export const NeesListing: React.FC = () => { scroll={{ y: '100%' }} rowKey={(row) => row.path} pagination={{ - total: data?.listing.length, + total: data?.total, showSizeChanger: false, current: currentPage, pageSize: 100, diff --git a/client/modules/datafiles/src/nees/index.ts b/client/modules/datafiles/src/nees/index.ts index ec5ae9b297..544ad7dd59 100644 --- a/client/modules/datafiles/src/nees/index.ts +++ b/client/modules/datafiles/src/nees/index.ts @@ -1 +1,2 @@ export { NeesListing } from './NeesListing'; +export { NeesDetails } from './NeesDetails'; diff --git a/client/modules/datafiles/src/projects/BaseProjectDetails.module.css b/client/modules/datafiles/src/projects/BaseProjectDetails.module.css index 8a66681587..ae1129fd79 100644 --- a/client/modules/datafiles/src/projects/BaseProjectDetails.module.css +++ b/client/modules/datafiles/src/projects/BaseProjectDetails.module.css @@ -12,5 +12,5 @@ } .prj-row td { - padding: 2px 0px; + padding: 3px 0px; } diff --git a/client/modules/datafiles/src/projects/BaseProjectDetails.tsx b/client/modules/datafiles/src/projects/BaseProjectDetails.tsx index dc61320f18..a9fa249fd9 100644 --- a/client/modules/datafiles/src/projects/BaseProjectDetails.tsx +++ b/client/modules/datafiles/src/projects/BaseProjectDetails.tsx @@ -4,6 +4,11 @@ import { TBaseProjectValue, TProjectUser } from '@client/hooks'; import styles from './BaseProjectDetails.module.css'; import { Button, Col, Popover, Row, Select, Tooltip } from 'antd'; import { useSearchParams } from 'react-router-dom'; +import { RelateDataModal } from './modals'; +import { ProjectInfoModal } from './modals/ProjectInfoModal'; +import { VersionChangesModal } from './modals/VersionChangesModal'; +import { SubmitFeedbackModal } from '../publications/modals/SubmitFeedbackModal'; +import { filterHazmapperMaps, getHazmapperUrl } from './utils'; export const DescriptionExpander: React.FC = ({ children, @@ -65,14 +70,15 @@ export const LicenseDisplay: React.FC<{ licenseType: string }> = ({
      - {licenseType} + {licenseType}
    ); }; export const UsernamePopover: React.FC<{ user: TProjectUser }> = ({ user }) => { const content = ( -
    = ({ user }) => { gap: '10px', }} > - - Name - + + + Name + + {user.fname} {user.lname} - - Email - + + + Email + + {user.email} - - Institution - + + + Institution + + {user.inst} -
    +
    ); return ( = ({ projectValue, publicationDate, versions }) => { + isPublished?: boolean; +}> = ({ projectValue, publicationDate, versions, isPublished }) => { const pi = projectValue.users.find((u) => u.role === 'pi'); const coPis = projectValue.users.filter((u) => u.role === 'co_pi'); const projectType = [ @@ -150,6 +163,14 @@ export const BaseProjectDetails: React.FC<{ }); }; + const currentVersion = versions + ? parseInt(searchParams.get('version') ?? Math.max(...versions).toString()) + : 1; + + const filteredHazmapperMaps = filterHazmapperMaps( + projectValue.hazmapperMaps ?? [] + ); + return (
    - + )} {(projectValue.dataTypes?.length ?? 0) > 0 && ( @@ -283,6 +312,7 @@ export const BaseProjectDetails: React.FC<{ )} + {(projectValue.referencedData?.length ?? 0) > 0 && ( + + + + + )} - {(projectValue.hazmapperMaps?.length ?? 0) > 0 && ( + {(filteredHazmapperMaps?.length ?? 0) > 0 && ( )}
    Project Type{projectType} + {projectType} + {!isPublished && ( + <> + {' '} + + + )} +
    {projectValue.associatedProjects.map((assoc) => (
    + {assoc.type} |{' '}
    Referenced Data and Software + {projectValue.referencedData.map((ref) => ( +
    + {ref.hrefType && `${ref.hrefType} | `} + + {ref.title} + +
    + ))} +
    Keywords {projectValue.keywords.join(', ')}
    Hazmapper Maps - {(projectValue.hazmapperMaps ?? []).map((m) => ( + {(filteredHazmapperMaps ?? []).map((m) => (
    - - Description: - {projectValue.description} - + + {isPublished && ( +
    + {!['other', 'field_reconnaissance'].includes( + projectValue.projectType + ) && ( + <> + + {({ onClick }) => ( + + )} + {' '} + |{' '} + + )} + +
    + )} + {projectValue.description && ( + + Description: + {projectValue.description} + + )}
    ); }; diff --git a/client/modules/datafiles/src/projects/EmptyProjectFileListing.tsx b/client/modules/datafiles/src/projects/EmptyProjectFileListing.tsx new file mode 100644 index 0000000000..09bf0892d4 --- /dev/null +++ b/client/modules/datafiles/src/projects/EmptyProjectFileListing.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +export const EmptyProjectFileListing: React.FC = () => { + return ( +

    + This folder is empty!
    + +   + +
    + + +   + + Learn how to move files to a project + +

    + ); +}; diff --git a/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.module.css b/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.module.css new file mode 100644 index 0000000000..f47cb1f1b2 --- /dev/null +++ b/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.module.css @@ -0,0 +1,3 @@ +.yellow-highlight { + background-color: #ece4bf; +} diff --git a/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx b/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx index f4fd8dc9d4..28c9fe72e7 100644 --- a/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx +++ b/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx @@ -1,4 +1,11 @@ -import { useProjectDetail, usePublicationDetail } from '@client/hooks'; +import React, { useState } from 'react'; +import { + useDataciteMetrics, + useProjectDetail, + usePublicationDetail, +} from '@client/hooks'; +import { MetricsModal } from '../modals/MetricsModal'; +import styles from './ProjectCitation.module.css'; export const ProjectCitation: React.FC<{ projectId: string; @@ -6,14 +13,17 @@ export const ProjectCitation: React.FC<{ }> = ({ projectId, entityUuid }) => { const { data } = useProjectDetail(projectId); const entityDetails = data?.entities.find((e) => e.uuid === entityUuid); - + const authors = + entityDetails?.value.authors?.filter((a) => a.fname && a.lname) ?? []; if (!data || !entityDetails) return null; return (
    - {(entityDetails.value.authors ?? []) + {authors .map((author, idx) => idx === 0 - ? `${author.lname}, ${author.fname[0]}.` + ? `${author.lname}, ${author.fname[0]}${ + authors.length > 1 ? '.' : '' + }` : `${author.fname[0]}. ${author.lname}` ) .join(', ')} @@ -33,20 +43,142 @@ export const PublishedCitation: React.FC<{ const entityDetails = (data?.tree.children ?? []).find( (child) => child.uuid === entityUuid && child.version === version ); + + const authors = entityDetails?.value.authors ?? []; if (!data || !entityDetails) return null; + const doi = + entityDetails.value.dois && entityDetails.value.dois.length > 0 + ? entityDetails.value.dois[0] + : ''; + return (
    - {(entityDetails.value.authors ?? []) + {authors .map((author, idx) => idx === 0 - ? `${author.lname}, ${author.fname[0]}.` + ? `${author.lname}, ${author.fname[0]}${ + authors.length > 1 ? '.' : '' + }` : `${author.fname[0]}. ${author.lname}` ) .join(', ')}{' '} ({new Date(entityDetails.publicationDate).getFullYear()}). " {entityDetails.value.title}", in {data.baseProject.title}. - DesignSafe-CI. ({entityDetails.value.dois && entityDetails.value.dois[0]}) + DesignSafe-CI.{' '} + {doi && ( + + https://doi.org/{doi} + + )} + {/* DesignSafe-CI. ({entityDetails.value.dois && entityDetails.value.dois[0]}) */} +
    + ); +}; + +export const DownloadCitation: React.FC<{ + projectId: string; + entityUuid: string; + preview?: boolean; +}> = ({ projectId, entityUuid, preview }) => { + const { + data, + isLoading: isProjectLoading, + isError: isProjectError, + error: projectError, + } = usePublicationDetail(projectId); + + const [isModalVisible, setIsModalVisible] = useState(false); + + const entityDetails = (data?.tree.children ?? []).find( + (child) => child.uuid === entityUuid + ); + + const doi = + entityDetails?.value.dois && entityDetails.value.dois.length > 0 + ? entityDetails.value.dois[0] + : ''; + + const { data: dataciteMetrics } = useDataciteMetrics(doi, !preview); + + const openModal = () => { + setIsModalVisible(true); + }; + + const closeModal = () => { + setIsModalVisible(false); + }; + + if (isProjectLoading) return
    Loading project details...
    ; + if (isProjectError) + return
    Error fetching project details: {projectError.message}
    ; + if (!entityDetails) return null; + + return ( +
    + {dataciteMetrics && !preview && ( +
    + Download Citation: + + DataCite XML + {' '} + | + + {' '} + RIS + {' '} + | + + {' '} + BibTeX + +
    + + {dataciteMetrics?.data.attributes.downloadCount ?? '--'} Downloads + +      + + {dataciteMetrics?.data.attributes.viewCount ?? '--'} Views + +      + + {dataciteMetrics?.data.attributes.citationCount ?? '--'} Citations + +      + + Details + + +
    +
    + )}
    ); }; diff --git a/client/modules/datafiles/src/projects/ProjectCurationFileListing/ProjectCurationFileListing.tsx b/client/modules/datafiles/src/projects/ProjectCurationFileListing/ProjectCurationFileListing.tsx index 61830f027c..754253be7f 100644 --- a/client/modules/datafiles/src/projects/ProjectCurationFileListing/ProjectCurationFileListing.tsx +++ b/client/modules/datafiles/src/projects/ProjectCurationFileListing/ProjectCurationFileListing.tsx @@ -1,11 +1,12 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FileListingTable, + FileTypeIcon, TFileListingColumns, -} from '../../FileListing/FileListingTable/FileListingTable'; +} from '@client/common-components'; import { toBytes } from '../../FileListing/FileListing'; import { PreviewModalBody } from '../../DatafilesModal/PreviewModal'; -import { NavLink } from 'react-router-dom'; +import { NavLink, useParams } from 'react-router-dom'; import { TEntityMeta, TFileListing, @@ -15,6 +16,7 @@ import { useFileTags, useProjectDetail, useRemoveFileAssociation, + useSelectedFiles, useSetFileTags, } from '@client/hooks'; import { Button, Select } from 'antd'; @@ -25,6 +27,7 @@ import { } from '../constants'; import { DefaultOptionType } from 'antd/es/select'; import { FILE_TAG_OPTIONS } from './ProjectFileTagOptions'; +import { EmptyProjectFileListing } from '../EmptyProjectFileListing'; const FileTagInput: React.FC<{ projectId: string; @@ -98,6 +101,15 @@ const FileCurationSelector: React.FC<{ const [selectedEntity, setSelectedEntity] = useState( undefined ); + const { path } = useParams(); + const { selectedFiles, unsetSelections } = useSelectedFiles( + 'tapis', + fileObj.system, + path ?? '' + ); + const showEntitySelector = + selectedFiles.length === 0 || + (selectedFiles.length > 0 && fileObj.path === selectedFiles[0].path); const entitiesForFile = useMemo(() => { const associatedEntities = Object.keys(filePathsToEntities) @@ -137,6 +149,7 @@ const FileCurationSelector: React.FC<{
      {entitiesForFile.map((e) => (
    • + {}
      ))}
    • -
      - - value={selectedEntity} - allowClear - onChange={(newVal) => setSelectedEntity(newVal)} - options={options} - placeholder="Select Category" - style={{ flex: 1 }} - /> - {selectedEntity && ( - - )} -
      + {showEntitySelector ? ( +
      + + virtual={false} + value={selectedEntity} + allowClear + onChange={(newVal) => setSelectedEntity(newVal)} + options={options} + placeholder={`Select Category ${ + selectedFiles.length > 0 + ? `for ${selectedFiles.length} selected file(s)` + : '' + }`} + style={{ flex: 1 }} + /> + {selectedEntity && ( + + )} +
      + ) : ( +
      + )}
    @@ -231,20 +262,21 @@ export const ProjectCurationFileListing: React.FC<{ const tagMapping = useFileTags(projectId); const options: DefaultOptionType[] = useMemo( () => - ENTITIES_WITH_FILES[data?.baseProject.value.projectType ?? 'None'].map( - (t) => ({ + ENTITIES_WITH_FILES[data?.baseProject.value.projectType ?? 'None'] + .map((t) => ({ label: DISPLAY_NAMES[t], options: data?.entities .filter((e) => e.name === t) .map((e) => ({ label: e.value.title, value: e.uuid })), - }) - ), + })) + .filter((t) => (t.options?.length ?? 0) > 0), [data] ); const [previewModalState, setPreviewModalState] = useState<{ isOpen: boolean; path?: string; + selectedFile?: TFileListing; }>({ isOpen: false }); const columns: TFileListingColumns = useMemo( @@ -273,21 +305,22 @@ export const ProjectCurationFileListing: React.FC<{ {data} ) : ( - + {data} + + )}
    {' '} } scroll={{ y: 500 }} /> - {previewModalState.path && ( + {previewModalState.path && previewModalState.selectedFile && ( setPreviewModalState({ isOpen: false })} /> )} diff --git a/client/modules/datafiles/src/projects/ProjectListing.tsx b/client/modules/datafiles/src/projects/ProjectListing.tsx index ab01a2a5f6..fb0baba90c 100644 --- a/client/modules/datafiles/src/projects/ProjectListing.tsx +++ b/client/modules/datafiles/src/projects/ProjectListing.tsx @@ -1,5 +1,5 @@ import { TBaseProject, useProjectListing } from '@client/hooks'; -import { Table, TableColumnsType } from 'antd'; +import { Alert, Table, TableColumnsType } from 'antd'; import React, { useState } from 'react'; import { Link } from 'react-router-dom'; @@ -19,7 +19,7 @@ const columns: TableColumnsType = [ { render: (_, record) => { const pi = record.value.users.find((u) => u.role === 'pi'); - return `${pi?.fname} ${pi?.lname}`; + return `${pi?.fname ?? '(N/A)'} ${pi?.lname ?? ''}`; }, title: 'Principal Investigator', }, @@ -32,7 +32,7 @@ const columns: TableColumnsType = [ export const ProjectListing: React.FC = () => { const limit = 100; const [currentPage, setCurrentPage] = useState(1); - const { data, isLoading } = useProjectListing(currentPage, limit); + const { data, isLoading, error } = useProjectListing(currentPage, limit); return ( { hideOnSinglePage: true, onChange: (page) => setCurrentPage(page), }} + locale={{ + emptyText: isLoading ? ( +
     
    + ) : ( + <> + {error && ( + There was an error retrieving your projects." + } + /> + )} + {!error && ( + + No projects found. You can create a project by clicking + "Add" in the left-hand sidebar and selecting "New Project". + + } + /> + )} + + ), + }} >
    ); }; diff --git a/client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.module.css b/client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.tsx b/client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.tsx new file mode 100644 index 0000000000..e6f4275f59 --- /dev/null +++ b/client/modules/datafiles/src/projects/ProjectMetrics/ProjectMetrics.tsx @@ -0,0 +1,29 @@ +import { usePublicationDetail } from '@client/hooks'; + +export const ProjectMetrics: React.FC<{ + projectId: string; + entityUuid: string; + version?: number; +}> = ({ projectId, entityUuid, version = 1 }) => { + const { data } = usePublicationDetail(projectId); + + const entityDetails = (data?.tree.children ?? []).find( + (child) => child.uuid === entityUuid && child.version === version + ); + if (!data || !entityDetails) return null; + + return ( +
    + {(entityDetails.value.authors ?? []) + .map((author, idx) => + idx === 0 + ? `${author.lname}, ${author.fname[0]}.` + : `${author.fname[0]}. ${author.lname}` + ) + .join(', ')}{' '} + ({new Date(entityDetails.publicationDate).getFullYear()}). " + {entityDetails.value.title}", in {data.baseProject.title}. + DesignSafe-CI. ({entityDetails.value.dois && entityDetails.value.dois[0]}) +
    + ); +}; diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx index 42b311d392..36cbb7c7e2 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineOrderAuthors.tsx @@ -56,7 +56,7 @@ export const PipelineOrderAuthors: React.FC<{ > - - + {searchParams.get('operation') === 'amend' ? ( + + File selections cannot be changed when amending a publication. + If you need to make a change to published files, please create a + new version instead. + + } + /> + ) : ( + <> +
    +

    Select Files

    {' '} + +
    + + + )} ); diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadCategories.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadCategories.tsx index dd75f0784d..01179555a2 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadCategories.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadCategories.tsx @@ -3,12 +3,14 @@ import { TPreviewTreeData, useProjectPreview } from '@client/hooks'; import { Button } from 'antd'; import { useSearchParams } from 'react-router-dom'; import { PublishedEntityDisplay } from '../ProjectPreview/ProjectPreview'; +import { PipelineEditCategoryModal } from '../modals'; export const PipelineProofreadCategories: React.FC<{ projectId: string; + displayName?: string; nextStep: () => void; prevStep: () => void; -}> = ({ projectId, nextStep, prevStep }) => { +}> = ({ projectId, displayName, nextStep, prevStep }) => { const { data } = useProjectPreview(projectId ?? ''); const { children } = (data?.tree ?? { children: [] }) as TPreviewTreeData; const [searchParams] = useSearchParams(); @@ -35,7 +37,7 @@ export const PipelineProofreadCategories: React.FC<{ > + )} + + ))} diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx index 26769efcde..be46b89243 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadProjectStep.tsx @@ -44,7 +44,7 @@ export const PipelineProofreadProjectStep: React.FC<{ target="_blank" aria-describedby="msg-open-new-window" > - Curation office hours + curation office hours {' '} for help with publishing. diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadPublications.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadPublications.tsx index 3cbe281691..c870de6dde 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadPublications.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineProofreadPublications.tsx @@ -4,12 +4,14 @@ import { Button } from 'antd'; import { useSearchParams } from 'react-router-dom'; import { TPreviewTreeData } from '@client/hooks'; import { PublishedEntityDisplay } from '../ProjectPreview/ProjectPreview'; +import { PipelineEditCategoryModal } from '../modals'; export const PipelineProofreadPublications: React.FC<{ projectId: string; + displayName?: string; nextStep: () => void; prevStep: () => void; -}> = ({ projectId, nextStep, prevStep }) => { +}> = ({ projectId, displayName, nextStep, prevStep }) => { const { data } = useProjectPreview(projectId ?? ''); const { children } = (data?.tree ?? { children: [] }) as TPreviewTreeData; const [searchParams] = useSearchParams(); @@ -47,10 +49,56 @@ export const PipelineProofreadPublications: React.FC<{ Continue +

    + Proofread your {displayName} Metadata +

    +
      +
    • If you selected the wrong collection, go back to Selection.
    • +
    • + If you need to add or modify files, click "Exit Prepare to Publish" + and make your changes in the Curation Directory. +
    • +
    • + If you need help, attend{' '} + + curation office hours + {' '} + for help with publishing. +
    • +
    -
    +
    {sortedChildren.map((child) => (
    +
    + + {({ onClick }) => ( + + )} + +
    = ({ projectId, entityUuids, projectType, disabled }) => { +}> = ({ projectId, entityUuids, operation, projectType, disabled }) => { const [isModalOpen, setIsModalOpen] = useState(false); const showModal = () => { @@ -17,12 +25,56 @@ export const PipelinePublishModal: React.FC<{ setIsModalOpen(false); }; + const [versionInfo, setVersionInfo] = useState(''); + + const { mutate: publishMutation } = usePublishProject(); + const { mutate: amendMutation } = useAmendProject(); + const { mutate: versionMutation } = useVersionProject(); + const navigate = useNavigate(); + const { notifyApi } = useNotifyContext(); + const successCallback = () => { + navigate(`/projects/${projectId}`); + notifyApi?.open({ + type: 'success', + message: '', + description: 'Your publication request has been submitted', + placement: 'bottomLeft', + }); + }; + + const doPublish = () => { + switch (operation) { + case 'publish': + publishMutation( + { projectId, entityUuids }, + { onSuccess: successCallback } + ); + break; + case 'amend': + amendMutation({ projectId }, { onSuccess: successCallback }); + break; + case 'version': + versionMutation( + { projectId, entityUuids, versionInfo }, + { onSuccess: successCallback } + ); + break; + } + }; + + const publishButtonText: Record = { + amend: 'Amend Publication', + version: 'Create a New Version', + publish: 'Request DOI & Publish', + }; + const [protectedDataAgreement, setProtectedDataAgreement] = useState(false); const [publishingAgreement, setPublishingAgreement] = useState(false); const canPublish = publishingAgreement && - (projectType === 'field_recon' ? protectedDataAgreement : true); + (projectType === 'field_recon' ? protectedDataAgreement : true) && + (operation === 'version' ? !!versionInfo : true); return ( <> @@ -33,37 +85,74 @@ export const PipelinePublishModal: React.FC<{ type="primary" onClick={showModal} > - Request DOI and Publish +   + {publishButtonText[operation]} ( -
    - - setPublishingAgreement(e.target.checked)} - /> - - - + + setPublishingAgreement(e.target.checked)} + /> + + + +
    )} onCancel={handleCancel} @@ -230,23 +319,15 @@ export const PipelinePublishModal: React.FC<{ > setProtectedDataAgreement(e.target.checked)} /> - -
    )} diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineSelectForPublish.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineSelectForPublish.tsx index b559670f98..fede1d21e0 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/PipelineSelectForPublish.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/PipelineSelectForPublish.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { TPipelineValidationResult, TPreviewTreeData, @@ -17,22 +17,44 @@ const PipelineValidationAlert: React.FC<{ + message={ + + {' '} Your selection has missing data or incomplete requirements. Please review the following fields: - {(validationErrors ?? []).map((validationError) => ( -
    - In the {DISPLAY_NAMES[validationError.name]}{' '} - {validationError.title}, the following - requirements are missing or incomplete: -
      - {validationError.missing.map((missingReq) => ( -
    • {DISPLAY_NAMES[missingReq]}
    • - ))} -
    -
    - ))}{' '} +
    + } + description={ +
    + {(validationErrors ?? []) + .filter((e) => e.errorType === 'MISSING_ENTITY') + .map((validationError) => ( +
    + In the {DISPLAY_NAMES[validationError.name]}{' '} + {validationError.title}, the following + requirements are missing or incomplete: +
      + {validationError.missing.map((missingReq) => ( +
    • {DISPLAY_NAMES[missingReq]}
    • + ))} +
    +
    + ))} + {(validationErrors ?? []) + .filter((e) => e.errorType === 'MISSING_FILES') + .map((validationError) => ( +
    + The {DISPLAY_NAMES[validationError.name]}{' '} + {validationError.title} has no associated data. +
    + ))} + {(validationErrors ?? []) + .filter((e) => e.errorType === 'NO_SELECTION') + .map((validationError) => ( +
    + No publishable collections are selected. +
    + ))}
    } /> @@ -55,24 +77,59 @@ export const PipelineSelectForPublish: React.FC<{ [children] ); const [searchParams, setSearchParams] = useSearchParams(); + const operation = searchParams.get('operation'); + const selectedEntities = searchParams.getAll('selected'); - const toggleEntitySelection = (uuid: string) => { - const selectedEntities = searchParams.getAll('selected'); - const newSearchParams = new URLSearchParams(searchParams); + const toggleEntitySelection = useCallback( + (uuid: string) => { + const selectedEntities = searchParams.getAll('selected'); + const newSearchParams = new URLSearchParams(searchParams); - if (selectedEntities.includes(uuid)) { - newSearchParams.delete('selected', uuid); - setSearchParams(newSearchParams, { replace: true }); - } else { - newSearchParams.append('selected', uuid); - setSearchParams(newSearchParams, { replace: true }); + if (selectedEntities.includes(uuid)) { + newSearchParams.delete('selected', uuid); + setSearchParams(newSearchParams, { replace: true }); + } else { + newSearchParams.append('selected', uuid); + setSearchParams(newSearchParams, { replace: true }); + } + }, + [setSearchParams, searchParams] + ); + + useEffect(() => { + if (operation !== 'publish') { + const publishableChildren = sortedChildren.filter((child) => + data?.entities.some( + (ent) => ent.uuid === child.uuid && (ent.value.dois?.length ?? 0) > 0 + ) + ); + publishableChildren.forEach((c) => { + if (!selectedEntities.includes(c.uuid)) { + toggleEntitySelection(c.uuid); + } + }); } - }; + }, [ + operation, + sortedChildren, + data, + toggleEntitySelection, + selectedEntities, + ]); const validateAndContinue = async () => { const entityUuids = searchParams.getAll('selected'); const res = await mutateAsync({ projectId, entityUuids: entityUuids }); - if (res.result.length > 0) { + if (entityUuids.length === 0) { + setValidationErrors([ + { + name: 'Project', + title: 'Project', + errorType: 'NO_SELECTION', + missing: [], + }, + ]); + } else if (res.result.length > 0) { setValidationErrors(res.result); } else { setValidationErrors(undefined); @@ -106,6 +163,31 @@ export const PipelineSelectForPublish: React.FC<{ Continue + {operation !== 'publish' && ( + + Amending or revising a project will impact all previously + published works. New datasets cannot be published through this + process. +
    + If you need to publish subsequent dataset(s), please{' '} + + submit a ticket + {' '} + with your project number, the name of the dataset(s), and the + author order of the dataset(s). + + } + /> + )} {(validationErrors?.length ?? 0) > 0 && ( )} @@ -113,6 +195,7 @@ export const PipelineSelectForPublish: React.FC<{ {sortedChildren.map((child) => (
    - + diff --git a/client/modules/datafiles/src/projects/ProjectPipeline/ProjectPipeline.tsx b/client/modules/datafiles/src/projects/ProjectPipeline/ProjectPipeline.tsx index 3ea46d3a60..dc407ce42a 100644 --- a/client/modules/datafiles/src/projects/ProjectPipeline/ProjectPipeline.tsx +++ b/client/modules/datafiles/src/projects/ProjectPipeline/ProjectPipeline.tsx @@ -8,7 +8,7 @@ import { PipelineOrderAuthors } from './PipelineOrderAuthors'; import { PipelineProofreadPublications } from './PipelineProofreadPublications'; import { PipelineProofreadCategories } from './PipelineProofreadCategories'; import { PipelineSelectLicense } from './PipelineSelectLicense'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; const getSteps = ( projectId: string, @@ -19,10 +19,10 @@ const getSteps = ( const proofreadStepMapping: Partial< Record > = { - experimental: 'Experiments', - field_recon: 'Missions', - hybrid_simulation: 'Hybrid Simulations', - simulation: 'Simulations', + experimental: 'Experiment', + field_recon: 'Mission/Documents', + hybrid_simulation: 'Hybrid Simulation', + simulation: 'Simulation', }; switch (projectType) { @@ -51,6 +51,7 @@ const getSteps = ( title: `Proofread ${proofreadStepMapping[projectType]}`, content: ( = ({ projectId, }) => { const [current, setCurrent] = useState(0); - const [, setSearchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const { data } = useProjectDetail(projectId); const projectType = data?.baseProject.value.projectType; @@ -172,10 +174,33 @@ export const ProjectPipeline: React.FC<{ projectId: string }> = ({ return getSteps(projectId, projectType, next, prev); }, [projectId, projectType, next, prev]); + const operationName = { + amend: 'Amending', + version: 'Versioning', + publish: 'Publishing', + }[searchParams.get('operation') ?? 'publish']; + const items = steps.map((item) => ({ key: item.title, title: item.title })); if (!data) return null; return (
    +
    +

    + {operationName} {projectId} +

    + + +   Exit Prepare to + Publish + +
    {steps[current].content}
    diff --git a/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.module.css b/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.module.css index 67f9b505b5..ed104d5ab2 100644 --- a/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.module.css +++ b/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.module.css @@ -19,3 +19,7 @@ background-color: rgba(0, 0, 0, 0.02); border: 1px solid #d9d9d9; } + +.yellow-highlight { + background-color: #ece4bf; +} diff --git a/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.tsx b/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.tsx index 80f7dc2290..44416efb52 100644 --- a/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.tsx +++ b/client/modules/datafiles/src/projects/ProjectPreview/ProjectPreview.tsx @@ -1,96 +1,194 @@ import React, { useEffect, useMemo, useState } from 'react'; import { + apiClient, + DoiContextProvider, + TFileListing, TPreviewTreeData, + useDataciteMetrics, + useDoiContext, useProjectPreview, usePublicationDetail, usePublicationVersions, useSelectedFiles, } from '@client/hooks'; -import { Button, Collapse } from 'antd'; +import { Alert, Button, Collapse, Tag } from 'antd'; import styles from './ProjectPreview.module.css'; import { DISPLAY_NAMES, PROJECT_COLORS } from '../constants'; import { ProjectCollapse } from '../ProjectCollapser/ProjectCollapser'; import { ProjectCitation, PublishedCitation, + DownloadCitation, } from '../ProjectCitation/ProjectCitation'; import { FileListingTable, + FileTypeIcon, TFileListingColumns, -} from '../../FileListing/FileListingTable/FileListingTable'; -import { NavLink } from 'react-router-dom'; +} from '@client/common-components'; +import { Link, useParams } from 'react-router-dom'; import { PublishedEntityDetails } from '../PublishedEntityDetails'; +import { PreviewModalBody } from '../../DatafilesModal/PreviewModal'; +import { SubEntityDetails } from '../SubEntityDetails'; +import { PipelineEditCategoryModal } from '../modals'; -const columns: TFileListingColumns = [ - { - title: 'File Name', - dataIndex: 'name', - ellipsis: true, - render: (data, record) => - record.type === 'dir' ? ( - - -    - - {data} - - ) : ( - +export const EntityFileListingTable: React.FC<{ + treeData: TPreviewTreeData; + preview?: boolean; +}> = ({ treeData, preview }) => { + const [previewModalState, setPreviewModalState] = useState<{ + isOpen: boolean; + path?: string; + selectedFile?: TFileListing; + }>({ isOpen: false }); + + const doi = useDoiContext(); + const columns: TFileListingColumns = [ + { + title: 'File Name', + dataIndex: 'name', + ellipsis: true, + render: (data, record) => ( +
    + {record.type === 'dir' ? ( + + +    + + {data} + + ) : ( + <> + +    + + + )} +
    + {treeData.value.fileTags + .filter((t) => t.path === record.path) + .map((t) => ( + + {t.tagName} + + ))} +
    +
    ), - }, -]; + }, + ]; + return ( + <> + + {previewModalState.path && previewModalState.selectedFile && ( + setPreviewModalState({ isOpen: false })} + /> + )} + + ); +}; function RecursiveTree({ treeData, + preview, defaultOpen = false, + showEditCategories = false, }: { treeData: TPreviewTreeData; defaultOpen?: boolean; + preview?: boolean; + showEditCategories?: boolean; }) { + const { projectId } = useParams(); return (
  • + {showEditCategories && ( +
    + + {({ onClick }) => ( + + )} + +
    + )} - {treeData.value.description} - + +
      - {(treeData.children ?? []).map((child) => ( -
      - - - - -
      - ))} + {(treeData.children ?? []) + .sort((a, b) => a.order - b.order) + .map((child) => ( +
      + + + + +
      + ))}
  • ); @@ -103,6 +201,7 @@ export const PublishedEntityDisplay: React.FC<{ treeData: TPreviewTreeData; defaultOpen?: boolean; defaultOpenChildren?: boolean; + showEditCategories?: boolean; }> = ({ projectId, preview, @@ -110,19 +209,53 @@ export const PublishedEntityDisplay: React.FC<{ license, defaultOpen = false, defaultOpenChildren = false, + showEditCategories = false, }) => { const [active, setActive] = useState(defaultOpen); const sortedChildren = useMemo( () => [...(treeData.children ?? [])].sort((a, b) => a.order - b.order), [treeData] ); + + const dois = + treeData.value.dois && treeData.value.dois.length > 0 + ? treeData.value.dois[0] + : ''; + const { data: citationMetrics } = useDataciteMetrics(dois, !preview); + + useEffect(() => { + if (active && !preview) { + const identifier = dois ?? treeData.uuid; + const path = `${projectId}/${treeData.name}/${identifier}`; + apiClient.get( + `/api/datafiles/agave/public/logentity/designsafe.storage.published/${path}`, + { params: { doi: dois } } + ); + } + }, [active, preview, dois, projectId, treeData.name, treeData.uuid]); + return (
    - {DISPLAY_NAMES[treeData.name]} | {treeData.value.title} + + {DISPLAY_NAMES[treeData.name]} |{' '} + {treeData.value.title} + + {preview && + ((treeData.value.dois?.length ?? 0) > 0 ? ( + Published + ) : ( + Unpublished + ))}
    )} +
    + {citationMetrics && ( +
    + +
    + )}
    null} @@ -171,21 +314,18 @@ export const PublishedEntityDisplay: React.FC<{ publicationDate={treeData.publicationDate} /> {(treeData.value.fileObjs?.length ?? 0) > 0 && ( - )} {(sortedChildren ?? []).map((child) => ( ))} @@ -209,6 +349,21 @@ export const ProjectPreview: React.FC<{ projectId: string }> = ({ ); if (!data) return null; + if (!sortedChildren.length) { + return ( + + No publishable collections were found for this project. You can add + a new collection under the "Curation Directory" tab. + + } + > + ); + } + return (
    {sortedChildren @@ -253,13 +408,15 @@ export const PublicationView: React.FC<{ child.name !== 'designsafe.project' ) .map((child, idx) => ( - + + + ))}
    ); diff --git a/client/modules/datafiles/src/projects/ProjectTitleHeader/ProjectTitleHeader.tsx b/client/modules/datafiles/src/projects/ProjectTitleHeader/ProjectTitleHeader.tsx index afc37943f3..36354a476f 100644 --- a/client/modules/datafiles/src/projects/ProjectTitleHeader/ProjectTitleHeader.tsx +++ b/client/modules/datafiles/src/projects/ProjectTitleHeader/ProjectTitleHeader.tsx @@ -17,6 +17,7 @@ export const ProjectTitleHeader: React.FC<{ projectId: string }> = ({ {baseProject.value.projectId} |{' '} {baseProject.value.title} + {({ onClick }) => ( + )} + + + {({ onClick }) => ( + + )} + + + ); + case 'experimental': + return ( + + {({ onClick }) => ( + + )} + + ); + case 'simulation': + return ( + + {({ onClick }) => ( + + )} + + ); + case 'hybrid_simulation': + return ( + + {({ onClick }) => ( + + )} + + ); + default: + return ( + + ); + } +}; diff --git a/client/modules/datafiles/src/projects/PublishedEntityDetails.tsx b/client/modules/datafiles/src/projects/PublishedEntityDetails.tsx index 88d2eb20ec..4910d4bf04 100644 --- a/client/modules/datafiles/src/projects/PublishedEntityDetails.tsx +++ b/client/modules/datafiles/src/projects/PublishedEntityDetails.tsx @@ -7,6 +7,7 @@ import { LicenseDisplay, UsernamePopover, } from './BaseProjectDetails'; +import { Alert } from 'antd'; export const PublishedEntityDetails: React.FC<{ entityValue: TEntityValue; @@ -15,6 +16,25 @@ export const PublishedEntityDetails: React.FC<{ }> = ({ entityValue, publicationDate, license }) => { return (
    + {entityValue.tombstone && ( + The following Dataset does not exist anymore + } + description={ +
    + The Dataset with DOI:{' '} + + {entityValue.dois?.[0]} + {' '} + was incomplete and removed. The metadata is still available. +
    + } + /> + )} @@ -56,7 +76,7 @@ export const PublishedEntityDetails: React.FC<{ {(entityValue.authors ?? []).length > 0 && ( - + - + @@ -173,14 +193,14 @@ export const PublishedEntityDetails: React.FC<{ )} - {publicationDate && ( - - - - - )} + + + + {entityValue.dois && entityValue.dois[0] && ( diff --git a/client/modules/datafiles/src/projects/SubEntityDetails.tsx b/client/modules/datafiles/src/projects/SubEntityDetails.tsx new file mode 100644 index 0000000000..39ab4a4b95 --- /dev/null +++ b/client/modules/datafiles/src/projects/SubEntityDetails.tsx @@ -0,0 +1,166 @@ +import React from 'react'; +import { TEntityValue } from '@client/hooks'; + +import styles from './BaseProjectDetails.module.css'; +import { DescriptionExpander, UsernamePopover } from './BaseProjectDetails'; + +export const SubEntityDetails: React.FC<{ + entityValue: TEntityValue; +}> = ({ entityValue }) => { + return ( +
    +
    AuthorsAuthor(s) {entityValue.authors ?.filter((a) => a.authorship !== false) @@ -76,7 +96,7 @@ export const PublishedEntityDetails: React.FC<{ {entityValue.facility && (
    Experiment TypeFacility {entityValue.facility?.name}
    Date Published - {new Date(publicationDate).toISOString().split('T')[0]} -
    Date Published + {publicationDate + ? new Date(publicationDate).toISOString().split('T')[0] + : '(Appears after publication)'} +
    + + + + + + {entityValue.event && ( + + + + + )} + + {entityValue.observationTypes && ( + + + + + )} + + {entityValue.unit && ( + + + + + )} + + {entityValue.modes && entityValue.modes.length > 0 && ( + + + + + )} + + {entityValue.sampleApproach && + entityValue.sampleApproach.length > 0 && ( + + + + + )} + + {entityValue.sampleSize && ( + + + + + )} + + {entityValue.dateStart && ( + + + + + )} + + {entityValue.simulationType && ( + + + + + )} + + {(entityValue.dataCollectors ?? []).length > 0 && ( + + + + + )} + + {entityValue.equipment && entityValue.equipment.length > 0 && ( + + + + + )} + + {entityValue.location && ( + + + + + )} + + {entityValue.restriction && ( + + + + + )} + +
    Event{entityValue.event}
    Observation Type(s) + {entityValue.observationTypes.map((t) => ( +
    {t.name}
    + ))} +
    Unit of Analysis + {entityValue.unit} +
    Mode(s) of Collection + {entityValue.modes.map((mode) => ( +
    {mode}
    + ))} +
    Sampling Approach(es) + {entityValue.sampleApproach.map((approach) => ( +
    {approach}
    + ))} +
    Sample Size + {entityValue.sampleSize} +
    Date(s) of Collection + {new Date(entityValue.dateStart).toISOString().split('T')[0]} + {entityValue.dateEnd && ( + + {' ― '} + {new Date(entityValue.dateEnd).toISOString().split('T')[0]} + + )} +
    Simulation Type + {entityValue.simulationType?.name} +
    Data Collectors + {entityValue.dataCollectors + ?.filter((a) => a.authorship !== false) + .map((u, i) => ( + + + {i !== + (entityValue.dataCollectors?.filter( + (a) => a.authorship !== false + ).length ?? 0) - + 1 && '; '} + + ))} +
    Equipment + {entityValue.equipment.map((t) => ( +
    {t.name}
    + ))} +
    Site Location + {entityValue.location} |{' '} + + Lat {entityValue.latitude} long {entityValue.longitude} + +
    Restriction + {entityValue.restriction} +
    + + Description: + {entityValue.description || '(N/A)'} + +
    + ); +}; diff --git a/client/modules/datafiles/src/projects/constants.ts b/client/modules/datafiles/src/projects/constants.ts index a14750a856..bda51640bd 100644 --- a/client/modules/datafiles/src/projects/constants.ts +++ b/client/modules/datafiles/src/projects/constants.ts @@ -167,7 +167,7 @@ export const DISPLAY_NAMES: Record = { // Experimental [EXPERIMENT]: 'Experiment', [EXPERIMENT_MODEL_CONFIG]: 'Model Configuration', - [EXPERIMENT_SENSOR]: 'Sensor', + [EXPERIMENT_SENSOR]: 'Sensor Information', [EXPERIMENT_ANALYSIS]: 'Analysis', [EXPERIMENT_EVENT]: 'Event', [EXPERIMENT_REPORT]: 'Report', @@ -183,7 +183,7 @@ export const DISPLAY_NAMES: Record = { [HYBRID_SIM_REPORT]: 'Report', [HYBRID_SIM_ANALYSIS]: 'Analysis', [HYBRID_SIM_GLOBAL_MODEL]: 'Global Model', - [HYBRID_SIM_COORDINATOR]: 'Simulation Coordinator', + [HYBRID_SIM_COORDINATOR]: 'Master Simulation Coordinator', [HYBRID_SIM_SIM_SUBSTRUCTURE]: 'Simulation Substructure', [HYBRID_SIM_EXP_SUBSTRUCTURE]: 'Experimental Substructure', [HYBRID_SIM_EXP_OUTPUT]: 'Experimental Output', diff --git a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx index e8dc54565a..bd576bcbf8 100644 --- a/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx +++ b/client/modules/datafiles/src/projects/forms/BaseProjectForm.tsx @@ -1,5 +1,5 @@ -import { Button, Form, Input, Select } from 'antd'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import { Alert, Button, Form, Input, Popconfirm, Select } from 'antd'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { nhTypeOptions, facilityOptions, @@ -16,10 +16,13 @@ import { ReferencedDataInput, } from './_fields'; import { TProjectUser } from './_fields/UserSelect'; -import { TBaseProjectValue, useProjectDetail } from '@client/hooks'; +import { + TBaseProjectValue, + useAuthenticatedUser, + useProjectDetail, +} from '@client/hooks'; import { customRequiredMark } from './_common'; import { AuthorSelect } from './_fields/AuthorSelect'; -import { ChangeProjectTypeModal } from '../modals'; import { ProjectTypeRadioSelect } from '../modals/ProjectTypeRadioSelect'; export const ProjectTypeInput: React.FC<{ @@ -80,11 +83,18 @@ export const ProjectTypeInput: React.FC<{ export const BaseProjectForm: React.FC<{ projectId: string; - onChangeType?: () => void; -}> = ({ projectId, onChangeType }) => { + projectType?: string; + onSubmit: (patchMetadata: Record) => void; + changeTypeModal?: React.ReactElement; +}> = ({ projectId, projectType, onSubmit, changeTypeModal }) => { const [form] = Form.useForm(); const { data } = useProjectDetail(projectId ?? ''); - const projectType = data?.baseProject.value.projectType; + + const [hasValidationErrors, setHasValidationErrors] = useState(false); + + if (!projectType) { + projectType = data?.baseProject.value.projectType; + } function processFormData(formData: Record) { const { pi, coPis, teamMembers, guestMembers, ...rest } = formData; @@ -113,29 +123,46 @@ export const BaseProjectForm: React.FC<{ }; } - const watchedValues = Form.useWatch([], form); + const watchedPi = Form.useWatch(['pi'], form); + const watchedCoPis = Form.useWatch(['coPis'], form); + const watchedMembers = Form.useWatch(['teamMembers'], form); + const watchedGuestMembers = Form.useWatch(['guestMembers'], form); const watchedUsers = useMemo( () => [ - ...(watchedValues?.pi ?? []), - ...(watchedValues?.coPis ?? []), - ...(watchedValues?.teamMembers ?? []), - ...(watchedValues?.guestMembers ?? []), + ...(watchedPi ?? []), + ...(watchedCoPis ?? []), + ...(watchedMembers ?? []), + ...(watchedGuestMembers?.filter( + (f: TProjectUser) => !!f && f.fname && f.lname && f.email && f.inst + ) ?? []), ], - [ - watchedValues?.pi, - watchedValues?.coPis, - watchedValues?.teamMembers, - watchedValues?.guestMembers, - ] + [watchedPi, watchedCoPis, watchedMembers, watchedGuestMembers] ); + const { user } = useAuthenticatedUser(); + const [showConfirm, setShowConfirm] = useState(false); + const onFormSubmit = ( + v: Record & { users: TProjectUser[] } + ) => { + setHasValidationErrors(false); + const currentUserInProject = v.users.find( + (u) => u.username === user?.username + ); + if (!currentUserInProject && !showConfirm) { + setShowConfirm(true); + } else { + onSubmit(v); + } + }; + if (!data) return
    Loading
    ; return (
    console.log(processFormData(v))} - onFinishFailed={(v) => console.log(processFormData(v.values))} + onFinish={(v) => onFormSubmit(processFormData(v))} + onFinishFailed={() => setHasValidationErrors(true)} requiredMark={customRequiredMark} > @@ -143,7 +170,12 @@ export const BaseProjectForm: React.FC<{ system, and research approach. Define all acronyms. @@ -151,62 +183,73 @@ export const BaseProjectForm: React.FC<{ {/*TODO: disable in situations where project type shouldn't be changed.*/} - - - - {({ onClick }) => ( - - )} - - + {changeTypeModal && ( + + + {changeTypeModal} + + )} {projectType === 'field_recon' && ( - Specify the Field Research being performed. + Specify the Field Research being performed. Enter a custom value by + typing it into the field and pressing "return". )} - - Specify the natural hazard being researched. - - + {projectType !== 'None' && ( + + Specify the natural hazard being researched. Enter a custom value by + typing it into the field and pressing "return". + + + - + )} {projectType === 'other' && ( <> - The nature or genre of the content. + The nature or genre of the content. Enter a custom value by typing + it into the field and pressing "return". - Specify the facilities involved in this research. + Specify the facilities involved in this research. Enter a custom + value by typing it into the field and pressing "return". - + These users can view, edit, curate, and publish. Include Co-PI(s). + Users can be looked up using their exact username{' '} + only. - -   + +
    +
    @@ -238,7 +298,8 @@ export const BaseProjectForm: React.FC<{ - These users can view, edit, curate, and publish. + These users can view, edit, curate, and publish. Users can be looked up + using their exact username only. - + You can order the authors during the publication process. - + - - Recommended for funded projects. - - - Published data used in the creation of this dataset. @@ -276,41 +341,107 @@ export const BaseProjectForm: React.FC<{ Information giving context, a linked dataset on DesignSafe, or works citing the DOI for this dataset. - + )} + + Recommended for funded projects. + + + Details related to specific events such as natural hazards (ex. Hurricane Katrina). - - Choose informative words that indicate the content of the project. - - + {projectType !== 'None' && ( + + Choose informative words that indicate the content of the project. + Keywords should be comma-separated. + + + - - + )} What is this project about? How can data in this project be reused? How is this project unique? Who is the audience? Description must be between 50 and 5000 characters in length. - + {hasValidationErrors && ( + + One or more fields could not be validated. Please check the form + for errors. + + } + /> + )} - + + If you save this project without adding yourself as a principal + investigator +
    or team member, you will lose access to the project and its + files. + + } + open={showConfirm} + okText="Proceed" + placement="topRight" + afterOpenChange={(isOpen) => { + if (isOpen) { + // Focus on opening so that the popover is accessible via keyboard + document.getElementById('prj-confirm-cancel')?.focus(); + } + }} + cancelButtonProps={{ id: 'prj-confirm-cancel' }} + onOpenChange={(newVal) => { + if (!newVal) setShowConfirm(newVal); + }} + onConfirm={() => onSubmit(processFormData(form.getFieldsValue()))} + > + +
    ); diff --git a/client/modules/datafiles/src/projects/forms/CreateProjectForm.tsx b/client/modules/datafiles/src/projects/forms/CreateProjectForm.tsx new file mode 100644 index 0000000000..4a7ac685df --- /dev/null +++ b/client/modules/datafiles/src/projects/forms/CreateProjectForm.tsx @@ -0,0 +1,152 @@ +import { Button, Form, Input } from 'antd'; +import React, { useEffect } from 'react'; + +import { UserSelect, GuestMembersInput } from './_fields'; +import { TProjectUser } from './_fields/UserSelect'; +import { customRequiredMark } from './_common'; +import { useAuthenticatedUser } from '@client/hooks'; + +export const BaseProjectCreateForm: React.FC<{ + onSubmit: (value: Record) => void; +}> = ({ onSubmit }) => { + const [form] = Form.useForm(); + + function processFormData(formData: Record) { + const { pi, coPis, teamMembers, guestMembers, ...rest } = formData; + return { + ...rest, + users: [...pi, ...coPis, ...teamMembers, ...guestMembers], + }; + } + const { user } = useAuthenticatedUser(); + + /* pre-populate form with logged-in user as PI. */ + useEffect(() => { + form.setFieldValue('pi', [ + { + fname: user?.firstName, + lname: user?.lastName, + username: user?.username, + email: user?.email, + inst: user?.institution, + role: 'pi', + }, + ]); + }, [form, user]); + + if (!user) return null; + return ( +
    { + onSubmit(processFormData(v)); + form.resetFields(); + }} + onFinishFailed={(v) => console.log(processFormData(v.values))} + requiredMark={customRequiredMark} + > + + Incorporate the project's focus with words indicating the hazard, model, + system, and research approach. Define all acronyms. + + + + + +
    + + These users can view, edit, curate, and publish. Include Co-PI(s). + Users can be looked up using their exact username{' '} + only. + + + + + +
    +
    + + + +
    +
    + + + These users can view, edit, curate, and publish. + + + + + + + Add members without a DesignSafe account. These names can be selected as + authors during the publication process. + + + + + What is this project about? How can data in this project be reused? How + is this project unique? Who is the audience? Description must be between + 50 and 5000 characters in length. + + + + + + + + +
    + ); +}; diff --git a/client/modules/datafiles/src/projects/forms/ProjectCategoryForm.tsx b/client/modules/datafiles/src/projects/forms/ProjectCategoryForm.tsx index 997a43e08f..d33f1e084b 100644 --- a/client/modules/datafiles/src/projects/forms/ProjectCategoryForm.tsx +++ b/client/modules/datafiles/src/projects/forms/ProjectCategoryForm.tsx @@ -1,56 +1,35 @@ -import { Form, Input, Button, Select, Checkbox } from 'antd'; -import React, { useCallback, useEffect, useState } from 'react'; +import { Form, Input, Button, Select, Alert } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; import { equipmentOptions, observationTypeOptions, } from './ProjectFormDropdowns'; //import { TProjectUser } from './_fields/UserSelect'; -import { - TBaseProjectValue, - TProjectUser, - useProjectDetail, -} from '@client/hooks'; +import { TBaseProjectValue, useProjectDetail } from '@client/hooks'; import { customRequiredMark } from './_common'; import { CATEGORIES_BY_PROJECT_TYPE, DISPLAY_NAMES } from '../constants'; import * as constants from '../constants'; import { DateInput, DropdownSelect, SampleApproachInput } from './_fields'; import { CollectionModeInput } from './_fields/CollectionModeInput'; - -const AuthorSelect: React.FC<{ - projectUsers: TProjectUser[]; - value?: TProjectUser[]; - onChange?: (value: TProjectUser[]) => void; -}> = ({ value, onChange, projectUsers }) => { - const options = projectUsers.map((author) => ({ - value: JSON.stringify(author), - label: `${author.fname} ${author.lname} (${author.email})`, - })); - - const onChangeCallback = useCallback( - (value: string[]) => { - if (onChange) onChange(value.map((a) => JSON.parse(a))); - }, - [onChange] - ); - - return ( - value?.some((v) => user.email === v.email)) - .map((v) => JSON.stringify(v) ?? [])} - options={options} - onChange={onChangeCallback} - /> - ); -}; +import { AuthorSelect } from './_fields/AuthorSelect'; +import { ProjectCategoryFormHelp } from './ProjectCategoryFormHelp'; export const ProjectCategoryForm: React.FC<{ projectType: TBaseProjectValue['projectType']; projectId: string; entityUuid?: string; mode: 'create' | 'edit'; -}> = ({ projectType, projectId, entityUuid, mode = 'edit' }) => { + onSubmit: CallableFunction; + onCancelEdit: CallableFunction; +}> = ({ + projectType, + projectId, + entityUuid, + mode = 'edit', + onSubmit, + onCancelEdit, +}) => { const [form] = Form.useForm(); const { data } = useProjectDetail(projectId ?? ''); const [selectedName, setSelectedName] = useState( @@ -63,38 +42,52 @@ export const ProjectCategoryForm: React.FC<{ label: DISPLAY_NAMES[name], })); - const category = data?.entities.find((e) => e.uuid === entityUuid); + const category = useMemo( + () => data?.entities.find((e) => e.uuid === entityUuid), + [data, entityUuid] + ); + + const [hasValidationErrors, setHasValidationErrors] = useState(false); - const setValues = useCallback(() => { + // Set initial form values + useEffect(() => { if (data && category && mode === 'edit') { form.setFieldsValue({ value: category.value }); setSelectedName(category.name); } - }, [data, form, category, mode]); - useEffect(() => setValues(), [setValues, projectId, category?.uuid]); + setHasValidationErrors(false); + }, [projectId, category, data, form, mode]); - if (!data) return
    Loading
    ; + if (!data) return null; return (
    setSelectedName(v.name)} + onValuesChange={(_, v) => mode === 'create' && setSelectedName(v.name)} layout="vertical" - onFinish={(v) => console.log(v)} + onFinish={(v) => { + onSubmit(v); + form.resetFields(); + setSelectedName(undefined); + onCancelEdit(); + setHasValidationErrors(false); + }} + onFinishFailed={() => setHasValidationErrors(true)} requiredMark={customRequiredMark} > {mode === 'create' && ( - Model Configuration Files describing the design and layout of what is - being tested (some call this a specimen). Sensor Information Files - about the sensor instrumentation used in a model configuration to - conduct one or more event. Event Files from unique occurrences during - which data are generated. Analysis Tables, graphs, visualizations, - Jupyter Notebooks, or other representations of the results. Report - Written accounts made to convey information about an entire project or - experiment. +
    + +
    @@ -114,11 +112,17 @@ export const ProjectCategoryForm: React.FC<{ {selectedName === constants.FIELD_RECON_PLANNING && ( - + Select data collectors for this collection. @@ -128,11 +132,17 @@ export const ProjectCategoryForm: React.FC<{ {selectedName === constants.FIELD_RECON_GEOSCIENCE && ( <> - The nature or subject of the data collected. + The nature or subject of the data collected. Enter a custom value by + typing it into the field and pressing "return". @@ -144,7 +154,12 @@ export const ProjectCategoryForm: React.FC<{
    @@ -159,11 +174,17 @@ export const ProjectCategoryForm: React.FC<{
    - + Select data collectors for this collection. @@ -196,11 +217,18 @@ export const ProjectCategoryForm: React.FC<{ - The equipment used to gather your data. + The equipment used to gather your data. Enter a custom value by + typing it into the field and pressing "return". @@ -240,7 +268,12 @@ export const ProjectCategoryForm: React.FC<{
    @@ -255,11 +288,17 @@ export const ProjectCategoryForm: React.FC<{
    - + Select data collectors for this collection. @@ -295,11 +334,18 @@ export const ProjectCategoryForm: React.FC<{ - The equipment used to gather your data. + The equipment used to gather your data. Enter a custom value by + typing it into the field and pressing "return". @@ -318,25 +364,64 @@ export const ProjectCategoryForm: React.FC<{ )} - + Summarize the purpose of the category and its files. What is it about? What are its features? Description must be between 50 and 5000 characters in length. + {hasValidationErrors && ( + + One or more fields could not be validated. Please check the form + for errors. + + } + /> + )} + + {mode === 'edit' && ( + + )} + )} + Download Dataset} + footer={null} + > + {isLoading && } + {isError && ( + + )} + {data && ( + <> +

    + {exceedsLimit ? ( +

    + This project zipped is {toBytes(data.length)}, + exceeding the 2 GB download limit. To download, + + {' '} + create an account + {' '} + and follow the + + {' '} + Data Transfer Guide + + . Alternatively, download files individually by selecting the + file and using the download button in the toolbar. +

    + ) : ( +

    + This download is a ZIP file of the complete project dataset. The + size of the ZIP file is {toBytes(data.length)}. +

    + )} +
    +

    The files are licensed by the following:

    + {license && LICENSE_INFO_MAP[license]} +

    + + Data Usage Agreement + +

    +
    + + {({ onClick }) => ( + + )} + +
    + + )} +
    + + ); +}; diff --git a/client/modules/datafiles/src/publications/modals/SubmitFeedbackModal.tsx b/client/modules/datafiles/src/publications/modals/SubmitFeedbackModal.tsx new file mode 100644 index 0000000000..8ea9b02521 --- /dev/null +++ b/client/modules/datafiles/src/publications/modals/SubmitFeedbackModal.tsx @@ -0,0 +1,144 @@ +import React, { useState } from 'react'; +import { Button, Form, Input, Modal } from 'antd'; +import { useAuthenticatedUser, useCreateFeedbackTicket } from '@client/hooks'; +import { notification } from 'antd'; + +export const SubmitFeedbackModal: React.FC<{ + projectId: string; + title: string; +}> = ({ projectId, title }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const showModal = () => setIsModalOpen(true); + const handleClose = () => { + setIsModalOpen(false); + }; + + const { mutate } = useCreateFeedbackTicket(projectId, title); + const [notifApi, contextHolder] = notification.useNotification(); + + const { user } = useAuthenticatedUser(); + const submitFeedback = (formData: { + name: string; + email: string; + body: string; + }) => { + mutate( + { formData: { ...formData, projectId, title } }, + { + onSuccess: () => { + handleClose(); + notifApi.open({ + type: 'success', + message: '', + description: 'Your feedback was successfully submitted', + placement: 'bottomLeft', + }); + }, + } + ); + }; + + return ( + <> + {contextHolder} + + Leave Feedback} + footer={null} + > +
    + submitFeedback(formData)} + > + + + + + + + +
    + Feedback +
    +
    + Leave constructive feedback for the author(s) of this + publication. +
    +
    + } + required + rules={[{ required: true, message: 'This field is required.' }]} + > + +
    + + + + +
    + Examples of constructive questions and concerns: +
      +
    • + Questions about the dataset that are not answered in the + published metadata and or documentation +
    • +
    • Missing documentation
    • +
    • + Questions about the method/instruments used to generate the data +
    • +
    • Questions about data validation
    • +
    • + Concerns about data organization and or inability to find + desired files +
    • +
    • + Interest in bibliography about the data/related to the data +
    • +
    • Interest in reusing the data
    • +
    • Comments about the experience of reusing the data
    • +
    • Request to access raw data if not published
    • +
    • Congratulations
    • +
    +
    + + + + ); +}; diff --git a/client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.module.css b/client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.module.css new file mode 100644 index 0000000000..9ada457fa5 --- /dev/null +++ b/client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.module.css @@ -0,0 +1,9 @@ +.root, +.root a { + font-size: 16px; + font-weight: medium; +} + +.root li:has(span, a) { + align-self: center; +} diff --git a/client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.tsx b/client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.tsx new file mode 100644 index 0000000000..611188e3d1 --- /dev/null +++ b/client/modules/workspace/src/AppsBreadcrumb/AppsBreadcrumb.tsx @@ -0,0 +1,117 @@ +import React, { ReactNode } from 'react'; +import { Breadcrumb, Button } from 'antd'; +import { Link, useLocation } from 'react-router-dom'; +import styles from './AppsBreadcrumb.module.css'; +import { + useGetApps, + TAppResponse, + useAppsListing, + TAppCategories, +} from '@client/hooks'; +import { useGetAppParams } from '../utils'; + +function getPathRoutes(path: string = '') { + const pathComponents = decodeURIComponent(path) + .split('/') + .filter((p) => !!p); + + return pathComponents.map((comp, i) => ({ + title: comp === 'history' ? 'Job Status' : comp, // Modify the path for Job Status + path: '/' + encodeURIComponent(pathComponents.slice(0, i + 1).join('/')), + })); +} + +export const AppsBreadcrumb: React.FC = () => { + const { pathname } = useLocation(); + const { appId, appVersion } = useGetAppParams(); + const { + data: { categories }, + } = useAppsListing() as { data: TAppCategories }; + const currentAppFromCategories = categories + .map((cat) => cat.apps) + .flat() + .find((app) => app.app_id === appId && app.version === (appVersion || '')); + + const breadcrumbItems = [ + { title: 'Home', path: window.location.origin }, + { title: 'Use DesignSafe' }, + { + title: 'Tools & Applications', + href: '/use-designsafe/tools-applications/', + }, + ]; + if (currentAppFromCategories?.bundle_category) { + breadcrumbItems.push({ + title: `${currentAppFromCategories.bundle_category}`, + href: `/use-designsafe/tools-applications/${currentAppFromCategories.bundle_category + .toLowerCase() + .replace(/ /g, '-')}`, + }); + } + if (currentAppFromCategories?.bundle_href) { + breadcrumbItems.push({ + title: `${currentAppFromCategories.bundle_label} Overview`, + href: currentAppFromCategories.bundle_href, + }); + } + + return ( + { + const isLast = obj?.path === items[items.length - 1]?.path; + + return appId && isLast ? ( + + ) : ( + + ); + }} + /> + ); +}; + +export const BreadcrumbRender: React.FC<{ + path?: string; + href?: string; + title: ReactNode; + isLast: boolean; +}> = ({ path, href, title, isLast }) => { + if (href && !isLast) + return ( + + ); + + if (isLast || !path) { + return {title}; + } + + return ( + + {title} + + ); +}; + +export const AppBreadcrumb: React.FC<{ + appId: string; + appVersion?: string; +}> = ({ appId, appVersion }) => { + const { data: appData } = useGetApps({ appId, appVersion }) as { + data: TAppResponse; + }; + const title = appData?.definition.notes?.label || appData?.definition.id; + + return ; +}; + +export default AppsBreadcrumb; diff --git a/client/modules/workspace/src/AppsSideNav/AppSideNav.spec.tsx b/client/modules/workspace/src/AppsSideNav/AppSideNav.spec.tsx new file mode 100644 index 0000000000..6fd3a0b818 --- /dev/null +++ b/client/modules/workspace/src/AppsSideNav/AppSideNav.spec.tsx @@ -0,0 +1,19 @@ +import { render, appsListingJson } from '@client/test-fixtures'; + +import { AppsSideNav } from './AppsSideNav'; + +describe('AppsSideNav', () => { + it('should render successfully', () => { + const { baseElement } = render( + + ); + expect(baseElement).toBeTruthy(); + }); + + it('should have nav text', () => { + const { getAllByText } = render( + + ); + expect(getAllByText(/Applications:/gi)).toBeTruthy(); + }); +}); diff --git a/client/modules/workspace/src/AppsSideNav/AppsSideNav.tsx b/client/modules/workspace/src/AppsSideNav/AppsSideNav.tsx new file mode 100644 index 0000000000..e26d31b0f0 --- /dev/null +++ b/client/modules/workspace/src/AppsSideNav/AppsSideNav.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Menu, MenuProps } from 'antd'; +import { NavLink } from 'react-router-dom'; +import { TAppCategory, TPortalApp } from '@client/hooks'; +import { useGetAppParams } from '../utils'; + +export const AppsSideNav: React.FC<{ categories: TAppCategory[] }> = ({ + categories, +}) => { + type MenuItem = Required['items'][number]; + + function getItem( + label: React.ReactNode, + key: string, + children?: MenuItem[], + type?: 'group' + ): MenuItem { + return { + label, + key, + children, + type, + } as MenuItem; + } + + const getCategoryApps = (category: TAppCategory) => { + const bundles: { + [dynamic: string]: { + apps: MenuItem[]; + label: string; + }; + } = {}; + const categoryItems: MenuItem[] = []; + + category.apps.forEach((app) => { + if (app.is_bundled) { + const bundleKey = `${app.bundle_label}${app.bundle_id}`; + if (bundles[bundleKey]) { + bundles[bundleKey].apps.push( + getItem( + + {app.shortLabel || app.label || app.bundle_label} + , + `${app.app_id}${app.version}${app.bundle_id}` + ) + ); + } else { + bundles[bundleKey] = { + apps: [ + getItem( + + {app.shortLabel || app.label || app.bundle_label} + , + `${app.app_id}${app.version}${app.bundle_id}` + ), + ], + label: app.bundle_label, + }; + } + } else { + categoryItems.push( + getItem( + + {app.shortLabel || app.label || app.bundle_label} + , + `${app.app_id}${app.version}${app.bundle_id}` + ) + ); + } + }); + const bundleItems = Object.entries(bundles).map(([bundleKey, bundle]) => + getItem(`${bundle.label} [${bundle.apps.length}]`, bundleKey, bundle.apps) + ); + + return categoryItems + .concat(bundleItems) + .sort((a, b) => (a?.key as string).localeCompare(b?.key as string)); + }; + + const items: MenuItem[] = categories.map((category) => { + return getItem( + `${category.title} [${category.apps.length}]`, + category.title, + getCategoryApps(category) + ); + }); + + const { appId, appVersion } = useGetAppParams(); + + const currentApp = categories + .map((cat) => cat.apps) + .flat() + .find((app) => app.app_id === appId && app.version === (appVersion || '')); + const currentCategory = categories.find((cat) => + cat.apps.includes(currentApp as TPortalApp) + ); + const currentSubMenu = currentApp?.is_bundled + ? `${currentApp.bundle_label}${currentApp.bundle_id}` + : ''; + const selectedKey = `${appId}${appVersion || ''}${currentApp?.bundle_id}`; + + return ( + <> +
    + Applications: +
    + + + ); +}; diff --git a/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.module.css b/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.module.css new file mode 100644 index 0000000000..d043aebcd0 --- /dev/null +++ b/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.module.css @@ -0,0 +1,12 @@ +.root { + > :first-child { + border-bottom: 1px solid; + padding-bottom: 10px; + } +} +.root .ant-descriptions-item-container { + padding: 0 5px; +} +.ant-descriptions-item-container { + padding: 0 5px; +} diff --git a/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx b/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx new file mode 100644 index 0000000000..f0a423b161 --- /dev/null +++ b/client/modules/workspace/src/AppsSubmissionDetails/AppsSubmissionDetails.tsx @@ -0,0 +1,270 @@ +import React from 'react'; +import { + Card, + ConfigProvider, + Descriptions, + DescriptionsProps, + Tag, + ThemeConfig, + Button, + Flex, +} from 'antd'; +import { useFormContext, useWatch, FieldValues } from 'react-hook-form'; +import { z, ZodTypeAny } from 'zod'; +import { TField, fieldDisplayOrder } from '../AppsWizard/AppsFormSchema'; +import { PrimaryButton } from '@client/common-components'; +import styles from './AppsSubmissionDetails.module.css'; +import { TTapisApp } from '@client/hooks'; + +const tagTheme: ThemeConfig = { + token: { + fontFamily: 'Helvetica Neue', + fontSizeSM: 12, + borderRadiusSM: 3, + lineWidth: 0, + }, + components: { + Tag: { + defaultBg: '#EB6E6E 0% 0% no-repeat padding-box', + defaultColor: '#FFFFFF', + }, + }, +}; + +const itemStyle = { + paddingBottom: 0, + backgroundColor: 'inherit', +}; + +const descriptionCardStyle = { + background: '#f4f4f4', + backgroundOrigin: 'padding-box', + border: '1px solid #dbdbdb', +}; + +// For summary, it is considered required : only if it is required and value is not valid. +function isFieldRequired( + fieldSchema: ZodTypeAny | undefined, + value: unknown +): boolean { + return ( + (!(value instanceof Object) && + fieldSchema && + !fieldSchema.isOptional() && + !fieldSchema.safeParse(value).success) ?? + false + ); +} + +export const AppsSubmissionDetails: React.FC<{ + schema: { [dynamic: string]: z.ZodType }; + fields: { + [dynamic: string]: { + [dynamic: string]: { [dynamic: string]: TField } | TField; + }; + }; + isSubmitting: boolean; + current: string; + setCurrent: CallableFunction; + definition: TTapisApp; +}> = ({ schema, fields, isSubmitting, current, setCurrent, definition }) => { + const { + control, + formState: { defaultValues, isValid }, + } = useFormContext(); + const formState = useWatch({ control, defaultValue: defaultValues }); + const getChildren = ( + key: string, + value: string | object, + parent: z.AnyZodObject, + index: number + ) => { + if (typeof value === 'object') { + if (!Object.keys(value).length) return -; + const items: DescriptionsProps['items'] = []; + const entries = Object.entries(value); + if (key in fieldDisplayOrder) { + const displayOrder = + fieldDisplayOrder[key as keyof typeof fieldDisplayOrder]; + entries.sort( + (a, b) => displayOrder.indexOf(a[0]) - displayOrder.indexOf(b[0]) + ); + } + entries.forEach(([k, v], childIndex) => { + if ( + definition.notes.hideQueue && + key === 'configuration' && + k === 'execSystemLogicalQueue' + ) { + return; // Hide the queue, if the app definition requires it + } + if ( + definition.notes.hideAllocation && + key === 'configuration' && + k === 'allocation' + ) { + return; // Hide the allocation, if that field is true + } + if (v instanceof Object) { + Object.entries(v as object).forEach(([kk, vv], zchildIndex) => { + const nestedFieldSchema = parent?.shape?.[k]?.shape?.[kk]; + const isRequired = isFieldRequired(nestedFieldSchema, vv); + items.push({ + key: kk, + label: ( + + {String(fields[key]?.[kk]?.label || kk)}{' '} + {isRequired && ( + + + Required + + + )} + + ), + children: getChildren( + `${key}.${kk}`, + vv, + parent?.shape?.[kk], + zchildIndex + ), + style: { + padding: '8px', + backgroundColor: zchildIndex % 2 === 0 ? '#fff' : '#f4f4f4', + borderBottom: '1px solid #DBDBDB', + }, + }); + }); + } else { + const fieldSchema = parent?.shape?.[k]; + const isRequired = isFieldRequired(fieldSchema, v); + items.push({ + key: k, + label: ( + + {String(fields[key]?.[k]?.label || k)}{' '} + {isRequired && ( + + + Required + + + )} + + ), + children: getChildren( + `${key}.${k}`, + v, + parent?.shape?.[k], + childIndex + ), + style: { + padding: '8px', + backgroundColor: childIndex % 2 === 0 ? '#fff' : '#f4f4f4', + borderBottom: '1px solid #DBDBDB', + }, + }); + } + }); + return ( + + ); + } else { + return {`${value}`}; + } + }; + + const getItems = (values: FieldValues) => { + const items: DescriptionsProps['items'] = Object.entries(values) + // Filter out empty items. Example: app with no inputs + .filter( + ([_, value]) => + typeof value !== 'object' || Object.keys(value).length > 0 + ) + // Filter out outputs fields for interactive apps + .filter( + ([key]) => + !(key === 'outputs' && definition.notes.isInteractive === true) + ) + .map(([key, value], index) => ({ + key: key, + label: ( + +
    + {key.charAt(0).toUpperCase() + key.slice(1)} +
    + {current !== key && ( + + )} +
    + ), + children: getChildren(key, value, schema[key] as z.AnyZodObject, index), + style: itemStyle, + labelStyle: { + color: 'rgba(0, 0, 0, 0.88)', + fontWeight: 'bold', + backgroundColor: 'inherit', + width: '100%', + borderBottom: '1px solid #707070', + lineHeight: 4, + }, + contentStyle: { + color: '#484848', + border: '0', + backgroundColor: 'inherit', + }, + })); + return items; + }; + + return ( + + + Submit Job + + } + /> + + ); +}; diff --git a/client/modules/workspace/src/AppsSubmissionForm/AppIcon.module.css b/client/modules/workspace/src/AppsSubmissionForm/AppIcon.module.css new file mode 100644 index 0000000000..3051f8aa49 --- /dev/null +++ b/client/modules/workspace/src/AppsSubmissionForm/AppIcon.module.css @@ -0,0 +1,5 @@ +.root { + width: unset; + height: unset; + padding-right: 10px; +} diff --git a/client/modules/workspace/src/AppsSubmissionForm/AppIcon.tsx b/client/modules/workspace/src/AppsSubmissionForm/AppIcon.tsx new file mode 100644 index 0000000000..b2ee5a9f4d --- /dev/null +++ b/client/modules/workspace/src/AppsSubmissionForm/AppIcon.tsx @@ -0,0 +1,9 @@ +import { Icon } from '@client/common-components'; +import styles from './AppIcon.module.css'; + +const AppIcon: React.FC<{ name: string }> = ({ name }) => { + const className = `ds-icon ds-icon-${name}`; + return ; +}; + +export default AppIcon; diff --git a/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx b/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx new file mode 100644 index 0000000000..cdecd591bd --- /dev/null +++ b/client/modules/workspace/src/AppsSubmissionForm/AppsSubmissionForm.tsx @@ -0,0 +1,628 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { NavLink } from 'react-router-dom'; +import { Layout, Form, Col, Row, Alert, Button } from 'antd'; +import { z } from 'zod'; +import { useForm, FormProvider } from 'react-hook-form'; +import { Link } from 'react-router-dom'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + useGetAppsSuspense, + useGetJobSuspense, + usePostJobs, + useGetSystems, + useAuthenticatedUser, + TTapisSystem, + TUser, + TAppFileInput, + TJobSubmit, + TParameterSetSubmit, + TJobBody, + useGetAllocationsSuspense, + TTapisJob, +} from '@client/hooks'; +import { AppsSubmissionDetails } from '../AppsSubmissionDetails/AppsSubmissionDetails'; +import { AppsWizard } from '../AppsWizard/AppsWizard'; +import { + default as FormSchema, + TField, + TFormValues, + TAppFieldSchema, + getConfigurationSchema, + getConfigurationFields, +} from '../AppsWizard/AppsFormSchema'; +import { + getInputsStep, + getParametersStep, + getConfigurationStep, + getOutputsStep, + stepKeys, +} from '../AppsWizard/Steps'; +import { SystemsPushKeysModal } from '../SystemsPushKeysModal/SystemsPushKeysModal'; +import { + getSystemName, + getExecSystemFromId, + getQueueValueForExecSystem, + isAppTypeBATCH, + isTargetPathField, + getInputFieldFromTargetPathField, + isTargetPathEmpty, + getExecSystemsFromApp, + useGetAppParams, + updateValuesForQueue, + getDefaultExecSystem, + getAllocationList, + mergeConfigurationDefaultsWithJobData, + mergeParameterSetDefaultsWithJobData, + mergeInputDefaultsWithJobData, +} from '../utils'; + +export const AppsSubmissionForm: React.FC = () => { + const { appId, appVersion, jobUUID } = useGetAppParams(); + const { data: app } = useGetAppsSuspense({ appId, appVersion }); + const { data: tasAllocations } = useGetAllocationsSuspense(); + + const { + data: { executionSystems, storageSystems, defaultStorageSystem }, + } = useGetSystems(); + + const { + user: { username }, + } = useAuthenticatedUser() as { user: TUser }; + const { data: jobData } = useGetJobSuspense('select', { uuid: jobUUID }) as { + data: TTapisJob; + }; + + const { definition, license, defaultSystemNeedsKeys } = app; + + const defaultStorageHost = defaultStorageSystem.host; + const hasCorral = ['data.tacc.utexas.edu', 'corral.tacc.utexas.edu'].some( + (s) => defaultStorageHost?.endsWith(s) + ); + + // Check if user has default allocation if defaultStorageHost is not corral + const hasDefaultAllocation = + hasCorral || tasAllocations.hosts[defaultStorageHost]; + + const hasStorageSystems = !!storageSystems.length; + + const execSystems = getExecSystemsFromApp( + definition, + executionSystems as TTapisSystem[] + ); + const defaultExecSystem = getDefaultExecSystem( + definition, + execSystems + ) as TTapisSystem; + const allocations = getAllocationList(defaultExecSystem, tasAllocations); + const portalAlloc = allocations.find((a) => a.startsWith('DS-HPC')); + + const { fileInputs, parameterSet, configuration, outputs } = FormSchema( + definition, + executionSystems, + allocations, + defaultStorageSystem, + username, + portalAlloc + ); + + // TODOv3: dynamic exec system and queues + const initialValues: TFormValues = useMemo( + () => ({ + inputs: mergeInputDefaultsWithJobData( + appId, + appVersion, + fileInputs.defaults, + jobData + ), + parameters: mergeParameterSetDefaultsWithJobData( + appId, + appVersion, + parameterSet.defaults, + jobData + ), + configuration: mergeConfigurationDefaultsWithJobData( + appId, + appVersion, + configuration.defaults, + jobData + ), + outputs: outputs.defaults, + }), + [definition, jobData] + ); + + let missingAllocation: string | undefined; + if (!hasDefaultAllocation && hasStorageSystems) { + // User does not have default storage allocation + missingAllocation = getSystemName(defaultStorageHost); + } else if (isAppTypeBATCH(definition) && !allocations.length) { + // User does not have allocation on execution system for a batch type app + missingAllocation = getSystemName(defaultExecSystem.host); + } + + // const exec_sys = getExecSystemFromId(app, state.execSystemId); + // const queue = getQueueValueForExecSystem( + // app, + // exec_sys, + // state.execSystemLogicalQueue + // ); + + // const currentExecSystem = getExecSystemFromId(app, state.execSystemId); + + const [schema, setSchema] = useState({ + inputs: z.object(fileInputs.schema), + parameters: z.object(parameterSet.schema), + configuration: z.object(configuration.schema), + outputs: z.object(outputs.schema), + }); + + const { Content } = Layout; + + const missingLicense = license.type && !license.enabled; + + const methods = useForm({ + defaultValues: initialValues, + resolver: zodResolver(z.object(schema)), + mode: 'onChange', + }); + const { handleSubmit, reset, setValue, getValues, watch } = methods; + + // Define type to support calls like method.trigger, which + // require literals instead of string or string[] + const fieldValues = getValues(); + type FieldNameUnion = keyof typeof fieldValues; + + const getSteps = (): TStep => { + const formSteps: TStep = { + configuration: getConfigurationStep(configuration.fields), + ...(definition.notes.isInteractive + ? {} + : { outputs: getOutputsStep(outputs.fields) }), + }; + if (fileInputs.fields && Object.keys(fileInputs.fields).length) { + formSteps.inputs = getInputsStep(fileInputs.fields); + } + if (parameterSet.fields && Object.keys(parameterSet.fields).length) { + formSteps.parameters = getParametersStep(parameterSet.fields); + } + // Setup prev and next steps based on what is available. + const formStepKeys = Object.keys(formSteps); + const availableStepKeys = stepKeys.filter((key) => + formStepKeys.includes(key) + ); + availableStepKeys.forEach((key, index) => { + if (index > 0 && formSteps[availableStepKeys[index - 1]]) { + formSteps[key].prevPage = availableStepKeys[index - 1]; + } + if ( + index < stepKeys.length - 1 && + formSteps[availableStepKeys[index + 1]] + ) { + formSteps[key].nextPage = availableStepKeys[index + 1]; + } + }); + + return formSteps; + }; + + const [fields, setFields] = useState<{ + [dynamic: string]: { + [dynamic: string]: TField | { [dynamic: string]: TField }; + }; + }>({ + inputs: fileInputs.fields, + parameters: parameterSet.fields, + configuration: configuration.fields, + outputs: outputs.fields, + }); + + const initialSteps = useMemo( + () => getSteps(), + [ + fileInputs.fields, + parameterSet.fields, + configuration.fields, + outputs.fields, + fields, + ] + ); + + const getInitialCurrentStep = (steps: TStep) => { + if (steps.inputs) return 'inputs'; + if (steps.parameters) return 'parameters'; + return 'configuration'; + }; + const [steps, setSteps] = useState(initialSteps); + const [current, setCurrent] = useState(getInitialCurrentStep(initialSteps)); + + useEffect(() => { + reset(initialValues); + const newSteps = getSteps(); + setSteps(newSteps); + setCurrent(getInitialCurrentStep(newSteps)); + }, [initialValues]); + + // Queue dependency handler. + const queueValue = watch('configuration.execSystemLogicalQueue'); + React.useEffect(() => { + if (queueValue) { + const execSystem = getExecSystemFromId( + execSystems, + definition.jobAttributes.execSystemId + ); + if (!execSystem) return; + updateValuesForQueue( + execSystems, + definition.jobAttributes.execSystemId, + getValues(), + setValue + ); + const queue = getQueueValueForExecSystem({ + exec_sys: execSystem, + queue_name: queueValue as string, + }); + if (!queue) return; + + // Only configuration is dependent on queue values + const updatedSchema = getConfigurationSchema( + definition, + allocations, + execSystem, + queue + ); + + setSchema((prevSchema) => ({ + ...prevSchema, + configuration: z.object(updatedSchema), + })); + + const updatedFields = getConfigurationFields( + definition, + allocations, + [execSystem], + queue + ); + + setFields((prevFields) => ({ + ...prevFields, + configuration: updatedFields, + })); + } + }, [queueValue, setValue]); + + // TODO: DES-2916: Use Zod's superRefine feature instead of manually updating schema and tracking schema changes. + React.useEffect(() => { + // Note: trigger is a no op if the field does not exist. So, it is fine to define all. + methods.trigger([ + 'configuration.nodeCount', + 'configuration.maxMinutes', + 'configuration.coresPerNode', + ]); + }, [schema, methods]); + + interface TStep { + [dynamic: string]: { + title: string; + nextPage?: string; + prevPage?: string; + content: JSX.Element; + }; + } + + // Note: currently configuration is the only + // step that needs. This can be more generic + // in future if the fields shape is same between + // Step and Submission Detail View (mostly related to env vars) + useEffect(() => { + const updatedConfigurationStep = getConfigurationStep( + fields.configuration as { [key: string]: TField } + ); + + const updatedSteps: TStep = { + ...steps, + configuration: { + ...steps.configuration, + ...updatedConfigurationStep, + }, + }; + + setSteps(updatedSteps); + }, [fields]); + + // next step transition does not block on invalid fields + const handleNextStep = useCallback(async () => { + const stepFields = Object.keys(fieldValues).filter((key) => + key.startsWith(current) + ) as FieldNameUnion[]; + await methods.trigger(stepFields); + const nextPage = steps[current].nextPage; + nextPage && setCurrent(nextPage); + }, [current, methods]); + const handlePreviousStep = useCallback(() => { + const prevPage = steps[current].prevPage; + prevPage && setCurrent(prevPage); + }, [current]); + const { + mutate: submitJob, + isPending, + isSuccess, + data: submitResult, + error: submitError, + variables: submitVariables, + } = usePostJobs(); + + const [pushKeysSystem, setPushKeysSystem] = useState< + TTapisSystem | undefined + >(); + + const readOnly = + !!missingLicense || + !hasStorageSystems || + (definition.jobType === 'BATCH' && !!missingAllocation) || + !!defaultSystemNeedsKeys || + isPending; + + useEffect(() => { + if (submitResult?.execSys) { + setPushKeysSystem(submitResult.execSys); + } else if (isSuccess) { + reset(initialValues); + } + }, [submitResult]); + + const submitJobCallback = (submitData: TFormValues) => { + const jobData: Omit & { job: TJobSubmit } = { + operation: 'submitJob' as const, + licenseType: license.type, + isInteractive: !!definition.notes.isInteractive, + job: { + archiveOnAppError: true, + appId: definition.id, + appVersion: definition.version, + execSystemId: definition.jobAttributes.execSystemId, + fileInputs: {} as TAppFileInput[], + parameterSet: {} as TParameterSetSubmit, + ...submitData.configuration, + ...submitData.outputs, + }, + }; + + // Transform input field values into format that jobs service wants. + // File Input structure will have 2 fields if target path is required by the app. + // field 1 - has source url + // field 2 - has target path for the source url. + // tapis wants only 1 field with 2 properties - source url and target path. + // The logic below handles that scenario by merging the related fields into 1 field. + jobData.job.fileInputs = Object.values( + Object.entries(submitData.inputs) + .map(([k, v]) => { + // filter out read only inputs. 'FIXED' inputs are tracked as readOnly + if (fileInputs.fields?.[k].readOnly) return; + return { + name: k, + sourceUrl: !isTargetPathField(k) ? v : null, + targetPath: isTargetPathField(k) ? v : null, + }; + }) + .filter((v): v is Required => !!v) //filter nulls + .reduce((acc: { [dynamic: string]: TAppFileInput }, entry) => { + // merge input field and targetPath fields into one. + const key = getInputFieldFromTargetPathField(entry.name); + if (!acc[key]) { + acc[key] = {} as TAppFileInput; + } + acc[key]['name'] = key; + acc[key]['sourceUrl'] = acc[key]['sourceUrl'] ?? entry.sourceUrl; + acc[key]['targetPath'] = acc[key]['targetPath'] ?? entry.targetPath; + return acc; + }, {}) + ) + .flat() + .filter((fileInput) => fileInput.sourceUrl) // filter out any empty values + .map((fileInput) => { + if (isTargetPathEmpty(fileInput.targetPath)) { + return { + name: fileInput.name, + sourceUrl: fileInput.sourceUrl, + }; + } + return fileInput; + }); + + jobData.job.parameterSet = Object.assign( + {}, + ...Object.entries(submitData.parameters).map( + ([sParameterSet, sParameterValue]) => { + return { + [sParameterSet]: Object.entries(sParameterValue) + .map(([k, v]) => { + if (!v) return; + const field = parameterSet.fields?.[sParameterSet]?.[k]; + // filter read only parameters. 'FIXED' parameters are tracked as readOnly + if (field?.readOnly) return; + // Convert the value to a string, if necessary + const transformedValue = + typeof v === 'number' ? v.toString() : v; + return sParameterSet === 'envVariables' + ? { key: field?.key ?? k, value: transformedValue } + : { name: field?.key ?? k, arg: transformedValue }; + }) + .filter((v) => v), // filter out any empty values + }; + } + ) + ); + + // Add allocation scheduler option + if (jobData.job.allocation) { + if (!jobData.job.parameterSet!.schedulerOptions) { + jobData.job.parameterSet!.schedulerOptions = []; + } + jobData.job.parameterSet!.schedulerOptions.push({ + name: 'TACC Allocation', + description: 'The TACC allocation associated with this job execution', + arg: `-A ${jobData.job.allocation}`, + }); + delete jobData.job.allocation; + } + + // Before job submission, ensure the memory limit is not above queue limit. + if (definition.jobType === 'BATCH') { + const queue = getExecSystemFromId( + execSystems, + definition.jobAttributes.execSystemId + )?.batchLogicalQueues.find( + (q) => q.name === jobData.job.execSystemLogicalQueue + ); + if (queue && app.definition.jobAttributes.memoryMB > queue.maxMemoryMB) { + jobData.job.memoryMB = queue.maxMemoryMB; + } + } + + submitJob(jobData); + }; + + const defaultSystemNeedsKeysMessage = defaultStorageSystem.notes + ?.keyservice ? ( + + For help,{' '} + + submit a ticket. + + + ) : ( + + If this is your first time logging in, you may need to  + setPushKeysSystem(defaultStorageSystem)} + > + push your keys + + . + + ); + + return ( + <> + {submitResult && !submitResult.execSys && ( + + Job submitted successfully. Monitor its progress in{' '} + Job Status. + + } + type="success" + closable + showIcon + style={{ marginBottom: '1rem' }} + /> + )} + {missingAllocation && ( + + Please submit a{' '} + + ticket + {' '} + to request an allocation of computing time to run this + application. + + } + type="warning" + showIcon + style={{ marginBottom: '1rem' }} + /> + )} + {submitError && ( + + Job Submit Error:{' '} + {submitError.response?.data.message || submitError.message} + + } + type="warning" + closable + showIcon + style={{ marginBottom: '1rem' }} + /> + )} + {defaultSystemNeedsKeys && ( + + There was a problem accessing your default My Data file system.{' '} + {defaultSystemNeedsKeysMessage} + + } + type="warning" + closable + showIcon + style={{ marginBottom: '1rem' }} + /> + )} + {!!(missingLicense && hasStorageSystems) && ( +
    + + Activate your {app.license.type} license in{' '} + + , then return to this form. + + } + /> +
    + )} + + +
    { + console.log('error submit data', error); + })} + > +
    + + + + + + + + +
    +
    +
    +
    + submitVariables && submitJob(submitVariables)} + /> + + ); +}; diff --git a/client/modules/workspace/src/AppsWizard/AppsFormSchema.ts b/client/modules/workspace/src/AppsWizard/AppsFormSchema.ts new file mode 100644 index 0000000000..4a04a58181 --- /dev/null +++ b/client/modules/workspace/src/AppsWizard/AppsFormSchema.ts @@ -0,0 +1,566 @@ +import { z, ZodType, ZodObject, ZodRawShape } from 'zod'; +import { + TTapisApp, + TJobKeyValuePair, + TJobArgSpec, + TConfigurationValues, + TOutputValues, + TTapisSystem, + TTapisSystemQueue, +} from '@client/hooks'; + +import { + checkAndSetDefaultTargetPath, + getTargetPathFieldName, + getNodeCountValidation, + getCoresPerNodeValidation, + getMaxMinutesValidation, + getAllocationValidation, + getExecSystemsFromApp, + getExecSystemFromId, + getAppQueueValues, + getQueueValueForExecSystem, + getQueueMaxMinutes, + isAppTypeBATCH, + getExecSystemLogicalQueueValidation, + preprocessStringToNumber, +} from '../utils'; + +export type TDynamicString = { [dynamic: string]: string | number }; +export type TDynamicField = { [dynamic: string]: TField }; +export type TParameterSetDefaults = { + [dynamic: string]: TDynamicString; +}; +export type TFileInputsDefaults = TDynamicString; + +export type TFieldOptions = { + label: string; + value?: string; + hidden?: boolean; + disabled?: boolean; +}; + +export type TFormValues = { + inputs: TFileInputsDefaults; + parameters: TParameterSetDefaults; + configuration: TConfigurationValues; + outputs: TOutputValues; +}; + +export type TField = { + label: string; + required: boolean; + name: string; + key: string; + type: string; + parameterSet?: string; + description?: string; + options?: TFieldOptions[]; + tapisFile?: boolean; + tapisFileSelectionMode?: string; + placeholder?: string; + readOnly?: boolean; +}; + +export type TAppFieldSchema = { + inputs: ZodObject; + parameters: ZodObject; + configuration: ZodObject; + outputs: ZodObject; +}; + +export type TAppFormSchema = { + fileInputs: { + defaults: TFileInputsDefaults; + fields: TDynamicField; + schema: { [dynamic: string]: ZodType }; + }; + parameterSet: { + defaults: TParameterSetDefaults; + fields: { + [dynamic: string]: TDynamicField; + }; + schema: { + [dynamic: string]: ZodType; + }; + }; + configuration: { + defaults: TConfigurationValues; + fields: TDynamicField; + schema: { [dynamic: string]: ZodType }; + }; + outputs: { + defaults: TOutputValues; + fields: TDynamicField; + schema: { [dynamic: string]: ZodType }; + }; +}; + +export const inputFileRegex = /^tapis:\/\/(?[^/]+)/; + +export const fieldDisplayOrder: Record = { + configuration: [ + 'execSystemLogicalQueue', + 'maxMinutes', + 'nodeCount', + 'coresPerNode', + 'allocation', + ], + outputs: ['name', 'archiveSystemId', 'archiveSystemDir'], +}; + +// See https://github.com/colinhacks/zod/issues/310 for Zod issue +const emptyStringToUndefined = z.literal('').transform(() => undefined); +function asOptionalField(schema: T) { + return schema.optional().or(emptyStringToUndefined); +} + +// Configuration Schema is pulled out of the default Schema +// building logic because configuration can change based on queue +// or exec system changes. +export const getConfigurationSchema = ( + definition: TTapisApp, + allocations: string[], + execSystem: TTapisSystem, + queue: TTapisSystemQueue +) => { + const configurationSchema: { [dynamic: string]: ZodType } = {}; + + if (definition.jobType === 'BATCH') { + configurationSchema['execSystemLogicalQueue'] = + getExecSystemLogicalQueueValidation(definition, execSystem); + configurationSchema['allocation'] = getAllocationValidation( + definition, + allocations + ); + } + + configurationSchema['maxMinutes'] = getMaxMinutesValidation( + definition, + queue + ); + if (!definition.notes.hideNodeCountAndCoresPerNode) { + configurationSchema['nodeCount'] = getNodeCountValidation( + definition, + queue + ); + + configurationSchema['coresPerNode'] = getCoresPerNodeValidation( + definition, + queue + ); + } + return configurationSchema; +}; + +// Pulling configuration fields out of the main schema building +// to allow for rebuilding of fields based on values changes. +// Example: description has max minutes value, which is dependent +// on queue. +export const getConfigurationFields = ( + definition: TTapisApp, + allocations: string[], + executionSystems: TTapisSystem[], + queue: TTapisSystemQueue +) => { + const configurationFields: TDynamicField = {}; + + const execSystems = getExecSystemsFromApp( + definition, + executionSystems as TTapisSystem[] + ); + + const defaultExecSystem = getExecSystemFromId( + execSystems, + definition.jobAttributes.execSystemId + ) as TTapisSystem; + + if (definition.jobType === 'BATCH' && !definition.notes.hideQueue) { + configurationFields['execSystemLogicalQueue'] = { + description: 'Select the queue this job will execute on.', + label: 'Queue', + name: 'configuration.execSystemLogicalQueue', + key: 'configuration.execSystemLogicalQueue', + required: true, + type: 'select', + options: getAppQueueValues( + definition, + execSystems[0].batchLogicalQueues + ).map((q) => ({ value: q, label: q })), + }; + } + + if (definition.jobType === 'BATCH' && !definition.notes.hideAllocation) { + configurationFields['allocation'] = { + description: + 'Select the project allocation you would like to use with this job submission.', + label: 'Allocation', + name: 'configuration.allocation', + key: 'configuration.allocation', + required: true, + type: 'select', + options: [ + { label: '', hidden: true, disabled: true }, + ...allocations.sort().map((projectId) => ({ + value: projectId, + label: projectId, + })), + ], + }; + } + + configurationFields['maxMinutes'] = { + description: `The maximum number of minutes you expect this job to run for. Maximum possible is ${getQueueMaxMinutes( + definition, + defaultExecSystem, + queue?.name + )} minutes. After this amount of time your job will end. Shorter run times result in shorter queue wait times.`, + label: 'Maximum Job Runtime (minutes)', + name: 'configuration.maxMinutes', + key: 'configuration.maxMinutes', + required: true, + type: 'number', + }; + + if (!definition.notes.hideNodeCountAndCoresPerNode) { + configurationFields['nodeCount'] = { + description: 'Number of requested process nodes for the job.', + label: 'Node Count', + name: 'configuration.nodeCount', + key: 'configuration.nodeCount', + required: true, + type: 'number', + }; + + configurationFields['coresPerNode'] = { + description: + 'Number of processors (cores) per node for the job. e.g. a selection of 16 processors per node along with 4 nodes will result in 16 processors on 4 nodes, with 64 processors total.', + label: 'Cores Per Node', + name: 'configuration.coresPerNode', + key: 'configuration.coresPerNode', + required: true, + type: 'number', + }; + } + + return configurationFields; +}; + +const FormSchema = ( + definition: TTapisApp, + executionSystems: TTapisSystem[], + allocations: string[], + defaultStorageSystem: TTapisSystem, + username: string, + portalAlloc?: string +) => { + const appFields: TAppFormSchema = { + fileInputs: { + defaults: {}, + fields: {}, + schema: {}, + }, + parameterSet: { + defaults: {}, + fields: {}, + schema: {}, + }, + configuration: { + defaults: { + maxMinutes: 0, + }, + fields: {}, + schema: {}, + }, + outputs: { + defaults: { + name: '', + archiveSystemId: '', + archiveSystemDir: '', + }, + fields: {}, + schema: {}, + }, + }; + + Object.entries(definition.jobAttributes.parameterSet).forEach( + ([parameterSet, parameterSetValue]) => { + if (!Array.isArray(parameterSetValue)) return; + const parameterSetSchema: { + [dynamic: string]: ZodType; + } = {}; + const parameterSetFields: { + [dynamic: string]: TField; + } = {}; + const parameterSetDefaults: { + [dynamic: string]: string; + } = {}; + + parameterSetValue.forEach((param) => { + if (param.notes?.isHidden) { + return; + } + const paramId = + (param).name ?? (param).key; + const label = + param.notes?.label ?? + (param).name ?? + (param).key; + + const field: TField = { + label: label, + description: param.description, + required: param.inputMode === 'REQUIRED', + readOnly: param.inputMode === 'FIXED', + parameterSet: parameterSet, + name: `parameters.${parameterSet}.${label}`, + key: paramId, + type: 'text', + }; + + if (param.notes?.enum_values) { + field.type = 'select'; + field.options = param.notes?.enum_values.map( + (item) => + Object.entries(item).map(([key, value]) => ({ + value: key, + label: value, + }))[0] + ); + parameterSetSchema[field.label] = z.enum( + field.options.map(({ value }) => value) as [string, ...string[]] + ); + } else if (param.notes?.fieldType === 'email') { + field.type = 'email'; + parameterSetSchema[field.label] = z + .string() + .email('Must be a valid email.'); + } else if (param.notes?.fieldType === 'number') { + field.type = 'number'; + parameterSetSchema[field.label] = z.preprocess( + preprocessStringToNumber, + z.number() + ); + } else { + field.type = 'text'; + // Need to do this for non empty strings. Zod does not handle + // string with 0 length even with required property. + parameterSetSchema[field.label] = z + .string() + .refine((data) => data.trim() !== ''); + } + + if (!field.required) { + parameterSetSchema[field.label] = asOptionalField( + parameterSetSchema[field.label] + ); + } + if (param.notes?.validator?.regex && param.notes?.validator?.message) { + try { + const regex = RegExp(param.notes.validator.regex); + parameterSetSchema[field.label] = parameterSetSchema[ + field.label + ].refine((value) => regex.test(value), { + message: param.notes.validator.message, + }); + } catch (SyntaxError) { + console.warn('Invalid regex pattern for app'); + } + } + parameterSetFields[field.label] = field; + parameterSetDefaults[field.label] = + (param).arg ?? (param).value ?? ''; + }); + + // Only create schema for parameterSet if it contains values + if (Object.keys(parameterSetSchema).length) { + appFields.parameterSet.schema[parameterSet] = + z.object(parameterSetSchema); + } + // Only create fields for parameterSet if it contains values + if (Object.keys(parameterSetFields).length) { + appFields.parameterSet.fields[parameterSet] = parameterSetFields; + } + // Only add defaults for parameterSet if it contains values + if (Object.keys(parameterSetDefaults).length) { + appFields.parameterSet.defaults[parameterSet] = parameterSetDefaults; + } + } + ); + + (definition.jobAttributes.fileInputs || []).forEach((input) => { + if (input.notes?.isHidden) { + return; + } + + const field: TField = { + label: input.name, + description: input.description, + required: input.inputMode === 'REQUIRED', + name: `inputs.${input.name}`, + key: `inputs.${input.name}`, + tapisFile: true, + type: 'text', + placeholder: 'Browse Data Files', + readOnly: input.inputMode === 'FIXED', + tapisFileSelectionMode: input.notes?.selectionMode ?? 'both', + }; + + appFields.fileInputs.schema[input.name] = z.string(); + appFields.fileInputs.schema[input.name] = (( + appFields.fileInputs.schema[input.name] + )).regex( + /^tapis:\/\//g, + "Input file must be a valid Tapis URI, starting with 'tapis://'" + ); + + if (!field.required) { + appFields.fileInputs.schema[input.name] = asOptionalField( + appFields.fileInputs.schema[input.name] + ); + } + + appFields.fileInputs.fields[input.name] = field; + appFields.fileInputs.defaults[input.name] = + input.sourceUrl === null || typeof input.sourceUrl === 'undefined' + ? '' + : input.sourceUrl; + + // The default is to not show target path for file inputs. + const showTargetPathForFileInputs = + (input.notes?.showTargetPath && input.targetPath) ?? false; + // Add targetDir for all sourceUrl + if (!showTargetPathForFileInputs) { + return; + } + const targetPathName = getTargetPathFieldName(input.name); + appFields.fileInputs.schema[targetPathName] = z.string(); + appFields.fileInputs.schema[targetPathName] = (( + appFields.fileInputs.schema[targetPathName] + )).regex( + /^tapis:\/\//g, + "Input file Target Directory must be a valid Tapis URI, starting with 'tapis://'" + ); + + appFields.fileInputs.schema[targetPathName] = z.string().optional(); + appFields.fileInputs.fields[targetPathName] = { + label: 'Target Path for ' + input.name, + description: + 'The name of the ' + + input.name + + ' after it is copied to the target system, but before the job is run. Leave this value blank to just use the name of the input file.', + required: false, + readOnly: field.readOnly, + name: `inputs.${targetPathName}`, + key: `inputs.${targetPathName}`, + type: 'text', + placeholder: 'Target Path Name', + }; + appFields.fileInputs.defaults[targetPathName] = + checkAndSetDefaultTargetPath(input.targetPath) as string; + }); + + // Configuration + const execSystems = getExecSystemsFromApp( + definition, + executionSystems as TTapisSystem[] + ); + + const defaultExecSystem = getExecSystemFromId( + execSystems, + definition.jobAttributes.execSystemId + ) as TTapisSystem; + + const queue = getQueueValueForExecSystem({ + definition, + exec_sys: defaultExecSystem, + queue_name: definition.jobAttributes.execSystemLogicalQueue, + }) as TTapisSystemQueue; + + if (definition.jobType === 'BATCH') { + appFields.configuration.defaults['execSystemLogicalQueue'] = isAppTypeBATCH( + definition + ) + ? definition.jobAttributes.execSystemLogicalQueue + : ''; + + appFields.configuration.defaults['allocation'] = isAppTypeBATCH(definition) + ? allocations.includes(portalAlloc || '') + ? portalAlloc + : allocations.length === 1 + ? allocations[0] + : '' + : ''; + } + + appFields.configuration.defaults['maxMinutes'] = + definition.jobAttributes.maxMinutes; + + if (!definition.notes.hideNodeCountAndCoresPerNode) { + appFields.configuration.defaults['nodeCount'] = + definition.jobAttributes.nodeCount; + + appFields.configuration.defaults['coresPerNode'] = + definition.jobAttributes.coresPerNode; + } + + appFields.configuration.schema = getConfigurationSchema( + definition, + allocations, + defaultExecSystem, + queue + ); + appFields.configuration.fields = getConfigurationFields( + definition, + allocations, + execSystems, + queue + ); + + // Outputs + appFields.outputs.schema['name'] = z.string().max(80); + appFields.outputs.defaults['name'] = `${definition.id}-${ + definition.version + }_${new Date().toISOString().split('.')[0]}`; + appFields.outputs.fields['name'] = { + description: 'A recognizable name for this job.', + label: 'Job Name', + name: 'outputs.name', + key: 'outputs.name', + required: true, + type: 'text', + }; + appFields.outputs.schema['archiveSystemId'] = z.string().optional(); + appFields.outputs.defaults['archiveSystemId'] = + defaultStorageSystem?.id || definition.jobAttributes.archiveSystemId; + appFields.outputs.fields['archiveSystemId'] = { + description: + 'System into which output files are archived after application execution.', + label: 'Archive System', + name: 'outputs.archiveSystemId', + key: 'outputs.archiveSystemId', + required: false, + type: 'text', + placeholder: + defaultStorageSystem.id || definition.jobAttributes.archiveSystemId, + }; + + appFields.outputs.schema['archiveSystemDir'] = z.string().optional(); + appFields.outputs.defaults[ + 'archiveSystemDir' + ] = `${username}/tapis-jobs-archive/\${JobCreateDate}/\${JobName}-\${JobUUID}`; + appFields.outputs.fields['archiveSystemDir'] = { + description: + 'Directory into which output files are archived after application execution.', + label: 'Archive Directory', + name: 'outputs.archiveSystemDir', + key: 'outputs.archiveSystemDir', + required: false, + type: 'text', + placeholder: `${username}/tapis-jobs-archive/\${JobCreateDate}/\${JobName}-\${JobUUID}`, + }; + + return appFields; +}; + +export default FormSchema; diff --git a/client/modules/workspace/src/AppsWizard/AppsWizard.module.css b/client/modules/workspace/src/AppsWizard/AppsWizard.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/client/modules/workspace/src/AppsWizard/AppsWizard.tsx b/client/modules/workspace/src/AppsWizard/AppsWizard.tsx new file mode 100644 index 0000000000..ea33cb0da1 --- /dev/null +++ b/client/modules/workspace/src/AppsWizard/AppsWizard.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Layout, Flex } from 'antd'; +import { SubmitHandler, FieldValues } from 'react-hook-form'; +import { SecondaryButton } from '@client/common-components'; + +// import styles from './AppsWizard.module.css'; + +// import React, { createContext, useContext, useState } from 'react'; + +// export const AppFormStateContext = createContext({}); + +// export function AppFormProvider({ children, initialState = {} }) { +// const value = useState(initialState); +// return ( +// +// {children} +// +// ); +// } + +// export function useAppFormState() { +// const context = useContext(AppFormStateContext); +// if (!context) { +// throw new Error('useAppFormState must be used within the AppFormProvider'); +// } +// return context; +// } + +export const AppsWizard: React.FC<{ + step: { + title: string; + prevPage?: string; + nextPage?: string; + content: JSX.Element; + }; + handlePreviousStep: SubmitHandler; + handleNextStep: SubmitHandler; +}> = ({ step, handlePreviousStep, handleNextStep }) => { + const contentStyle = { + lineHeight: '260px', + textAlign: 'center' as const, + marginTop: 16, + }; + const headerStyle = { + paddingLeft: 0, + paddingRight: 0, + textAlign: 'center' as const, + height: 64, + lineHeight: '64px', + background: 'transparent', + borderBottom: '1px solid #707070', + }; + const { Header, Content } = Layout; + + return ( + + +
    + + {step.title} + + + Back + + + Continue + + + +
    + {step.content} +
    +
    + ); +}; diff --git a/client/modules/workspace/src/AppsWizard/FormField.tsx b/client/modules/workspace/src/AppsWizard/FormField.tsx new file mode 100644 index 0000000000..1ec10262f0 --- /dev/null +++ b/client/modules/workspace/src/AppsWizard/FormField.tsx @@ -0,0 +1,151 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Form, Input, Select } from 'antd'; +import { FormItem } from 'react-hook-form-antd'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { TFieldOptions, inputFileRegex } from '../AppsWizard/AppsFormSchema'; +import { SecondaryButton } from '@client/common-components'; +import { SelectModal } from '../SelectModal/SelectModal'; + +export const FormField: React.FC<{ + name: string; + tapisFile?: boolean; + parameterSet?: string; + description?: string; + label: string; + required?: boolean; + type: string; + tapisFileSelectionMode?: string; + placeholder?: string; + options?: TFieldOptions[]; +}> = ({ + name, + tapisFile = false, + parameterSet = null, + description, + label, + required = false, + type, + tapisFileSelectionMode = null, + ...props +}) => { + const { resetField, control, getValues, setValue, trigger } = + useFormContext(); + const fieldState = useWatch({ control, name }); + let parameterSetLabel: React.ReactElement | null = null; + const [isModalOpen, setIsModalOpen] = useState(false); + const [storageSystem, setStorageSystem] = useState(null); + + const handleSelectModalOpen = () => { + setIsModalOpen(true); + }; + useEffect(() => { + if (tapisFile) { + const inputFileValue = getValues(name); + const match = inputFileValue?.match(inputFileRegex); + if (match && match.groups) { + setStorageSystem(match.groups.storageSystem); + } else { + setStorageSystem(null); + } + } + }, [tapisFile, name, fieldState]); + + if (parameterSet) { + parameterSetLabel = ( + + ( + + {parameterSet} + + ) + + ); + } + + return ( +
    + + {label} {parameterSetLabel} + + } + htmlFor={name} + key={name} + style={{ textAlign: 'left', marginBottom: description ? 0 : 16 }} + > + {type === 'select' ? ( + + +
    + )} + + {description && ( + + {description} + + )} + {/* Select Modal has Form and input which cause state sharing with above FormItem + So, SelectModal is outside FormItem. + */} + {tapisFile && ( + setIsModalOpen(false)} + onSelect={(value: string) => { + setValue(name, value); + trigger(name); + }} + /> + )} + + ); +}; diff --git a/client/modules/workspace/src/AppsWizard/Steps.tsx b/client/modules/workspace/src/AppsWizard/Steps.tsx new file mode 100644 index 0000000000..6f06fae227 --- /dev/null +++ b/client/modules/workspace/src/AppsWizard/Steps.tsx @@ -0,0 +1,61 @@ +import { FormField } from './FormField'; +import { TField, fieldDisplayOrder } from '../AppsWizard/AppsFormSchema'; + +export const stepKeys = ['inputs', 'parameters', 'configuration', 'outputs']; + +export const getInputsStep = (fields: { [dynamic: string]: TField }) => ({ + title: 'Inputs', + content: ( + <> + {Object.values(fields).map((field) => { + // TODOv3 handle fileInputArrays https://jira.tacc.utexas.edu/browse/WP-81 + return ; + })} + + ), +}); + +export const getParametersStep = (fields: { + [dynamic: string]: { [dynamic: string]: TField }; +}) => ({ + title: 'Parameters', + content: ( + <> + {Object.values(fields).map((parameterValue) => { + return Object.values(parameterValue).map((field) => { + return ; + }); + })} + + ), +}); + +export const getConfigurationStep = (fields: { [key: string]: TField }) => ({ + title: 'Configuration', + content: ( + <> + {fieldDisplayOrder['configuration'].map((key) => { + const field = fields[key]; + if (!field) { + return null; + } + return ; + })} + + ), +}); + +export const getOutputsStep = (fields: { [key: string]: TField }) => ({ + title: 'Outputs', + content: ( + <> + {fieldDisplayOrder['outputs'].map((key) => { + const field = fields[key]; + if (!field) { + return null; + } + return ; + })} + + ), +}); diff --git a/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.module.css b/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.module.css new file mode 100644 index 0000000000..06d6f18f3f --- /dev/null +++ b/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.module.css @@ -0,0 +1,19 @@ +.session-modal-header { + background-color: #f4f4f4; + h5 { + font-weight: normal; + } +} + +.session-modal-body { + display: flex; + flex-direction: column; + & > * { + margin: 0.4rem; + } +} + +.url { + color: grey; + font-style: italic; +} diff --git a/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx b/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx new file mode 100644 index 0000000000..d184d61a55 --- /dev/null +++ b/client/modules/workspace/src/InteractiveSessionModal/InteractiveSessionModal.tsx @@ -0,0 +1,42 @@ +import { Modal } from 'antd'; +import React from 'react'; +import { PrimaryButton } from '@client/common-components'; +import styles from './InteractiveSessionModal.module.css'; + +export const InteractiveSessionModal: React.FC<{ + isOpen: boolean; + interactiveSessionLink: string; + message?: string; + onCancel: VoidFunction; +}> = ({ isOpen, interactiveSessionLink, message, onCancel }) => { + return ( + Open Session} + width="500px" + open={isOpen} + footer={ + + Connect + + } + onCancel={onCancel} + > +
    + + Click the button below to connect to the interactive session. + + {message && {message}} + To end the job, quit the application within the session. + + Files may take some time to appear in the output location after the + job has ended. + + + For security purposes, this is the URL that the connect button will + open: + + {interactiveSessionLink} +
    +
    + ); +}; diff --git a/client/modules/workspace/src/InteractiveSessionModal/index.ts b/client/modules/workspace/src/InteractiveSessionModal/index.ts new file mode 100644 index 0000000000..65fec46209 --- /dev/null +++ b/client/modules/workspace/src/InteractiveSessionModal/index.ts @@ -0,0 +1 @@ +export { InteractiveSessionModal } from './InteractiveSessionModal'; diff --git a/client/modules/workspace/src/JobStatusNav/JobStatusNav.module.css b/client/modules/workspace/src/JobStatusNav/JobStatusNav.module.css new file mode 100644 index 0000000000..9d232488ef --- /dev/null +++ b/client/modules/workspace/src/JobStatusNav/JobStatusNav.module.css @@ -0,0 +1,21 @@ +.icon { + color: #cc443e; + font-size: 16px; +} + +.badge { + margin: 0 10px; +} + +.root:hover, +.root:focus, +.highlighted-row, +.highlighted-row:hover, +.highlighted-row:focus { + background-color: #cbdded; + text-decoration: none; +} + +.highlighted-row:hover { + cursor: unset; +} diff --git a/client/modules/workspace/src/JobStatusNav/JobStatusNav.spec.tsx b/client/modules/workspace/src/JobStatusNav/JobStatusNav.spec.tsx new file mode 100644 index 0000000000..27a82cc8cb --- /dev/null +++ b/client/modules/workspace/src/JobStatusNav/JobStatusNav.spec.tsx @@ -0,0 +1,15 @@ +import { render } from '@client/test-fixtures'; + +import { JobStatusNav } from './JobStatusNav'; + +describe('JobStatusNav', () => { + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + + it('should have nav text', () => { + const { getByText } = render(); + expect(getByText(/Job Status/gi)).toBeTruthy(); + }); +}); diff --git a/client/modules/workspace/src/JobStatusNav/JobStatusNav.tsx b/client/modules/workspace/src/JobStatusNav/JobStatusNav.tsx new file mode 100644 index 0000000000..f0cc17749b --- /dev/null +++ b/client/modules/workspace/src/JobStatusNav/JobStatusNav.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { NavLink } from 'react-router-dom'; +import { Layout, Badge } from 'antd'; +import { Icon } from '@client/common-components'; +import styles from './JobStatusNav.module.css'; +import { useGetNotifications } from '@client/hooks'; + +export const JobStatusNav: React.FC = () => { + const { data } = useGetNotifications({ + eventTypes: ['interactive_session_ready', 'job'], + read: false, + markRead: false, + }); + const unreadNotifs = new Set(data?.notifs.map((x) => x.extra.uuid)).size; + + const { Header } = Layout; + + const headerStyle = { + background: 'transparent', + padding: 0, + borderBottom: '1px solid var(--global-color-primary--normal)', + fontSize: 14, + borderRight: '1px solid var(--global-color-primary--normal)', + }; + return ( + + isActive ? styles['highlighted-row'] : styles.root + } + > +
    + + + + Job Status +
    +
    + ); +}; diff --git a/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.module.css b/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.module.css new file mode 100644 index 0000000000..4b427efb77 --- /dev/null +++ b/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.module.css @@ -0,0 +1,160 @@ +/* From Core */ +.root { + font-size: 0.75rem; + --border: 1px solid var(--global-color-primary--light); +} + +.spinner { + position: absolute; + top: 50%; + width: 100%; +} + +.link:hover { + color: var(--global-color-accent--normal); +} + +.header-details { + font-weight: 400; + font-size: 1.25rem; + padding-top: 10px; + display: flex; + flex-wrap: wrap; +} + +dl.header-details dd + dt { + border-left: var(--border); + padding-left: 10px; + margin-left: 10px; +} + +dl.header-details dt { + padding-right: 1px; + margin-right: 1px; +} +.modal-body-container { + width: 100%; + height: 60vh; + display: flex; + flex-direction: row; +} + +.panel-content { + --padding: 20px; + + padding: var(--padding); +} +.panel-content dd dt { + font-weight: normal; +} +/* Any preformatted values (like system output) should use a `pre` tag */ +.panel-content pre { + white-space: pre-wrap; + margin-bottom: 0; /* overwrite Bootstrap's `_code.scss` */ +} + +.submit-button { + display: block; + width: 100%; +} + +.left-panel { + border-right: var(--border); + min-width: 210px; + padding: 20px; +} + +.left-panel .submit-button + .submit-button { + margin-top: 10px; +} + +.right-panel { + width: 100%; + overflow-y: scroll; + border-right: var(--border); + white-space: normal; + /* Cross-browser solution to padding ignored by overflow (in spec-compliant Firefox) */ + /* SEE: https://stackoverflow.com/a/38997047/11817077 */ + padding-bottom: 0; + &::after { + content: ''; + display: block; + height: var(--padding); + } +} + +/* Messaging */ +.error { + display: flex; + justify-content: center; + align-items: center; + width: 100%; +} + +/* `DefinitionList` Component */ +/* FAQ: Why are we relying on tags defined by other components?! Because the tags are unit tested. Opposition welcome. — Wes B */ +/* FAQ: Why not integrate these styles into `DefinitionList`? Because Design has not decided final UI for component / this modal */ + +/* Generic */ +dl.panel-content { + --buffer-horz: 12px; /* ~10px design * 1.2 design-to-app ratio */ + --buffer-vert: 10px; /* gut feel based loosely on random space from design */ + --border: var(--global-border-width--normal) solid + var(--global-color-primary--light); +} +dl.panel-content, +dl.panel-content dl, +dl.panel-content dd { + margin-bottom: 0; /* overwrite Bootstrap's `_reboot.scss` */ +} + +/* Top-Level */ +dl.panel-content > dt { + padding-top: var(--buffer-vert); + padding-bottom: calc(var(--buffer-vert) / 2); +} +dl.panel-content > dd { + padding-top: calc(var(--buffer-vert) / 2); + padding-bottom: var(--buffer-vert); +} + +/* Top-Level: Right Panel */ +dl.right-panel > dt, +dl.right-panel > dd { + padding-left: var(--buffer-horz); + padding-right: var(--buffer-horz); +} +dl.right-panel > dt { + font-weight: bold; +} +dl.right-panel > dt { + border-top: var(--border); +} +dl.right-panel > dt:first-of-type { + border-top: none; +} +dl.right-panel > dd:last-of-type { + border-bottom: var(--border); +} +dl.right-panel > dt:nth-of-type(even), +dl.right-panel > dd:nth-of-type(even) { + background-color: var(--global-color-primary--x-light); +} + +/* Remove the colon from top-level labels */ +dl.panel-content > dt::after { + display: none; +} +/* Prevent adding extra space to existing `dl.panel-content` padding */ +dl.panel-content > dt:first-of-type { + padding-top: 0; +} +.job-history-modal .ant-modal-body { + padding: 0; +} +.ant-modal .ant-modal-content { + padding: 0; +} +.descriptionRow { + border-bottom: var(--border); +} diff --git a/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx b/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx new file mode 100644 index 0000000000..e4bc16be9a --- /dev/null +++ b/client/modules/workspace/src/JobsDetailModal/JobsDetailModal.tsx @@ -0,0 +1,329 @@ +import React, { useState, useEffect } from 'react'; +import { + Collapse, + Descriptions, + DescriptionsProps, + Modal, + Layout, + Button, +} from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { useGetApps, useGetJobs, TAppResponse, TTapisJob } from '@client/hooks'; +import styles from './JobsDetailModal.module.css'; +import { + getOutputPath, + getStatusText, + isOutputState, + isTerminalState, + getJobDisplayInformation, + TJobDisplayInputOrParameter, + TJobDisplayInfo, + isInteractiveJob, +} from '../utils/jobs'; +import { formatDateTimeFromValue } from '../utils/timeFormat'; +import { JobActionButton } from '../JobsListing/JobsListing'; +import { Spinner } from '@client/common-components'; +import { JobsReuseInputsButton } from '../JobsReuseInputsButton/JobsReuseInputsButton'; + +type InputParamsObj = { + [key: string]: string; +}; +const reduceInputParameters = (data: TJobDisplayInputOrParameter[]) => + data.reduce((acc: InputParamsObj, item: TJobDisplayInputOrParameter) => { + acc[item.label] = item.value ?? ''; + return acc; + }, {}); + +export const JobsDetailModalBody: React.FC<{ + jobData: TTapisJob; + appData: TAppResponse | undefined; +}> = ({ jobData, appData }) => { + const jobDisplay = getJobDisplayInformation( + jobData, + appData + ) as TJobDisplayInfo; + + const outputLocation = getOutputPath(jobData); + const created = formatDateTimeFromValue(new Date(jobData.created)); + const lastUpdated = formatDateTimeFromValue(new Date(jobData.lastUpdated)); + const hasFailedStatus = jobData.status === 'FAILED'; + + const appDataObj = { + 'App ID': jobDisplay.appId, + ...('appVersion' in jobDisplay + ? { 'App Version': jobDisplay.appVersion } + : {}), + }; + const lastMessageTitle = hasFailedStatus + ? 'Failure Report' + : 'Last Status Message'; + const statusDataObj = { + Submitted: created, + [`${getStatusText(jobData.status)}`]: lastUpdated, + [lastMessageTitle]: ( + + {hasFailedStatus ? 'Last Status Message' : 'System Output'} + + ), + children:
    {jobData.lastMessage}
    , + }, + ]} + >
    + ), + ...(jobData.remoteOutcome && { 'Remote Outcome': jobData.remoteOutcome }), + }; + + if (jobData.remoteOutcome) { + statusDataObj['Remote Outcome'] = jobData.remoteOutcome; + } + + const configDataObj = { + 'Execution System': jobDisplay.systemName, + 'Max Minutes': String(jobData.maxMinutes), + Queue: 'queue' in jobDisplay ? jobDisplay.queue : undefined, + 'Cores On Each Node': + 'coresPerNode' in jobDisplay + ? String(jobDisplay.coresPerNode) + : undefined, + 'Node Count': + 'coresPerNode' in jobDisplay + ? String(jobDisplay.coresPerNode) + : undefined, + Allocation: 'allocation' in jobDisplay ? jobDisplay.allocation : undefined, + 'Execution Directory': + 'execSystemExecDir' in jobData ? jobData.execSystemExecDir : undefined, + }; + const outputDataObj = { + 'Job Name': jobData.name, + 'Output Location': outputLocation, + 'Archive System': jobData.archiveSystemId, + 'Archive Directory': jobData.archiveSystemDir, + }; + + const inputDataObj = { + ...reduceInputParameters(jobDisplay.inputs), + }; + + const paramsDataObj = { + ...reduceInputParameters(jobDisplay.appArgs), + ...reduceInputParameters(jobDisplay.envVars), + }; + + const valueStyle = { + paddingLeft: '40px', + paddingBottom: '0px', + width: '100%', + }; + + const labelStyle = { + color: '#484848', + fontWeight: 700, + paddingTop: '10px', + border: '0', + }; + + const baseRowStyle = { + paddingLeft: '12px', + paddingRight: '12px', + paddingBottom: '5px', + }; + + const itemLabelStyle = { + color: 'rgba(0, 0, 0, 0.88)', + font: 'normal normal 14px Helvetica Neue', + paddingBottom: '0px', + }; + + interface DisplayItemsProps { + items: DescriptionsProps['items']; + } + + const DisplayItems: React.FC = ({ items }) => ( + + ); + + const getItems = ( + data: Record + ): DescriptionsProps['items'] => + Object.entries(data) + .filter(([_, value]) => value !== undefined) // allow empty strings + .map(([key, value]) => ({ + label: key, + children: {value}, + })); + + const dataObjects = [ + { label: 'Application', data: appDataObj }, + { label: 'Status', data: statusDataObj }, + { label: 'Inputs', data: inputDataObj }, + { label: 'Parameters', data: paramsDataObj }, + { label: 'Configuration', data: configDataObj }, + { label: 'Outputs', data: outputDataObj }, + ]; + + const items: DescriptionsProps['items'] = dataObjects + .filter(({ data }) => Object.keys(data).length > 0) + .map(({ label, data }, index) => { + const backgroundColor = index % 2 === 0 ? '#fff' : '#f4f4f4'; + + return { + label, + children: , + labelStyle, + style: { + ...baseRowStyle, + backgroundColor, + }, + }; + }); + + return ( +
    +
    +
    + {!isOutputState(jobData.status) && ( + <> +
    Execution:
    +
    + +
    + + )} +
    Output:
    +
    + +
    +
    + {isTerminalState(jobData.status) && + (isInteractiveJob(jobData) ? ( + + ) : ( + + ))} + {!isTerminalState(jobData.status) && ( + + )} +
    + +
    + ); +}; + +export const JobsDetailModal: React.FC<{ uuid: string }> = ({ uuid }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const navigate = useNavigate(); + + const handleCancel = () => { + navigate('..', { relative: 'path' }); + }; + const { data: jobData, isLoading } = useGetJobs('select', { uuid }) as { + data: TTapisJob; + isLoading: boolean; + }; + + const appId = jobData?.appId; + const appVersion = jobData?.appVersion; + + const { data: appData, isLoading: isAppLoading } = useGetApps({ + appId, + appVersion, + }) as { + data: TAppResponse; + isLoading: boolean; + }; + + useEffect(() => { + setIsModalOpen(!!uuid); + }, [uuid]); + + return ( + +
    + Job Detail: {uuid} + {jobData && ( +
    +
    Job UUID:
    +
    {jobData.uuid}
    +
    Application:
    +
    {JSON.parse(jobData.notes).label || jobData.appId}
    +
    System:
    +
    {jobData.execSystemId}
    +
    + )} +
    + + } + width="60%" + open={isModalOpen} + onCancel={handleCancel} + footer={null} + > + {isLoading && isAppLoading ? ( + + + + ) : ( + jobData && + )} +
    + ); +}; diff --git a/client/modules/workspace/src/JobsListing/JobsListing.module.css b/client/modules/workspace/src/JobsListing/JobsListing.module.css new file mode 100644 index 0000000000..40aa4e58f7 --- /dev/null +++ b/client/modules/workspace/src/JobsListing/JobsListing.module.css @@ -0,0 +1,10 @@ +.jobActions { + padding-top: 15px; + :is(a, button) + :is(a, button) { + margin-left: 15px; + } +} + +.link:hover { + color: var(--global-color-accent--normal); +} diff --git a/client/modules/workspace/src/JobsListing/JobsListing.tsx b/client/modules/workspace/src/JobsListing/JobsListing.tsx new file mode 100644 index 0000000000..d2dcf3404d --- /dev/null +++ b/client/modules/workspace/src/JobsListing/JobsListing.tsx @@ -0,0 +1,250 @@ +import React, { useMemo, useState, useEffect } from 'react'; +import { TableProps, Row, Flex, Button as AntButton } from 'antd'; +import type { ButtonSize } from 'antd/es/button'; +import { useQueryClient } from '@tanstack/react-query'; +import { NavLink } from 'react-router-dom'; +import { PrimaryButton, SecondaryButton } from '@client/common-components'; +import { BaseButtonProps } from 'antd/es/button/button'; +import { + useGetNotifications, + TJobStatusNotification, + usePostJobs, + TJobPostOperations, + useReadNotifications, + TGetNotificationsResponse, +} from '@client/hooks'; +import { + JobsListingTable, + TJobsListingColumns, +} from './JobsListingTable/JobsListingTable'; +import { + getStatusText, + truncateMiddle, + getJobInteractiveSessionInfo, + isOutputState, + isInteractiveJob, + isTerminalState, +} from '../utils'; +import { InteractiveSessionModal } from '../InteractiveSessionModal'; +import styles from './JobsListing.module.css'; +import { formatDateTimeFromValue } from '../utils/timeFormat'; +import { JobsReuseInputsButton } from '../JobsReuseInputsButton/JobsReuseInputsButton'; + +export const JobActionButton: React.FC<{ + uuid: string; + operation: TJobPostOperations; + title: string; + type?: BaseButtonProps['type']; + size?: ButtonSize; + danger?: boolean; +}> = ({ uuid, operation, title, type, size, danger = false }) => { + const { mutate: mutateJob, isPending, isSuccess } = usePostJobs(); + const Button = + type === 'primary' ? (danger ? AntButton : PrimaryButton) : SecondaryButton; + return ( + + ); +}; + +const InteractiveSessionButtons: React.FC<{ + uuid: string; + interactiveSessionLink: string; + message?: string; +}> = ({ uuid, interactiveSessionLink, message }) => { + const [interactiveModalState, setInteractiveModalState] = useState(false); + + return ( + <> + setInteractiveModalState(true)} + > + Open + + + setInteractiveModalState(false)} + /> + + ); +}; + +export const JobsListing: React.FC> = ({ + ...tableProps +}) => { + const queryClient = useQueryClient(); + const { data: interactiveSessionNotifs } = useGetNotifications({ + eventTypes: ['interactive_session_ready'], + markRead: false, + }); + const { mutate: readNotifications } = useReadNotifications(); + + // mark all as read on component mount + useEffect(() => { + readNotifications({ + eventTypes: ['interactive_session_ready', 'job'], + }); + + // update unread count state + queryClient.setQueryData( + [ + 'workspace', + 'notifications', + { + eventTypes: ['interactive_session_ready', 'job'], + read: false, + markRead: false, + }, + ], + (oldData: TGetNotificationsResponse) => { + return { + ...oldData, + notifs: [], + unread: 0, + }; + } + ); + }, []); + + const columns: TJobsListingColumns = useMemo( + () => [ + { + title: 'Job Name', + dataIndex: 'name', + ellipsis: true, + width: '30%', + render: (_, job) => { + const { interactiveSessionLink, message } = + getJobInteractiveSessionInfo( + job, + interactiveSessionNotifs?.notifs as TJobStatusNotification[] + ); + + return ( + + {truncateMiddle(job.name, 35)} + + {!!interactiveSessionLink && ( + + )} + {!isInteractiveJob(job) && ( + + {isOutputState(job.status) + ? 'View Output' + : 'Output Pending'} + + )} + {isTerminalState(job.status) && + (isInteractiveJob(job) ? ( + + ) : ( + + ))} + + View Details + + + + ); + }, + }, + { + width: '10%', + title: 'Application', + dataIndex: 'appId', + render: (appId, job) => { + const appNotes = JSON.parse(job.notes); + + return ( + appNotes.label || + `${appId.charAt(0).toUpperCase()}${appId.slice(1)}` + ); + }, + }, + { + width: '10%', + title: 'Job Status', + dataIndex: 'status', + render: (status) => <>{getStatusText(status)}, + }, + { width: '10%', title: 'Nodes', dataIndex: 'nodeCount' }, + { width: '10%', title: 'Cores', dataIndex: 'coresPerNode' }, + { + width: '30%', + title: 'Time Submitted - Finished', + dataIndex: 'created', + render: (_, job) => { + const formatDuration = (start: string, end: string) => { + if (!start || !end) return ''; + const startDate = new Date(start).getTime(); + const endDate = new Date(end).getTime(); + const duration = endDate - startDate; // duration in milliseconds + const seconds = Math.floor(duration / 1000); + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + return `${hours.toString().padStart(2, '0')}:${minutes + .toString() + .padStart(2, '0')}:${remainingSeconds + .toString() + .padStart(2, '0')}`; + }; + const formattedStart = formatDateTimeFromValue(job.created); + const formattedEnd = formatDateTimeFromValue(job.ended); + const runtime = formatDuration(job.created, job.ended); + + return ( +
    + {formattedStart} - {formattedEnd} +
    + Total Runtime: {runtime} +
    + ); + }, + }, + ], + [interactiveSessionNotifs] + ); + + return ( + <> + + + ); +}; diff --git a/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.module.css b/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.module.css new file mode 100644 index 0000000000..c8a455355d --- /dev/null +++ b/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.module.css @@ -0,0 +1,7 @@ +.listing-table-base { + height: 100%; +} + +.highlighted-row { + background-color: #e6f4ff; +} diff --git a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx b/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.tsx similarity index 59% rename from client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx rename to client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.tsx index 893e00efc5..d3d11deb8e 100644 --- a/client/modules/datafiles/src/FileListing/FileListingTable/FileListingTable.tsx +++ b/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTable.tsx @@ -1,58 +1,50 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import styles from './FileListingTable.module.css'; import { Table, TableColumnType, TableProps } from 'antd'; -import { useFileListing, TFileListing, useSelectedFiles } from '@client/hooks'; -import { FileListingTableCheckbox } from './FileListingTableCheckbox'; +import useWebSocket from 'react-use-websocket'; +import { + useJobsListing, + TTapisJob, + TJobStatusNotification, + useGetNotifications, +} from '@client/hooks'; +import styles from './JobsListingTable.module.css'; type TableRef = { nativeElement: HTMLDivElement; scrollTo: (config: { index?: number; key?: React.Key; top?: number }) => void; }; -export type TFileListingColumns = (TableColumnType & { - dataIndex: keyof TFileListing; +export type TJobsListingColumns = (TableColumnType & { + dataIndex: keyof TTapisJob; })[]; -export const FileListingTable: React.FC< +export const JobsListingTable: React.FC< { - api: string; - system?: string; - path?: string; - scheme?: string; - columns: TFileListingColumns; - filterFn?: (listing: TFileListing[]) => TFileListing[]; - disabled?: boolean; + columns: TJobsListingColumns; + filterFn?: (listing: TTapisJob[]) => TTapisJob[]; className?: string; } & Omit -> = ({ - api, - system, - path = '', - scheme = 'private', - filterFn, - columns, - disabled = false, - className, - ...props -}) => { +> = ({ filterFn, columns, className, ...props }) => { + const { lastMessage } = useWebSocket( + `wss://${window.location.host}/ws/websockets/` + ); + const { data: unreadNotifs } = useGetNotifications({ + eventTypes: ['interactive_session_ready', 'job'], + read: false, + markRead: false, + }); const limit = 100; const [scrollElement, setScrollElement] = useState( undefined ); - /* FETCH FILE LISTINGS */ + /* FETCH JOB LISTING */ const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = - useFileListing({ - api, - system: system ?? '-', - path: path ?? '', - scheme, - disabled, - pageSize: limit, - }); + useJobsListing(limit); + // const jobs = data?.pages.flatMap((page) => page.listing) || []; const combinedListing = useMemo(() => { - const cl: TFileListing[] = []; + const cl: TTapisJob[] = []; data?.pages.forEach((page) => cl.push(...page.listing)); if (filterFn) { return filterFn(cl); @@ -60,21 +52,6 @@ export const FileListingTable: React.FC< return cl; }, [data, filterFn]); - /* HANDLE FILE SELECTION */ - const { selectedFiles, setSelectedFiles } = useSelectedFiles( - api, - system ?? '-', - path - ); - const onSelectionChange = useCallback( - (_: React.Key[], selection: TFileListing[]) => setSelectedFiles(selection), - [setSelectedFiles] - ); - const selectedRowKeys = useMemo( - () => selectedFiles.map((s) => s.path), - [selectedFiles] - ); - /* HANDLE INFINITE SCROLL */ const scrollRefCallback = useCallback( (node: TableRef) => { @@ -114,6 +91,11 @@ export const FileListingTable: React.FC< isLoading, ]); + const lastNotificationJobUUID = lastMessage + ? (JSON.parse(lastMessage.data) as TJobStatusNotification).extra.uuid + : ''; + const unreadJobUUIDs = unreadNotifs?.notifs.map((x) => x.extra.uuid) ?? []; + /* RENDER THE TABLE */ return ( 0 ? 'table--pull-spinner-bottom' : '' } ${className ?? ''}`} - rowSelection={{ - type: 'checkbox', - onChange: onSelectionChange, - selectedRowKeys, - renderCell: (checked, _rc, _idx, node) => ( - - ), - }} scroll={{ y: '100%', x: '500px' }} // set to undefined to disable sticky header columns={columns} - rowKey={(record) => record.path} + rowKey={(record) => record.id} dataSource={combinedListing} pagination={false} loading={isLoading || isFetchingNextPage} + rowClassName={(record: TTapisJob) => { + if ( + unreadJobUUIDs.concat(lastNotificationJobUUID).includes(record.uuid) + ) { + return styles['highlighted-row']; + } + return ''; + }} locale={{ emptyText: isLoading || isFetchingNextPage ? (
     
    ) : ( -
    Placeholder for empty data.
    +
    No recent jobs.
    ), }} {...props} diff --git a/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTableCheckbox.tsx b/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTableCheckbox.tsx new file mode 100644 index 0000000000..35f75c759a --- /dev/null +++ b/client/modules/workspace/src/JobsListing/JobsListingTable/JobsListingTableCheckbox.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import styles from './JobsListingTable.module.css'; + +export const JobsListingTableCheckbox: React.FC<{ + checked?: boolean; + onChange: React.ChangeEventHandler; +}> = ({ checked, onChange }) => { + /* + This checkbox component is more barebones than the checkbox exported by Ant, + and is suited to large file listings where it will be rerendered extensively. + */ + return ( + + ); +}; diff --git a/client/modules/workspace/src/JobsReuseInputsButton/JobsReuseInputsButton.tsx b/client/modules/workspace/src/JobsReuseInputsButton/JobsReuseInputsButton.tsx new file mode 100644 index 0000000000..6aedfc30fb --- /dev/null +++ b/client/modules/workspace/src/JobsReuseInputsButton/JobsReuseInputsButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Button } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { TTapisJob } from '@client/hooks'; +import { SecondaryButton } from '@client/common-components'; + +export const JobsReuseInputsButton: React.FC<{ + job: TTapisJob; + isSecondaryButton?: boolean; +}> = ({ job, isSecondaryButton = false }) => { + const navigate = useNavigate(); + + const handleReuseInputs = () => { + const path = + '/' + + `${job.appId}` + + (job.appVersion ? `?appVersion=${job.appVersion}` : '') + + `&jobUUID=${job.uuid}`; + navigate(path); + }; + + return isSecondaryButton ? ( + handleReuseInputs()}> + Reuse Inputs + + ) : ( + + ); +}; diff --git a/client/modules/workspace/src/SelectModal/SelectModal.module.css b/client/modules/workspace/src/SelectModal/SelectModal.module.css new file mode 100644 index 0000000000..646a3e4866 --- /dev/null +++ b/client/modules/workspace/src/SelectModal/SelectModal.module.css @@ -0,0 +1,76 @@ +.modalContent { + display: flex; + max-height: 60vh; + min-height: 400px; + gap: 50px; + padding-bottom: 40px !important; + padding-right: 40px !important; + padding-left: 40px !important; +} + +.modalContent td { + vertical-align: middle; +} + +.modalPanel { + display: flex; + flex: 1; + flex-direction: column; +} + +.filesSection { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + border: 1px solid #707070; +} + +.filesTableContainer { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; +} + +.modalTitle { + color: #222222; + font-weight: 400; +} + +.dataFilesModalColHeader { + font-weight: bold; + margin-top: 20px; + margin-bottom: 10px; +} + +.systemSelectRow { + flex: 0 1 auto; +} + +.systemSelect { + padding-top: 5px; + padding-bottom: 5px; + font-style: italic; + display: inline-block; + vertical-align: text-bottom; + padding-left: 0px; + padding-right: 0px; +} + +.selectRowContainer { + display: flex; + justify-content: flex-start; + gap: 8px; + margin-bottom: 10px; + align-items: center; +} + +.selectRowItem { + flex: 0 1 auto; +} + +.selectBreadcrumb .datafilesBreadcrumb { + padding: 0; + background-color: #ffffff; +} diff --git a/client/modules/workspace/src/SelectModal/SelectModal.tsx b/client/modules/workspace/src/SelectModal/SelectModal.tsx new file mode 100644 index 0000000000..e49f37b007 --- /dev/null +++ b/client/modules/workspace/src/SelectModal/SelectModal.tsx @@ -0,0 +1,552 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + Button, + Modal, + Select, + Input, + Form, + ConfigProvider, + ThemeConfig, +} from 'antd'; +import { + CaretDownOutlined, + CaretUpOutlined, + LeftOutlined, +} from '@ant-design/icons'; + +import { + useAuthenticatedUser, + usePathDisplayName, + useGetSystems, + TTapisSystem, + TUser, + TFileListing, +} from '@client/hooks'; + +import { + BaseFileListingBreadcrumb, + FileListingTable, + SecondaryButton, + TFileListingColumns, +} from '@client/common-components'; +import styles from './SelectModal.module.css'; +import { SelectModalProjectListing } from './SelectModalProjectListing'; + +const api = 'tapis'; +const portalName = 'DesignSafe'; +const projectPrefix = 'project-'; + +const SystemSuffixIcon = () => { + return ( +
    + + +
    + ); +}; + +const modalStyles = { + header: { + borderRadius: 0, + borderBottom: 0, + paddingBottom: '20px', + paddingTop: '20px', + paddingLeft: '20px', + paddingRight: '20px', + backgroundColor: '#f4f4f4', + }, + body: { + paddingLeft: '40px', + paddingRight: '40px', + paddingBottom: '20px', + paddingTop: '20px', + }, + content: { + boxShadow: '0 0 30px #999', + padding: '0px', + }, +}; + +const systemSelectThemeConfig: ThemeConfig = { + token: { + borderRadius: 4, + }, +}; + +// Use isMyData in notes as an indicate of private vs public +const getScheme = (storageSystem: TTapisSystem): string => { + return storageSystem.notes?.isMyData ? 'private' : 'public'; +}; + +const getSystemRootPath = ( + storageSystem: TTapisSystem | undefined, + user: TUser | undefined +): string => { + return storageSystem?.notes?.isMyData + ? encodeURIComponent('/' + user?.username) + : ''; +}; + +const getBackPath = ( + encodedPath: string, + searchTerm: string | null, + clearSearchTerm: () => void, + system: TTapisSystem | undefined, + user: TUser | undefined +): string => { + if (searchTerm) { + clearSearchTerm(); + return encodedPath; + } + const pathParts = decodeURIComponent(encodedPath).split('/'); + const isRootPath = pathParts.join('/') === getSystemRootPath(system, user); + if (!isRootPath) { + pathParts.pop(); + } + return encodeURIComponent(pathParts.join('/')); +}; + +const getParentFolder = ( + name: string, + system: string, + path: string +): TFileListing => { + return { + format: 'folder', + lastModified: '', + length: 1, + type: 'dir', + mimeType: '', + name: name, + permissions: '', + path: decodeURIComponent(path), + system: system, + }; +}; + +function getFilesColumns( + api: string, + path: string, + selectionMode: string, + searchTerm: string | null, + clearSearchTerm: () => void, + selectionCallback: (path: string) => void, + navCallback: (path: string) => void, + user: TUser | undefined, + selectedSystem?: TTapisSystem +): TFileListingColumns { + return [ + { + title: ( +
    + +
    + ), + dataIndex: 'name', + ellipsis: true, + + render: (data, record, index) => { + const isFolder = record.format === 'folder'; + const marginLeft = index === 0 ? '3rem' : '6rem'; + const commonStyle = { + marginLeft, + textAlign: 'center' as const, + display: 'flex', + alignItems: 'center', + }; + const iconClassName = isFolder ? 'fa fa-folder-o' : 'fa fa-file-o'; + + if (isFolder && index > 0) { + return ( + + ); + } + + return ( + + + {data} + + ); + }, + }, + { + dataIndex: 'path', + align: 'end', + title: '', + render: (_, record, index) => { + const selectionModeAllowed = + (record.type === 'dir' && selectionMode === 'directory') || + (record.type === 'file' && selectionMode === 'file') || + selectionMode === 'both'; + const isNotRoot = + index > 0 || + record.system.startsWith(projectPrefix) || + path !== getSystemRootPath(selectedSystem, user); + const shouldRenderSelectButton = isNotRoot && selectionModeAllowed; + + return shouldRenderSelectButton ? ( + + selectionCallback(`${api}://${record.system}${record.path}`) + } + > + Select + + ) : null; + }, + }, + ]; +} + +export const SelectModal: React.FC<{ + inputLabel: string; + system: string | null; + selectionMode: string; + isOpen: boolean; + onClose: () => void; + onSelect: (value: string) => void; +}> = ({ inputLabel, system, selectionMode, isOpen, onClose, onSelect }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(null); + const [form] = Form.useForm(); + const getPathName = usePathDisplayName(); + + const handleClose = () => { + setIsModalOpen(false); + form.resetFields(); + onClose(); + }; + const clearSearchTerm = () => { + form.resetFields(); + setSearchTerm(null); + }; + useEffect(() => { + if (isModalOpen) { + clearSearchTerm(); + } + }, [form, isModalOpen]); + const { user } = useAuthenticatedUser(); + useEffect(() => { + setIsModalOpen(isOpen); + }, [isOpen]); + const { + data: { storageSystems, defaultStorageSystem }, + } = useGetSystems(); + + // only pick up enabled systems in this portal + const includedSystems = storageSystems.filter( + (s) => s.enabled && s.notes?.portalNames?.includes(portalName) + ); + + // Sort - so mydata is shown first. + const systemOptions = includedSystems + .sort((a, b) => { + return (a.notes?.isMyData ? 0 : 1) - (b.notes?.isMyData ? 0 : 1); + }) + .map((system) => ({ + label: system.notes.label, + value: system.id, + })); + systemOptions.push({ label: 'My Projects', value: 'myprojects' }); + + const defaultParams = useMemo( + () => ({ + selectedApi: api, + selectedSystemId: defaultStorageSystem.id, + selectedSystem: defaultStorageSystem, + selectedPath: getSystemRootPath(defaultStorageSystem, user), + scheme: getScheme(defaultStorageSystem), + }), + [defaultStorageSystem, user] + ); + + const [selection, setSelection] = useState<{ + selectedApi: string; + selectedSystemId: string; + selectedPath: string; + scheme?: string; + projectId?: string; + selectedSystem?: TTapisSystem; + }>(defaultParams); + const [systemLabel, setSystemLabel] = useState( + defaultStorageSystem.notes.label ?? defaultStorageSystem.id + ); + const [showProjects, setShowProjects] = useState(false); + const { + selectedApi, + selectedSystemId, + selectedSystem, + selectedPath, + scheme, + } = selection; + useEffect(() => setSelection(defaultParams), [isModalOpen, defaultParams]); + const [dropdownValue, setDropdownValue] = useState( + defaultStorageSystem.id + ); + const dropdownCallback = (newValue: string) => { + setDropdownValue(newValue); + if (newValue === 'myprojects') { + setShowProjects(true); + setSelection({ + selectedApi: api, + selectedSystemId: '', + selectedSystem: undefined, + selectedPath: '', + scheme: '', + }); + setSystemLabel('My Projects'); + return; + } + + const system = includedSystems.find((s) => s.id === newValue); + if (!system) return; + + setShowProjects(false); + setSelection({ + selectedApi: api, + selectedSystemId: system.id, + selectedSystem: system, + selectedPath: getSystemRootPath(system, user), + scheme: getScheme(system), + }); + setSystemLabel(system.notes.label ?? system.id); + }; + + const onProjectSelect = (uuid: string, projectId: string) => { + setShowProjects(false); + setSelection({ + selectedApi: api, + selectedSystemId: `project-${uuid}`, + selectedPath: '', + projectId: projectId, + selectedSystem: undefined, //not using system for project + }); + // Intended to indicate searching the root path of a project. + setSystemLabel('root'); + }; + + useEffect(() => { + if (isModalOpen) { + let systemValue = system ?? defaultStorageSystem.id; + if (systemValue.startsWith(projectPrefix)) { + systemValue = 'myprojects'; + } + dropdownCallback(systemValue); + } + }, [system, isModalOpen]); + + const navCallback = useCallback( + (path: string) => { + if (path === 'PROJECT_LISTING') { + setShowProjects(true); + return; + } + const newPath = path.split('/').slice(-1)[0]; + setSelection({ ...selection, selectedPath: newPath }); + }, + [selection] + ); + + const selectCallback = useCallback( + (path: string) => { + onSelect(path); + handleClose(); + }, + [handleClose, onSelect] + ); + + const FileColumns = useMemo( + () => + getFilesColumns( + selectedApi, + selectedPath, + selectionMode, + searchTerm, + clearSearchTerm, + (selection: string) => selectCallback(selection), + navCallback, + user, + selectedSystem + ), + [ + navCallback, + selectedApi, + selectedSystemId, + selectedSystem, + selectedPath, + systemLabel, + selectionMode, + selectCallback, + ] + ); + + return ( + Select} + > +
    +
    +
    + Select {inputLabel} +
    +
    +
    + + + + +
    + {!showProjects && ( +
    + +
    + )} + {showProjects && ( +
    + + onProjectSelect(uuid, projectId) + } + /> +
    + )} +
    +
    +
    +
    + ); +}; diff --git a/client/modules/workspace/src/SelectModal/SelectModalProjectListing.tsx b/client/modules/workspace/src/SelectModal/SelectModalProjectListing.tsx new file mode 100644 index 0000000000..eb991a7ac0 --- /dev/null +++ b/client/modules/workspace/src/SelectModal/SelectModalProjectListing.tsx @@ -0,0 +1,57 @@ +import { TBaseProject, useProjectListing } from '@client/hooks'; +import { Button, Table, TableColumnsType } from 'antd'; +import React, { useState } from 'react'; + +export const SelectModalProjectListing: React.FC<{ + onSelect: (uuid: string, projectId: string) => void; +}> = ({ onSelect }) => { + const limit = 100; + const [currentPage, setCurrentPage] = useState(1); + const { data, isLoading } = useProjectListing(currentPage, limit); + + const columns: TableColumnsType = [ + { + render: (_, record) => record.value.projectId, + title: 'Project ID', + }, + { + render: (_, record) => ( + + ), + title: 'Title', + width: '50%', + }, + { + render: (_, record) => { + const pi = record.value.users.find((u) => u.role === 'pi'); + return `${pi?.fname} ${pi?.lname}`; + }, + title: 'Principal Investigator', + }, + ]; + + return ( +
    row.uuid} + pagination={{ + total: data?.total, + showSizeChanger: false, + current: currentPage, + pageSize: 100, + hideOnSinglePage: true, + onChange: (page) => setCurrentPage(page), + }} + >
    + ); +}; diff --git a/client/modules/workspace/src/SystemsPushKeysModal/SystemsPushKeysModal.tsx b/client/modules/workspace/src/SystemsPushKeysModal/SystemsPushKeysModal.tsx new file mode 100644 index 0000000000..e139cf3911 --- /dev/null +++ b/client/modules/workspace/src/SystemsPushKeysModal/SystemsPushKeysModal.tsx @@ -0,0 +1,144 @@ +import React, { useEffect } from 'react'; +import { Modal, Form, Input, Alert } from 'antd'; +import { usePushKeys, TTapisSystem, TPushKeysBody } from '@client/hooks'; +import { PrimaryButton } from '@client/common-components'; + +export const SystemsPushKeysModalBody: React.FC<{ + system?: TTapisSystem; + onSuccess?: () => void; + handleCancel: () => void; +}> = ({ system, onSuccess, handleCancel }) => { + const { + mutate: pushKeys, + error: pushKeysError, + isPending, + isSuccess, + } = usePushKeys(); + const [form] = Form.useForm(); + + const initialValues = { + systemId: system?.id, + hostname: system?.host, + password: '', + token: '', + }; + + useEffect(() => { + if (isSuccess) { + // Keys pushed successfully, close the modal and call onSuccess() + handleCancel(); + if (onSuccess) { + onSuccess(); + } + } + }, [isSuccess]); + + const getErrorMessage = (error?: string) => { + if (!error) return 'There was a problem pushing your keys to the server.'; + + if (error?.includes('SYSAPI_CRED_VALID_FAIL')) + return 'Invalid credentials. Please input a valid TACC Token and password.'; + + return error; + }; + + return ( + Authenticate with TACC Token} + width="600px" + open={!!system} + destroyOnClose + onCancel={handleCancel} + footer={null} + > + + To proceed, you must authenticate to this system with a six-digit + one time passcode at least once using the same MFA App you used to + log in to DesignSafe. A public key will be pushed to your{' '} + authorized_keys file on the system below. This will + allow you to access this system from this portal. + + } + showIcon + style={{ margin: '15px 10px' }} + /> +
    pushKeys(data)} + initialValues={initialValues} + form={form} + preserve={false} + > + + {initialValues.systemId} + + + {initialValues.hostname} + + + + + + + +
    + {pushKeysError && ( + + )} + + + Authenticate + + +
    +
    +
    + ); +}; + +export const SystemsPushKeysModal: React.FC<{ + onSuccess?: () => void; + isModalOpen: TTapisSystem | undefined; + setIsModalOpen: React.Dispatch< + React.SetStateAction + >; +}> = ({ onSuccess, isModalOpen, setIsModalOpen }) => { + const handleCancel = () => { + setIsModalOpen(undefined); + }; + + return ( + + ); +}; diff --git a/client/modules/workspace/src/Toast/Notifications.module.css b/client/modules/workspace/src/Toast/Notifications.module.css new file mode 100644 index 0000000000..44beb6563c --- /dev/null +++ b/client/modules/workspace/src/Toast/Notifications.module.css @@ -0,0 +1,3 @@ +.toast-is-error { + color: #eb6e6e; +} diff --git a/client/modules/workspace/src/Toast/Toast.tsx b/client/modules/workspace/src/Toast/Toast.tsx new file mode 100644 index 0000000000..1956cd1102 --- /dev/null +++ b/client/modules/workspace/src/Toast/Toast.tsx @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import useWebSocket from 'react-use-websocket'; +import { useQueryClient } from '@tanstack/react-query'; +import { notification } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { Icon } from '@client/common-components'; +import { TJobStatusNotification } from '@client/hooks'; +import { getToastMessage } from '../utils'; +import styles from './Notifications.module.css'; + +const Notifications = () => { + const { lastMessage } = useWebSocket( + `wss://${window.location.host}/ws/websockets/` + ); + + const [api, contextHolder] = notification.useNotification({ maxCount: 1 }); + + const queryClient = useQueryClient(); + + const navigate = useNavigate(); + + const handleNotification = (notification: TJobStatusNotification) => { + if ( + notification.event_type === 'job' || + notification.event_type === 'interactive_session_ready' + ) { + queryClient.invalidateQueries({ + queryKey: ['workspace', 'notifications'], + }); + queryClient.invalidateQueries({ + queryKey: ['workspace', 'jobsListing'], + }); + api.open({ + message: getToastMessage(notification), + placement: 'bottomLeft', + icon: , + className: `${ + notification.extra.status === 'FAILED' && styles['toast-is-error'] + }`, + closeIcon: false, + duration: 5, + onClick: () => { + navigate('/history'); + }, + style: { cursor: 'pointer' }, + }); + } + }; + + useEffect(() => { + if (lastMessage !== null) { + handleNotification(JSON.parse(lastMessage.data)); + } + }, [lastMessage]); + + return <>{contextHolder}; +}; + +export default Notifications; diff --git a/client/modules/workspace/src/Toast/index.tsx b/client/modules/workspace/src/Toast/index.tsx new file mode 100644 index 0000000000..116863cfaa --- /dev/null +++ b/client/modules/workspace/src/Toast/index.tsx @@ -0,0 +1 @@ +export { default as Toast } from './Toast'; diff --git a/client/modules/workspace/src/constants.ts b/client/modules/workspace/src/constants.ts new file mode 100644 index 0000000000..598ac3e777 --- /dev/null +++ b/client/modules/workspace/src/constants.ts @@ -0,0 +1,19 @@ +export const STATUS_TEXT_MAP: Record = { + PENDING: 'Processing', + PROCESSING_INPUTS: 'Processing', + STAGING_INPUTS: 'Queueing', + STAGING_JOB: 'Queueing', + SUBMITTING_JOB: 'Queueing', + QUEUED: 'Queueing', + RUNNING: 'Running', + ARCHIVING: 'Finishing', + FINISHED: 'Finished', + STOPPED: 'Stopped', + FAILED: 'Failure', + BLOCKED: 'Blocked', + PAUSED: 'Paused', + CANCELLED: 'Cancelled', + ARCHIVED: 'Archived', +}; + +export const TERMINAL_STATES = [`FINISHED`, `CANCELLED`, `FAILED`]; diff --git a/client/modules/workspace/src/index.ts b/client/modules/workspace/src/index.ts index c70e490350..694e66c4f2 100644 --- a/client/modules/workspace/src/index.ts +++ b/client/modules/workspace/src/index.ts @@ -1 +1,11 @@ -export * from './lib/workspace'; +export * from './AppsSideNav/AppsSideNav'; +export * from './AppsSubmissionForm/AppsSubmissionForm'; +export { default as AppIcon } from './AppsSubmissionForm/AppIcon'; +export * from './JobsListing/JobsListing'; +export * from './JobsDetailModal/JobsDetailModal'; +export * from './JobStatusNav/JobStatusNav'; +export * from './AppsBreadcrumb/AppsBreadcrumb'; +export * from './SystemsPushKeysModal/SystemsPushKeysModal'; +export * from './Toast'; +export * from './utils'; +export * from './constants'; diff --git a/client/modules/workspace/src/lib/workspace.module.css b/client/modules/workspace/src/lib/workspace.module.css deleted file mode 100644 index 45c2aa47e9..0000000000 --- a/client/modules/workspace/src/lib/workspace.module.css +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Replace this with your own classes - * - * e.g. - * .container { - * } -*/ diff --git a/client/modules/workspace/src/lib/workspace.spec.tsx b/client/modules/workspace/src/lib/workspace.spec.tsx deleted file mode 100644 index b3016485bb..0000000000 --- a/client/modules/workspace/src/lib/workspace.spec.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { render } from '@client/test-fixtures'; -import Workspace from './workspace'; - -describe('Workspace', () => { - it('should render successfully', async () => { - const { baseElement, findByText } = render(); - expect(baseElement).toBeTruthy(); - expect(await findByText(/FigureGen/)).toBeTruthy(); - }); -}); diff --git a/client/modules/workspace/src/lib/workspace.tsx b/client/modules/workspace/src/lib/workspace.tsx deleted file mode 100644 index 6058aad2ed..0000000000 --- a/client/modules/workspace/src/lib/workspace.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import styles from './workspace.module.css'; -import { useAppsListing } from '@client/hooks'; - -/* eslint-disable-next-line */ -export interface WorkspaceProps {} - -export function Workspace(props: WorkspaceProps) { - const { data, isLoading } = useAppsListing(); - - return ( -
    -

    Welcome to Workspace!

    - {isLoading &&
    Loading app listing...
    } - {data && ( -
      - {data.map((appMeta) => ( -
    • {appMeta.value.definition.id}
    • - ))} -
    - )} -
    - ); -} - -export default Workspace; diff --git a/client/modules/workspace/src/utils/apps.ts b/client/modules/workspace/src/utils/apps.ts new file mode 100644 index 0000000000..ed7f98cac5 --- /dev/null +++ b/client/modules/workspace/src/utils/apps.ts @@ -0,0 +1,474 @@ +import { useParams, useLocation } from 'react-router-dom'; +import { z } from 'zod'; +import { + TAppCategories, + TAppParamsType, + TTapisSystem, + TTapisApp, + TTapisSystemQueue, + TTasAllocations, +} from '@client/hooks'; +import { TFormValues } from '../AppsWizard/AppsFormSchema'; +import { UseFormSetValue } from 'react-hook-form'; + +export const TARGET_PATH_FIELD_PREFIX = '_TargetPath_'; +export const DEFAULT_JOB_MAX_MINUTES = 60 * 24; + +/** + * Get the execution system object for a given id of the execution system. + */ +export const getExecSystemFromId = ( + execSystems: TTapisSystem[], + execSystemId: string +) => { + if (execSystems?.length) { + return execSystems.find((exec_sys) => exec_sys.id === execSystemId); + } + + return null; +}; + +/** + * Filters available execution systems if dynamicExecSystems is defined. + * Otherwise, return all available systems. + */ +export const getExecSystemsFromApp = ( + definition: TTapisApp, + execSystems: TTapisSystem[] +) => { + if (isAppUsingDynamicExecSystem(definition)) { + if ( + definition.notes.dynamicExecSystems?.length === 1 && + definition.notes.dynamicExecSystems[0] === 'ALL' + ) + return execSystems; + + return execSystems.filter((s) => + definition.notes.dynamicExecSystems?.includes(s.id) + ); + } + + const sys = execSystems.find( + (s) => s.id === definition.jobAttributes.execSystemId + ); + return sys ? [sys] : []; +}; + +/** + * Gets the exec system for the default set in the job attributes. + * Otherwise, get the first entry. + */ +export const getDefaultExecSystem = ( + definition: TTapisApp, + execSystems: TTapisSystem[] +) => { + // If dynamic exec system is not setup, use from job attributes. + if (!definition.notes.dynamicExecSystems) { + return getExecSystemFromId( + execSystems, + definition.jobAttributes.execSystemId + ); + } + + if (execSystems?.length) { + const execSystemId = definition.jobAttributes.execSystemId; + + // Check if the app's default execSystemId is in provided list + // If not found, return the first execSystem from the provided list + return ( + getExecSystemFromId(execSystems, execSystemId) || + getExecSystemFromId(execSystems, execSystems[0].id) + ); + } + + return null; +}; + +export const getQueueMaxMinutes = ( + definition: TTapisApp, + exec_sys: TTapisSystem, + queueName: string +) => { + if (!isAppTypeBATCH(definition)) { + return DEFAULT_JOB_MAX_MINUTES; + } + + return ( + exec_sys?.batchLogicalQueues.find((q) => q.name === queueName) + ?.maxMinutes ?? 0 + ); +}; + +export const preprocessStringToNumber = (value: unknown): unknown => { + if (typeof value === 'string' && !isNaN(Number(value))) { + return Number(value); + } + return value; +}; + +/** + * Get validator for max minutes of a queue + * + * @function + * @param {Object} definition App definition + * @param {Object} queue + * @returns {z.number()} min/max validation of max minutes + */ +export const getMaxMinutesValidation = ( + definition: TTapisApp, + queue: TTapisSystemQueue +) => { + if (!isAppTypeBATCH(definition)) { + return z.preprocess( + preprocessStringToNumber, + z.number().lte(DEFAULT_JOB_MAX_MINUTES) + ); + } + if (!queue) { + return z.preprocess(preprocessStringToNumber, z.number()); + } + + return z.preprocess( + preprocessStringToNumber, + z + .number() + .gte( + queue.minMinutes, + `Max Minutes must be greater than or equal to ${queue.minMinutes} for the ${queue.name} queue` + ) + .lte( + queue.maxMinutes, + `Max Minutes must be less than or equal to ${queue.maxMinutes} for the ${queue.name} queue` + ) + ); +}; + +/** + * Get validator for a node count of a queue + * + * @function + * @param {Object} definition App definition + * @param {Object} queue + * @returns {z.number()} min/max validation of node count + */ +export const getNodeCountValidation = ( + definition: TTapisApp, + queue: TTapisSystemQueue +) => { + if (!isAppTypeBATCH(definition) || !queue) { + return z.number().positive().optional(); + } + return z.preprocess( + preprocessStringToNumber, + z + .number() + .int('Node Count must be an integer.') + .gte( + queue.minNodeCount, + `Node Count must be greater than or equal to ${queue.minNodeCount} for the ${queue.name} queue.` + ) + .lte( + queue.maxNodeCount, + `Node Count must be less than or equal to ${queue.maxNodeCount} for the ${queue.name} queue.` + ) + ); +}; + +/** + * Get validator for cores on each node + * + * @function + * @param {Object} definition App definition + * @param {Object} queue + * @returns {z.number()} min/max validation of coresPerNode + */ +export const getCoresPerNodeValidation = ( + definition: TTapisApp, + queue: TTapisSystemQueue +) => { + if (!isAppTypeBATCH(definition) || !queue || queue.maxCoresPerNode === -1) { + return z.preprocess( + preprocessStringToNumber, + z.number().int().positive().optional() + ); + } + return z.preprocess( + preprocessStringToNumber, + z.number().int().gte(queue.minCoresPerNode).lte(queue.maxCoresPerNode) + ); +}; + +/** + * Get corrected values for a new queue + * + * Check values and if any do not work with the current queue, then fix those + * values. + * + * @function + * @param {Object} execSystems + * @param {Object} values + * @returns {Object} updated/fixed values + */ +export const updateValuesForQueue = ( + execSystems: TTapisSystem[], + execSystemId: string, + values: TFormValues, + setValue: UseFormSetValue +) => { + const exec_sys = getExecSystemFromId(execSystems, execSystemId); + if (!exec_sys) { + return; + } + + const queue = getQueueValueForExecSystem({ + exec_sys, + queue_name: values.configuration.execSystemLogicalQueue as string, + }); + if (!queue) return; + + if ((values.configuration.nodeCount as number) < queue.minNodeCount) { + setValue('configuration.nodeCount', queue.minNodeCount); + } + if ((values.configuration.nodeCount as number) > queue.maxNodeCount) { + setValue('configuration.nodeCount', queue.maxNodeCount); + } + + if ((values.configuration.coresPerNode as number) < queue.minCoresPerNode) { + setValue('configuration.coresPerNode', queue.minCoresPerNode); + } + if ( + queue.maxCoresPerNode !== -1 /* e.g. Frontera rtx/rtx-dev queue */ && + (values.configuration.coresPerNode as number) > queue.maxCoresPerNode + ) { + setValue('configuration.coresPerNode', queue.maxCoresPerNode); + } + + if ((values.configuration.maxMinutes as number) < queue.minMinutes) { + setValue('configuration.maxMinutes', queue.minMinutes); + } + if ((values.configuration.maxMinutes as number) > queue.maxMinutes) { + setValue('configuration.maxMinutes', queue.maxMinutes); + } +}; + +/** + * Get the default queue for a execution system. + * Queue Name determination order: + * 1. Use given queue name. + * 2. Otherwise, use the app default queue. + * 3. Otherwise, use the execution system default queue. + */ +export const getQueueValueForExecSystem = ({ + definition, + exec_sys, + queue_name, +}: { + definition?: TTapisApp; + exec_sys?: TTapisSystem; + queue_name?: string; +}) => { + const queueName = + queue_name ?? + definition?.jobAttributes.execSystemLogicalQueue ?? + exec_sys?.batchDefaultLogicalQueue; + return ( + exec_sys?.batchLogicalQueues.find((q) => q.name === queueName) || + exec_sys?.batchLogicalQueues[0] + ); +}; + +/** + * Apply the following two filters and get the list of queues applicable. + * Filters: + * 1. If Node and Core per Node is enabled, only allow + * queues which match min and max node count with job attributes + * 2. if queue filter list is set, only allow queues in that list. + * @function + * @param {any} definition App definition + * @param {any} queues + * @returns list of queues in sorted order + */ +export const getAppQueueValues = ( + definition: TTapisApp, + queues: TTapisSystemQueue[] +) => { + return ( + (queues ?? []) + /* + Hide queues for which the app default nodeCount does not meet the minimum or maximum requirements + while hideNodeCountAndCoresPerNode is true + */ + .filter( + (q) => + !definition.notes.hideNodeCountAndCoresPerNode || + (definition.jobAttributes.nodeCount >= q.minNodeCount && + definition.jobAttributes.nodeCount <= q.maxNodeCount) + ) + .map((q) => q.name) + // Hide queues when app includes a queueFilter and queue is not present in queueFilter + .filter( + (queueName) => + !definition.notes.queueFilter || + definition.notes.queueFilter.includes(queueName) + ) + .sort() + ); +}; + +/** + * Get the field name used for target path in AppForm + * + * @function + * @param {String} inputFieldName + * @returns {String} field Name prefixed with target path + */ +export const getTargetPathFieldName = (inputFieldName: string) => { + return TARGET_PATH_FIELD_PREFIX + inputFieldName; +}; + +/** + * Whether a field name is a system defined field for Target Path + * + * @function + * @param {String} inputFieldName + * @returns {String} field Name suffixed with target path + */ +export const isTargetPathField = (inputFieldName: string) => { + return inputFieldName && inputFieldName.startsWith(TARGET_PATH_FIELD_PREFIX); +}; + +/** + * From target path field name, derive the original input field name. + * + * @function + * @param {String} targetPathFieldName + * @returns {String} actual field name + */ +export const getInputFieldFromTargetPathField = ( + targetPathFieldName: string +) => { + return targetPathFieldName.replace(TARGET_PATH_FIELD_PREFIX, ''); +}; + +/** + * Check if targetPath is empty on input field + * + * @function + * @param {String} targetPathFieldValue + * @returns {boolean} if target path is empty + */ +export const isTargetPathEmpty = (targetPathFieldValue?: string) => { + if (targetPathFieldValue === null || targetPathFieldValue === undefined) { + return true; + } + + targetPathFieldValue = targetPathFieldValue.trim(); + + if (targetPathFieldValue.trim() === '') { + return true; + } + + return false; +}; + +/** + * Sets the default value if target path is not set. + * + * @function + * @param {String} targetPathFieldValue + * @returns {String} target path value + */ +export const checkAndSetDefaultTargetPath = (targetPathFieldValue?: string) => { + if (isTargetPathEmpty(targetPathFieldValue)) { + return '*'; + } + + return targetPathFieldValue; +}; + +export const isAppUsingDynamicExecSystem = (definition: TTapisApp) => { + return !!definition.notes.dynamicExecSystems; +}; + +export const getAllocationValidation = ( + definition: TTapisApp, + allocations: string[] +) => { + if (!isAppTypeBATCH(definition)) { + return z.string().optional(); + } + return z.enum(allocations as [string, ...string[]], { + errorMap: (issue, ctx) => ({ + message: 'Please select an allocation from the dropdown.', + }), + }); +}; + +export const isAppTypeBATCH = (definition: TTapisApp) => { + return definition.jobType === 'BATCH'; +}; + +export const getExecSystemLogicalQueueValidation = ( + definition: TTapisApp, + exec_sys: TTapisSystem +) => { + if (!isAppTypeBATCH(definition)) { + return z.string().optional(); + } + + return z.enum( + (exec_sys?.batchLogicalQueues.map((q) => q.name) ?? []) as [ + string, + ...string[] + ] + ); +}; + +/** + * Provides allocation list matching + * the execution host of the selected app. + */ +export const getAllocationList = ( + execSystem: TTapisSystem, + allocations: TTasAllocations +) => { + const matchingExecutionHost = Object.keys(allocations.hosts).find( + (host) => execSystem.host === host || execSystem.host.endsWith(`.${host}`) + ); + + return matchingExecutionHost ? allocations.hosts[matchingExecutionHost] : []; +}; + +export const useGetAppParams = () => { + const { appId } = useParams() as TAppParamsType; + const location = useLocation(); + const appVersion = new URLSearchParams(location.search).get('appVersion') as + | string + | undefined; + const jobUUID = new URLSearchParams(location.search).get('jobUUID') as + | string + | undefined; + + return { appId, appVersion, jobUUID }; +}; + +/** + * Find app in app tray categories and get the icon info. + * @param data TAppCategories or undefined + * @param appId string - id of an app. + * @returns icon name if available, otherwise null + */ +export const findAppById = ( + data: TAppCategories | undefined, + appId: string +) => { + if (!data) return null; + for (const category of data.categories) { + for (const app of category.apps) { + if (app.app_id === appId) { + return app; + } + } + } + return null; +}; diff --git a/client/modules/workspace/src/utils/index.ts b/client/modules/workspace/src/utils/index.ts new file mode 100644 index 0000000000..c882054cfb --- /dev/null +++ b/client/modules/workspace/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './jobs'; +export * from './systems'; +export * from './apps'; +export * from './truncateMiddle'; +export * from './notifications'; diff --git a/client/modules/workspace/src/utils/jobs.ts b/client/modules/workspace/src/utils/jobs.ts new file mode 100644 index 0000000000..89b58a00fd --- /dev/null +++ b/client/modules/workspace/src/utils/jobs.ts @@ -0,0 +1,395 @@ +import { STATUS_TEXT_MAP, TERMINAL_STATES } from '../constants'; +import { + TJobStatusNotification, + TAppResponse, + TConfigurationValues, + TTapisJob, + TAppFileInput, + TJobArgSpecs, + TJobArgSpec, + TJobKeyValuePair, + TParameterSetNotes, + TParameterSetSubmit, +} from '@client/hooks'; +import { + TFileInputsDefaults, + TParameterSetDefaults, +} from '../AppsWizard/AppsFormSchema'; + +export function getStatusText(status: string) { + if (status in STATUS_TEXT_MAP) { + return STATUS_TEXT_MAP[status]; + } + return 'Unknown'; +} + +export function isTerminalState(status: string) { + return TERMINAL_STATES.includes(status); +} + +/** + * Determine if job has output + * + * @param status + * @returns boolean + */ +export function isOutputState(status: string) { + return isTerminalState(status) && status !== 'CANCELLED'; +} + +export function getArchivePath(job: TTapisJob) { + return `${job.archiveSystemId}${ + job.archiveSystemDir.charAt(0) === '/' ? '' : '/' + }${job.archiveSystemDir}`; +} + +export function getExecutionPath(job: TTapisJob) { + return `${job.execSystemId}${ + job.execSystemExecDir.charAt(0) === '/' ? '' : '/' + }${job.execSystemExecDir}`; +} + +export function getExecSysOutputPath(job: TTapisJob) { + return `${job.execSystemId}${ + job.execSystemOutputDir.charAt(0) === '/' ? '' : '/' + }${job.execSystemOutputDir}`; +} + +export function getOutputPath(job: TTapisJob) { + if (!job.remoteOutcome || !isOutputState(job.status)) { + return ''; + } + + if (job.remoteOutcome === 'FAILED_SKIP_ARCHIVE') { + return getExecSysOutputPath(job); + } + + return getArchivePath(job); +} + +export function isInteractiveJob(job: TTapisJob) { + return job.tags.includes('isInteractive'); +} + +export function getJobInteractiveSessionInfo( + job: TTapisJob, + interactiveNotifications: TJobStatusNotification[] +) { + const jobConcluded = + isTerminalState(job.status) || job.status === 'ARCHIVING'; + if (jobConcluded || !isInteractiveJob(job)) return {}; + + const notif = interactiveNotifications?.find( + (n) => n.extra.uuid === job.uuid + ); + + return { + interactiveSessionLink: notif?.action_link, + message: notif?.message, + }; +} + +/* Return allocation + queue directive has form: '-A TACC-ACI' + */ +export function getAllocationFromDirective(directive: string | undefined) { + if (!directive) return directive; + const parts = directive.split(' '); + const allocationArgIndex = parts.findIndex((obj) => obj === '-A') + 1; + if (allocationArgIndex !== 0 && allocationArgIndex < parts.length) { + return parts[allocationArgIndex]; + } + return null; +} + +export type TJobDisplayInputOrParameter = { + label: string; + id?: string; + value?: string; +}; + +export type TJobDisplayInfo = { + appId: string; + appVersion?: string; + applicationName: string; + systemName: string; + inputs: TJobDisplayInputOrParameter[]; + appArgs: TJobDisplayInputOrParameter[]; + envVars: TJobDisplayInputOrParameter[]; + archiveOnAppError: boolean; + archiveSystemDir: string; + archiveSystemId: string; + archiveTransactionId?: string; + exec_system_id: string; + workPath: string; + allocation?: string; + queue?: string; + coresPerNode?: number; + nodeCount?: number; +}; + +function getParameterSetNotesLabel(obj: unknown): string | undefined { + if (obj && typeof obj === 'string') { + try { + return (JSON.parse(obj) as TParameterSetNotes)?.label; + } catch (e) { + //ignore. + } + } + return undefined; +} + +function isNotHidden( + obj: TJobArgSpec | TAppFileInput | TJobKeyValuePair +): boolean { + return !obj.notes || !obj.notes.isHidden; +} + +// Notes Reviver for JSON.parse +function reviver(key: string, value: unknown) { + if (key === 'notes' && typeof value === 'string') { + try { + return JSON.parse(value); + } catch (e) { + return value; + } + } + return value; +} + +/** + * Get display values from job, app, and execution system info. + */ +export function getJobDisplayInformation( + job: TTapisJob, + app: TAppResponse | undefined +): TJobDisplayInfo { + const filterAppArgs = (objects: TJobArgSpecs) => objects.filter(isNotHidden); + + const filterInputs = (objects: TAppFileInput[]) => + objects + .filter(isNotHidden) + .filter((obj) => !(obj.name || obj.sourceUrl || '').startsWith('_')); + + const filterParameters = (objects: TJobKeyValuePair[]) => + objects.filter(isNotHidden).filter((obj) => !obj.key.startsWith('_')); + + const fileInputs = filterInputs( + JSON.parse(job.fileInputs, reviver) as TAppFileInput[] + ); + const parameterSet = JSON.parse(job.parameterSet, reviver); + const parameters = filterAppArgs(parameterSet.appArgs) as TJobArgSpecs; + + const displayEnvVariables = filterParameters(parameterSet.envVariables); + const envVariables = parameterSet.envVariables as TJobKeyValuePair[]; + const schedulerOptions = parameterSet.schedulerOptions as TJobArgSpecs; + + const display: TJobDisplayInfo = { + appId: job.appId, + applicationName: job.appId, + appVersion: job.appVersion, + systemName: job.execSystemId, + inputs: fileInputs.map((input) => ({ + label: input.name || 'Unnamed Input', + id: input.sourceUrl, + value: input.sourceUrl, + })), + appArgs: parameters.map((parameter) => ({ + label: parameter.name, + id: parameter.name, + value: parameter.arg, + })), + envVars: displayEnvVariables.map((d) => ({ + label: d.notes?.label ?? d.key, + id: d.key, + value: d.value, + })), + archiveOnAppError: false, + archiveSystemDir: '', + archiveSystemId: '', + exec_system_id: job.execSystemId, + workPath: '', + }; + + if (app) { + try { + display.applicationName = + app.definition.notes.label || display.applicationName; + + const workPath = envVariables.find( + (env) => env.key === '_tapisJobWorkingDir' + ); + display.workPath = workPath ? workPath.value : ''; + + if (app.definition.jobType === 'BATCH') { + const allocationParam = schedulerOptions.find( + (opt) => opt.name === 'TACC Allocation' + ); + if (allocationParam) { + const allocation = getAllocationFromDirective(allocationParam.arg); + if (allocation) { + display.allocation = allocation; + } + } + display.queue = job.execSystemLogicalQueue; + } + + if (!app.definition.notes.hideNodeCountAndCoresPerNode) { + display.coresPerNode = job.coresPerNode; + display.nodeCount = job.nodeCount; + } + + // Tapis adds env variables when envKey is used, filter those out + display.envVars = display.envVars.filter( + (env) => + env.id && + app.definition.jobAttributes.parameterSet.envVariables.find( + (e) => e.key === env.id + ) + ); + } catch (ignore) { + // Ignore if there is a problem using the app definition to improve display + } + } + + return display; +} + +function isJobDataFromSameAppVersion( + jobData: TTapisJob, + appId: string, + appVersion: string | undefined +): boolean { + return ( + jobData.appId === appId && + (!appVersion || jobData.appVersion === appVersion) + ); +} + +/** + * Merge app configuration values with job data. + * Used in reusing submitted job data for a new job. + * @param appId + * @param appVersion + * @param configuration + * @param jobData + * @returns merged data if job is valid otherwise, original values + */ +export const mergeConfigurationDefaultsWithJobData = ( + appId: string, + appVersion: string | undefined, + configuration: TConfigurationValues, + jobData: TTapisJob | null +): TConfigurationValues => { + if (!jobData || !isJobDataFromSameAppVersion(jobData, appId, appVersion)) + return configuration; + const parameterSet = JSON.parse(jobData.parameterSet); + const schedulerOptions = parameterSet.schedulerOptions as TJobArgSpecs; + const allocationParam = schedulerOptions.find( + (opt) => opt.name === 'TACC Allocation' + ); + + return { + ...configuration, + execSystemLogicalQueue: + jobData.execSystemLogicalQueue ?? configuration.execSystemLogicalQueue, + maxMinutes: jobData.maxMinutes ?? configuration.maxMinutes, + nodeCount: jobData.nodeCount ?? configuration.nodeCount, + coresPerNode: jobData.coresPerNode ?? configuration.coresPerNode, + allocation: + getAllocationFromDirective(allocationParam?.arg) ?? + configuration.allocation, + }; +}; + +/** + * Merge app input defaults with same values from job inputs. + * Uses app input keys as source and ensures only those values are updated. + * @param appId + * @param appVersion + * @param inputs + * @param jobData + * @returns merged data if job is valid otherwise, original values + */ +export const mergeInputDefaultsWithJobData = ( + appId: string, + appVersion: string | undefined, + inputs: TFileInputsDefaults, + jobData: TTapisJob | null +) => { + if (!jobData || !isJobDataFromSameAppVersion(jobData, appId, appVersion)) + return inputs; + const jobInputs = Object.fromEntries( + (JSON.parse(jobData.fileInputs) as TAppFileInput[]).map((input) => [ + input.name, + input.sourceUrl, + ]) + ); + + const mergedInputs: TFileInputsDefaults = { ...inputs }; + for (const key in inputs) { + if (key in jobInputs && jobInputs[key]) { + mergedInputs[key] = jobInputs[key] as string; + } + } + return mergedInputs; +}; + +/** + * Merge app parameterSet defaults with same values from job parameterSet. + * Uses app parameter keys as source and ensures only those values are updated. + * @param appId + * @param appVersion + * @param inputs + * @param jobData + * @returns merged data if job is valid otherwise, original values + */ +export const mergeParameterSetDefaultsWithJobData = ( + appId: string, + appVersion: string | undefined, + parameterSet: TParameterSetDefaults, + jobData: TTapisJob | null +): TParameterSetDefaults => { + if (!jobData || !isJobDataFromSameAppVersion(jobData, appId, appVersion)) + return parameterSet; + + const jobParameterSet = JSON.parse( + jobData.parameterSet + ) as TParameterSetSubmit; + const mergedParameterSet: TParameterSetDefaults = { ...parameterSet }; + + for (const key in parameterSet) { + const jobParams = jobParameterSet[key as keyof TParameterSetSubmit]; + if ( + jobParams && + typeof jobParams === 'object' && + typeof parameterSet[key] === 'object' + ) { + mergedParameterSet[key] = { ...parameterSet[key] }; + + const jobArgs = jobParams.map((p: TJobArgSpec | TJobKeyValuePair) => + key === 'envVariables' + ? { + key: + getParameterSetNotesLabel(p.notes) ?? + (p as TJobKeyValuePair).key, + value: (p as TJobKeyValuePair).value, + } + : { + key: + getParameterSetNotesLabel(p.notes) ?? (p as TJobArgSpec).name, + value: (p as TJobArgSpec).arg, + } + ); + + for (const jobArg of jobArgs) { + const argName = jobArg.key; + if (argName in mergedParameterSet[key] && jobArg.value) { + mergedParameterSet[key][argName] = jobArg.value as string; + } + } + } + } + + return mergedParameterSet; +}; diff --git a/client/modules/workspace/src/utils/notifications.ts b/client/modules/workspace/src/utils/notifications.ts new file mode 100644 index 0000000000..9e38c5e5ca --- /dev/null +++ b/client/modules/workspace/src/utils/notifications.ts @@ -0,0 +1,40 @@ +import { truncateMiddle, getStatusText } from '../utils'; +import { TJobStatusNotification } from '@client/hooks'; +/* + * Post-process mapped status message to get a toast message translation. + */ +const toastMap = (status: string) => { + const mappedStatus = getStatusText(status); + switch (mappedStatus) { + case 'Running': + return 'is now running'; + case 'Failure' || 'Stopped': + return status.toLowerCase(); + case 'Finished': + return 'finished successfully'; + case 'Unknown': + return 'is in an unknown state'; + default: + return `is ${mappedStatus.toLowerCase()}`; + } +}; + +/* + * Returns a human readable message from a job update event. + */ +export const getToastMessage = ({ + extra, + event_type: eventType, + message, +}: TJobStatusNotification) => { + switch (eventType) { + case 'job': + return `${truncateMiddle(extra.name, 25)} ${toastMap(extra.status)}`; + case 'interactive_session_ready': + return `${truncateMiddle(extra.name, 25)} ${ + message ? message.toLowerCase() : 'session ready to view.' + }`; + default: + return message; + } +}; diff --git a/client/modules/workspace/src/utils/systems.ts b/client/modules/workspace/src/utils/systems.ts new file mode 100644 index 0000000000..3f6efc9579 --- /dev/null +++ b/client/modules/workspace/src/utils/systems.ts @@ -0,0 +1,18 @@ +/** + * Returns a capitalized system name readable message from a job update event. + * + * @param {string} host + * @return {string} system name + */ +export function getSystemName(host: string) { + if ( + host.startsWith('data.tacc') || + host.startsWith('cloud.corral') || + host.startsWith('secure.corral') || + host.startsWith('cloud.data') + ) { + return 'Corral'; + } + const systemName = host.split('.')[0]; + return systemName.substring(0, 1).toUpperCase() + systemName.slice(1); +} diff --git a/client/modules/workspace/src/utils/timeFormat.ts b/client/modules/workspace/src/utils/timeFormat.ts new file mode 100644 index 0000000000..4d5da0dc12 --- /dev/null +++ b/client/modules/workspace/src/utils/timeFormat.ts @@ -0,0 +1,55 @@ +/** + * Create a string representation of date using internal standard + * @param {Date} dateTime - A date object + * @returns {string} + */ +export function formatDate(dateTime: Date) { + return dateTime.toLocaleDateString('en-US', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); +} + +/** + * Create a string representation of time using internal standard + * @param {Date} dateTime - A date object + * @returns {string} + */ +export function formatTime(dateTime: Date) { + return dateTime.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + }); +} + +/** + * Create a string representation of date and time using internal standard + * @param {Date} dateTime - A date object + * @returns {string} + */ +export function formatDateTime(dateTime: Date) { + return `${formatDate(dateTime)} ${formatTime(dateTime)}`; +} + +/** + * A standard-format date string or UNIX timestamp + * @typedef {string|number} DateTimeValue + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + */ +/** + * Create a string representation of date/time using internal standard + * @param {DateTimeValue} dateTimeValue - A single value date-time representation + * @returns {string} + */ +export function formatDateTimeFromValue( + dateTimeValue?: string | number | Date +) { + if (!dateTimeValue) { + return ''; + } + const date = new Date(dateTimeValue); + + return formatDateTime(date); +} diff --git a/client/modules/workspace/src/utils/truncateMiddle.ts b/client/modules/workspace/src/utils/truncateMiddle.ts new file mode 100644 index 0000000000..732e34e173 --- /dev/null +++ b/client/modules/workspace/src/utils/truncateMiddle.ts @@ -0,0 +1,29 @@ +/** + * Truncates a string with ellipses in the middle. + * + * @param {string} s - A string param + * @param {number} maxLen - Maximum length of resulting string, including ellipses + * @return {string} Truncated string + * + * @example + * // returns "this...ing" + * truncateMiddle('this is a long string', 10) + */ +export function truncateMiddle(s: string, maxLen: number) { + if (!s) { + return ''; + } + if (maxLen < 5) { + throw new Error( + 'Cannot middle truncate string with a maximum length less than 5.' + ); + } + if (s.length > maxLen) { + // starting index of ellipses + const start = Math.max(1, Math.floor(maxLen / 2) - 1); + // ending index of ellipses + const end = -Math.max(1, Math.ceil(maxLen / 2) - 2); + return `${s.substring(0, start)}...${s.slice(end)}`; + } + return s; +} diff --git a/client/package-lock.json b/client/package-lock.json index 4c178649dd..d3c3fb56c3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,15 +9,22 @@ "version": "0.0.0", "license": "MIT", "dependencies": { + "@hookform/resolvers": "^3.3.4", "@swc/helpers": "~0.5.2", "@tanstack/react-query": "^5.15.0", - "antd": "^5.13.2", + "antd": "^5.16.4", "axios": "^1.6.3", "dayjs": "^1.11.0", + "html-react-parser": "^5.1.10", "react": "18.2.0", "react-dom": "18.2.0", + "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.51.3", + "react-hook-form-antd": "^1.1.0", "react-router-dom": "^6.21.1", - "tslib": "^2.3.0" + "react-use-websocket": "^4.8.1", + "tslib": "^2.3.0", + "zod": "^3.23.4" }, "devDependencies": { "@babel/core": "^7.14.5", @@ -39,6 +46,7 @@ "@types/node": "18.14.2", "@types/react": "18.2.33", "@types/react-dom": "18.2.14", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", "@vitejs/plugin-react": "^4.2.0", @@ -91,9 +99,9 @@ } }, "node_modules/@ant-design/cssinjs": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.18.2.tgz", - "integrity": "sha512-514V9rjLaFYb3v4s55/8bg2E6fb81b99s3crDZf4nSwtiDLLXs8axnIph+q2TVkY2hbJPZOn/cVsVcnLkzFy7w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.20.0.tgz", + "integrity": "sha512-uG3iWzJxgNkADdZmc6W0Ci3iQAUOvLMcM8SnnmWq3r6JeocACft4ChnY/YWvI2Y+rG/68QBla/O+udke1yH3vg==", "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", @@ -109,12 +117,12 @@ } }, "node_modules/@ant-design/icons": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.2.6.tgz", - "integrity": "sha512-4wn0WShF43TrggskBJPRqCD0fcHbzTYjnaoskdiJrVHg86yxoZ8ZUqsXvyn4WUqehRiFKnaclOhqk9w4Ui2KVw==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.6.tgz", + "integrity": "sha512-JeWsgNjvkTTC73YDPgWOgdScRku/iHN9JU0qk39OSEmJSCiRghQMLlxGTCY5ovbRRoXjxHXnUKgQEgBDnQfKmA==", "dependencies": { "@ant-design/colors": "^7.0.0", - "@ant-design/icons-svg": "^4.3.0", + "@ant-design/icons-svg": "^4.4.0", "@babel/runtime": "^7.11.2", "classnames": "^2.2.6", "rc-util": "^5.31.1" @@ -128,14 +136,14 @@ } }, "node_modules/@ant-design/icons-svg": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.3.1.tgz", - "integrity": "sha512-4QBZg8ccyC6LPIRii7A0bZUk3+lEDCLnhB+FVsflGdcWPPmV+j3fire4AwwoqHV/BibgvBmR9ZIo4s867smv+g==" + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" }, "node_modules/@ant-design/react-slick": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.0.2.tgz", - "integrity": "sha512-Wj8onxL/T8KQLFFiCA4t8eIRGpRR+UPgOdac2sYzonv+i0n3kXHmvHLLiOYL655DQx2Umii9Y9nNgL7ssu5haQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", @@ -2172,9 +2180,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2305,9 +2313,9 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.10.tgz", - "integrity": "sha512-Q+mk96KJ+FZ30h9fsJl+67IjNJm3x2eX+GBWGmocAKgzp27cowCOOqSdscX80s0SpdFXZnIv/+1xD1EctFx96Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], @@ -2321,9 +2329,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.10.tgz", - "integrity": "sha512-7W0bK7qfkw1fc2viBfrtAEkDKHatYfHzr/jKAHNr9BvkYDXPcC6bodtm8AyLJNNuqClLNaeTLuwURt4PRT9d7w==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], @@ -2337,9 +2345,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.10.tgz", - "integrity": "sha512-1X4CClKhDgC3by7k8aOWZeBXQX8dHT5QAMCAQDArCLaYfkppoARvh0fit3X2Qs+MXDngKcHv6XXyQCpY0hkK1Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], @@ -2353,9 +2361,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.10.tgz", - "integrity": "sha512-O/nO/g+/7NlitUxETkUv/IvADKuZXyH4BHf/g/7laqKC4i/7whLpB0gvpPc2zpF0q9Q6FXS3TS75QHac9MvVWw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], @@ -2369,9 +2377,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.10.tgz", - "integrity": "sha512-YSRRs2zOpwypck+6GL3wGXx2gNP7DXzetmo5pHXLrY/VIMsS59yKfjPizQ4lLt5vEI80M41gjm2BxrGZ5U+VMA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], @@ -2385,9 +2393,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.10.tgz", - "integrity": "sha512-alfGtT+IEICKtNE54hbvPg13xGBe4GkVxyGWtzr+yHO7HIiRJppPDhOKq3zstTcVf8msXb/t4eavW3jCDpMSmA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], @@ -2401,9 +2409,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.10.tgz", - "integrity": "sha512-dMtk1wc7FSH8CCkE854GyGuNKCewlh+7heYP/sclpOG6Cectzk14qdUIY5CrKDbkA/OczXq9WesqnPl09mj5dg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], @@ -2417,9 +2425,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.10.tgz", - "integrity": "sha512-G5UPPspryHu1T3uX8WiOEUa6q6OlQh6gNl4CO4Iw5PS+Kg5bVggVFehzXBJY6X6RSOMS8iXDv2330VzaObm4Ag==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], @@ -2433,9 +2441,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.10.tgz", - "integrity": "sha512-j6gUW5aAaPgD416Hk9FHxn27On28H4eVI9rJ4az7oCGTFW48+LcgNDBN+9f8rKZz7EEowo889CPKyeaD0iw9Kg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], @@ -2449,9 +2457,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.10.tgz", - "integrity": "sha512-QxaouHWZ+2KWEj7cGJmvTIHVALfhpGxo3WLmlYfJ+dA5fJB6lDEIg+oe/0//FuyVHuS3l79/wyBxbHr0NgtxJQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], @@ -2465,9 +2473,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.10.tgz", - "integrity": "sha512-4ub1YwXxYjj9h1UIZs2hYbnTZBtenPw5NfXCRgEkGb0b6OJ2gpkMvDqRDYIDRjRdWSe/TBiZltm3Y3Q8SN1xNg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], @@ -2481,9 +2489,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.10.tgz", - "integrity": "sha512-lo3I9k+mbEKoxtoIbM0yC/MZ1i2wM0cIeOejlVdZ3D86LAcFXFRdeuZmh91QJvUTW51bOK5W2BznGNIl4+mDaA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], @@ -2497,9 +2505,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.10.tgz", - "integrity": "sha512-J4gH3zhHNbdZN0Bcr1QUGVNkHTdpijgx5VMxeetSk6ntdt+vR1DqGmHxQYHRmNb77tP6GVvD+K0NyO4xjd7y4A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], @@ -2513,9 +2521,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.10.tgz", - "integrity": "sha512-tgT/7u+QhV6ge8wFMzaklOY7KqiyitgT1AUHMApau32ZlvTB/+efeCtMk4eXS+uEymYK249JsoiklZN64xt6oQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], @@ -2529,9 +2537,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.10.tgz", - "integrity": "sha512-0f/spw0PfBMZBNqtKe5FLzBDGo0SKZKvMl5PHYQr3+eiSscfJ96XEknCe+JoOayybWUFQbcJTrk946i3j9uYZA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], @@ -2545,9 +2553,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.10.tgz", - "integrity": "sha512-pZFe0OeskMHzHa9U38g+z8Yx5FNCLFtUnJtQMpwhS+r4S566aK2ci3t4NCP4tjt6d5j5uo4h7tExZMjeKoehAA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], @@ -2561,9 +2569,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.10.tgz", - "integrity": "sha512-SpYNEqg/6pZYoc+1zLCjVOYvxfZVZj6w0KROZ3Fje/QrM3nfvT2llI+wmKSrWuX6wmZeTapbarvuNNK/qepSgA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], @@ -2577,9 +2585,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.10.tgz", - "integrity": "sha512-ACbZ0vXy9zksNArWlk2c38NdKg25+L9pr/mVaj9SUq6lHZu/35nx2xnQVRGLrC1KKQqJKRIB0q8GspiHI3J80Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], @@ -2593,9 +2601,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.10.tgz", - "integrity": "sha512-PxcgvjdSjtgPMiPQrM3pwSaG4kGphP+bLSb+cihuP0LYdZv1epbAIecHVl5sD3npkfYBZ0ZnOjR878I7MdJDFg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], @@ -2609,9 +2617,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.10.tgz", - "integrity": "sha512-ZkIOtrRL8SEJjr+VHjmW0znkPs+oJXhlJbNwfI37rvgeMtk3sxOQevXPXjmAPZPigVTncvFqLMd+uV0IBSEzqA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], @@ -2625,9 +2633,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.10.tgz", - "integrity": "sha512-+Sa4oTDbpBfGpl3Hn3XiUe4f8TU2JF7aX8cOfqFYMMjXp6ma6NJDztl5FDG8Ezx0OjwGikIHw+iA54YLDNNVfw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], @@ -2641,9 +2649,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.10.tgz", - "integrity": "sha512-EOGVLK1oWMBXgfttJdPHDTiivYSjX6jDNaATeNOaCOFEVcfMjtbx7WVQwPSE1eIfCp/CaSF2nSrDtzc4I9f8TQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], @@ -2657,9 +2665,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.10.tgz", - "integrity": "sha512-whqLG6Sc70AbU73fFYvuYzaE4MNMBIlR1Y/IrUeOXFrWHxBEjjbZaQ3IXIQS8wJdAzue2GwYZCjOrgrU1oUHoA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], @@ -2755,6 +2763,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -3485,9 +3501,9 @@ "dev": true }, "node_modules/@rc-component/color-picker": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.1.tgz", - "integrity": "sha512-onyAFhWKXuG4P162xE+7IgaJkPkwM94XlOYnQuu69XdXWMfxpeFi6tpJBsieIMV7EnyLV5J3lDzdLiFeK0iEBA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.3.tgz", + "integrity": "sha512-+tGGH3nLmYXTalVe0L8hSZNs73VTP5ueSHwUlDC77KKRaN7G4DS4wcpG5DTDzdcV/Yas+rzA6UGgIyzd8fS4cw==", "dependencies": { "@babel/runtime": "^7.23.6", "@ctrl/tinycolor": "^3.6.1", @@ -3558,13 +3574,13 @@ } }, "node_modules/@rc-component/tour": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.12.3.tgz", - "integrity": "sha512-U4mf1FiUxGCwrX4ed8op77Y8VKur+8Y/61ylxtqGbcSoh1EBC7bWd/DkLu0ClTUrKZInqEi1FL7YgFtnT90vHA==", + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.14.2.tgz", + "integrity": "sha512-A75DZ8LVvahBIvxooj3Gvf2sxe+CGOkmzPNX7ek0i0AJHyKZ1HXe5ieIGo3m0FMdZfVOlbCJ952Duq8VKAHk6g==", "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", - "@rc-component/trigger": "^1.3.6", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, @@ -3577,9 +3593,9 @@ } }, "node_modules/@rc-component/trigger": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-1.18.2.tgz", - "integrity": "sha512-jRLYgFgjLEPq3MvS87fIhcfuywFSRDaDrYw1FLku7Cm4esszvzTbA0JBsyacAyLrK9rF3TiHFcvoEDMzoD3CTA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.1.1.tgz", + "integrity": "sha512-UjHkedkgtEcgQu87w1VuWug1idoDJV7VUt0swxHXRcmei2uu1AuUzGBPEUlmOmXGJ+YtTgZfVLi7kuAUKoZTMA==", "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", @@ -3605,9 +3621,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.1.tgz", - "integrity": "sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.1.tgz", + "integrity": "sha512-fH8/o8nSUek8ceQnT7K4EQbSiV7jgkHq81m9lWZFIXjJ7lJzpWXbQFpT/Zh6OZYnpFykvzC3fbEvEAFZu03dPA==", "cpu": [ "arm" ], @@ -3618,9 +3634,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.1.tgz", - "integrity": "sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.1.tgz", + "integrity": "sha512-Y/9OHLjzkunF+KGEoJr3heiD5X9OLa8sbT1lm0NYeKyaM3oMhhQFvPB0bNZYJwlq93j8Z6wSxh9+cyKQaxS7PQ==", "cpu": [ "arm64" ], @@ -3631,9 +3647,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz", - "integrity": "sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.1.tgz", + "integrity": "sha512-+kecg3FY84WadgcuSVm6llrABOdQAEbNdnpi5X3UwWiFVhZIZvKgGrF7kmLguvxHNQy+UuRV66cLVl3S+Rkt+Q==", "cpu": [ "arm64" ], @@ -3644,9 +3660,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.1.tgz", - "integrity": "sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.1.tgz", + "integrity": "sha512-2pYRzEjVqq2TB/UNv47BV/8vQiXkFGVmPFwJb+1E0IFFZbIX8/jo1olxqqMbo6xCXf8kabANhp5bzCij2tFLUA==", "cpu": [ "x64" ], @@ -3657,9 +3673,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.1.tgz", - "integrity": "sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.1.tgz", + "integrity": "sha512-mS6wQ6Do6/wmrF9aTFVpIJ3/IDXhg1EZcQFYHZLHqw6AzMBjTHWnCG35HxSqUNphh0EHqSM6wRTT8HsL1C0x5g==", "cpu": [ "arm" ], @@ -3670,9 +3686,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.1.tgz", - "integrity": "sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.1.tgz", + "integrity": "sha512-p9rGKYkHdFMzhckOTFubfxgyIO1vw//7IIjBBRVzyZebWlzRLeNhqxuSaZ7kCEKVkm/kuC9fVRW9HkC/zNRG2w==", "cpu": [ "arm64" ], @@ -3683,9 +3699,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.1.tgz", - "integrity": "sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.1.tgz", + "integrity": "sha512-nDY6Yz5xS/Y4M2i9JLQd3Rofh5OR8Bn8qe3Mv/qCVpHFlwtZSBYSPaU4mrGazWkXrdQ98GB//H0BirGR/SKFSw==", "cpu": [ "arm64" ], @@ -3695,10 +3711,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.1.tgz", + "integrity": "sha512-im7HE4VBL+aDswvcmfx88Mp1soqL9OBsdDBU8NqDEYtkri0qV0THhQsvZtZeNNlLeCUQ16PZyv7cqutjDF35qw==", + "cpu": [ + "ppc64le" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.1.tgz", - "integrity": "sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.1.tgz", + "integrity": "sha512-RWdiHuAxWmzPJgaHJdpvUUlDz8sdQz4P2uv367T2JocdDa98iRw2UjIJ4QxSyt077mXZT2X6pKfT2iYtVEvOFw==", "cpu": [ "riscv64" ], @@ -3708,10 +3737,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.1.tgz", + "integrity": "sha512-VMgaGQ5zRX6ZqV/fas65/sUGc9cPmsntq2FiGmayW9KMNfWVG/j0BAqImvU4KTeOOgYSf1F+k6at1UfNONuNjA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz", - "integrity": "sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.1.tgz", + "integrity": "sha512-9Q7DGjZN+hTdJomaQ3Iub4m6VPu1r94bmK2z3UeWP3dGUecRC54tmVu9vKHTm1bOt3ASoYtEz6JSRLFzrysKlA==", "cpu": [ "x64" ], @@ -3722,9 +3764,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.1.tgz", - "integrity": "sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.1.tgz", + "integrity": "sha512-JNEG/Ti55413SsreTguSx0LOVKX902OfXIKVg+TCXO6Gjans/k9O6ww9q3oLGjNDaTLxM+IHFMeXy/0RXL5R/g==", "cpu": [ "x64" ], @@ -3735,9 +3777,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.1.tgz", - "integrity": "sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.1.tgz", + "integrity": "sha512-ryS22I9y0mumlLNwDFYZRDFLwWh3aKaC72CWjFcFvxK0U6v/mOkM5Up1bTbCRAhv3kEIwW2ajROegCIQViUCeA==", "cpu": [ "arm64" ], @@ -3748,9 +3790,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.1.tgz", - "integrity": "sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.1.tgz", + "integrity": "sha512-TdloItiGk+T0mTxKx7Hp279xy30LspMso+GzQvV2maYePMAWdmrzqSNZhUpPj3CGw12aGj57I026PgLCTu8CGg==", "cpu": [ "ia32" ], @@ -3761,9 +3803,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz", - "integrity": "sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.1.tgz", + "integrity": "sha512-wQGI+LY/Py20zdUPq+XCem7JcPOyzIJBm3dli+56DJsQOHbnXZFEwgmnC6el1TPAfC8lBT3m+z69RmLykNUbew==", "cpu": [ "x64" ], @@ -4623,8 +4665,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", @@ -4681,13 +4722,13 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.33", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4716,7 +4757,7 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.5.6", @@ -4730,6 +4771,12 @@ "integrity": "sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==", "dev": true }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.16.0.tgz", @@ -5629,55 +5676,56 @@ } }, "node_modules/antd": { - "version": "5.13.2", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.13.2.tgz", - "integrity": "sha512-P+N8gc0NOPy2WqJj/57Ey3dZUmb7nEUwAM+CIJaR5SOEjZnhEtMGRJSt+3lnhJ3MNRR39aR6NYkRVp2mYfphiA==", + "version": "5.16.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.16.4.tgz", + "integrity": "sha512-H3LtVz5hiNgs0lL8U6pzi11rluR6RDRw1cm2pWX6CsvgZmybWsaTBV2h+d+zmgFfuch53TWs5uztLdAldIoYYw==", "dependencies": { "@ant-design/colors": "^7.0.2", - "@ant-design/cssinjs": "^1.18.2", - "@ant-design/icons": "^5.2.6", - "@ant-design/react-slick": "~1.0.2", + "@ant-design/cssinjs": "^1.18.5", + "@ant-design/icons": "^5.3.6", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.24.4", "@ctrl/tinycolor": "^3.6.1", - "@rc-component/color-picker": "~1.5.1", + "@rc-component/color-picker": "~1.5.3", "@rc-component/mutate-observer": "^1.1.0", - "@rc-component/tour": "~1.12.2", - "@rc-component/trigger": "^1.18.2", + "@rc-component/tour": "~1.14.2", + "@rc-component/trigger": "^2.1.1", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.10", "qrcode.react": "^3.1.0", - "rc-cascader": "~3.21.0", - "rc-checkbox": "~3.1.0", - "rc-collapse": "~3.7.2", - "rc-dialog": "~9.3.4", - "rc-drawer": "~7.0.0", - "rc-dropdown": "~4.1.0", - "rc-field-form": "~1.41.0", - "rc-image": "~7.5.1", - "rc-input": "~1.4.3", - "rc-input-number": "~8.6.1", - "rc-mentions": "~2.10.1", - "rc-menu": "~9.12.4", + "rc-cascader": "~3.24.1", + "rc-checkbox": "~3.2.0", + "rc-collapse": "~3.7.3", + "rc-dialog": "~9.4.0", + "rc-drawer": "~7.1.0", + "rc-dropdown": "~4.2.0", + "rc-field-form": "~1.44.0", + "rc-image": "~7.6.0", + "rc-input": "~1.4.5", + "rc-input-number": "~9.0.0", + "rc-mentions": "~2.11.1", + "rc-menu": "~9.13.0", "rc-motion": "^2.9.0", - "rc-notification": "~5.3.0", + "rc-notification": "~5.4.0", "rc-pagination": "~4.0.4", - "rc-picker": "~3.14.6", - "rc-progress": "~3.5.1", + "rc-picker": "~4.4.1", + "rc-progress": "~4.0.0", "rc-rate": "~2.12.0", "rc-resize-observer": "^1.4.0", - "rc-segmented": "~2.2.2", - "rc-select": "~14.11.0", - "rc-slider": "~10.5.0", + "rc-segmented": "~2.3.0", + "rc-select": "~14.13.1", + "rc-slider": "~10.6.1", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.37.0", - "rc-tabs": "~14.0.0", + "rc-table": "~7.45.4", + "rc-tabs": "~14.1.1", "rc-textarea": "~1.6.3", - "rc-tooltip": "~6.1.3", - "rc-tree": "~5.8.2", - "rc-tree-select": "~5.17.0", + "rc-tooltip": "~6.2.0", + "rc-tree": "~5.8.5", + "rc-tree-select": "~5.19.0", "rc-upload": "~4.5.2", - "rc-util": "^5.38.1", + "rc-util": "^5.39.1", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.0" }, @@ -7122,7 +7170,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -7136,7 +7183,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, "funding": [ { "type": "github", @@ -7161,7 +7207,6 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -7176,7 +7221,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -7224,9 +7268,9 @@ "dev": true }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "dependencies": { "jake": "^10.8.5" @@ -7298,7 +7342,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -7436,9 +7479,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz", - "integrity": "sha512-S1Y27QGt/snkNYrRcswgRFqZjaTG5a5xM3EQo97uNBnH505pdzSNe/HLBq1v0RO7iK/ngdbhJB6mDAp0OK+iUA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, "bin": { @@ -7448,29 +7491,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.10", - "@esbuild/android-arm": "0.19.10", - "@esbuild/android-arm64": "0.19.10", - "@esbuild/android-x64": "0.19.10", - "@esbuild/darwin-arm64": "0.19.10", - "@esbuild/darwin-x64": "0.19.10", - "@esbuild/freebsd-arm64": "0.19.10", - "@esbuild/freebsd-x64": "0.19.10", - "@esbuild/linux-arm": "0.19.10", - "@esbuild/linux-arm64": "0.19.10", - "@esbuild/linux-ia32": "0.19.10", - "@esbuild/linux-loong64": "0.19.10", - "@esbuild/linux-mips64el": "0.19.10", - "@esbuild/linux-ppc64": "0.19.10", - "@esbuild/linux-riscv64": "0.19.10", - "@esbuild/linux-s390x": "0.19.10", - "@esbuild/linux-x64": "0.19.10", - "@esbuild/netbsd-x64": "0.19.10", - "@esbuild/openbsd-x64": "0.19.10", - "@esbuild/sunos-x64": "0.19.10", - "@esbuild/win32-arm64": "0.19.10", - "@esbuild/win32-ia32": "0.19.10", - "@esbuild/win32-x64": "0.19.10" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/escalade": { @@ -8447,9 +8490,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -8910,6 +8953,15 @@ "node": "14 || >=16.14" } }, + "node_modules/html-dom-parser": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.8.tgz", + "integrity": "sha512-vuWiX9EXgu8CJ5m9EP5c7bvBmNSuQVnrY8tl0z0ZX96Uth1IPlYH/8W8VZ/hBajFf18EN+j2pukbCNd01HEd1w==", + "dependencies": { + "domhandler": "5.0.3", + "htmlparser2": "9.1.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -8928,6 +8980,44 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-react-parser": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.1.10.tgz", + "integrity": "sha512-gV22PvLij4wdEdtrZbGVC7Zy2OVWnQ0bYhX63S196ZRSx4+K0TuutCreHSXr+saUia8KeKB+2TYziVfijpH4Tw==", + "dependencies": { + "domhandler": "5.0.3", + "html-dom-parser": "5.0.8", + "react-property": "2.0.2", + "style-to-js": "1.1.12" + }, + "peerDependencies": { + "@types/react": "17 || 18", + "react": "0.14 || 15 || 16 || 17 || 18" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -9106,6 +9196,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/inline-style-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" + }, "node_modules/inquirer": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", @@ -11037,9 +11132,9 @@ } }, "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -11058,7 +11153,7 @@ "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -11248,14 +11343,14 @@ } }, "node_modules/rc-cascader": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.21.0.tgz", - "integrity": "sha512-7aADjbfqiR4HrTHG9S019p2jeKM/AxISPA5+sBJR7Mlhm/i+lR7VjBju3KQulJNJLKNEnQYg4TFhcPf2SLua9g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.24.1.tgz", + "integrity": "sha512-RgKuYgEGPx+6wCgguYFHjMsDZdCyydZd58YJRCfYQ8FObqLnZW0x/vUcEyPjhWIj1EhjV958IcR+NFPDbbj9kg==", "dependencies": { "@babel/runtime": "^7.12.5", "array-tree-filter": "^2.1.0", "classnames": "^2.3.1", - "rc-select": "~14.11.0-0", + "rc-select": "~14.13.0", "rc-tree": "~5.8.1", "rc-util": "^5.37.0" }, @@ -11265,9 +11360,9 @@ } }, "node_modules/rc-checkbox": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.1.0.tgz", - "integrity": "sha512-PAwpJFnBa3Ei+5pyqMMXdcKYKNBMS+TvSDiLdDnARnMJHC8ESxwPfm4Ao1gJiKtWLdmGfigascnCpwrHFgoOBQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.2.0.tgz", + "integrity": "sha512-8inzw4y9dAhZmv/Ydl59Qdy5tdp9CKg4oPVcRigi+ga/yKPZS5m5SyyQPtYSgbcqHRYOdUhiPSeKfktc76du1A==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -11279,9 +11374,9 @@ } }, "node_modules/rc-collapse": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.2.tgz", - "integrity": "sha512-ZRw6ipDyOnfLFySxAiCMdbHtb5ePAsB9mT17PA6y1mRD/W6KHRaZeb5qK/X9xDV1CqgyxMpzw0VdS74PCcUk4A==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.3.tgz", + "integrity": "sha512-60FJcdTRn0X5sELF18TANwtVi7FtModq649H11mYF1jh83DniMoM4MqY627sEKRCTm4+WXfGDcB7hY5oW6xhyw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -11294,9 +11389,9 @@ } }, "node_modules/rc-dialog": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.3.4.tgz", - "integrity": "sha512-975X3018GhR+EjZFbxA2Z57SX5rnu0G0/OxFgMMvZK4/hQWEm3MHaNvP4wXpxYDoJsp+xUvVW+GB9CMMCm81jA==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.4.0.tgz", + "integrity": "sha512-AScCexaLACvf8KZRqCPz12BJ8olszXOS4lKlkMyzDQHS1m0zj1KZMYgmMCh39ee0Dcv8kyrj8mTqxuLyhH+QuQ==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", @@ -11310,15 +11405,15 @@ } }, "node_modules/rc-drawer": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.0.0.tgz", - "integrity": "sha512-ePcS4KtQnn57bCbVXazHN2iC8nTPCXlWEIA/Pft87Pd9U7ZeDkdRzG47jWG2/TAFXFlFltRAMcslqmUM8NPCGA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.1.0.tgz", + "integrity": "sha512-nBE1rF5iZvpavoyqhSSz2mk/yANltA7g3aF0U45xkx381n3we/RKs9cJfNKp9mSWCedOKWt9FLEwZDaAaOGn2w==", "dependencies": { - "@babel/runtime": "^7.10.1", + "@babel/runtime": "^7.23.9", "@rc-component/portal": "^1.1.1", "classnames": "^2.2.6", "rc-motion": "^2.6.1", - "rc-util": "^5.36.0" + "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", @@ -11326,12 +11421,12 @@ } }, "node_modules/rc-dropdown": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.1.0.tgz", - "integrity": "sha512-VZjMunpBdlVzYpEdJSaV7WM7O0jf8uyDjirxXLZRNZ+tAC+NzD3PXPEtliFwGzVwBBdCmGuSqiS9DWcOLxQ9tw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.0.tgz", + "integrity": "sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==", "dependencies": { "@babel/runtime": "^7.18.3", - "@rc-component/trigger": "^1.7.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-util": "^5.17.0" }, @@ -11341,9 +11436,9 @@ } }, "node_modules/rc-field-form": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.41.0.tgz", - "integrity": "sha512-k9AS0wmxfJfusWDP/YXWTpteDNaQ4isJx9UKxx4/e8Dub4spFeZ54/EuN2sYrMRID/+hUznPgVZeg+Gf7XSYCw==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.44.0.tgz", + "integrity": "sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==", "dependencies": { "@babel/runtime": "^7.18.0", "async-validator": "^4.1.0", @@ -11358,14 +11453,14 @@ } }, "node_modules/rc-image": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.5.1.tgz", - "integrity": "sha512-Z9loECh92SQp0nSipc0MBuf5+yVC05H/pzC+Nf8xw1BKDFUJzUeehYBjaWlxly8VGBZJcTHYri61Fz9ng1G3Ag==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.6.0.tgz", + "integrity": "sha512-tL3Rvd1sS+frZQ01i+tkeUPaOeFz2iG9/scAt/Cfs0hyCRVA/w0Pu1J/JxIX8blalvmHE0bZQRYdOmRAzWu4Hg==", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", - "rc-dialog": "~9.3.4", + "rc-dialog": "~9.4.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, @@ -11375,9 +11470,9 @@ } }, "node_modules/rc-input": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.3.tgz", - "integrity": "sha512-aHyQUAIRmTlOnvk5EcNqEpJ+XMtfMpYRAJayIlJfsvvH9cAKUWboh4egm23vgMA7E+c/qm4BZcnrDcA960GC1w==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.4.5.tgz", + "integrity": "sha512-AjzykhwnwYTRSwwgCu70CGKBIAv6bP2nqnFptnNTprph/TF1BAs0Qxl91mie/BR6n827WIJB6ZjaRf9iiMwAfw==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -11389,9 +11484,9 @@ } }, "node_modules/rc-input-number": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-8.6.1.tgz", - "integrity": "sha512-gaAMUKtUKLktJ3Yx93tjgYY1M0HunnoqzPEqkb9//Ydup4DcG0TFL9yHBA3pgVdNIt5f0UWyHCgFBj//JxeD6A==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.0.0.tgz", + "integrity": "sha512-RfcDBDdWFFetouWFXBA+WPEC8LzBXyngr9b+yTLVIygfFu7HiLRGn/s/v9wwno94X7KFvnb28FNynMGj9XJlDQ==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", @@ -11405,15 +11500,15 @@ } }, "node_modules/rc-mentions": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.10.1.tgz", - "integrity": "sha512-72qsEcr/7su+a07ndJ1j8rI9n0Ka/ngWOLYnWMMv0p2mi/5zPwPrEDTt6Uqpe8FWjWhueDJx/vzunL6IdKDYMg==", + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.11.1.tgz", + "integrity": "sha512-upb4AK1SRFql7qGnbLEvJqLMugVVIyjmwBJW9L0eLoN9po4JmJZaBzmKA4089fNtsU8k6l/tdZiVafyooeKnLw==", "dependencies": { "@babel/runtime": "^7.22.5", - "@rc-component/trigger": "^1.5.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-input": "~1.4.0", - "rc-menu": "~9.12.0", + "rc-menu": "~9.13.0", "rc-textarea": "~1.6.1", "rc-util": "^5.34.1" }, @@ -11423,12 +11518,12 @@ } }, "node_modules/rc-menu": { - "version": "9.12.4", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.12.4.tgz", - "integrity": "sha512-t2NcvPLV1mFJzw4F21ojOoRVofK2rWhpKPx69q2raUsiHPDP6DDevsBILEYdsIegqBeSXoWs2bf6CueBKg3BFg==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.13.0.tgz", + "integrity": "sha512-1l8ooCB3HcYJKCltC/s7OxRKRjgymdl9htrCeGZcXNaMct0RxZRK6OPV3lPhVksIvAGMgzPd54ClpZ5J4b8cZA==", "dependencies": { "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^1.17.0", + "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", @@ -11454,9 +11549,9 @@ } }, "node_modules/rc-notification": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.3.0.tgz", - "integrity": "sha512-WCf0uCOkZ3HGfF0p1H4Sgt7aWfipxORWTPp7o6prA3vxwtWhtug3GfpYls1pnBp4WA+j8vGIi5c2/hQRpGzPcQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.4.0.tgz", + "integrity": "sha512-li19y9RoYJciF3WRFvD+DvWS70jdL8Fr+Gfb/OshK+iY6iTkwzoigmSIp76/kWh5tF5i/i9im12X3nsF85GYdA==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -11501,14 +11596,16 @@ } }, "node_modules/rc-picker": { - "version": "3.14.6", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-3.14.6.tgz", - "integrity": "sha512-AdKKW0AqMwZsKvIpwUWDUnpuGKZVrbxVTZTNjcO+pViGkjC1EBcjMgxVe8tomOEaIHJL5Gd13vS8Rr3zzxWmag==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.4.2.tgz", + "integrity": "sha512-MdbAXvwiGyhb+bHe66qPps8xPQivzEgcyCp3/MPK4T+oER0gOmVRCEDxaD4FhYG/7GLH3rDrHpu79BvEn2JFTQ==", "dependencies": { "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^1.5.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", - "rc-util": "^5.30.0" + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.38.1" }, "engines": { "node": ">=8.x" @@ -11537,9 +11634,9 @@ } }, "node_modules/rc-progress": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-3.5.1.tgz", - "integrity": "sha512-V6Amx6SbLRwPin/oD+k1vbPrO8+9Qf8zW1T8A7o83HdNafEVvAxPV5YsgtKFP+Ud5HghLj33zKOcEHrcrUGkfw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", @@ -11583,9 +11680,9 @@ } }, "node_modules/rc-segmented": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.2.2.tgz", - "integrity": "sha512-Mq52M96QdHMsNdE/042ibT5vkcGcD5jxKp7HgPC2SRofpia99P5fkfHy1pEaajLMF/kj0+2Lkq1UZRvqzo9mSA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.3.0.tgz", + "integrity": "sha512-I3FtM5Smua/ESXutFfb8gJ8ZPcvFR+qUgeeGFQHBOvRiRKyAk4aBE5nfqrxXx+h8/vn60DQjOt6i4RNtrbOobg==", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -11598,12 +11695,12 @@ } }, "node_modules/rc-select": { - "version": "14.11.0", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.11.0.tgz", - "integrity": "sha512-8J8G/7duaGjFiTXCBLWfh5P+KDWyA3KTlZDfV3xj/asMPqB2cmxfM+lH50wRiPIRsCQ6EbkCFBccPuaje3DHIg==", + "version": "14.13.1", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.13.1.tgz", + "integrity": "sha512-A1VHqjIOemxLnUGRxLGVqXBs8jGcJemI5NXxOJwU5PQc1wigAu1T4PRLgMkTPDOz8gPhlY9dwsPzMgakMc2QjQ==", "dependencies": { "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^1.5.0", + "@rc-component/trigger": "^2.1.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-overflow": "^1.3.1", @@ -11619,13 +11716,13 @@ } }, "node_modules/rc-slider": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.5.0.tgz", - "integrity": "sha512-xiYght50cvoODZYI43v3Ylsqiw14+D7ELsgzR40boDZaya1HFa1Etnv9MDkQE8X/UrXAffwv2AcNAhslgYuDTw==", + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", + "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", - "rc-util": "^5.27.0" + "rc-util": "^5.36.0" }, "engines": { "node": ">=8.x" @@ -11667,9 +11764,9 @@ } }, "node_modules/rc-table": { - "version": "7.37.0", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.37.0.tgz", - "integrity": "sha512-hEB17ktLRVfVmdo+U8MjGr+PuIgdQ8Cxj/N5lwMvP/Az7TOrQxwTMLVEDoj207tyPYLTWifHIF9EJREWwyk67g==", + "version": "7.45.4", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.45.4.tgz", + "integrity": "sha512-6aSbGrnkN2GLSt3s1x+wa4f3j/VEgg1uKPpaLY5qHH1/nFyreS2V7DFJ0TfUb18allf2FQl7oVYEjTixlBXEyQ==", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", @@ -11687,14 +11784,14 @@ } }, "node_modules/rc-tabs": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.0.0.tgz", - "integrity": "sha512-lp1YWkaPnjlyhOZCPrAWxK6/P6nMGX/BAZcAC3nuVwKz0Byfp+vNnQKK8BRCP2g/fzu+SeB5dm9aUigRu3tRkQ==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-14.1.1.tgz", + "integrity": "sha512-5nOr9PVpJy2SWHTLgv1+kESDOb0tFzl0cYU9r9d8LfL0Wg9i/n1B558rmkxdQHgBwMqxmwoyPSAbQROxMQe8nw==", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", - "rc-dropdown": "~4.1.0", - "rc-menu": "~9.12.0", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.13.0", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.34.1" @@ -11724,12 +11821,12 @@ } }, "node_modules/rc-tooltip": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.1.3.tgz", - "integrity": "sha512-HMSbSs5oieZ7XddtINUddBLSVgsnlaSb3bZrzzGWjXa7/B7nNedmsuz72s7EWFEro9mNa7RyF3gOXKYqvJiTcQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.0.tgz", + "integrity": "sha512-iS/3iOAvtDh9GIx1ulY7EFUXUtktFccNLsARo3NPgLf0QW9oT0w3dA9cYWlhqAKmD+uriEwdWz1kH0Qs4zk2Aw==", "dependencies": { "@babel/runtime": "^7.11.2", - "@rc-component/trigger": "^1.18.0", + "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.1" }, "peerDependencies": { @@ -11738,9 +11835,9 @@ } }, "node_modules/rc-tree": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.2.tgz", - "integrity": "sha512-xH/fcgLHWTLmrSuNphU8XAqV7CdaOQgm4KywlLGNoTMhDAcNR3GVNP6cZzb0GrKmIZ9yae+QLot/cAgUdPRMzg==", + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.5.tgz", + "integrity": "sha512-PRfcZtVDNkR7oh26RuNe1hpw11c1wfgzwmPFL0lnxGnYefe9lDAO6cg5wJKIAwyXFVt5zHgpjYmaz0CPy1ZtKg==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -11757,13 +11854,13 @@ } }, "node_modules/rc-tree-select": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.17.0.tgz", - "integrity": "sha512-7sRGafswBhf7n6IuHyCEFCildwQIgyKiV8zfYyUoWfZEFdhuk7lCH+DN0aHt+oJrdiY9+6Io/LDXloGe01O8XQ==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.19.0.tgz", + "integrity": "sha512-f4l5EsmSGF3ggj76YTzKNPY9SnXfFaer7ZccTSGb3urUf54L+cCqyT+UsPr+S5TAr8mZSxJ7g3CgkCe+cVQ6sw==", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~14.11.0-0", + "rc-select": "~14.13.0", "rc-tree": "~5.8.1", "rc-util": "^5.16.1" }, @@ -11787,9 +11884,9 @@ } }, "node_modules/rc-util": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.38.1.tgz", - "integrity": "sha512-e4ZMs7q9XqwTuhIK7zBIVFltUtMSjphuPPQXHoHlzRzNdOwUxDejo0Zls5HYaJfRKNURcsS/ceKVULlhjBrxng==", + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.39.1.tgz", + "integrity": "sha512-OW/ERynNDgNr4y0oiFmtes3rbEamXw7GHGbkbNd9iRr7kgT03T6fT0b9WpJ3mbxKhyOcAHnGcIoh5u/cjrC2OQ==", "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -11800,9 +11897,9 @@ } }, "node_modules/rc-virtual-list": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.3.tgz", - "integrity": "sha512-tu5UtrMk/AXonHwHxUogdXAWynaXsrx1i6dsgg+lOo/KJSF8oBAcprh1z5J3xgnPJD5hXxTL58F8s8onokdt0Q==", + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.11.5.tgz", + "integrity": "sha512-iZRW99m5jAxtwKNPLwUrPryurcnKpXBdTyhuBp6ythf7kg/otKO5cCiIvL55GQwU0QGSlouQS0tnkciRMJUwRQ==", "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", @@ -11813,8 +11910,8 @@ "node": ">=8.x" }, "peerDependencies": { - "react": "*", - "react-dom": "*" + "react": ">=16.9.0", + "react-dom": ">=16.9.0" } }, "node_modules/react": { @@ -11840,11 +11937,53 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.51.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz", + "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-hook-form-antd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/react-hook-form-antd/-/react-hook-form-antd-1.1.0.tgz", + "integrity": "sha512-Hk/7rFlYxm0g7qpRNOyKtierDBhgWVWP9sfEnR/X/BJmpMmOGzccxe56SzNBlsFUzcYWXFa5dl0RtzvW2+P6dA==", + "peerDependencies": { + "antd": "^5", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18", + "react-hook-form": "^7" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/react-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==" + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -11884,6 +12023,15 @@ "react-dom": ">=16.8" } }, + "node_modules/react-use-websocket": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.8.1.tgz", + "integrity": "sha512-FTXuG5O+LFozmu1BRfrzl7UIQngECvGJmL7BHsK4TYXuVt+mCizVA8lT0hGSIF0Z0TedF7bOo1nRzOUdginhDw==", + "peerDependencies": { + "react": ">= 18.0.0", + "react-dom": ">= 18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12116,10 +12264,13 @@ } }, "node_modules/rollup": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.1.tgz", - "integrity": "sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.1.tgz", + "integrity": "sha512-4LnHSdd3QK2pa1J6dFbfm1HN0D7vSK/ZuZTsdyUAlA6Rr1yTouUTL13HaDOGJVgby461AhrNGBS7sCGXXtT+SA==", "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, @@ -12128,19 +12279,21 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.1", - "@rollup/rollup-android-arm64": "4.9.1", - "@rollup/rollup-darwin-arm64": "4.9.1", - "@rollup/rollup-darwin-x64": "4.9.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.1", - "@rollup/rollup-linux-arm64-gnu": "4.9.1", - "@rollup/rollup-linux-arm64-musl": "4.9.1", - "@rollup/rollup-linux-riscv64-gnu": "4.9.1", - "@rollup/rollup-linux-x64-gnu": "4.9.1", - "@rollup/rollup-linux-x64-musl": "4.9.1", - "@rollup/rollup-win32-arm64-msvc": "4.9.1", - "@rollup/rollup-win32-ia32-msvc": "4.9.1", - "@rollup/rollup-win32-x64-msvc": "4.9.1", + "@rollup/rollup-android-arm-eabi": "4.14.1", + "@rollup/rollup-android-arm64": "4.14.1", + "@rollup/rollup-darwin-arm64": "4.14.1", + "@rollup/rollup-darwin-x64": "4.14.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.1", + "@rollup/rollup-linux-arm64-gnu": "4.14.1", + "@rollup/rollup-linux-arm64-musl": "4.14.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.1", + "@rollup/rollup-linux-riscv64-gnu": "4.14.1", + "@rollup/rollup-linux-s390x-gnu": "4.14.1", + "@rollup/rollup-linux-x64-gnu": "4.14.1", + "@rollup/rollup-linux-x64-musl": "4.14.1", + "@rollup/rollup-win32-arm64-msvc": "4.14.1", + "@rollup/rollup-win32-ia32-msvc": "4.14.1", + "@rollup/rollup-win32-x64-msvc": "4.14.1", "fsevents": "~2.3.2" } }, @@ -12500,9 +12653,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -12785,10 +12938,26 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/style-to-js": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.12.tgz", + "integrity": "sha512-tv+/FkgNYHI2fvCoBMsqPHh5xovwiw+C3X0Gfnss/Syau0Nr3IqGOJ9XiOYXoPnToHVbllKFf5qCNFJGwFg5mg==", + "dependencies": { + "style-to-object": "1.0.6" + } + }, + "node_modules/style-to-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "dependencies": { + "inline-style-parser": "0.2.3" + } + }, "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" }, "node_modules/supports-color": { "version": "7.2.0", @@ -13492,14 +13661,14 @@ } }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", + "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" @@ -14045,6 +14214,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.4.tgz", + "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/client/package.json b/client/package.json index 7cac6ee3a4..6597c37856 100644 --- a/client/package.json +++ b/client/package.json @@ -9,15 +9,22 @@ }, "private": true, "dependencies": { + "@hookform/resolvers": "^3.3.4", "@swc/helpers": "~0.5.2", "@tanstack/react-query": "^5.15.0", - "antd": "^5.13.2", + "antd": "^5.16.4", "axios": "^1.6.3", "dayjs": "^1.11.0", + "html-react-parser": "^5.1.10", "react": "18.2.0", "react-dom": "18.2.0", + "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.51.3", + "react-hook-form-antd": "^1.1.0", "react-router-dom": "^6.21.1", - "tslib": "^2.3.0" + "react-use-websocket": "^4.8.1", + "tslib": "^2.3.0", + "zod": "^3.23.4" }, "devDependencies": { "@babel/core": "^7.14.5", @@ -39,6 +46,7 @@ "@types/node": "18.14.2", "@types/react": "18.2.33", "@types/react-dom": "18.2.14", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", "@vitejs/plugin-react": "^4.2.0", diff --git a/client/project.json b/client/project.json index ab8bb5f36b..a14436f35c 100644 --- a/client/project.json +++ b/client/project.json @@ -10,7 +10,7 @@ "defaultConfiguration": "production", "options": { "outputPath": "dist/client", - "base": "/static" + "base": "/static/react-assets" }, "configurations": { "development": { diff --git a/client/index.html b/client/react-assets.html similarity index 100% rename from client/index.html rename to client/react-assets.html diff --git a/client/src/datafiles/datafilesRouter.tsx b/client/src/datafiles/datafilesRouter.tsx index ec036dce18..694dc6e36e 100644 --- a/client/src/datafiles/datafilesRouter.tsx +++ b/client/src/datafiles/datafilesRouter.tsx @@ -73,6 +73,7 @@ const datafilesRouter = createBrowserRouter( }, { path: 'public/nees.public', + id: 'nees', children: [ { path: '', @@ -81,7 +82,7 @@ const datafilesRouter = createBrowserRouter( { path: ':neesid', element: , - children: [{ path: ':path', element: }], + children: [{ path: ':path', element: }], }, ], }, @@ -96,6 +97,7 @@ const datafilesRouter = createBrowserRouter( }, { path: 'public', + id: 'published', children: [ { path: '', @@ -111,6 +113,7 @@ const datafilesRouter = createBrowserRouter( children: [ { path: '', + id: 'entity-listing', element: , }, { @@ -128,6 +131,7 @@ const datafilesRouter = createBrowserRouter( { path: 'projects', + id: 'project', children: [ { path: ':projectId/prepare-to-publish/start', @@ -139,6 +143,7 @@ const datafilesRouter = createBrowserRouter( }, { path: '', + id: 'project-listing', element: , }, { diff --git a/client/src/datafiles/layouts/DataFilesBaseLayout.tsx b/client/src/datafiles/layouts/DataFilesBaseLayout.tsx index 1e62be838d..fed94c60c0 100644 --- a/client/src/datafiles/layouts/DataFilesBaseLayout.tsx +++ b/client/src/datafiles/layouts/DataFilesBaseLayout.tsx @@ -1,8 +1,12 @@ import React from 'react'; import { Navigate, Outlet, useLocation } from 'react-router-dom'; -import { Layout } from 'antd'; -import { AddFileFolder, DatafilesSideNav } from '@client/datafiles'; -import { useAuthenticatedUser } from '@client/hooks'; +import { Layout, notification } from 'antd'; +import { + AddFileFolder, + DatafilesHelpDropdown, + DatafilesSideNav, +} from '@client/datafiles'; +import { useAuthenticatedUser, notifyContext } from '@client/hooks'; const { Sider } = Layout; @@ -13,28 +17,34 @@ const DataFilesRoot: React.FC = () => { : '/tapis/designsafe.storage.community'; const { pathname } = useLocation(); + const [notifyApi, contextHolder] = notification.useNotification(); + return ( - - -

    - Data Depot -

    - - -
    - {pathname === '/' && } - -
    + + {contextHolder} + + +

    + Data Depot +

    + + + +
    + {pathname === '/' && } + +
    +
    ); }; diff --git a/client/src/datafiles/layouts/FileListingLayout.tsx b/client/src/datafiles/layouts/FileListingLayout.tsx index ec873cce61..599c9f2e1c 100644 --- a/client/src/datafiles/layouts/FileListingLayout.tsx +++ b/client/src/datafiles/layouts/FileListingLayout.tsx @@ -1,8 +1,5 @@ -import { - BaseFileListingBreadcrumb, - DatafilesToolbar, - FileListing, -} from '@client/datafiles'; +import { DatafilesToolbar, FileListing } from '@client/datafiles'; +import { BaseFileListingBreadcrumb } from '@client/common-components'; import { useAuthenticatedUser, useFileListingRouteParams } from '@client/hooks'; import { Button, Form, Input, Layout } from 'antd'; import React from 'react'; diff --git a/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx b/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx index 269286e60f..9c71148ca0 100644 --- a/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx +++ b/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx @@ -1,10 +1,48 @@ import React from 'react'; -import { Outlet } from 'react-router-dom'; +import { Button, Form, Input, Layout } from 'antd'; +import { useParams } from 'react-router-dom'; +import { DatafilesToolbar, NeesDetails } from '@client/datafiles'; + +import { useSearchParams } from 'react-router-dom'; + +const NeesFileSearchbar = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const onSubmit = (queryString: string) => { + const newSearchParams = searchParams; + if (queryString) { + newSearchParams.set('q', queryString); + } else { + newSearchParams.delete('q'); + } + + setSearchParams(newSearchParams); + }; + return ( +
    onSubmit(data.query)} + style={{ display: 'inline-flex' }} + > + + + + +
    + ); +}; export const NeesDetailLayout: React.FC = () => { + const { neesid } = useParams(); + if (!neesid) return null; + const nees = neesid?.split('.')[0]; + return ( -
    - Placeholder for the NEES detail view. -
    + +
    + } /> + +
    +
    ); }; diff --git a/client/src/datafiles/layouts/nees/NeesListingLayout.tsx b/client/src/datafiles/layouts/nees/NeesListingLayout.tsx index f80c3777fb..aa0e21494f 100644 --- a/client/src/datafiles/layouts/nees/NeesListingLayout.tsx +++ b/client/src/datafiles/layouts/nees/NeesListingLayout.tsx @@ -1,11 +1,42 @@ import React from 'react'; -import { Layout } from 'antd'; -import { NeesListing } from '@client/datafiles'; +import { Button, Form, Input, Layout } from 'antd'; +import { DatafilesToolbar, NeesListing } from '@client/datafiles'; +import { useSearchParams } from 'react-router-dom'; + +const NeesListingSearchbar = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const onSubmit = (queryString: string) => { + const newSearchParams = searchParams; + if (queryString) { + newSearchParams.set('q', queryString); + } else { + newSearchParams.delete('q'); + } + + setSearchParams(newSearchParams); + }; + return ( +
    onSubmit(data.query)} + style={{ display: 'inline-flex' }} + > + + + + +
    + ); +}; export const NEESListingLayout: React.FC = () => { return ( -
    Placeholder for the NEES Search.
    + } />
    diff --git a/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx b/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx index 8b0b06f4a8..d98e3aebf4 100644 --- a/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx +++ b/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx @@ -1,11 +1,11 @@ import { - DatafilesBreadcrumb, ManageCategoryModal, ManagePublishableEntityModal, ProjectCurationFileListing, ProjectNavbar, RelateDataModal, } from '@client/datafiles'; +import { DatafilesBreadcrumb } from '@client/common-components'; import { useProjectDetail } from '@client/hooks'; import { Button } from 'antd'; @@ -130,44 +130,55 @@ export const ProjectCurationLayout: React.FC = () => { if (!data) return
    loading...
    ; return (
    -
    +
    - - 1 | 2 | - - {({ onClick }) => ( - - )} - {' '} - 3 | - - {({ onClick }) => ( - - )} - - + {data.baseProject.value.projectType !== 'other' && ( + + 1 | 2 | + + {({ onClick }) => ( + + )} + {' '} + 3 | + + {({ onClick }) => ( + + )} + + + )}
    { const [searchParams, setSearchParams] = useSearchParams(); @@ -36,16 +36,48 @@ const FileListingSearchBar = () => { }; export const ProjectDetailLayout: React.FC = () => { + const { user } = useAuthenticatedUser(); const { projectId } = useParams(); - const { data } = useProjectDetail(projectId ?? ''); - if (!data || !projectId) return
    loading...
    ; + const { data, isError } = useProjectDetail(projectId ?? ''); + if (isError) { + return ( + + + + ); + } + + if (!user) + return ( + + } /> + + + ); + + if (!data || !projectId) + return ( + + + + ); return ( -
    + } /> -
    + ); }; diff --git a/client/src/datafiles/layouts/projects/ProjectPipelineSelectLayout.tsx b/client/src/datafiles/layouts/projects/ProjectPipelineSelectLayout.tsx index 14e79732ed..98589e7bf0 100644 --- a/client/src/datafiles/layouts/projects/ProjectPipelineSelectLayout.tsx +++ b/client/src/datafiles/layouts/projects/ProjectPipelineSelectLayout.tsx @@ -37,18 +37,21 @@ export const ProjectPipelineSelectLayout: React.FC = () => {
    • Publish new dataset(s) in your project.
    • - If you need to publish subsequent dataset(s), + If you need to publish subsequent dataset(s),  submit a ticket - + {' '} with your project number and the name of the dataset(s).
    - + + +

    Versioning

    @@ -108,22 +119,6 @@ export const ProjectPipelineSelectLayout: React.FC = () => {
    -
    -

    Add/Remove Authors

    -
    -
    - If you need to add or remove authors to/from a publication, - - submit a ticket - - . -
    -
    -
    diff --git a/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx b/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx index c64d00c21e..141e76a00d 100644 --- a/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx +++ b/client/src/datafiles/layouts/projects/ProjectPreviewLayout.tsx @@ -1,13 +1,14 @@ import { BaseProjectDetails, + ProjectBestPracticesModal, ProjectNavbar, ProjectPreview, ProjectTitleHeader, } from '@client/datafiles'; import { useProjectDetail } from '@client/hooks'; -import { Button } from 'antd'; +import { Alert } from 'antd'; import React from 'react'; -import { NavLink, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; export const ProjectPreviewLayout: React.FC = () => { const { projectId } = useParams(); @@ -16,7 +17,7 @@ export const ProjectPreviewLayout: React.FC = () => { if (!projectId) return null; if (!data) return null; return ( -
    +
    { }} > - - - +
    + {data.baseProject.value.projectType === 'other' && ( + + You will select the data to be published in the next step. + + } + /> + )}
    ); }; diff --git a/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx b/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx index 0fd579cca7..0c8805640e 100644 --- a/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx +++ b/client/src/datafiles/layouts/projects/ProjectWorkdirLayout.tsx @@ -1,22 +1,69 @@ import React from 'react'; import { Link, useParams } from 'react-router-dom'; import { - DatafilesBreadcrumb, + ChangeProjectTypeModal, + EmptyProjectFileListing, FileListing, + ProjectDataTransferModal, ProjectNavbar, } from '@client/datafiles'; +import { DatafilesBreadcrumb } from '@client/common-components'; import { useProjectDetail } from '@client/hooks'; +import { Alert, Button } from 'antd'; export const ProjectWorkdirLayout: React.FC = () => { const { projectId, path } = useParams(); const { data } = useProjectDetail(projectId ?? ''); if (!projectId) return null; if (!data) return
    loading...
    ; + + const changeTypeModal = ( + + {({ onClick }) => ( + + )} + + ); + return ( <> - + {data.baseProject.value.projectType === 'None' ? ( +
    + +

    + Please {changeTypeModal} in order to access data curation + features and publish your data set. +

    +

    + +

    +
    + } + /> +
    + ) : ( +
    + + +
    + )} { system={`project-${data.baseProject.uuid}`} path={path ?? ''} scroll={{ y: 500 }} + emptyListingDisplay={} /> )} diff --git a/client/src/datafiles/layouts/published/PublishedDetailLayout.module.css b/client/src/datafiles/layouts/published/PublishedDetailLayout.module.css new file mode 100644 index 0000000000..f47cb1f1b2 --- /dev/null +++ b/client/src/datafiles/layouts/published/PublishedDetailLayout.module.css @@ -0,0 +1,3 @@ +.yellow-highlight { + background-color: #ece4bf; +} diff --git a/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx b/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx index d6ceea8309..f50ee09e13 100644 --- a/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx +++ b/client/src/datafiles/layouts/published/PublishedDetailLayout.tsx @@ -1,7 +1,13 @@ -import { BaseProjectDetails, DatafilesToolbar } from '@client/datafiles'; +import { + BaseProjectDetails, + DatafilesToolbar, + DownloadDatasetModal, + PublishedCitation, + DownloadCitation, +} from '@client/datafiles'; import { usePublicationDetail, usePublicationVersions } from '@client/hooks'; import React, { useEffect } from 'react'; -import { Button, Form, Input } from 'antd'; +import { Alert, Button, Form, Input, Layout, Spin } from 'antd'; import { Navigate, Outlet, useParams, useSearchParams } from 'react-router-dom'; const FileListingSearchBar = () => { @@ -22,7 +28,10 @@ const FileListingSearchBar = () => { style={{ display: 'inline-flex' }} > - +

    @@ -61,4 +61,4 @@

    Register an A {% endblock %} -{% block footer %}{% include 'includes/footer.html' %}{% endblock footer %} \ No newline at end of file +{% block footer %}{% include 'includes/footer.html' %}{% endblock footer %} diff --git a/designsafe/apps/accounts/tests.py b/designsafe/apps/accounts/tests.py index 86f1b0d398..08f775d209 100644 --- a/designsafe/apps/accounts/tests.py +++ b/designsafe/apps/accounts/tests.py @@ -38,7 +38,7 @@ def test_mailing_list_access(self): self.client.login(username='ds_user', password='user/password') resp = self.client.get(url) self.assertEqual(resp.status_code, 403) - self.client.logout() + user = get_user_model().objects.get(pk=2) perm = Permission.objects.get(codename='view_notification_subscribers') user.user_permissions.add(perm) diff --git a/designsafe/apps/accounts/views.py b/designsafe/apps/accounts/views.py index 4e22674053..346fb04764 100644 --- a/designsafe/apps/accounts/views.py +++ b/designsafe/apps/accounts/views.py @@ -11,7 +11,7 @@ from designsafe.apps.accounts import forms, integrations from designsafe.apps.accounts.models import (NEESUser, DesignSafeProfile, NotificationPreferences) -from designsafe.apps.auth.tasks import check_or_create_agave_home_dir +from designsafe.apps.auth.tasks import check_or_configure_system_and_user_directory, get_systems_to_configure from designsafe.apps.accounts.tasks import create_report from pytas.http import TASClient from pytas.models import User as TASUser @@ -279,7 +279,7 @@ def register(request): if not captcha_json.get("success", False): messages.error(request, "Please complete the reCAPTCHA before submitting your account request.") return render(request,'designsafe/apps/accounts/register.html', context) - + # Once captcha is verified, send request to TRAM. tram_headers = {"tram-services-key": settings.TRAM_SERVICES_KEY} tram_body = {"project_id": settings.TRAM_PROJECT_ID, @@ -291,7 +291,7 @@ def register(request): tram_resp.raise_for_status() logger.info("Received response from TRAM: %s", tram_resp.json()) messages.success(request, "Your request has been received. Please check your email for a project invitation.") - + except requests.HTTPError as exc: logger.debug(exc) messages.error(request, "An unknown error occurred. Please try again later.") @@ -467,9 +467,12 @@ def email_confirmation(request, code=None): user = tas.get_user(username=username) if tas.verify_user(user['id'], code, password=password): logger.info('TAS Account activation succeeded.') - from django.conf import settings - check_or_create_agave_home_dir.apply_async(args=(user.username, settings.AGAVE_STORAGE_SYSTEM)) - check_or_create_agave_home_dir.apply_async(args=(user.username, settings.AGAVE_WORKING_SYSTEM)) + systems_to_configure = get_systems_to_configure(username) + for system in systems_to_configure: + check_or_configure_system_and_user_directory.apply_async(args=(user.username, + system["system_id"], + system["path"], + system["create_path"])) return HttpResponseRedirect(reverse('designsafe_accounts:manage_profile')) else: messages.error(request, diff --git a/designsafe/apps/api/agave/__init__.py b/designsafe/apps/api/agave/__init__.py index 8e05b4d037..2518a258e8 100644 --- a/designsafe/apps/api/agave/__init__.py +++ b/designsafe/apps/api/agave/__init__.py @@ -6,19 +6,17 @@ import logging import requests from agavepy.agave import Agave, load_resource +from tapipy.tapis import Tapis from django.conf import settings logger = logging.getLogger(__name__) AGAVE_RESOURCES = load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) -def get_service_account_client(): +def get_service_account_client_v2(): """Return service account agave client. - This service account should use 'ds_admin' token. - ..note:: This service account is an admin account on the Agave tenant. - ..todo:: Should we, instead, use `ds_user`? There might be some issues because of permissionas, but it might be a bit safer.""" @@ -27,11 +25,32 @@ def get_service_account_client(): token=settings.AGAVE_SUPER_TOKEN, resources=AGAVE_RESOURCES) + +def get_service_account_client(): + """Return service account tapis client. + + This service account uses 'wma_prtl' token. + """ + + return Tapis( + base_url=settings.TAPIS_TENANT_BASEURL, + access_token=settings.TAPIS_ADMIN_JWT) + + +def get_tg458981_client(): + """Return tg458981 tapis client.""" + + return Tapis( + base_url=settings.TAPIS_TENANT_BASEURL, + access_token=settings.TAPIS_TG458981_JWT) + + +# TODOV3: Remove sandbox account code def get_sandbox_service_account_client(): """Return sandbox service account""" - return Agave(api_server=settings.AGAVE_SANDBOX_TENANT_BASEURL, - token=settings.AGAVE_SANDBOX_SUPER_TOKEN, - resources=AGAVE_RESOURCES) + return Tapis( + base_url=settings.TAPIS_TENANT_BASEURL, + access_token=settings.TAPIS_ADMIN_JWT) def service_account(): """Return prod or sandbox service client depending on setting.AGAVE_USE_SANDBOX""" diff --git a/designsafe/apps/api/datafiles/handlers.py b/designsafe/apps/api/datafiles/handlers.py index fd67122694..1e4029ab24 100644 --- a/designsafe/apps/api/datafiles/handlers.py +++ b/designsafe/apps/api/datafiles/handlers.py @@ -1,12 +1,13 @@ import logging from designsafe.apps.api.datafiles.notifications import notify -from designsafe.apps.api.datafiles.operations import agave_operations +from designsafe.apps.api.datafiles.operations import tapis_operations from designsafe.apps.api.datafiles.operations import googledrive_operations from designsafe.apps.api.datafiles.operations import dropbox_operations from designsafe.apps.api.datafiles.operations import box_operations from designsafe.apps.api.datafiles.operations import shared_operations from designsafe.apps.api.exceptions import ApiException from django.core.exceptions import PermissionDenied +from tapipy.errors import BaseTapyException from django.urls import reverse logger = logging.getLogger(__name__) @@ -20,8 +21,8 @@ notify_actions = ['move', 'copy', 'rename', 'trash', 'mkdir', 'upload'] operations_mapping = { - 'agave': agave_operations, - 'tapis': agave_operations, + 'agave': tapis_operations, + 'tapis': tapis_operations, 'googledrive': googledrive_operations, 'box': box_operations, 'dropbox': dropbox_operations, @@ -34,7 +35,10 @@ def datafiles_get_handler(api, client, scheme, system, path, operation, username raise PermissionDenied op = getattr(operations_mapping[api], operation) - return op(client, system, path, username=username, **kwargs) + try: + return op(client, system, path, username=username, **kwargs) + except BaseTapyException as exc: + raise ApiException(message=exc.message, status=500) from exc def datafiles_post_handler(api, username, client, scheme, system, @@ -65,7 +69,10 @@ def datafiles_put_handler(api, username, client, scheme, system, try: result = op(client, system, path, **body) - operation in notify_actions and notify(username, operation, '{} operation was successful.'.format(operation.capitalize()), 'SUCCESS', result) + if operation == 'copy' and system != body.get('dest_system', None): + notify(username, operation, 'Your file transfer request has been received and will be processed shortly.'.format(operation.capitalize()), 'SUCCESS', result) + else: + operation in notify_actions and notify(username, operation, '{} operation was successful.'.format(operation.capitalize()), 'SUCCESS', result) return result except Exception as exc: operation in notify_actions and notify(username, operation, 'File operation {} could not be completed.'.format(operation.capitalize()), 'ERROR', {}) diff --git a/designsafe/apps/api/datafiles/operations/shared_operations.py b/designsafe/apps/api/datafiles/operations/shared_operations.py index cc5c7bc46b..17a18f664c 100644 --- a/designsafe/apps/api/datafiles/operations/shared_operations.py +++ b/designsafe/apps/api/datafiles/operations/shared_operations.py @@ -9,7 +9,7 @@ from elasticsearch_dsl import Q import magic from designsafe.apps.data.models.elasticsearch import IndexedFile -from designsafe.apps.api.datafiles.operations.agave_operations import preview, copy, download, download_bytes, listing as agave_listing +from designsafe.apps.api.datafiles.operations.tapis_operations import preview, copy, download, download_bytes, listing as agave_listing # from portal.libs.elasticsearch.indexes import IndexedFile # from portal.apps.search.tasks import agave_indexer, agave_listing_indexer diff --git a/designsafe/apps/api/datafiles/operations/agave_operations.py b/designsafe/apps/api/datafiles/operations/tapis_operations.py similarity index 65% rename from designsafe/apps/api/datafiles/operations/agave_operations.py rename to designsafe/apps/api/datafiles/operations/tapis_operations.py index 875518c16c..fc5cd6889b 100644 --- a/designsafe/apps/api/datafiles/operations/agave_operations.py +++ b/designsafe/apps/api/datafiles/operations/tapis_operations.py @@ -3,9 +3,13 @@ import logging import os import urllib +from pathlib import Path +import tapipy from designsafe.apps.api.datafiles.utils import * from designsafe.apps.data.models.elasticsearch import IndexedFile from designsafe.apps.data.tasks import agave_indexer, agave_listing_indexer +from designsafe.apps.api.filemeta.models import FileMetaModel +from designsafe.apps.api.filemeta.tasks import move_file_meta_async, copy_file_meta_async from django.conf import settings from elasticsearch_dsl import Q import requests @@ -14,7 +18,7 @@ logger = logging.getLogger(__name__) -def listing(client, system, path, offset=0, limit=100, *args, **kwargs): +def listing(client, system, path, offset=0, limit=100, q=None, *args, **kwargs): """ Perform a Tapis file listing @@ -36,18 +40,32 @@ def listing(client, system, path, offset=0, limit=100, *args, **kwargs): list List of dicts containing file metadata """ - raw_listing = client.files.list(systemId=system, - filePath=urllib.parse.quote(path), - offset=int(offset) + 1, - limit=int(limit)) + + if q: + return search(client, system, path, offset=0, limit=100, query_string=q, **kwargs) + + raw_listing = client.files.listFiles(systemId=system, + path=(path or '/'), + offset=int(offset), + limit=int(limit)) try: # Convert file objects to dicts for serialization. - listing = list(map(dict, raw_listing)) + listing = list(map(lambda f: { + 'system': system, + 'type': 'dir' if f.type == 'dir' else 'file', + 'format': 'folder' if f.type == 'dir' else 'raw', + 'mimeType': f.mimeType, + 'path': f"/{f.path}", + 'name': f.name, + 'length': f.size, + 'lastModified': f.lastModified, + '_links': { + 'self': {'href': f.url} + }}, raw_listing)) except IndexError: # Return [] if the listing is empty. listing = [] - # Update Elasticsearch after each listing. # agave_listing_indexer.delay(listing) agave_listing_indexer.delay(listing) @@ -65,13 +83,22 @@ def detail(client, system, path, *args, **kwargs): """ Retrieve the uuid for a file by parsing the query string in _links.metadata.href """ - listing = client.files.list(systemId=system, filePath=urllib.parse.quote(path), offset=0, limit=1) - - href = listing[0]['_links']['metadata']['href'] - qs = urllib.parse.urlparse(href).query - parsed_qs = urllib.parse.parse_qs(qs)['q'][0] - qs_json = json.loads(parsed_qs) - return {**dict(listing[0]), 'uuid': qs_json['associationIds']} + _listing = client.files.listFiles(systemId=system, path=urllib.parse.quote(path), offset=0, limit=1) + f = _listing[0] + listing_res = { + 'system': system, + 'type': 'dir' if f.type == 'dir' else 'file', + 'format': 'folder' if f.type == 'dir' else 'raw', + 'mimeType': f.mimeType, + 'path': f"/{f.path}", + 'name': f.name, + 'length': f.size, + 'lastModified': f.lastModified, + '_links': { + 'self': {'href': f.url} + }} + + return listing_res def iterate_listing(client, system, path, limit=100): @@ -163,11 +190,11 @@ def download(client, system, path=None, paths=None, *args, **kwargs): token = None if client is not None: - token = client.token.token_info['access_token'] + token = client.access_token.access_token zip_endpoint = "https://designsafe-download01.tacc.utexas.edu/check" data = json.dumps({'system': system, 'paths': paths}) # data = json.dumps({'system': 'designsafe.storage.published', 'paths': ['PRJ-2889']}) - resp = requests.put(zip_endpoint, headers={"Authorization": f"Bearer {token}"}, data=data) + resp = requests.put(zip_endpoint, headers={"x-tapis-token": token}, data=data) resp.raise_for_status() download_key = resp.json()["key"] return {"href": f"https://designsafe-download01.tacc.utexas.edu/download/{download_key}"} @@ -191,19 +218,15 @@ def mkdir(client, system, path, dir_name): ------- dict """ - body = { - 'action': 'mkdir', - 'path': dir_name - } - result = client.files.manage(systemId=system, - filePath=urllib.parse.quote(path), - body=body) + path_input = str(Path(path) / Path(dir_name)) + client.files.mkdir(systemId=system, path=path_input) + agave_indexer.apply_async(kwargs={'systemId': system, 'filePath': path, 'recurse': False}, queue='indexing') - return dict(result) + return {"result": "OK"} def move(client, src_system, src_path, dest_system, dest_path): @@ -228,42 +251,25 @@ def move(client, src_system, src_path, dest_system, dest_path): dict """ - # do not allow moves to the same location or across systems - if (os.path.dirname(src_path) == dest_path.strip('/') or src_system != dest_system): - return { - 'system': src_system, - 'path': urllib.parse.quote(src_path), - 'name': os.path.basename(src_path) - } + src_filename = Path(src_path).name + dest_path_full = str(Path(dest_path) / src_filename) + + if src_system != dest_system: + raise ValueError("src_system and dest_system must be identical for move.") + client.files.moveCopy(systemId=src_system, + path=src_path, + operation="MOVE", + newPath=dest_path_full) + + move_file_meta_async.delay(src_system, src_path, dest_system, dest_path_full) - src_file_name = os.path.basename(src_path) - try: - client.files.list(systemId=dest_system, filePath=os.path.join(dest_path, src_file_name)) - dst_file_name = rename_duplicate_path(src_file_name) - full_dest_path = os.path.join(dest_path.strip('/'), dst_file_name) - except: - dst_file_name = src_file_name - full_dest_path = os.path.join(dest_path.strip('/'), src_file_name) - body = {'action': 'move', 'path': full_dest_path} - move_result = client.files.manage( - systemId=src_system, - filePath=urllib.parse.quote(src_path), - body=body - ) - update_meta.apply_async(kwargs={ - "src_system": src_system, - "src_path": src_path, - "dest_system": dest_system, - "dest_path": full_dest_path - }, queue="indexing") - - if os.path.dirname(src_path) != full_dest_path or src_path != full_dest_path: - agave_indexer.apply_async(kwargs={ - 'systemId': src_system, - 'filePath': os.path.dirname(src_path), - 'recurse': False - }, queue='indexing') + #update_meta.apply_async(kwargs={ + # "src_system": src_system, + # "src_path": src_path, + # "dest_system": dest_system, + # "dest_path": dest_path_full + #}, queue="indexing") agave_indexer.apply_async(kwargs={ 'systemId': dest_system, @@ -271,14 +277,13 @@ def move(client, src_system, src_path, dest_system, dest_path): 'recurse': False }, queue='indexing') - if move_result['nativeFormat'] == 'dir': - agave_indexer.apply_async(kwargs={ - 'systemId': dest_system, - 'filePath': full_dest_path, - 'recurse': True - }, queue='indexing') + agave_indexer.apply_async(kwargs={ + 'systemId': dest_system, + 'filePath': dest_path_full, + 'recurse': True + }, queue='indexing') - return move_result + return {"result": "OK"} def copy(client, src_system, src_path, dest_system, dest_path): @@ -305,7 +310,7 @@ def copy(client, src_system, src_path, dest_system, dest_path): """ src_file_name = os.path.basename(src_path) try: - client.files.list(systemId=dest_system, filePath=os.path.join(dest_path, src_file_name)) + client.files.listFiles(systemId=dest_system, path=os.path.join(dest_path, src_file_name)) dst_file_name = rename_duplicate_path(src_file_name) full_dest_path = os.path.join(dest_path.strip('/'), dst_file_name) except: @@ -313,41 +318,46 @@ def copy(client, src_system, src_path, dest_system, dest_path): full_dest_path = os.path.join(dest_path.strip('/'), src_file_name) if src_system == dest_system: - body = {'action': 'copy', 'path': full_dest_path} - copy_result = client.files.manage( - systemId=src_system, - filePath=urllib.parse.quote(src_path.strip('/')), # don't think we need to strip '/' here... - body=body - ) + copy_result = client.files.moveCopy(systemId=src_system, + path=src_path, + operation="COPY", + newPath=full_dest_path) else: - src_url = 'agave://{}/{}'.format(src_system, urllib.parse.quote(src_path)) - copy_result = client.files.importData( - systemId=dest_system, - filePath=urllib.parse.quote(dest_path), - fileName=dst_file_name, - urlToIngest=src_url - ) - - copy_meta.apply_async(kwargs={ - "src_system": src_system, - "src_path": src_path, - "dest_system": dest_system, - "dest_path": full_dest_path + src_url = f'tapis://{src_system}/{src_path}' + dest_url = f'tapis://{dest_system}/{full_dest_path}' + + copy_response = client.files.createTransferTask(elements=[{ + 'sourceURI': src_url, + 'destinationURI': dest_url + }]) + copy_result = { + 'uuid': copy_response.uuid, + 'status': copy_response.status, + } + + + + #copy_meta.apply_async(kwargs={ + # "src_system": src_system, + # "src_path": src_path, + # "dest_system": dest_system, + # "dest_path": full_dest_path + #}, queue='indexing') + + copy_file_meta_async.delay(src_system, src_path, dest_system, full_dest_path) + + agave_indexer.apply_async(kwargs={ + 'systemId': dest_system, + 'filePath': full_dest_path, + 'recurse': True }, queue='indexing') - if copy_result['nativeFormat'] == 'dir': - agave_indexer.apply_async(kwargs={ - 'systemId': dest_system, - 'filePath': full_dest_path, - 'recurse': True - }, queue='indexing') - else: - agave_indexer.apply_async(kwargs={ - 'username': 'ds_admin', - 'systemId': dest_system, - 'filePath': dest_path, - 'recurse': False - }, queue='indexing') + agave_indexer.apply_async(kwargs={ + 'username': 'ds_admin', + 'systemId': dest_system, + 'filePath': dest_path, + 'recurse': False + }, queue='indexing') return dict(copy_result) @@ -381,44 +391,28 @@ def rename(client, system, path, new_name): # a directory... # listing[0].type == 'file' # listing[0].type == 'dir' - listing = client.files.list(systemId=system, filePath=path) path = path.strip('/') - body = {'action': 'rename', 'path': new_name} - - rename_result = client.files.manage( - systemId=system, - filePath=urllib.parse.quote(os.path.join('/', path)), - body=body - ) - update_meta.apply_async(kwargs={ - "src_system": system, - "src_path": path, - "dest_system": system, - "dest_path": os.path.join(os.path.dirname(path), new_name) - }, queue="indexing") - - # if rename_result['nativeFormat'] == 'dir': - if listing[0].type == 'dir': - agave_indexer.apply_async( - kwargs={ - 'systemId': system, - 'filePath': os.path.dirname(path), - 'recurse': False - }, queue='indexing') - agave_indexer.apply_async(kwargs={ - 'systemId': system, - 'filePath': rename_result['path'], - 'recurse': True - }, queue='indexing') - else: - agave_indexer.apply_async( - kwargs={ - 'systemId': system, - 'filePath': os.path.dirname(path), - 'recurse': False - }, queue='indexing') + new_path = str(Path(path).parent / new_name) + + client.files.moveCopy(systemId=system, + path=path, + operation="MOVE", + newPath=new_path) + + move_file_meta_async.delay(system, path, system, new_path) + + agave_indexer.apply_async(kwargs={'systemId': system, + 'filePath': os.path.dirname(path), + 'recurse': False}, + queue='indexing') + + agave_indexer.apply_async(kwargs={'systemId': system, + 'filePath': new_path, + 'recurse': True}, + queue='indexing') + + return {"result": "OK"} - return dict(rename_result) def trash(client, system, path, trash_path): @@ -443,12 +437,9 @@ def trash(client, system, path, trash_path): # Create a trash path if none exists try: - client.files.list(systemId=system, - filePath=trash_path) - except HTTPError as err: - if err.response.status_code != 404: - logger.error("Unexpected exception listing .trash path in {}".format(system)) - raise + client.files.listFiles(systemId=system, + path=trash_path) + except tapipy.errors.NotFoundError: mkdir(client, system, trash_root, trash_foldername) resp = move(client, system, path, system, trash_path) @@ -487,11 +478,10 @@ def upload(client, system, path, uploaded_file, webkit_relative_path=None, *args upload_name = os.path.basename(uploaded_file.name) - resp = client.files.importData(systemId=system, - filePath=urllib.parse.quote(path), - fileName=str(upload_name), - fileToUpload=uploaded_file) + dest_path = os.path.join(path.strip('/'), uploaded_file.name) + response_json = client.files.insert(systemId=system, path=dest_path, file=uploaded_file) + return {"result": "OK"} agave_indexer.apply_async(kwargs={'systemId': system, 'filePath': path, 'recurse': False}, @@ -536,24 +526,28 @@ def preview(client, system, path, href="", max_uses=3, lifetime=600, *args, **kw file_name = path.strip('/').split('/')[-1] file_ext = os.path.splitext(file_name)[1].lower() - href = client.files.list(systemId=system, filePath=path)[0]['_links']['self']['href'] + # href = client.files.list(systemId=system, filePath=path)[0]['_links']['self']['href'] - meta_result = query_file_meta(system, os.path.join('/', path)) - meta = meta_result[0] if len(meta_result) else {} - - args = { - 'url': urllib.parse.unquote(href), - 'maxUses': max_uses, - 'method': 'GET', - 'lifetime': lifetime, - 'noauth': False - } + # meta_result = query_file_meta(system, os.path.join('/', path)) + # meta = meta_result[0] if len(meta_result) else {} + meta = {} + try: + meta = FileMetaModel.get_by_path_and_system(system, path).value + meta.pop("system", None) + meta.pop("path", None) + meta.pop("basePath", None) + meta = {k: json.dumps(meta[k]) for k in meta} + except FileMetaModel.DoesNotExist: + meta = {} - postit_result = client.postits.create(body=args) - url = postit_result['_links']['self']['href'] + postit_result = client.files.createPostIt(systemId=system, path=path, allowedUses=max_uses, validSeconds=lifetime) + url = postit_result.redeemUrl if file_ext in settings.SUPPORTED_TEXT_PREVIEW_EXTS: - file_type = 'text' + if file_ext == '.hazmapper': + file_type = 'hazmapper' + else: + file_type = 'text' elif file_ext in settings.SUPPORTED_IMAGE_PREVIEW_EXTS: file_type = 'image' elif file_ext in settings.SUPPORTED_OBJECT_PREVIEW_EXTS: @@ -590,7 +584,7 @@ def download_bytes(client, system, path): BytesIO object representing the downloaded file. """ file_name = os.path.basename(path) - resp = client.files.download(systemId=system, filePath=path) - result = io.BytesIO(resp.content) + resp = client.files.getContents(systemId=system, path=path) + result = io.BytesIO(resp) result.name = file_name return result diff --git a/designsafe/apps/api/datafiles/operations/transfer_operations.py b/designsafe/apps/api/datafiles/operations/transfer_operations.py index 47e727acd1..52b0f4e754 100644 --- a/designsafe/apps/api/datafiles/operations/transfer_operations.py +++ b/designsafe/apps/api/datafiles/operations/transfer_operations.py @@ -1,4 +1,4 @@ -from designsafe.apps.api.datafiles.operations import agave_operations +from designsafe.apps.api.datafiles.operations import tapis_operations from designsafe.apps.api.datafiles.operations import googledrive_operations from designsafe.apps.api.datafiles.operations import dropbox_operations from designsafe.apps.api.datafiles.operations import box_operations @@ -11,10 +11,10 @@ 'mkdir': googledrive_operations.mkdir }, 'agave': { - 'upload': agave_operations.upload, - 'download': agave_operations.download_bytes, - 'iterate_listing': agave_operations.iterate_listing, - 'mkdir': agave_operations.mkdir + 'upload': tapis_operations.upload, + 'download': tapis_operations.download_bytes, + 'iterate_listing': tapis_operations.iterate_listing, + 'mkdir': tapis_operations.mkdir }, 'dropbox': { 'upload': dropbox_operations.upload, diff --git a/designsafe/apps/api/datafiles/views.py b/designsafe/apps/api/datafiles/views.py index 594b5304e5..347151f064 100644 --- a/designsafe/apps/api/datafiles/views.py +++ b/designsafe/apps/api/datafiles/views.py @@ -23,9 +23,9 @@ def get_client(user, api): client_mappings = { - 'agave': 'agave_oauth', - 'tapis': 'agave_oauth', - 'shared': 'agave_oauth', + 'agave': 'tapis_oauth', + 'tapis': 'tapis_oauth', + 'shared': 'tapis_oauth', 'googledrive': 'googledrive_user_token', 'box': 'box_user_token', 'dropbox': 'dropbox_user_token' diff --git a/designsafe/apps/api/decorators.py b/designsafe/apps/api/decorators.py index f931f421e0..269453cecb 100644 --- a/designsafe/apps/api/decorators.py +++ b/designsafe/apps/api/decorators.py @@ -5,6 +5,7 @@ from functools import wraps from base64 import b64decode from django.conf import settings +from django.http import HttpRequest from django.contrib.auth import get_user_model from django.contrib.auth import login from django.core.exceptions import ObjectDoesNotExist @@ -13,6 +14,8 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import load_der_public_key from cryptography.exceptions import UnsupportedAlgorithm +from tapipy.tapis import Tapis +from tapipy.errors import BaseTapyException #pylint: disable=invalid-name logger = logging.getLogger(__name__) @@ -57,6 +60,50 @@ def _get_jwt_payload(request): return payload + +def tapis_jwt_login(func): + """Decorator to log in a user with their Tapis OAuth token + + ..note:: + It will silently fail and continue executing the wrapped function + if the JWT payload header IS NOT present in the request. If the JWT payload + header IS present then it will continue executing the wrapped function passing + the request object with the correct user logged-in. + """ + #pylint: disable=missing-docstring + @wraps(func) + def decorated_function(request: HttpRequest, *args, **kwargs): + if request.user.is_authenticated: + return func(request, *args, **kwargs) + + tapis_jwt = request.headers.get('X-Tapis-Token') + if not tapis_jwt: + logger.debug('No JWT payload found. Falling back') + return func(request, *args, **kwargs) + + tapis_client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL) + try: + validation_response = tapis_client.validate_token(tapis_jwt) + except BaseTapyException: + return func(request, *args, **kwargs) + + tapis_username = validation_response['tapis/username'] + + try: + user = get_user_model().objects.get(username=tapis_username) + except ObjectDoesNotExist: + logger.exception('Could not find JWT user: %s', tapis_username) + user = None + + if user is not None: + login(request, user, backend="django.contrib.auth.backends.ModelBackend") + + return func(request, *args, **kwargs) + + return decorated_function + #pylint: enable=missing-docstring + + def agave_jwt_login(func): """Decorator to login user with a jwt diff --git a/designsafe/apps/api/filemeta/management/commands/populate_filemeta_table_from_tavpisv2_metadata.py b/designsafe/apps/api/filemeta/management/commands/populate_filemeta_table_from_tavpisv2_metadata.py index 4907a36f58..c1b23a9dff 100644 --- a/designsafe/apps/api/filemeta/management/commands/populate_filemeta_table_from_tavpisv2_metadata.py +++ b/designsafe/apps/api/filemeta/management/commands/populate_filemeta_table_from_tavpisv2_metadata.py @@ -5,7 +5,7 @@ # pylint: disable=logging-fstring-interpolation # pylint: disable=no-member - +import os import logging import json @@ -83,6 +83,10 @@ def populate_filemeta_table(dry_run, do_not_update_existing): continue if not dry_run: + meta_data["value"]["path"] = ( + f"/{meta_data['value']['path'].lstrip('/')}".replace("//", "/") + ) + meta_data["value"]["basePath"] = os.path.dirname(meta_data["value"]["path"]) FileMetaModel.create_or_update_file_meta(meta_data["value"]) updated += 1 diff --git a/designsafe/apps/api/filemeta/models.py b/designsafe/apps/api/filemeta/models.py index 1f8992bea4..2be50c3d13 100644 --- a/designsafe/apps/api/filemeta/models.py +++ b/designsafe/apps/api/filemeta/models.py @@ -5,13 +5,14 @@ from django.utils import timezone -def _get_normalized_path(path) -> str: +def _get_normalized_path(path: str) -> str: """ Return a file path that begins with /" For example, "file.jpg" becomes "/file.jpg" """ if not path.startswith('/'): path = '/' + path + path = path.replace('//', '/') return path diff --git a/designsafe/apps/api/filemeta/tasks.py b/designsafe/apps/api/filemeta/tasks.py new file mode 100644 index 0000000000..209a4cd6be --- /dev/null +++ b/designsafe/apps/api/filemeta/tasks.py @@ -0,0 +1,70 @@ +"""Utils for bulk move/copy of file metadata objects.""" + +import os +from celery import shared_task +from designsafe.apps.api.filemeta.models import FileMetaModel + + +def copy_file_meta(src_system: str, src_path: str, dest_system: str, dest_path: str): + """Create new copies of file metadata when files are copied to a new system/path.""" + clean_src_path = f"/{src_path.lstrip('/').replace('//', '/')}" + clean_dest_path = f"/{dest_path.lstrip('/').replace('//', '/')}" + meta_objs_to_create = ( + FileMetaModel( + value={ + **meta_obj.value, + "system": dest_system, + "path": meta_obj.value["path"].replace(clean_src_path, clean_dest_path), + "basePath": os.path.dirname( + meta_obj.value["path"].replace(clean_src_path, clean_dest_path) + ), + } + ) + for meta_obj in FileMetaModel.objects.filter( + value__system=src_system, + value__path__startswith=f"/{src_path.lstrip('/').replace('//', '/')}", + ) + ) + # return meta_objs_to_create + FileMetaModel.objects.bulk_create(meta_objs_to_create) + + +@shared_task +def copy_file_meta_async( + src_system: str, src_path: str, dest_system: str, dest_path: str +): + """async wrapper around copy_file_meta""" + copy_file_meta(src_system, src_path, dest_system, dest_path) + + +def move_file_meta(src_system: str, src_path: str, dest_system: str, dest_path: str): + """Update system and path of metadata objects to reflect movement to a new path.""" + clean_src_path = f"/{src_path.lstrip('/').replace('//', '/')}" + clean_dest_path = f"/{dest_path.lstrip('/').replace('//', '/')}" + + meta_to_update = list( + FileMetaModel.objects.filter( + value__system=src_system, + value__path__startswith=f"/{src_path.lstrip('/').replace('//', '/')}", + ) + ) + + for meta_obj in meta_to_update: + meta_obj.value = { + **meta_obj.value, + "system": dest_system, + "path": meta_obj.value["path"].replace(clean_src_path, clean_dest_path), + "basePath": os.path.dirname( + meta_obj.value["path"].replace(clean_src_path, clean_dest_path) + ), + } + + FileMetaModel.objects.bulk_update(meta_to_update, ["value"]) + + +@shared_task +def move_file_meta_async( + src_system: str, src_path: str, dest_system: str, dest_path: str +): + """Async wrapper around move_file_meta""" + move_file_meta(src_system, src_path, dest_system, dest_path) diff --git a/designsafe/apps/api/filemeta/tests.py b/designsafe/apps/api/filemeta/tests.py index 3ab665b7bc..cba2d9d0e5 100644 --- a/designsafe/apps/api/filemeta/tests.py +++ b/designsafe/apps/api/filemeta/tests.py @@ -85,6 +85,23 @@ def test_get_file_meta( } +@pytest.mark.django_db +def test_get_file_meta_using_jwt( + regular_user_using_jwt, client, filemeta_db_mock, mock_access_success +): + system_id, path, file_meta = filemeta_db_mock + response = client.get(f"/api/filemeta/{system_id}/{path}") + assert response.status_code == 200 + + assert response.json() == { + "value": file_meta.value, + "name": "designsafe.file", + "lastUpdated": file_meta.last_updated.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z"), + } + + @pytest.mark.django_db def test_create_file_meta_no_access( client, authenticated_user, filemeta_value_mock, mock_access_failure @@ -122,6 +139,21 @@ def test_create_file_meta( assert file_meta.value == filemeta_value_mock +@pytest.mark.django_db +def test_create_file_meta_using_jwt( + client, regular_user_using_jwt, filemeta_value_mock, mock_access_success +): + response = client.post( + "/api/filemeta/", + data=json.dumps(filemeta_value_mock), + content_type="application/json", + ) + assert response.status_code == 200 + + file_meta = FileMetaModel.objects.first() + assert file_meta.value == filemeta_value_mock + + @pytest.mark.django_db def test_create_file_meta_update_existing_entry( client, @@ -147,8 +179,6 @@ def test_create_file_meta_update_existing_entry( def test_create_file_metadata_missing_system_or_path( client, authenticated_user, - filemeta_db_mock, - filemeta_value_mock, mock_access_success, ): value_missing_system_path = {"foo": "bar"} diff --git a/designsafe/apps/api/filemeta/views.py b/designsafe/apps/api/filemeta/views.py index d15dd49dd3..014cccabc3 100644 --- a/designsafe/apps/api/filemeta/views.py +++ b/designsafe/apps/api/filemeta/views.py @@ -1,11 +1,12 @@ """File Meta view""" + import logging import json from django.http import JsonResponse, HttpRequest -from designsafe.apps.api.datafiles.operations.agave_operations import listing +from designsafe.apps.api.datafiles.operations.tapis_operations import listing from designsafe.apps.api.exceptions import ApiException from designsafe.apps.api.filemeta.models import FileMetaModel -from designsafe.apps.api.views import AuthenticatedApiView +from designsafe.apps.api.views import AuthenticatedAllowJwtApiView logger = logging.getLogger(__name__) @@ -28,8 +29,7 @@ def check_access(request, system_id: str, path: str, check_for_writable_access=F raise ApiException(error_msg, status=403) try: - # TODO_V3 update to use renamed (i.e. "tapis") client - listing(request.user.agave_oauth.client, system_id, path) + listing(request.user.tapis_oauth.client, system_id, path) except Exception as exc: # pylint:disable=broad-exception-caught logger.error( f"user cannot access any related metadata as listing failed for {system_id}/{path} with error {str(exc)}." @@ -37,8 +37,7 @@ def check_access(request, system_id: str, path: str, check_for_writable_access=F raise ApiException("User forbidden to access metadata", status=403) from exc -# TODO_V3 update to allow JWT access DES-2706: https://github.com/DesignSafe-CI/portal/pull/1192 -class FileMetaView(AuthenticatedApiView): +class FileMetaView(AuthenticatedAllowJwtApiView): """View for creating and getting file metadata""" def get(self, request: HttpRequest, system_id: str, path: str): @@ -65,8 +64,7 @@ def get(self, request: HttpRequest, system_id: str, path: str): return JsonResponse(result, safe=False) -# TODO_V3 update to allow JWT access DES-2706: https://github.com/DesignSafe-CI/portal/pull/1192 -class CreateFileMetaView(AuthenticatedApiView): +class CreateFileMetaView(AuthenticatedAllowJwtApiView): """View for creating (and updating) file metadata""" def post(self, request: HttpRequest): @@ -80,7 +78,9 @@ def post(self, request: HttpRequest): raise ApiException("System and path are required in payload", status=400) system_id = value["system"] - path = value["path"] + raw_path = value["path"] + # Normalize raw path to ensure leading slash and remove duplicate slashes. + path = f"/{raw_path.lstrip('/')}".replace("//", "/") check_access(request, system_id, path, check_for_writable_access=True) diff --git a/designsafe/apps/api/fixtures/agave-oauth-token-data.json b/designsafe/apps/api/fixtures/agave-oauth-token-data.json deleted file mode 100644 index c5204eb8a8..0000000000 --- a/designsafe/apps/api/fixtures/agave-oauth-token-data.json +++ /dev/null @@ -1,28 +0,0 @@ -[ -{ - "fields": { - "created": 1461727485, - "access_token": "dc48198091d73c8933c2c0ee96afb01b", - "expires_in": 14400, - "token_type": "bearer", - "user": 1, - "scope": "default", - "refresh_token": "2f715c8eb6962a883c7cd29af7d1165" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 -}, -{ - "fields": { - "created": 1463178660, - "access_token": "7834a55e92f3f9b86dc1627bff8d43", - "expires_in": 14400, - "token_type": "bearer", - "user": 2, - "scope": "default", - "refresh_token": "dc1c5b9a5124f88147c783e35b5ca9c" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 2 -} -] diff --git a/designsafe/apps/api/fixtures/user-data.json b/designsafe/apps/api/fixtures/user-data.json deleted file mode 100644 index 42379513c1..0000000000 --- a/designsafe/apps/api/fixtures/user-data.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "user_id": "2", - "token_type": "Bearer", - "scope": "PRODUCTION", - "access_token": "fakeaccesstoken", - "refresh_token": "fakerefreshtoken", - "expires_in": "14400", - "created": "1459433273" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 - } -] diff --git a/designsafe/apps/api/licenses/views.py b/designsafe/apps/api/licenses/views.py index 9210e9ca09..8904b201f1 100644 --- a/designsafe/apps/api/licenses/views.py +++ b/designsafe/apps/api/licenses/views.py @@ -1,31 +1,32 @@ -from designsafe.apps.api.views import BaseApiView -from designsafe.apps.api.mixins import SecureMixin -from designsafe.libs.common.decorators import profile as profile_fn +"""Views for the licenses API.""" + from django.contrib.auth import get_user_model from django.http.response import HttpResponseForbidden, HttpResponseNotFound from django.http import JsonResponse from django.apps import apps -from designsafe.apps.data.models.agave.util import AgaveJSONEncoder +from designsafe.apps.api.views import AuthenticatedAllowJwtApiView import logging logger = logging.getLogger(__name__) -class LicenseView(SecureMixin, BaseApiView): - @profile_fn +class LicenseView(AuthenticatedAllowJwtApiView): + """View for retrieving licenses for a specific app.""" + def get(self, request, app_name): + """Return the license for the given app.""" if not request.user.is_staff: return HttpResponseForbidden() try: - app_license = apps.get_model('designsafe_licenses', '{}License'.format(app_name)) + app_license = apps.get_model("designsafe_licenses", f"{app_name}License") except LookupError: return HttpResponseNotFound() - username = request.GET.get('username', None) + username = request.GET.get("username", None) if not username: return HttpResponseNotFound() user = get_user_model().objects.get(username=username) licenses = app_license.objects.filter(user=user) - user_license = licenses[0].license_as_str() if len(licenses) > 0 else '' - return JsonResponse({'license': user_license}, encoder=AgaveJSONEncoder) + user_license = licenses[0].license_as_str() if len(licenses) > 0 else "" + return JsonResponse({"license": user_license}) diff --git a/designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json b/designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json deleted file mode 100644 index c5204eb8a8..0000000000 --- a/designsafe/apps/api/notifications/fixtures/agave-oauth-token-data.json +++ /dev/null @@ -1,28 +0,0 @@ -[ -{ - "fields": { - "created": 1461727485, - "access_token": "dc48198091d73c8933c2c0ee96afb01b", - "expires_in": 14400, - "token_type": "bearer", - "user": 1, - "scope": "default", - "refresh_token": "2f715c8eb6962a883c7cd29af7d1165" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 -}, -{ - "fields": { - "created": 1463178660, - "access_token": "7834a55e92f3f9b86dc1627bff8d43", - "expires_in": 14400, - "token_type": "bearer", - "user": 2, - "scope": "default", - "refresh_token": "dc1c5b9a5124f88147c783e35b5ca9c" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 2 -} -] diff --git a/designsafe/apps/api/notifications/fixtures/user-data.json b/designsafe/apps/api/notifications/fixtures/user-data.json deleted file mode 100644 index 42379513c1..0000000000 --- a/designsafe/apps/api/notifications/fixtures/user-data.json +++ /dev/null @@ -1,69 +0,0 @@ -[ - { - "fields": { - "username": "ds_admin", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 1 - }, - { - "fields": { - "username": "envision", - "first_name": "DesignSafe", - "last_name": "Admin", - "is_active": true, - "is_superuser": true, - "is_staff": true, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "admin@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 3 - }, - { - "fields": { - "username": "ds_user", - "first_name": "DesignSafe", - "last_name": "User", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2016-03-01T00:00:00.000Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "user@designsafe-ci.org", - "date_joined": "2016-03-01T00:00:00.000Z" - }, - "model": "auth.user", - "pk": 2 - }, - { - "fields": { - "user_id": "2", - "token_type": "Bearer", - "scope": "PRODUCTION", - "access_token": "fakeaccesstoken", - "refresh_token": "fakerefreshtoken", - "expires_in": "14400", - "created": "1459433273" - }, - "model": "designsafe_auth.agaveoauthtoken", - "pk": 1 - } -] diff --git a/designsafe/apps/api/notifications/tests.py b/designsafe/apps/api/notifications/tests.py index cf93dfc8fc..b0faf72ef8 100644 --- a/designsafe/apps/api/notifications/tests.py +++ b/designsafe/apps/api/notifications/tests.py @@ -1,15 +1,12 @@ -import requests import json import os from django.test import TestCase from django.test import Client from django.contrib.auth import get_user_model from django.db.models.signals import post_save -from mock import Mock, patch -from designsafe.apps.auth.models import AgaveOAuthToken +from mock import patch from urllib.parse import urlencode from unittest import skip -from django.dispatch import receiver from django.urls import reverse from designsafe.apps.api.notifications.models import Notification from .receivers import send_notification_ws @@ -19,42 +16,41 @@ logger = logging.getLogger(__name__) -FILEDIR_PENDING = os.path.join(os.path.dirname(__file__), './json/pending.json') -FILEDIR_SUBMITTING = os.path.join(os.path.dirname(__file__), './json/submitting.json') -FILEDIR_PENDING2 = os.path.join(os.path.dirname(__file__), './json/pending2.json') +FILEDIR_PENDING = os.path.join(os.path.dirname(__file__), "./json/pending.json") +FILEDIR_SUBMITTING = os.path.join(os.path.dirname(__file__), "./json/submitting.json") +FILEDIR_PENDING2 = os.path.join(os.path.dirname(__file__), "./json/pending2.json") webhook_body_pending = json.dumps(json.load(open(FILEDIR_PENDING))) webhook_body_pending2 = json.dumps(json.load(open(FILEDIR_PENDING2))) webhook_body_submitting = json.dumps(json.load(open(FILEDIR_SUBMITTING))) - # Create your tests here. @skip("Need to mock websocket call to redis") class NotificationsTestCase(TestCase): - fixtures = ['user-data.json', 'agave-oauth-token-data.json'] + fixtures = ["user-data.json", "auth.json"] def setUp(self): - self.wh_url = reverse('designsafe_api:jobs_wh_handler') + self.wh_url = reverse("designsafe_api:jobs_wh_handler") user = get_user_model().objects.get(pk=2) - user.set_password('password') + user.set_password("password") user.save() self.user = user self.client = Client() - with open('designsafe/apps/api/fixtures/agave-model-config-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-model-config-meta.json") as f: model_config_meta = json.load(f) self.model_config_meta = model_config_meta - with open('designsafe/apps/api/fixtures/agave-file-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-file-meta.json") as f: file_meta = json.load(f) self.file_meta = file_meta - with open('designsafe/apps/api/fixtures/agave-experiment-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-experiment-meta.json") as f: experiment_meta = json.load(f) self.experiment_meta = experiment_meta - with open('designsafe/apps/api/fixtures/agave-project-meta.json') as f: + with open("designsafe/apps/api/fixtures/agave-project-meta.json") as f: project_meta = json.load(f) self.project_meta = project_meta @@ -62,42 +58,59 @@ def test_current_user_is_ds_user(self): """ just making sure the db setup worked. """ - self.assertEqual(self.user.username, 'ds_user') + self.assertEqual(self.user.username, "ds_user") def test_submitting_webhook_returns_200_and_creates_notification(self): - r = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') + r = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) self.assertEqual(r.status_code, 200) n = Notification.objects.last() - status_from_notification = n.to_dict()['extra']['status'] - self.assertEqual(status_from_notification, 'PENDING') + status_from_notification = n.to_dict()["extra"]["status"] + self.assertEqual(status_from_notification, "PENDING") def test_2_webhooks_same_status_same_jobId_should_give_1_notification(self): - r = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') - - #assert that sending the same status twice doesn't trigger a second notification. - r2 = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') + r = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) + + # assert that sending the same status twice doesn't trigger a second notification. + r2 = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) self.assertEqual(Notification.objects.count(), 1) def test_2_webhooks_different_status_same_jobId_should_give_2_notifications(self): - r1 = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') + r1 = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) - r2 = self.client.post(self.wh_url, webhook_body_submitting, content_type='application/json') + r2 = self.client.post( + self.wh_url, webhook_body_submitting, content_type="application/json" + ) self.assertEqual(Notification.objects.count(), 2) def test_2_webhooks_same_status_different_jobId_should_give_2_notifications(self): - r = self.client.post(self.wh_url, webhook_body_pending, content_type='application/json') - r2 = self.client.post(self.wh_url, webhook_body_pending2, content_type='application/json') + r = self.client.post( + self.wh_url, webhook_body_pending, content_type="application/json" + ) + r2 = self.client.post( + self.wh_url, webhook_body_pending2, content_type="application/json" + ) self.assertEqual(Notification.objects.count(), 2) +@skip("TODOv3: Update webhooks with Tapisv3") class TestWebhookViews(TestCase): - fixtures = ['user-data', 'agave-oauth-token-data'] + fixtures = ["user-data", "auth"] def setUp(self): - self.wh_url = reverse('designsafe_api:jobs_wh_handler') - self.mock_agave_patcher = patch('designsafe.apps.auth.models.AgaveOAuthToken.client', autospec=True) + self.wh_url = reverse("designsafe_api:jobs_wh_handler") + self.mock_agave_patcher = patch( + "designsafe.apps.auth.models.TapisOAuthToken.client", autospec=True + ) self.mock_agave = self.mock_agave_patcher.start() self.client.force_login(get_user_model().objects.get(username="ds_user")) @@ -109,7 +122,7 @@ def setUp(self): "port": "1234", "address": "http://designsafe-exec-01.tacc.utexas.edu:1234", "job_uuid": "3373312947011719656-242ac11b-0001-007", - "owner": "ds_user" + "owner": "ds_user", } self.vnc_event = { @@ -117,7 +130,7 @@ def setUp(self): "host": "stampede2.tacc.utexas.edu", "port": "2234", "password": "3373312947011719656-242ac11b-0001-007", - "owner": "ds_user" + "owner": "ds_user", } self.agave_job_running = {"owner": "ds_user", "status": "RUNNING"} @@ -125,62 +138,80 @@ def setUp(self): def tearDown(self): self.mock_agave_patcher.stop() - post_save.connect(send_notification_ws, sender=Notification, dispatch_uid="notification_msg") + post_save.connect( + send_notification_ws, sender=Notification, dispatch_uid="notification_msg" + ) def test_unsupported_event_type(self): - response = self.client.post(reverse('interactive_wh_handler'), - urlencode({'event_type': 'DUMMY'}), - content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode({"event_type": "DUMMY"}), + content_type="application/x-www-form-urlencoded", + ) self.assertTrue(response.status_code == 400) def test_webhook_job_post(self): - job_event = json.load(open(os.path.join(os.path.dirname(__file__), 'json/submitting.json'))) + job_event = json.load( + open(os.path.join(os.path.dirname(__file__), "json/submitting.json")) + ) - response = self.client.post(self.wh_url, json.dumps(job_event), content_type='application/json') + response = self.client.post( + self.wh_url, json.dumps(job_event), content_type="application/json" + ) self.assertEqual(response.status_code, 200) n = Notification.objects.last() - n_status = n.to_dict()['extra']['status'] - self.assertEqual(n_status, job_event['status']) + n_status = n.to_dict()["extra"]["status"] + self.assertEqual(n_status, job_event["status"]) def test_webhook_vnc_post(self): self.mock_agave.jobs.get.return_value = self.agave_job_running link_from_event = "https://tap.tacc.utexas.edu/noVNC/?host=stampede2.tacc.utexas.edu&port=2234&autoconnect=true&encrypt=true&resize=scale&password=3373312947011719656-242ac11b-0001-007" - response = self.client.post(reverse('interactive_wh_handler'), urlencode(self.vnc_event), content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.vnc_event), + content_type="application/x-www-form-urlencoded", + ) self.assertEqual(response.status_code, 200) self.assertTrue(self.mock_agave.meta.addMetadata.called) self.assertEqual(Notification.objects.count(), 1) n = Notification.objects.last() - action_link = n.to_dict()['action_link'] + action_link = n.to_dict()["action_link"] self.assertEqual(action_link, link_from_event) - self.assertEqual(n.operation, 'web_link') + self.assertEqual(n.operation, "web_link") def test_webhook_web_post(self): self.mock_agave.jobs.get.return_value = self.agave_job_running link_from_event = "http://designsafe-exec-01.tacc.utexas.edu:1234" - response = self.client.post(reverse('interactive_wh_handler'), urlencode(self.web_event), content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.web_event), + content_type="application/x-www-form-urlencoded", + ) self.assertEqual(response.status_code, 200) self.assertTrue(self.mock_agave.meta.addMetadata.called) self.assertEqual(Notification.objects.count(), 1) n = Notification.objects.last() - action_link = n.to_dict()['action_link'] + action_link = n.to_dict()["action_link"] self.assertEqual(action_link, link_from_event) - self.assertEqual(n.operation, 'web_link') + self.assertEqual(n.operation, "web_link") def test_webhook_vnc_post_no_matching_job(self): self.mock_agave.jobs.get.return_value = self.agave_job_failed - response = self.client.post(reverse('interactive_wh_handler'), - urlencode(self.vnc_event), - content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.vnc_event), + content_type="application/x-www-form-urlencoded", + ) # no matching running job so it fails self.assertEqual(response.status_code, 400) self.assertEqual(Notification.objects.count(), 0) @@ -188,9 +219,11 @@ def test_webhook_vnc_post_no_matching_job(self): def test_webhook_web_post_no_matching_job(self): self.mock_agave.jobs.get.return_value = self.agave_job_failed - response = self.client.post(reverse('interactive_wh_handler'), - urlencode(self.web_event), - content_type='application/x-www-form-urlencoded') + response = self.client.post( + reverse("interactive_wh_handler"), + urlencode(self.web_event), + content_type="application/x-www-form-urlencoded", + ) # no matching running job so it fails self.assertEqual(response.status_code, 400) self.assertEqual(Notification.objects.count(), 0) diff --git a/designsafe/apps/api/notifications/urls.py b/designsafe/apps/api/notifications/urls.py index 6bdd3f0de6..6faab4b567 100644 --- a/designsafe/apps/api/notifications/urls.py +++ b/designsafe/apps/api/notifications/urls.py @@ -1,14 +1,10 @@ from django.urls import re_path as url from designsafe.apps.api.notifications.views.api import ManageNotificationsView, NotificationsBadgeView -from designsafe.apps.api.notifications.views.webhooks import JobsWebhookView, FilesWebhookView urlpatterns = [ url(r'^$', ManageNotificationsView.as_view(), name='index'), url(r'^badge/$', NotificationsBadgeView.as_view(), name='badge'), - url(r'^notifications/(?P\w+)/?$', ManageNotificationsView.as_view(), name='event_type_notifications'), url(r'^delete/(?P\w+)?$', ManageNotificationsView.as_view(), name='delete_notification'), - url(r'^wh/jobs/$', JobsWebhookView.as_view(), name='jobs_wh_handler'), - url(r'^wh/files/$', FilesWebhookView.as_view(), name='files_wh_handler'), ] diff --git a/designsafe/apps/api/notifications/views/api.py b/designsafe/apps/api/notifications/views/api.py index 85546e0bf9..7e569938ab 100644 --- a/designsafe/apps/api/notifications/views/api.py +++ b/designsafe/apps/api/notifications/views/api.py @@ -1,60 +1,101 @@ import logging -from django.http.response import HttpResponseBadRequest -from django.http import HttpResponse -from django.urls import reverse -from django.shortcuts import render - +import json +from django.http import HttpResponse, JsonResponse from designsafe.apps.api.notifications.models import Notification - from designsafe.apps.api.views import BaseApiView from designsafe.apps.api.mixins import JSONResponseMixin, SecureMixin -from designsafe.apps.api.exceptions import ApiException -import json logger = logging.getLogger(__name__) class ManageNotificationsView(SecureMixin, JSONResponseMixin, BaseApiView): - def get(self, request, event_type = None, *args, **kwargs): - limit = request.GET.get('limit', 0) - page = request.GET.get('page', 0) - - if event_type is not None: - notifs = Notification.objects.filter(event_type = event_type, - deleted = False, - user = request.user.username).order_by('-datetime') - total = Notification.objects.filter(event_type = event_type, - deleted = False, - user = request.user.username).count() + def get(self, request, *args, **kwargs): + """List all notifications of a certain event type.""" + limit = request.GET.get("limit", 0) + page = request.GET.get("page", 0) + read = request.GET.get("read") + event_types = request.GET.getlist("eventTypes[]") + mark_read = request.GET.get( + "markRead", True + ) # mark read by default to support legacy behavior + + query_params = {} + if read is not None: + query_params["read"] = json.loads(read) + + if event_types: + notifs = Notification.objects.filter( + event_type__in=event_types, + deleted=False, + user=request.user.username, + **query_params + ).order_by("-datetime") + total = Notification.objects.filter( + event_type__in=event_types, deleted=False, user=request.user.username + ).count() + unread = Notification.objects.filter( + event_type__in=event_types, + deleted=False, + read=False, + user=request.user.username, + ).count() else: - notifs = Notification.objects.filter(deleted = False, - user = request.user.username).order_by('-datetime') - total = Notification.objects.filter(deleted = False, - user = request.user.username).count() + notifs = Notification.objects.filter( + deleted=False, user=request.user.username, **query_params + ).order_by("-datetime") + total = Notification.objects.filter( + deleted=False, user=request.user.username + ).count() + unread = Notification.objects.filter( + deleted=False, read=False, user=request.user.username + ).count() if limit: limit = int(limit) page = int(page) offset = page * limit - notifs = notifs[offset:offset+limit] + notifs = notifs[offset : offset + limit] - for n in notifs: - if not n.read: - n.mark_read() + if mark_read: + for n in notifs: + if not n.read: + n.mark_read() notifs = [n.to_dict() for n in notifs] - return self.render_to_json_response({'notifs':notifs, 'page':page, 'total': total}) - # return self.render_to_json_response(notifs) + return JsonResponse( + {"notifs": notifs, "page": page, "total": total, "unread": unread} + ) def post(self, request, *args, **kwargs): body_json = json.loads(request.body) - nid = body_json['id'] - read = body_json['read'] - n = Notification.get(id = nid) + nid = body_json["id"] + read = body_json["read"] + n = Notification.get(id=nid) n.read = read n.save() + def patch(self, request, *args, **kwargs): + """Mark notifications as read.""" + body = json.loads(request.body) + event_types = body.get("eventTypes") + + if event_types is not None: + notifs = Notification.objects.filter( + deleted=False, + read=False, + event_type__in=event_types, + user=request.user.username, + ) + else: + notifs = Notification.objects.filter( + deleted=False, read=False, user=request.user.username + ) + for n in notifs: + n.mark_read() + + return JsonResponse({"message": "OK"}) + def delete(self, request, pk, *args, **kwargs): # body_json = json.loads(request.body) # nid = body_json['id'] @@ -62,19 +103,34 @@ def delete(self, request, pk, *args, **kwargs): # n = Notification.objects.get(pk = pk) # n.deleted = deleted # n.save() - if pk == 'all': - items=Notification.objects.filter(deleted=False, user=str(request.user)) + if pk == "all": + items = Notification.objects.filter(deleted=False, user=str(request.user)) for i in items: i.mark_deleted() else: x = Notification.objects.get(pk=pk) x.mark_deleted() - return HttpResponse('OK') + return HttpResponse("OK") + class NotificationsBadgeView(SecureMixin, JSONResponseMixin, BaseApiView): + """View for notifications badge count""" def get(self, request, *args, **kwargs): - unread = Notification.objects.filter(deleted = False, read = False, - user = request.user.username).count() - return self.render_to_json_response({'unread': unread}) + """Get count of unread notifications of a certain event type.""" + event_types = request.GET.getlist("eventTypes[]") + + if event_types: + unread = Notification.objects.filter( + event_type__in=event_types, + deleted=False, + read=False, + user=request.user.username, + ).count() + else: + unread = Notification.objects.filter( + deleted=False, read=False, user=request.user.username + ).count() + + return JsonResponse({"unread": unread}) diff --git a/designsafe/apps/api/notifications/views/webhooks.py b/designsafe/apps/api/notifications/views/webhooks.py deleted file mode 100644 index 3dd9470757..0000000000 --- a/designsafe/apps/api/notifications/views/webhooks.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.http.response import HttpResponseBadRequest -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth import get_user_model -from django.urls import reverse -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator -from django.shortcuts import render -from django.http import HttpResponse -from django.contrib.sessions.models import Session -from django.conf import settings - -from celery import shared_task -from requests import ConnectionError, HTTPError -from agavepy.agave import Agave, AgaveException - -from designsafe.apps.api.notifications.models import Notification - -from designsafe.apps.api.views import BaseApiView -from designsafe.apps.api.mixins import JSONResponseMixin, SecureMixin -from designsafe.apps.api.exceptions import ApiException - -from designsafe.apps.workspace.tasks import handle_webhook_request - -import json -import logging - -logger = logging.getLogger(__name__) - - - -class JobsWebhookView(JSONResponseMixin, BaseApiView): - """ - Dispatches notifications when receiving a POST request from the Agave - webhook service. - - """ - - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(JobsWebhookView, self).dispatch(*args, **kwargs) - - def get(self, request, *args, **kwargs): - return HttpResponse(settings.WEBHOOK_POST_URL.strip('/') + '/api/notifications/wh/jobs/') - - def post(self, request, *args, **kwargs): - """ - Calls handle_webhook_request on webhook JSON body - to notify the user of the progress of the job. - - """ - - job = json.loads(request.body) - - handle_webhook_request(job) - return HttpResponse('OK') - - -class FilesWebhookView(SecureMixin, JSONResponseMixin, BaseApiView): - @method_decorator(csrf_exempt) - def dispatch(self, *args, **kwargs): - return super(FilesWebhookView, self).dispatch(*args, **kwargs) - - def post(self, request, *args, **kwargs): - notification = json.loads(request.body) - logger.debug(notification) - - return HttpResponse('OK') diff --git a/designsafe/apps/api/projects/tests.py b/designsafe/apps/api/projects/tests.py index c2c63069e7..0b844416a2 100644 --- a/designsafe/apps/api/projects/tests.py +++ b/designsafe/apps/api/projects/tests.py @@ -1,18 +1,20 @@ from designsafe.apps.api.projects.fixtures import exp_instance_meta, exp_instance_resp, exp_entity_meta, exp_entity_json import pytest +@pytest.mark.skip(reason="TODOv3: Update projects with Tapisv3") @pytest.mark.django_db -def test_project_instance_get(client, mock_agave_client, authenticated_user): - mock_agave_client.meta.getMetadata.return_value = exp_instance_meta +def test_project_instance_get(client, mock_tapis_client, authenticated_user): + mock_tapis_client.meta.getMetadata.return_value = exp_instance_meta resp = client.get('/api/projects/1052668239654088215-242ac119-0001-012/') actual = resp.json() expected = exp_instance_resp assert actual == expected +@pytest.mark.skip(reason="TODOv3: Update projects with Tapisv3") @pytest.mark.django_db -def test_project_meta_all(client, mock_agave_client, authenticated_user): - mock_agave_client.meta.getMetadata.return_value = exp_instance_meta - mock_agave_client.meta.listMetadata.return_value = exp_entity_meta +def test_project_meta_all(client, mock_tapis_client, authenticated_user): + mock_tapis_client.meta.getMetadata.return_value = exp_instance_meta + mock_tapis_client.meta.listMetadata.return_value = exp_entity_meta resp = client.get('/api/projects/1052668239654088215-242ac119-0001-012/meta/all/') actual = resp.json() expected = exp_entity_json diff --git a/designsafe/apps/api/projects_v2/conftest.py b/designsafe/apps/api/projects_v2/conftest.py new file mode 100644 index 0000000000..65d4d4a4ef --- /dev/null +++ b/designsafe/apps/api/projects_v2/conftest.py @@ -0,0 +1,65 @@ +"""Fixtures related to project metadata.""" +import pytest +from designsafe.apps.api.projects_v2 import constants +from designsafe.apps.api.projects_v2.operations.project_meta_operations import ( + create_project_metdata, + create_entity_metadata, + add_file_associations, + set_file_tags, + FileObj, +) +from designsafe.apps.api.projects_v2.operations.graph_operations import ( + initialize_project_graph, + add_node_to_project, +) + + +@pytest.fixture +def project_with_associations(regular_user): + """Project with associations fixture""" + project_value = { + "title": "Test Project", + "projectId": "PRJ-1234", + "users": [{"username": regular_user.username, "role": "pi"}], + "projectType": "experimental", + } + experiment_value = {"title": "Test Experiment", "description": "Experiment test"} + model_config_value = { + "title": "Test Entity", + "description": "Entity with file associations", + } + project = create_project_metdata(project_value) + initialize_project_graph("PRJ-1234") + + experiment = create_entity_metadata( + "PRJ-1234", name=constants.EXPERIMENT, value=experiment_value + ) + model_config = create_entity_metadata( + "PRJ-1234", name=constants.EXPERIMENT_MODEL_CONFIG, value=model_config_value + ) + + experiment_node = add_node_to_project( + "PRJ-1234", "NODE_ROOT", experiment.uuid, experiment.name + ) + add_node_to_project( + "PRJ-1234", experiment_node, model_config.uuid, model_config.name + ) + + file_objs = [ + FileObj( + system="project.system", name="file1", path="/path/to/file1", type="file" + ), + FileObj( + system="project.system", + name="file1", + path="/path/to/other/file1", + type="file", + ), + FileObj(system="project.system", name="dir1", path="/path/to/dir1", type="dir"), + ] + add_file_associations(model_config.uuid, file_objs) + set_file_tags(model_config.uuid, "/path/to/file1", ["test_tag"]) + set_file_tags(model_config.uuid, "/path/to/dir1/nested/file", ["test_tag"]) + set_file_tags(model_config.uuid, "/path/to/other/file1", ["test_tag"]) + + yield (project, experiment.uuid, project.uuid) diff --git a/designsafe/apps/api/projects_v2/management/__init__.py b/designsafe/apps/api/projects_v2/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/api/projects_v2/management/commands/__init__.py b/designsafe/apps/api/projects_v2/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/api/projects_v2/management/commands/migrate_projects_tapis_systems_from_v2_to_v3.py b/designsafe/apps/api/projects_v2/management/commands/migrate_projects_tapis_systems_from_v2_to_v3.py new file mode 100644 index 0000000000..4326fbe788 --- /dev/null +++ b/designsafe/apps/api/projects_v2/management/commands/migrate_projects_tapis_systems_from_v2_to_v3.py @@ -0,0 +1,272 @@ +"""Migrate Projects Tapis Systems from V2 to V3. + +This module contains a Django management command which migrates Tapis systems associated with +projects from Tapis V2 to Tapis V3. +""" + +# pylint: disable=logging-fstring-interpolation +# pylint: disable=no-member + + +import logging +import os + +from tapipy.tapis import Tapis +from django.conf import settings +from django.core.management.base import BaseCommand +from designsafe.apps.api.projects_v2.tests.schema_integration import iterate_entities + +try: + from designsafe.apps.api.agave import get_service_account_client_v2 +except ImportError: + # TODOV3 drop this + from designsafe.apps.api.agave import ( + get_service_account_client as get_service_account_client_v2, + ) + + +logger = logging.getLogger(__name__) + +service_account_v2 = get_service_account_client_v2() + + +def remove_user(client, system_id: str, username: str): + """ + Unshare the system and remove all permissions and credentials. + """ + client.systems.removeUserCredential(systemId=system_id, userName=username) + client.systems.unShareSystem(systemId=system_id, users=[username]) + client.systems.revokeUserPerms( + systemId=system_id, userName=username, permissions=["READ", "MODIFY", "EXECUTE"] + ) + client.files.deletePermissions(systemId=system_id, username=username, path="/") + + +def set_workspace_permissions(client: Tapis, username: str, system_id: str, role: str): + """Apply read/write/execute permissions to a user on a system.""" + + system_pems = {"reader": ["READ", "EXECUTE"], "writer": ["READ", "EXECUTE"]} + + files_pems = {"reader": "READ", "writer": "MODIFY"} + + logger.info(f"Adding {username} permissions to Tapis system {system_id}") + client.systems.grantUserPerms( + systemId=system_id, userName=username, permissions=system_pems[role] + ) + + if role == "reader": + client.systems.revokeUserPerms( + systemId=system_id, userName=username, permissions=["MODIFY"] + ) + client.files.deletePermissions(systemId=system_id, path="/", username=username) + + client.files.grantPermissions( + systemId=system_id, path="/", username=username, permission=files_pems[role] + ) + + +def create_or_update_workspace_system( # pylint: disable=too-many-arguments + create, + client, + system_id: str, + title: str, + description: str, + project_root_dir: str, + owner=None, +) -> str: + """Create or update a system.""" + + system_args = { + "id": system_id, + "host": HOST, + "port": int(PORT), + "systemType": "LINUX", + "defaultAuthnMethod": "PKI_KEYS", + "canExec": False, + "rootDir": project_root_dir, + "effectiveUserId": TG_USER, + "authnCredential": { + "privateKey": TG_USER_PRIVATE_KEY, + "publicKey": TG_USER_PUBLIC_KEY, + }, + "notes": {"title": title, "description": description}, + } + if owner: + system_args["owner"] = owner + if create: + client.systems.createSystem(**system_args) + else: + client.systems.patchSystem(systemId=system_id, **system_args) + + +# pylint: disable=invalid-name +HOST = settings.PROJECT_STORAGE_SYSTEM_TEMPLATE["storage"][ + "host" +] # cloud.corral.tacc.utexas.edu +PORT = 22 +TG_USER = settings.PROJECT_STORAGE_SYSTEM_TEMPLATE["storage"]["auth"][ + "username" +] # i.e. tg458981 +TG_USER_PRIVATE_KEY = settings.PROJECT_STORAGE_SYSTEM_TEMPLATE["storage"]["auth"][ + "privateKey" +] +TG_USER_PUBLIC_KEY = settings.PROJECT_STORAGE_SYSTEM_TEMPLATE["storage"]["auth"][ + "publicKey" +] +OWNER = "wma_prtl" +# pylint: enable=invalid-name + + +class Command(BaseCommand): + """Command for migrating projects from Tapis v2 to v3""" + + help = ( + "Facilitates the migration of project systems from Tapis v2 to v3," + " with options for a dry run and updating existing systems." + ) + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Executes the command in a simulation mode, logging actions " + "without applying any changes to the systems or data.", + ) + + parser.add_argument( + "--update-existing", + action="store_true", + help="Allows the command to update systems that already exist in Tapis V3," + "ensuring they are synchronized with their V2 counterparts.", + ) + + def handle( + self, *args, **options + ): # pylint: disable=too-many-statements disable=too-many-locals + dry_run = options["dry_run"] + update_existing = options["update_existing"] + + TAPIS_TENANT_BASEURL = os.environ.get( # pylint: disable=invalid-name + "TAPIS_TENANT_BASEURL" + ) + TAPIS_ADMIN_JWT = os.environ.get( # pylint: disable=invalid-name + "TAPIS_ADMIN_JWT" + ) + client = Tapis(base_url=TAPIS_TENANT_BASEURL, access_token=TAPIS_ADMIN_JWT) + + total_number_projects = len(list(iterate_entities())) + + for i, project in enumerate(iterate_entities()): + uuid = project["uuid"] + project_id = project["value"]["projectId"] + system = f"project-{uuid}" + project_root_dir = ( + f"/corral-repl/projects/NHERI/projects/{uuid}" + if uuid != "7997906542076432871-242ac11c-0001-012" + else "/corral-repl/projects/NHERI/community" # community data system has as a special path + ) + title = project["value"]["title"] + description = ( + project["value"]["description"] + if "description" in project["value"] + else "" + ) + pi = project["value"]["pi"] # pylint: disable=invalid-name + co_pis = project["value"]["coPis"] if "coPis" in project["value"] else [] + team_members = ( + project["value"]["teamMembers"] + if "teamMembers" in project["value"] + else [] + ) + guest_members = ( + project["value"]["guestMembers"] + if ("guestMembers" in project["value"]) + else [] + ) + guest_members = [u["user"] for u in guest_members if u is not None] + + all_writers = ( + co_pis + team_members + guest_members + ([pi] if pi is not None else []) + ) + + try: + tapis_v2_system_roles = service_account_v2.systems.listRoles( + systemId=f"project-{uuid}" + ) + except Exception: # pylint: disable=broad-exception-caught: + tapis_v2_system_roles = [] + logger.error(f"Unable to get roles on uuid:{uuid}") + users_from_roles = [u["username"] for u in tapis_v2_system_roles] + users_from_roles_not_listed_elsewhere = [ + u for u in users_from_roles if u not in all_writers + ] + + all_writers = all_writers + users_from_roles_not_listed_elsewhere + + msg = ( + f"Migrating {i}/{total_number_projects}, {project_id}, ({uuid}), " + f"('{title}'), pi:{pi}, coPis:{co_pis}, teamMembers:{team_members}, " + f"guestMembers:{guest_members}," + f"users_from_roles:{users_from_roles}," + f"users_from_roles_not_listed_elsewhere:{users_from_roles_not_listed_elsewhere}" + ) + logger.info(msg) + + all_readers = guest_members + all_users = all_readers + all_writers + + system_exists = True + try: + client.systems.getSystem(systemId=system) + except Exception: # pylint: disable=broad-exception-caught + system_exists = False + + if not dry_run: + if system_exists and not update_existing: + logger.info( + "System already exists so skipping (update_existing=False)" + ) + continue + if system_exists: + shared_information = client.systems.getShareInfo(systemId=system) + user_to_remove = set(shared_information.get("users")) - set( + all_users + ) + for user in user_to_remove: + logger.info(f"removing user: {user}") + remove_user(client, system, user) + + create = not system_exists + + try: + create_or_update_workspace_system( + create, + client, + system_id=system, + title=title, + description=description, + project_root_dir=project_root_dir, + owner=OWNER, + ) + + client.systems.shareSystem(systemId=system, users=all_users) + + for user in all_writers: + set_workspace_permissions(client, user, system, "writer") + + for user in all_readers: + set_workspace_permissions(client, user, system, "reader") + except Exception: # pylint: disable=broad-exception-caught + logger.exception(f"Error for system:{system}") + else: + system_exists_text = ( + "System already exists" + if system_exists + else "System does not exist" + ) + logger.info( + f"Running in dry-run mode. No changes will be made. " + f"" + f"Note: {system_exists_text}" + ) + logger.info("Successfully migrated systems") diff --git a/designsafe/apps/api/projects_v2/migration_utils/file_obj_ingest.py b/designsafe/apps/api/projects_v2/migration_utils/file_obj_ingest.py index 1eacc52596..152f95432b 100644 --- a/designsafe/apps/api/projects_v2/migration_utils/file_obj_ingest.py +++ b/designsafe/apps/api/projects_v2/migration_utils/file_obj_ingest.py @@ -1,6 +1,7 @@ """Utilities to update the database with existing project file associations""" import json +import os from typing import Iterator import requests from urllib3.util import Retry @@ -8,7 +9,7 @@ from requests.exceptions import RetryError from requests.adapters import HTTPAdapter from django.conf import settings -from designsafe.apps.api.agave import service_account +from designsafe.apps.api.agave import get_service_account_client_v2 as service_account from designsafe.apps.api.projects_v2.schema_models._field_models import FileObj from designsafe.apps.api.projects_v2.models.project_metadata import ProjectMetadata from designsafe.apps.api.projects_v2.operations.project_meta_operations import ( @@ -98,7 +99,7 @@ def get_files_for_entity( file_meta = resp.json()["result"][0] entity_file_objs.append( FileObj( - name=file_meta["name"], + name=os.path.basename(file_meta["path"]), system=file_meta["system"], path=file_meta["path"], type=file_meta["type"], diff --git a/designsafe/apps/api/projects_v2/migration_utils/graph_constructor.py b/designsafe/apps/api/projects_v2/migration_utils/graph_constructor.py index 5650bff5c5..0c0d9c2d80 100644 --- a/designsafe/apps/api/projects_v2/migration_utils/graph_constructor.py +++ b/designsafe/apps/api/projects_v2/migration_utils/graph_constructor.py @@ -3,10 +3,11 @@ import json from typing import TypedDict, Optional from uuid import uuid4 +import copy from pathlib import Path import networkx as nx from django.utils.text import slugify -from designsafe.apps.api.agave import service_account +from designsafe.apps.api.agave import get_service_account_client_v2 as service_account from designsafe.apps.data.models.elasticsearch import IndexedPublication from designsafe.apps.projects.managers.publication import FIELD_MAP from designsafe.apps.api.projects_v2.schema_models import PATH_SLUGS @@ -158,6 +159,7 @@ def construct_graph_recurse( entity_list: list[dict], parent: dict, parent_node_id: str, + allow_published_analysis=False, ): """Recurse through an entity's children and add nodes/edges. B is a child of A if all of A's descendants are referenced in B's association IDs.""" @@ -170,9 +172,17 @@ def construct_graph_recurse( ]: association_path.pop(-2) + # Account for legacy pubs that associated experimental analysis at top level + _allowed_relations = copy.deepcopy(ALLOWED_RELATIONS) + + if allow_published_analysis: + _allowed_relations[names.PROJECT].append(names.EXPERIMENT_ANALYSIS) + _allowed_relations[names.PROJECT].append(names.SIMULATION_ANALYSIS) + _allowed_relations[names.PROJECT].append(names.SIMULATION_REPORT) + children = filter( lambda child: ( - child["name"] in ALLOWED_RELATIONS.get(parent["name"], []) + child["name"] in _allowed_relations.get(parent["name"], []) and set(child["associationIds"]) >= set(association_path) ), entity_list, @@ -196,7 +206,13 @@ def construct_graph_recurse( } graph.add_node(child_node_id, **child_data) graph.add_edge(parent_node_id, child_node_id) - construct_graph_recurse(graph, entity_list, child, child_node_id) + construct_graph_recurse( + graph, + entity_list, + child, + child_node_id, + allow_published_analysis=allow_published_analysis, + ) def get_entities_from_publication(project_id: str, version=None): @@ -215,7 +231,9 @@ def get_entities_from_publication(project_id: str, version=None): return entity_list -def construct_publication_graph(project_id, version=None) -> nx.DiGraph: +def construct_publication_graph( + project_id, version=None, allow_published_analysis=False +) -> nx.DiGraph: """Construct a directed graph from a publications's association IDs.""" entity_listing = get_entities_from_publication(project_id, version=version) root_entity = entity_listing[0] @@ -225,7 +243,7 @@ def construct_publication_graph(project_id, version=None) -> nx.DiGraph: project_type = root_entity["value"]["projectType"] root_node_id = "NODE_ROOT" - if project_type == "other": + if project_type == "other" or project_type == "field_reconnaissance": root_node_data = {"uuid": None, "name": None, "projectType": "other"} else: root_node_data = { @@ -234,7 +252,7 @@ def construct_publication_graph(project_id, version=None) -> nx.DiGraph: "projectType": root_entity["value"]["projectType"], } pub_graph.add_node(root_node_id, **root_node_data) - if project_type == "other": + if project_type == "other" or project_type == "field_reconnaissance": base_node_data = { "uuid": root_entity["uuid"], "name": root_entity["name"], @@ -243,7 +261,13 @@ def construct_publication_graph(project_id, version=None) -> nx.DiGraph: base_node_id = f"NODE_{uuid4()}" pub_graph.add_node(base_node_id, **base_node_data) pub_graph.add_edge(root_node_id, base_node_id) - construct_graph_recurse(pub_graph, entity_listing, root_entity, root_node_id) + construct_graph_recurse( + pub_graph, + entity_listing, + root_entity, + root_node_id, + allow_published_analysis=allow_published_analysis, + ) pub_graph.nodes["NODE_ROOT"]["basePath"] = f"/{project_id}" # pub_graph = construct_entity_filepaths(entity_listing, pub_graph, version) @@ -325,7 +349,11 @@ def transform_pub_entities(project_id: str, version: Optional[int] = None): """Validate publication entities against their corresponding model.""" entity_listing = get_entities_from_publication(project_id, version=version) base_pub_meta = IndexedPublication.from_id(project_id, revision=version).to_dict() - pub_graph = construct_publication_graph(project_id, version) + schema_version = base_pub_meta.get("version", 1) + + pub_graph = construct_publication_graph( + project_id, version, allow_published_analysis=(schema_version == 1) + ) path_mappings = [] for _, node_data in pub_graph.nodes.items(): @@ -366,7 +394,7 @@ def transform_pub_entities(project_id: str, version: Optional[int] = None): ) else: pub_graph.nodes[pub]["version"] = 1 - pub_graph.nodes[pub]["publicationDate"] = str(base_pub_meta["created"]) + pub_graph.nodes[pub]["publicationDate"] = base_pub_meta["created"].isoformat() pub_graph.nodes[pub]["status"] = "published" return pub_graph, path_mappings diff --git a/designsafe/apps/api/projects_v2/migration_utils/project_db_ingest.py b/designsafe/apps/api/projects_v2/migration_utils/project_db_ingest.py index 6d968fed4e..b8ed38c96c 100644 --- a/designsafe/apps/api/projects_v2/migration_utils/project_db_ingest.py +++ b/designsafe/apps/api/projects_v2/migration_utils/project_db_ingest.py @@ -72,6 +72,10 @@ def ingest_entities_by_name(name): entities = iterate_entities(name) for entity in entities: schema_model = SCHEMA_MAPPING[entity["name"]] + if entity["value"].get("fileObjs"): + entity["value"]["fileObjs"] = [ + e for e in entity["value"]["fileObjs"] if e.get("path", None) + ] try: value_model = schema_model.model_validate(entity["value"]) except ValidationError as err: @@ -126,7 +130,7 @@ def fix_authors(meta: ProjectMetadata): base_project = meta.base_project def get_complete_author(partial_author): - if partial_author["name"] and not partial_author["guest"]: + if partial_author.get("name") and not partial_author.get("guest"): author_info = next( ( user @@ -143,7 +147,7 @@ def get_complete_author(partial_author): meta.value["authors"] = [ get_complete_author(author) for author in meta.value["authors"] - if author["authorship"] is True + if author.get("authorship", True) is True ] if meta.value.get("projectType") == "other": @@ -153,13 +157,22 @@ def get_complete_author(partial_author): meta.value["dataCollectors"] = [ get_complete_author(author) for author in meta.value["dataCollectors"] - if author["authorship"] is True + if author.get("authorship", True) is True ] schema_model = SCHEMA_MAPPING[meta.name] schema_model.model_validate(meta.value) meta.save() +def fix_modified_dates(): + """Set last_updated time to match existing metadata""" + name = "designsafe.project" + for project_meta in iterate_entities(name): + ProjectMetadata.objects.filter(uuid=project_meta["uuid"]).update( + last_updated=project_meta["lastUpdated"] + ) + + def ingest_v2_projects(): """Perform a complete ingest of Tapis V2 projects into the db.""" ingest_base_projects() @@ -167,6 +180,7 @@ def ingest_v2_projects(): ingest_graphs() for meta in ProjectMetadata.objects.exclude(name="designsafe.project.graph"): fix_authors(meta) + fix_modified_dates() def ingest_publications(): @@ -209,13 +223,33 @@ def ingest_publications(): def ingest_tombstones(): """Ingest Elasticsearch tombstones into the db""" + tombstone_ids = [ + "PRJ-1945", + "PRJ-1895", + "PRJ-2329", + "PRJ-2016", + "PRJ-2227", + "PRJ-2420", + "PRJ-3815", + "PRJ-3908", + "PRJ-4151", + "PRJ-4014", + ] all_pubs = ( - IndexedPublication.search().filter(Q("term", status="tombstone")).execute().hits + IndexedPublication.search() + .filter( + Q("term", status="tombstone") + | Q("terms", **{"projectId._exact": tombstone_ids}) + ) + .execute() + .hits ) print(all_pubs) for pub in all_pubs: try: pub_graph = combine_pub_versions(pub["projectId"]) + for published_entity_node_id in pub_graph.successors("NODE_ROOT"): + pub_graph.nodes[published_entity_node_id]["value"]["tombstone"] = True latest_version: int = IndexedPublication.max_revision(pub["projectId"]) or 1 pub_base = next( ( diff --git a/designsafe/apps/api/projects_v2/migration_utils/publication_transforms.py b/designsafe/apps/api/projects_v2/migration_utils/publication_transforms.py index 0f794dfa12..6cbba3be24 100644 --- a/designsafe/apps/api/projects_v2/migration_utils/publication_transforms.py +++ b/designsafe/apps/api/projects_v2/migration_utils/publication_transforms.py @@ -76,6 +76,9 @@ def convert_v2_user(user): role = "team_member" username = user["name"] + if not user.get("fname"): + user = {**user, **get_user_info(user["name"], role)} + return { "fname": user["fname"], "lname": user["lname"], @@ -279,6 +282,8 @@ def transform_entity(entity: dict, base_pub_meta: dict, base_path: str): reprsentation of the `value` attribute.""" model = SCHEMA_MAPPING[entity["name"]] authors = entity["value"].get("authors", None) + if not authors: + authors = entity.get("authors", None) schema_version = base_pub_meta.get("version", 1) if authors and schema_version == 1: updated_authors = get_v1_authors(entity, base_pub_meta["users"]) @@ -287,6 +292,24 @@ def transform_entity(entity: dict, base_pub_meta: dict, base_path: str): fixed_authors = list(map(convert_v2_user, entity["authors"])) entity["value"]["authors"] = sorted(fixed_authors, key=lambda a: a["order"]) + data_collectors = entity["value"].get("dataCollectors", None) + if data_collectors: + fixed_data_collectors = [] + for collector in data_collectors: + if collector.get("guest", None): + fixed_data_collectors.append(collector) + else: + fixed_data_collectors.append( + {**collector, **get_user_info(collector["name"])} + ) + entity["value"]["dataCollectors"] = sorted( + fixed_data_collectors, key=lambda a: a["order"] + ) + + legacy_doi = entity.get("doi", None) + if legacy_doi: + entity["value"]["dois"] = [legacy_doi.lstrip("doi:")] + tombstone_uuids = base_pub_meta.get("tombstone", []) if entity["uuid"] in tombstone_uuids: entity["value"]["tombstone"] = True @@ -302,16 +325,18 @@ def transform_entity(entity: dict, base_pub_meta: dict, base_path: str): file_objs = entity.get("fileObjs", None) # Some legacy experiment/hybrid sim entities have file_objs incorrectly # populated from their children. In these cases, _filepaths is empty. - if file_objs and entity.get("_filePaths", None) != []: + if file_objs and not ( + entity.get("_filePaths", None) == [] + and ( + entity["name"] + in [ + "designsafe.project.experiment", + "designsafe.project.hybrid_simulation", + ] + ) + ): entity["value"]["fileObjs"] = file_objs # Avoid "fixing" tags for legacy projects that don't have tree-based file layouts - if entity["value"].get("fileTags", False): - # entity["value"]["fileTags"] = update_file_tag_paths( - # entity, base_path - # ) - entity["value"]["fileTags"] = update_file_tag_paths_legacy( - entity, base_path - ) # new_file_objs, path_mapping = update_file_objs( # entity, base_path, system_id=settings.PUBLISHED_SYSTEM @@ -323,6 +348,13 @@ def transform_entity(entity: dict, base_pub_meta: dict, base_path: str): entity["value"]["fileObjs"] = new_file_objs else: path_mapping = {} + + if entity["value"].get("fileTags", False): + # entity["value"]["fileTags"] = update_file_tag_paths( + # entity, base_path + # ) + entity["value"]["fileTags"] = update_file_tag_paths_legacy(entity, base_path) + validated_model = model.model_validate(entity["value"]) if getattr(validated_model, "project_type", None) == "other": diff --git a/designsafe/apps/api/projects_v2/operations/_tests/publish_unit_test.py b/designsafe/apps/api/projects_v2/operations/_tests/publish_unit_test.py index c8a222faea..8405870e28 100644 --- a/designsafe/apps/api/projects_v2/operations/_tests/publish_unit_test.py +++ b/designsafe/apps/api/projects_v2/operations/_tests/publish_unit_test.py @@ -22,56 +22,6 @@ ) -@pytest.fixture -def project_with_associations(): - project_value = { - "title": "Test Project", - "projectId": "PRJ-1234", - "users": [], - "projectType": "experimental", - } - experiment_value = {"title": "Test Experiment", "description": "Experiment test"} - model_config_value = { - "title": "Test Entity", - "description": "Entity with file associations", - } - project = create_project_metdata(project_value) - initialize_project_graph("PRJ-1234") - - experiment = create_entity_metadata( - "PRJ-1234", name=constants.EXPERIMENT, value=experiment_value - ) - model_config = create_entity_metadata( - "PRJ-1234", name=constants.EXPERIMENT_MODEL_CONFIG, value=model_config_value - ) - - experiment_node = add_node_to_project( - "PRJ-1234", "NODE_ROOT", experiment.uuid, experiment.name - ) - add_node_to_project( - "PRJ-1234", experiment_node, model_config.uuid, model_config.name - ) - - file_objs = [ - FileObj( - system="project.system", name="file1", path="/path/to/file1", type="file" - ), - FileObj( - system="project.system", - name="file1", - path="/path/to/other/file1", - type="file", - ), - FileObj(system="project.system", name="dir1", path="/path/to/dir1", type="dir"), - ] - add_file_associations(model_config.uuid, file_objs) - set_file_tags(model_config.uuid, "/path/to/file1", ["test_tag"]) - set_file_tags(model_config.uuid, "/path/to/dir1/nested/file", ["test_tag"]) - set_file_tags(model_config.uuid, "/path/to/other/file1", ["test_tag"]) - - yield (project, experiment.uuid, project.uuid) - - @pytest.mark.django_db def test_publication_subtree(project_with_associations): (project, exp_uuid, project_uuid) = project_with_associations diff --git a/designsafe/apps/api/projects_v2/operations/datacite_operations.py b/designsafe/apps/api/projects_v2/operations/datacite_operations.py index 21e4b48937..1f7e978ef5 100644 --- a/designsafe/apps/api/projects_v2/operations/datacite_operations.py +++ b/designsafe/apps/api/projects_v2/operations/datacite_operations.py @@ -97,6 +97,7 @@ def get_datacite_json( datacite_json["types"]["resourceType"] += f"/{location}" datacite_json["types"]["resourceTypeGeneral"] = "Dataset" + datacite_json["version"] = version datacite_json["descriptions"] = [ { diff --git a/designsafe/apps/api/projects_v2/operations/graph_operations.py b/designsafe/apps/api/projects_v2/operations/graph_operations.py index 9661234820..c8963bab5b 100644 --- a/designsafe/apps/api/projects_v2/operations/graph_operations.py +++ b/designsafe/apps/api/projects_v2/operations/graph_operations.py @@ -170,6 +170,23 @@ def remove_nodes_from_project(project_id: str, node_ids: list[str]): graph_model.save() +def remove_nodes_for_entity(project_id: str, entity_uuid: str): + """ + Remove an entity from the tree by deleting all nodes that point to its UUID. + """ + graph_model = ProjectMetadata.objects.get( + name=constants.PROJECT_GRAPH, base_project__value__projectId=project_id + ) + project_graph = nx.node_link_graph(graph_model.value) + nodes_to_remove = [ + node + for node in project_graph.nodes + if project_graph.nodes[node].get("uuid") == entity_uuid + ] + if nodes_to_remove: + remove_nodes_from_project(project_id, nodes_to_remove) + + def reorder_project_nodes(project_id: str, node_id: str, new_index: int): """Update the database entry for the project graph to reorder nodes.""" # Lock the project graph's tale row to prevent conflicting updates. diff --git a/designsafe/apps/api/projects_v2/operations/project_archive_operations.py b/designsafe/apps/api/projects_v2/operations/project_archive_operations.py index d94a4031fc..58b47b4feb 100644 --- a/designsafe/apps/api/projects_v2/operations/project_archive_operations.py +++ b/designsafe/apps/api/projects_v2/operations/project_archive_operations.py @@ -8,6 +8,7 @@ import zipfile from typing import Optional from django.conf import settings +from celery import shared_task from designsafe.apps.api.publications_v2.models import Publication logger = logging.getLogger(__name__) @@ -85,3 +86,9 @@ def create_metadata(): set_perms(arc_dir, 0o755) create_metadata() create_archive() + + +@shared_task +def archive_publication_async(project_id: str, version: Optional[str] = 1): + """async wrapper around archive""" + archive(project_id, version) diff --git a/designsafe/apps/api/projects_v2/operations/project_meta_operations.py b/designsafe/apps/api/projects_v2/operations/project_meta_operations.py index 45abdf3820..51e972b51f 100644 --- a/designsafe/apps/api/projects_v2/operations/project_meta_operations.py +++ b/designsafe/apps/api/projects_v2/operations/project_meta_operations.py @@ -7,6 +7,7 @@ FileObj, FileTag, PartialEntityWithFiles, + BaseProject, ) from designsafe.apps.api.projects_v2 import constants from designsafe.apps.api.projects_v2.models import ProjectMetadata @@ -57,6 +58,7 @@ def delete_entity(uuid: str): if entity.name in (constants.PROJECT, constants.PROJECT_GRAPH): raise ValueError("Cannot delete a top-level project or graph object.") entity.delete() + return "OK" @@ -72,9 +74,33 @@ def clear_entities(project_id): return "OK" +def get_changed_users(old_value: BaseProject, new_value: BaseProject): + """ + Diff users between incoming and existing project metadata to determine which users + need permissions to be added/removed via Tapis. + """ + old_users = set( + (u.username for u in old_value.users if u.username and u.role != "guest") + ) + new_users = set( + (u.username for u in new_value.users if u.username and u.role != "guest") + ) + + users_to_add = list(new_users - old_users) + users_to_remove = list(old_users - new_users) + + return users_to_add, users_to_remove + + def change_project_type(project_id, new_value): """Change the type of a project and update its value.""" project = ProjectMetadata.get_project_by_id(project_id) + + # persist Hazmapper maps when changing project type + hazmapper_maps = project.value.get("hazmapperMaps", None) + if hazmapper_maps: + new_value["hazmapperMaps"] = hazmapper_maps + schema_model = SCHEMA_MAPPING[constants.PROJECT] validated_model = schema_model.model_validate(new_value) project.value = validated_model.model_dump() @@ -138,6 +164,20 @@ def remove_file_associations(uuid: str, file_paths: list[str]): filtered_file_objs = _filter_file_objs(entity_file_model.file_objs, file_paths) entity.value["fileObjs"] = [f.model_dump() for f in filtered_file_objs] + + # Remove tags associated with these entity/file path combinations. + tagged_paths = [] + for path in file_paths: + tagged_paths += [ + t["path"] + for t in entity.value.get("fileTags", []) + if t["path"].startswith(path) + ] + entity.value["fileTags"] = [ + t + for t in entity.value.get("fileTags", []) + if not (t["path"] in tagged_paths) + ] entity.save() return entity diff --git a/designsafe/apps/api/projects_v2/operations/project_publish_operations.py b/designsafe/apps/api/projects_v2/operations/project_publish_operations.py index dbcc8ef511..41e58517ab 100644 --- a/designsafe/apps/api/projects_v2/operations/project_publish_operations.py +++ b/designsafe/apps/api/projects_v2/operations/project_publish_operations.py @@ -9,6 +9,7 @@ import logging from django.conf import settings import networkx as nx +from celery import shared_task from designsafe.apps.api.projects_v2 import constants from designsafe.apps.api.projects_v2.models.project_metadata import ProjectMetadata @@ -17,8 +18,13 @@ publish_datacite_doi, upsert_datacite_json, ) - +from designsafe.apps.api.projects_v2.operations.project_archive_operations import ( + archive_publication_async, +) from designsafe.apps.api.publications_v2.models import Publication +from designsafe.apps.api.publications_v2.elasticsearch import index_publication +from designsafe.apps.data.tasks import agave_indexer +from designsafe.apps.api.publications_v2.tasks import ingest_pub_fedora_async logger = logging.getLogger(__name__) @@ -45,8 +51,32 @@ constants.HYBRID_SIM_SIM_SUBSTRUCTURE, constants.HYBRID_SIM_EXP_SUBSTRUCTURE, ], + constants.FIELD_RECON_REPORT: [], } +ENTITIES_WITH_REQUIRED_FILES = [ + constants.EXPERIMENT_MODEL_CONFIG, + constants.EXPERIMENT_SENSOR, + constants.EXPERIMENT_EVENT, + constants.SIMULATION_MODEL, + constants.SIMULATION_INPUT, + constants.SIMULATION_OUTPUT, + constants.SIMULATION_REPORT, + constants.FIELD_RECON_SOCIAL_SCIENCE, + constants.FIELD_RECON_GEOSCIENCE, + constants.FIELD_RECON_PLANNING, + constants.HYBRID_SIM_GLOBAL_MODEL, + constants.HYBRID_SIM_COORDINATOR, + constants.HYBRID_SIM_SIM_SUBSTRUCTURE, + constants.HYBRID_SIM_EXP_SUBSTRUCTURE, + constants.HYBRID_SIM_COORDINATOR_OUTPUT, + constants.HYBRID_SIM_EXP_OUTPUT, + constants.HYBRID_SIM_SIM_OUTPUT, + constants.FIELD_RECON_REPORT, + constants.HYBRID_SIM_REPORT, + constants.HYBRID_SIM_ANALYSIS, +] + def check_missing_entities( project_id: str, entity_uuid: str, default_operator: Literal["AND", "OR"] = "AND" @@ -57,8 +87,7 @@ def check_missing_entities( (e.g. field recon missions require a planning, social science, OR geoscience colleciton but not all 3) """ - project_tree = ProjectMetadata.get_project_by_id(project_id) - project_graph: nx.DiGraph = nx.node_link_graph(project_tree.project_graph.value) + project_graph: nx.DiGraph = add_values_to_tree(project_id) entity_node = next( ( @@ -74,6 +103,7 @@ def check_missing_entities( project_graph.nodes[node] for node in nx.dfs_preorder_nodes(project_graph, entity_node) ] + logger.debug(child_nodes) missing_entities = [] for required_entity_name in REQUIRED_ENTITIES.get(entity_name, []): @@ -90,7 +120,17 @@ def check_missing_entities( # At least one of the required entity types is associated missing_entities = [] - return missing_entities + # Check for entities with missing files: + missing_file_objs = [] + for child_node in child_nodes: + if child_node["name"] in ENTITIES_WITH_REQUIRED_FILES and not child_node[ + "value" + ].get("fileObjs", []): + missing_file_objs.append( + {"name": child_node["name"], "title": child_node["value"]["title"]} + ) + + return missing_entities, missing_file_objs def validate_entity_selection(project_id: str, entity_uuids: list[str]): @@ -100,7 +140,9 @@ def validate_entity_selection(project_id: str, entity_uuids: list[str]): entity_meta = ProjectMetadata.objects.get(uuid=uuid) match entity_meta.name: case constants.EXPERIMENT | constants.SIMULATION | constants.HYBRID_SIM: - missing_entities = check_missing_entities(project_id, uuid) + missing_entities, missing_file_objs = check_missing_entities( + project_id, uuid + ) if len(missing_entities) > 0: validation_errors.append( { @@ -110,8 +152,17 @@ def validate_entity_selection(project_id: str, entity_uuids: list[str]): "missing": missing_entities, } ) - case constants.FIELD_RECON_MISSION: - missing_entities = check_missing_entities( + for missing_file_obj in missing_file_objs: + validation_errors.append( + { + "errorType": "MISSING_FILES", + "name": missing_file_obj["name"], + "title": missing_file_obj["title"], + } + ) + + case constants.FIELD_RECON_MISSION | constants.FIELD_RECON_REPORT: + missing_entities, missing_file_objs = check_missing_entities( project_id, uuid, default_operator="OR" ) if len(missing_entities) > 0: @@ -123,6 +174,14 @@ def validate_entity_selection(project_id: str, entity_uuids: list[str]): "missing": missing_entities, } ) + for missing_file_obj in missing_file_objs: + validation_errors.append( + { + "errorType": "MISSING_FILES", + "name": missing_file_obj["name"], + "title": missing_file_obj["title"], + } + ) return validation_errors @@ -315,40 +374,52 @@ def copy_publication_files( `path_mapping` is a dict mapping project paths to their corresponding paths in the published area. """ - pub_dirname = project_id - if version and version > 1: - pub_dirname = f"{project_id}v{version}" - - pub_root_dir = str(Path(f"{settings.DESIGNSAFE_PUBLISHED_PATH}") / pub_dirname) - os.makedirs(pub_root_dir, exist_ok=True) - - for src_path in path_mapping: - src_path_obj = Path(src_path) - if not src_path_obj.exists(): - raise ProjectFileNotFound(f"File not found: {src_path}") - - os.makedirs(src_path_obj.parent, exist_ok=True) - - if src_path_obj.is_dir(): - shutil.copytree( - src_path, - path_mapping[src_path], - dirs_exist_ok=True, - symlinks=True, - copy_function=shutil.copy, - ) - else: - shutil.copy(src_path, path_mapping[src_path]) - - # Lock the publication directory so that non-root users can only read files and list directories - subprocess.run(["chmod", "-R", "a-x,a=rX", pub_root_dir], check=True) + os.chmod("/corral-repl/tacc/NHERI/published", 0o755) + try: + pub_dirname = project_id + if version and version > 1: + pub_dirname = f"{project_id}v{version}" + + pub_root_dir = str(Path(f"{settings.DESIGNSAFE_PUBLISHED_PATH}") / pub_dirname) + os.makedirs(pub_root_dir, exist_ok=True) + + for src_path in path_mapping: + src_path_obj = Path(src_path) + if not src_path_obj.exists(): + raise ProjectFileNotFound(f"File not found: {src_path}") + + dest_path_obj = Path(path_mapping[src_path]) + os.makedirs(dest_path_obj.parent, exist_ok=True) + + if src_path_obj.is_dir(): + shutil.copytree( + src_path, + path_mapping[src_path], + dirs_exist_ok=True, + copy_function=shutil.copy, + ) + else: + shutil.copy(src_path, path_mapping[src_path]) + + # Lock the publication directory so that non-root users can only read files and list directories + subprocess.run(["chmod", "-R", "a-x,a=rX", pub_root_dir], check=True) + agave_indexer.apply_async( + kwargs={ + "systemId": "designsafe.storage.published", + "filePath": pub_dirname, + "recurse": True, + }, + queue="indexing", + ) + finally: + os.chmod("/corral-repl/tacc/NHERI/published", 0o555) # pylint: disable=too-many-locals, too-many-branches, too-many-statements def publish_project( project_id: str, entity_uuids: list[str], - version: Optional[int] = None, + version: Optional[int] = 1, version_info: Optional[str] = None, dry_run: bool = False, ): @@ -369,6 +440,10 @@ def publish_project( if dry_run: return pub_tree, path_mapping + if not settings.DEBUG: + # Copy files first so if it fails we don't create orphan metadata/datacite entries. + copy_publication_files(path_mapping, project_id, version=version) + new_dois = [] for entity_uuid in entity_uuids: @@ -376,7 +451,7 @@ def publish_project( existing_dois = entity_meta.value.get("dois", []) existing_doi = next(iter(existing_dois), None) - datacite_json = get_datacite_json(pub_tree, entity_uuid) + datacite_json = get_datacite_json(pub_tree, entity_uuid, version) datacite_resp = upsert_datacite_json(datacite_json, doi=existing_doi) doi = datacite_resp["data"]["id"] new_dois.append(doi) @@ -393,7 +468,6 @@ def publish_project( pub_tree.nodes[node]["value"]["dois"] = [doi] if not settings.DEBUG: - copy_publication_files(path_mapping, project_id) for doi in new_dois: publish_datacite_doi(doi) @@ -413,4 +487,93 @@ def publish_project( ) pub_metadata.save() + index_publication(project_id) + if not settings.DEBUG: + archive_publication_async.apply_async( + args=[project_id, version], queue="default" + ) + ingest_pub_fedora_async.apply_async( + args=[project_id, version, True], queue="default" + ) + return pub_metadata + + +@shared_task +def publish_project_async( + project_id: str, + entity_uuids: list[str], + version: Optional[int] = 1, + version_info: Optional[str] = None, + dry_run: bool = False, +): + """Async wrapper arount publication""" + publish_project(project_id, entity_uuids, version, version_info, dry_run) + + +def amend_publication(project_id: str): + """ + Update metadata values in a publication to match the latest changes made in the + underlying project. Does NOT affect file associations or tags. + """ + + pub_root = Publication.objects.get(project_id=project_id) + pub_tree: nx.DiGraph = nx.node_link_graph(pub_root.tree) + latest_version = max( + pub_tree.nodes[node]["version"] for node in pub_tree.successors("NODE_ROOT") + ) + pubs_to_amend = [ + node + for node in pub_tree.successors("NODE_ROOT") + if pub_tree.nodes[node]["version"] == latest_version + ] + + for pub_node in pubs_to_amend: + for node in nx.dfs_preorder_nodes(pub_tree, pub_node): + uuid = pub_tree.nodes[node]["uuid"] + published_meta_value = pub_tree.nodes[node]["value"] + try: + prj_meta_value = ProjectMetadata.objects.get(uuid=uuid).value + prj_meta_value.pop("fileObjs", None) + prj_meta_value.pop("fileTags", None) + amended_meta_value = {**published_meta_value, **prj_meta_value} + pub_tree.nodes[node]["value"] = amended_meta_value + except ProjectMetadata.DoesNotExist: + continue + + base_prj_meta_value = ProjectMetadata.get_project_by_id(project_id).value + base_prj_meta_value.pop("fileObjs", None) + base_prj_meta_value.pop("fileTags", None) + + # If not type Other, we also amend the NODE_ROOT metadata. + if pub_tree.nodes["NODE_ROOT"].get("uuid", None): + base_published_meta_value = pub_tree.nodes["NODE_ROOT"]["value"] + amended_root_meta_value = {**base_published_meta_value, **base_prj_meta_value} + pub_tree.nodes["NODE_ROOT"]["value"] = amended_root_meta_value + + pub_root.tree = nx.node_link_data(pub_tree) + pub_root.value = {**pub_root.value, **base_prj_meta_value} + pub_root.save() + + # Update datacite metadata + for node in pubs_to_amend: + datacite_json = get_datacite_json( + pub_tree, pub_tree.nodes[node]["uuid"], latest_version + ) + upsert_datacite_json( + datacite_json, doi=pub_tree.nodes[node]["value"]["dois"][0] + ) + + # Index publication in Elasticsearch + index_publication(project_id) + + if not settings.DEBUG: + ingest_pub_fedora_async.apply_async( + args=[project_id, latest_version, True], queue="default" + ) + + +@shared_task +def amend_publication_async(project_id: str): + """async wrapper around amend_publication""" + amend_publication(project_id) diff --git a/designsafe/apps/api/projects_v2/operations/project_system_operations.py b/designsafe/apps/api/projects_v2/operations/project_system_operations.py index af5e2966dc..743cd16b4f 100644 --- a/designsafe/apps/api/projects_v2/operations/project_system_operations.py +++ b/designsafe/apps/api/projects_v2/operations/project_system_operations.py @@ -1 +1,217 @@ """Utilities for creating project systems and managing access permissions.""" + +# from portal.utils.encryption import createKeyPair +import logging +from typing import Literal +from tapipy.tapis import Tapis +from django.conf import settings +import celery +from designsafe.apps.api.agave import service_account +from celery import shared_task + +# from portal.apps.onboarding.steps.system_access_v3 import create_system_credentials, register_public_key + + +logger = logging.getLogger(__name__) + + +def set_workspace_permissions( + client: Tapis, username: str, system_id: str, role: str = "writer" +): + """Apply read/write/execute permissions to a user on a system.""" + + system_pems = {"reader": ["READ", "EXECUTE"], "writer": ["READ", "EXECUTE"]} + + files_pems = {"reader": "READ", "writer": "MODIFY"} + + logger.info(f"Adding {username} permissions to Tapis system {system_id}") + client.systems.grantUserPerms( + systemId=system_id, userName=username, permissions=system_pems[role] + ) + + if role == "reader": + client.systems.revokeUserPerms( + systemId=system_id, userName=username, permissions=["MODIFY"] + ) + client.files.deletePermissions(systemId=system_id, path="/", username=username) + + client.files.grantPermissions( + systemId=system_id, path="/", username=username, permission=files_pems[role] + ) + + +def set_workspace_acls(client, system_id, path, username, operation, role="writer"): + operation_map = {"add": "ADD", "remove": "REMOVE"} + + acl_string_map = { + "reader": f"d:u:{username}:rX,u:{username}:rX", + "writer": f"d:u:{username}:rwX,u:{username}:rwX", + "none": f"d:u:{username},u:{username}", + } + + client.files.setFacl( + systemId=system_id, + path=path, + operation=operation_map[operation], + recursionMethod="PHYSICAL", + aclString=acl_string_map[role], + ) + + +def submit_workspace_acls_job( + username: str, project_uuid: str, action=Literal["add", "remove"] +): + """ + Submit a job to set ACLs on a project for a specific user. This should be used if + we are setting ACLs on an existing project, since there might be too many files for + the synchronous Tapis endpoint to be performant. + """ + client = service_account() + + job_body = { + "name": f"setfacl-project-{project_uuid.split('-')[0]}-{username}-{action}", + "appId": "setfacl-corral-tg458981", + "appVersion": "0.0.1", + "description": "Add/Remove ACLs on a directory", + "fileInputs": [], + "parameterSet": { + "appArgs": [], + "schedulerOptions": [], + "envVariables": [ + {"key": "username", "value": username}, + { + "key": "directory", + "value": f"/corral/projects/NHERI/projects/{project_uuid}", + }, + {"key": "action", "value": action}, + ], + }, + "tags": ["portalName:designsafe"], + } + res = client.jobs.submitJob(**job_body) + return res + + +def create_workspace_dir(project_uuid: str) -> str: + client = service_account() + system_id = "designsafe.storage.projects" + path = f"{project_uuid}" + client.files.mkdir(systemId=system_id, path=path) + return path + + +def create_workspace_system(client, project_uuid: str) -> str: + system_id = f"project-{project_uuid}" + system_args = { + "id": system_id, + "host": "cloud.corral.tacc.utexas.edu", + "port": 22, + "systemType": "LINUX", + "defaultAuthnMethod": "PKI_KEYS", + "canExec": False, + "rootDir": f"/corral-repl/projects/NHERI/projects/{project_uuid}", + "effectiveUserId": "tg458981", + "authnCredential": { + "privateKey": settings.PROJECT_STORAGE_SYSTEM_CREDENTIALS["privateKey"], + "publicKey": settings.PROJECT_STORAGE_SYSTEM_CREDENTIALS["username"], + }, + } + + client.systems.createSystem(**system_args) + return system_id + + +def increment_workspace_count(force=None) -> int: + client = service_account() + root_sys = client.systems.getSystem(systemId="designsafe.storage.projects") + new_count = int(root_sys.notes.count) + 1 + + # Allow manual setting of the increment. + if force: + new_count = force + + client.systems.patchSystem( + systemId="designsafe.storage.projects", notes={"count": new_count} + ) + return new_count + + +########################################## +# HIGH-LEVEL OPERATIONS TIED TO API ROUTES +########################################## + + +def setup_project_file_system(project_uuid: str, users: list[str]): + """ + Create a workspace system owned by user whose client is passed. + """ + service_client = service_account() + + # Service client creates directory and gives owner write permissions + create_workspace_dir(project_uuid) + + # User creates the system and adds their credential + resp = create_workspace_system(service_client, project_uuid) + + for username in users: + add_user_to_project_async.apply_async(args=[project_uuid, username]) + + return resp + + +def add_user_to_project(project_uuid: str, username: str, set_acls=True): + """ + Give a user POSIX and Tapis permissions on a workspace system. + """ + service_client = service_account() + system_id = f"project-{project_uuid}" + logger.debug("Adding user %s to system %s", username, system_id) + if set_acls: + job_res = submit_workspace_acls_job(username, project_uuid, action="add") + logger.debug( + "Submitted workspace ACL job %s with UUID %s", job_res.name, job_res.uuid + ) + service_client.systems.shareSystem(systemId=system_id, users=[username]) + set_workspace_permissions(service_client, username, system_id, role="writer") + + return project_uuid + + +def remove_user_from_project(project_uuid: str, username: str): + """ + Unshare the system and remove all permissions and credentials. + """ + service_client = service_account() + system_id = f"project-{project_uuid}" + logger.debug("Removing user %s from system %s", username, system_id) + job_res = submit_workspace_acls_job(username, project_uuid, action="remove") + logger.debug( + "Submitted workspace ACL job %s with UUID %s", job_res.name, job_res.uuid + ) + + service_client.systems.unShareSystem(systemId=system_id, users=[username]) + service_client.systems.revokeUserPerms( + systemId=system_id, userName=username, permissions=["READ", "MODIFY", "EXECUTE"] + ) + service_client.files.deletePermissions( + systemId=system_id, username=username, path="/" + ) + + return project_uuid + + +########################################## +# ASYNC TASKS FOR USER ADDITION/REMOVAL +########################################## + + +@shared_task(bind=True) +def add_user_to_project_async(self, project_uuid: str, username: str): + """Async wrapper around add_user_to_project""" + add_user_to_project(project_uuid, username) + + +@shared_task(bind=True) +def remove_user_from_project_async(self, project_uuid: str, username: str): + """Async wrapper around remove_user_from_project""" + remove_user_from_project(project_uuid, username) diff --git a/designsafe/apps/api/projects_v2/schema_models/_field_models.py b/designsafe/apps/api/projects_v2/schema_models/_field_models.py index d138b305d1..69223c3a6a 100644 --- a/designsafe/apps/api/projects_v2/schema_models/_field_models.py +++ b/designsafe/apps/api/projects_v2/schema_models/_field_models.py @@ -26,6 +26,10 @@ def model_dump(self, *args, **kwargs): *args, **kwargs ) + def to_fedora_json(self) -> dict: + """Placeholder method for formatting metadata for Fedora.""" + return {} + class ProjectUser(MetadataModel): """Model for project users.""" @@ -56,7 +60,7 @@ def from_username(cls, username: str, role: str = "team_member", **kwargs): email=user_obj.email, inst=user_obj.profile.institution, role=role, - **kwargs + **kwargs, ) except user_model.DoesNotExist: try: @@ -71,7 +75,7 @@ def from_username(cls, username: str, role: str = "team_member", **kwargs): email=tas_user["email"], inst=tas_user["institution"], role=role, - **kwargs + **kwargs, ) # pylint:disable=broad-exception-caught except Exception as _: @@ -96,9 +100,9 @@ class ProjectAward(MetadataModel): """Model for awards.""" order: int = 0 - name: Annotated[ - str, BeforeValidator(lambda n: n if isinstance(n, str) else "") - ] = "" + name: Annotated[str, BeforeValidator(lambda n: n if isinstance(n, str) else "")] = ( + "" + ) number: str = "" funding_source: Optional[str] = None @@ -117,6 +121,15 @@ class AssociatedProject(MetadataModel): # Some legacy projects have a doi attribute. doi: str = "" + def to_fedora_json(self) -> dict: + if self.type == "Linked Dataset": + return {"isPartOf": f"{self.title} ({self.href})"} + if self.type == "Context": + return {"references": f"{self.title} ({self.href})"} + if self.type == "Cited By": + return {"isReferencedBy": f"{self.title} ({self.href})"} + return {} + class ReferencedWork(MetadataModel): """Model for referenced works.""" @@ -125,6 +138,9 @@ class ReferencedWork(MetadataModel): doi: str = Field(validation_alias=AliasChoices("doi", "url")) href_type: str = "URL" + def to_fedora_json(self): + return {"references": f"{self.title} ({self.doi})"} + class FileTag(MetadataModel): """Model for file tags.""" diff --git a/designsafe/apps/api/projects_v2/schema_models/base.py b/designsafe/apps/api/projects_v2/schema_models/base.py index ad39660b79..d042b8d97b 100644 --- a/designsafe/apps/api/projects_v2/schema_models/base.py +++ b/designsafe/apps/api/projects_v2/schema_models/base.py @@ -1,4 +1,5 @@ """Pydantic schema models for base-level project entities.""" + from datetime import datetime from typing import Literal, Optional, Annotated from pydantic import ( @@ -173,3 +174,63 @@ def post_validate(self): if (not self.users) and (users := self.construct_users()): self.users = users return self + + def to_fedora_json(self): + """format project metadata for Fedora""" + fedora_json = {} + fedora_json["title"] = self.title + fedora_json["description"] = self.description + fedora_json["identifier"] = [ + self.project_id, + f"https://www.designsafe-ci.org/data/browser/public/designsafe.storage.published/{self.project_id}", + ] + if self.dois: + fedora_json["identifier"] += self.dois + + fedora_json["coverage"] = [] + for nh_event in self.nh_events: + if nh_event.event_start: + fedora_json["coverage"].append(nh_event.event_start.isoformat()) + if nh_event.event_end: + fedora_json["coverage"].append(nh_event.event_end.isoformat()) + fedora_json["coverage"].append(nh_event.location) + + fedora_json["subject"] = self.keywords + if self.nh_event: + fedora_json["subject"].append(self.nh_event) + if self.fr_types: + fedora_json["subject"] += [t.name for t in self.fr_types] + if self.nh_types: + fedora_json["subject"] += [t.name for t in self.nh_types] + fedora_json["subject"] = [s for s in fedora_json["subject"] if s] + + fedora_json["contributors"] = [] + for award in self.award_numbers: + fedora_json["contributors"].append(award.name) + fedora_json["contributors"].append(award.number) + for facility in self.facilities: + fedora_json["contributors"].append(facility.name) + fedora_json["contributors"] = [c for c in fedora_json["contributors"] if c] + + fedora_json["type"] = self.project_type + if self.project_type == "other": + fedora_json["type"] = [t.name for t in self.data_types] + + fedora_json["creator"] = [ + f"{author.lname}, {author.fname}" for author in self.authors + ] + if self.license: + fedora_json["license"] = self.license + fedora_json["publisher"] = "Designsafe" + + for referenced_data in self.referenced_data: + reference_mapping = referenced_data.to_fedora_json() + for key in reference_mapping: + fedora_json[key] = fedora_json.get(key, []) + [reference_mapping[key]] + + for related_work in self.associated_projects: + related_mapping = related_work.to_fedora_json() + for key in related_mapping: + fedora_json[key] = fedora_json.get(key, []) + [related_mapping[key]] + + return fedora_json diff --git a/designsafe/apps/api/projects_v2/schema_models/experimental.py b/designsafe/apps/api/projects_v2/schema_models/experimental.py index 491ef6ce48..07a60bc3c8 100644 --- a/designsafe/apps/api/projects_v2/schema_models/experimental.py +++ b/designsafe/apps/api/projects_v2/schema_models/experimental.py @@ -1,4 +1,5 @@ """Pydantic models for Experimental entities""" + import itertools from typing import Optional, Annotated from pydantic import BeforeValidator, Field, ConfigDict, model_validator, AliasChoices @@ -57,8 +58,8 @@ class Experiment(MetadataModel): ] = None equipment_type_other: str = Field(default="", exclude=True) - procedure_start: str = "" - procedure_end: str = "" + procedure_start: Optional[str] = None + procedure_end: Optional[str] = None authors: Annotated[list[ProjectUser], BeforeValidator(handle_legacy_authors)] = [] project: list[str] = [] @@ -89,6 +90,40 @@ def handle_other(self): self.facility.name = self.experimental_facility_other return self + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "title": self.title, + "description": self.description, + "publisher": "Designsafe", + } + fedora_json["creator"] = [ + f"{author.lname}, {author.fname}" for author in self.authors + ] + if self.experiment_type: + fedora_json["type"] = self.experiment_type.name + fedora_json["identifier"] = self.dois + if self.facility: + fedora_json["contributor"] = self.facility.name + + if self.equipment_type: + fedora_json["subject"] = self.equipment_type.name + + if self.procedure_start: + fedora_json["_created"] = self.procedure_start + + for referenced_data in self.referenced_data: + reference_mapping = referenced_data.to_fedora_json() + for key in reference_mapping: + fedora_json[key] = fedora_json.get(key, []) + [reference_mapping[key]] + + for related_work in self.related_work: + related_mapping = related_work.to_fedora_json() + for key in related_mapping: + fedora_json[key] = fedora_json.get(key, []) + [related_mapping[key]] + + return fedora_json + class ExperimentModelConfig(MetadataModel): """Model for model configurations.""" @@ -113,6 +148,14 @@ class ExperimentModelConfig(MetadataModel): spatial: Optional[str] = Field(default=None, exclude=True) coverage: Optional[str] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "model configuration", + "title": self.title, + "description": self.description, + } + class ExperimentSensor(MetadataModel): """Model for sensors.""" @@ -142,6 +185,14 @@ class ExperimentSensor(MetadataModel): # This field ONLY Present on sensor 8078182091498319385-242ac11c-0001-012 load: Optional[list[str]] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "sensor information", + "title": self.title, + "description": self.description, + } + class ExperimentEvent(MetadataModel): """Model for experimental events.""" @@ -164,6 +215,10 @@ class ExperimentEvent(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) load: Optional[list[str]] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return {"type": "event", "title": self.title, "description": self.description} + class ExperimentAnalysis(MetadataModel): """Model for experimental analysis.""" @@ -182,10 +237,20 @@ class ExperimentAnalysis(MetadataModel): file_tags: list[FileTag] = [] file_objs: list[FileObj] = [] + dois: list[str] = [] + tags: Optional[dict] = Field(default=None, exclude=True) reference: Optional[str] = Field(default=None, exclude=True) referencedoi: Optional[str] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "analysis", + "title": self.title, + "description": self.description, + } + class ExperimentReport(MetadataModel): """Model for experimental reports.""" @@ -198,3 +263,7 @@ class ExperimentReport(MetadataModel): files: list[str] = [] file_tags: list[FileTag] = [] file_objs: list[FileObj] = [] + + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return {"type": "report", "title": self.title, "description": self.description} diff --git a/designsafe/apps/api/projects_v2/schema_models/field_recon.py b/designsafe/apps/api/projects_v2/schema_models/field_recon.py index f4e48fa646..3c533cd978 100644 --- a/designsafe/apps/api/projects_v2/schema_models/field_recon.py +++ b/designsafe/apps/api/projects_v2/schema_models/field_recon.py @@ -1,4 +1,5 @@ """Pydantic schema models for Field Recon entities""" + from typing import Annotated, Optional import itertools from pydantic import BeforeValidator, Field, AliasChoices @@ -34,8 +35,8 @@ class Mission(MetadataModel): related_work: list[AssociatedProject] = [] event: str = "" - date_start: str = "" - date_end: str = "" + date_start: Optional[str] = None + date_end: Optional[str] = None location: str = "" latitude: str = "" longitude: str = "" @@ -49,6 +50,41 @@ class Mission(MetadataModel): # Deprecate these later facility: Optional[DropdownValue] = None + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "title": self.title, + "description": self.description, + "publisher": "Designsafe", + } + fedora_json["creator"] = [ + f"{author.lname}, {author.fname}" for author in self.authors + ] + + fedora_json["coverage"] = [] + if self.date_start: + fedora_json["coverage"].append(self.date_start) + if self.date_end: + fedora_json["coverage"].append(self.date_end) + if self.location: + fedora_json["coverage"].append(self.location) + + fedora_json["identifier"] = self.dois + if self.facility: + fedora_json["contributor"] = self.facility.name + + for referenced_data in self.referenced_data: + reference_mapping = referenced_data.to_fedora_json() + for key in reference_mapping: + fedora_json[key] = fedora_json.get(key, []) + [reference_mapping[key]] + + for related_work in self.related_work: + related_mapping = related_work.to_fedora_json() + for key in related_mapping: + fedora_json[key] = fedora_json.get(key, []) + [related_mapping[key]] + + return fedora_json + class FieldReconReport(MetadataModel): """Model for field recon reports.""" @@ -62,9 +98,9 @@ class FieldReconReport(MetadataModel): related_work: list[AssociatedProject] = [] file_tags: list[FileTag] = [] - authors: Annotated[ - list[ProjectUser], BeforeValidator(handle_legacy_authors) - ] = Field(default=[], validation_alias=AliasChoices("authors", "dataCollectors")) + authors: Annotated[list[ProjectUser], BeforeValidator(handle_legacy_authors)] = ( + Field(default=[], validation_alias=AliasChoices("authors", "dataCollectors")) + ) guest_data_collectors: list[str] = [] project: list[str] = [] @@ -79,6 +115,34 @@ class FieldReconReport(MetadataModel): tombstone: bool = False + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "title": self.title, + "description": self.description, + "publisher": "Designsafe", + } + # pylint:disable=not-an-iterable + fedora_json["creator"] = [ + f"{author.lname}, {author.fname}" for author in self.authors + ] + + fedora_json["identifier"] = self.dois + if self.facility: + fedora_json["contributor"] = self.facility.name + + for referenced_data in self.referenced_data: + reference_mapping = referenced_data.to_fedora_json() + for key in reference_mapping: + fedora_json[key] = fedora_json.get(key, []) + [reference_mapping[key]] + + for related_work in self.related_work: + related_mapping = related_work.to_fedora_json() + for key in related_mapping: + fedora_json[key] = fedora_json.get(key, []) + [related_mapping[key]] + + return fedora_json + class Instrument(MetadataModel): """model for instruments used in field recon projects.""" @@ -93,9 +157,12 @@ class FieldReconCollection(MetadataModel): title: str description: str = "" - observation_types: list[str | None] = [] - date_start: str = "" - date_end: str = "" + observation_types: Annotated[ + list[DropdownValue], + BeforeValidator(lambda v: handle_dropdown_values(v, FR_OBSERVATION_TYPES)), + ] = [] + date_start: Optional[str] = None + date_end: Optional[str] = None data_collectors: list[ProjectUser] = [] guest_data_collectors: list[str] = [] @@ -124,9 +191,9 @@ class SocialScienceCollection(MetadataModel): unit: str = "" modes: Annotated[list[str], BeforeValidator(handle_array_of_none)] = [] sample_approach: Annotated[list[str], BeforeValidator(handle_array_of_none)] = [] - sample_size: str + sample_size: str = "" date_start: str - date_end: str + date_end: Optional[str] = None data_collectors: list[ProjectUser] = [] location: str = "" latitude: str = "" @@ -148,6 +215,41 @@ class SocialScienceCollection(MetadataModel): # Deprecated test fields methods: list[str | None] = Field(default=[], exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "type": "Social Science/dataset", + "title": self.title, + "description": self.description, + } + fedora_json["subject"] = [] + if self.unit: + fedora_json["subject"] += self.unit + if self.modes: + fedora_json["subject"].append(self.modes) + if self.sample_approach: + fedora_json["subject"] += self.sample_approach + if self.sample_size: + fedora_json["subject"].append(self.sample_size) + for equipment in self.equipment: + fedora_json["subject"].append(equipment.name) + + fedora_json["coverage"] = [] + if self.date_start: + fedora_json["coverage"].append(self.date_start) + if self.date_end: + fedora_json["coverage"].append(self.date_end) + if self.location: + fedora_json["coverage"].append(self.location) + + if self.restriction: + fedora_json["accessRights"] = self.restriction + + fedora_json["contributor"] = [ + f"{author.lname}, {author.fname}" for author in self.data_collectors + ] + return fedora_json + class PlanningCollection(MetadataModel): """Model for planning collections.""" @@ -165,6 +267,18 @@ class PlanningCollection(MetadataModel): file_objs: list[FileObj] = [] file_tags: list[FileTag] = [] + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "type": "Research Planning Collection", + "title": self.title, + "description": self.description, + } + fedora_json["contributor"] = [ + f"{author.lname}, {author.fname}" for author in self.data_collectors + ] + return fedora_json + class GeoscienceCollection(MetadataModel): """Model for geoscience collections.""" @@ -181,7 +295,7 @@ class GeoscienceCollection(MetadataModel): BeforeValidator(lambda v: handle_dropdown_values(v, FR_OBSERVATION_TYPES)), ] = [] date_start: str - date_end: str + date_end: Optional[str] = None location: str = "" latitude: str = "" longitude: str = "" @@ -195,3 +309,26 @@ class GeoscienceCollection(MetadataModel): files: list[str] = [] file_objs: list[FileObj] = [] file_tags: list[FileTag] = [] + + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "type": "Engineering Geosciences Collection", + "title": self.title, + "description": self.description, + } + + fedora_json["subject"] = [] + fedora_json["subject"] += [o.name for o in self.observation_types] + fedora_json["subject"] += [e.name for e in self.equipment] + + fedora_json["coverage"] = [] + if self.date_start: + fedora_json["coverage"] += self.date_start + if self.date_end: + fedora_json["coverage"] += self.date_end + + if self.location: + fedora_json["coverage"].append(self.location) + + return fedora_json diff --git a/designsafe/apps/api/projects_v2/schema_models/hybrid_sim.py b/designsafe/apps/api/projects_v2/schema_models/hybrid_sim.py index 99f1f024ed..f9b2683a09 100644 --- a/designsafe/apps/api/projects_v2/schema_models/hybrid_sim.py +++ b/designsafe/apps/api/projects_v2/schema_models/hybrid_sim.py @@ -1,4 +1,5 @@ """Pydantic schema models for Hybrid Simulation entities""" + from typing import Annotated, Optional from pydantic import BeforeValidator, Field, model_validator from designsafe.apps.api.projects_v2.schema_models._field_models import MetadataModel @@ -29,8 +30,8 @@ class HybridSimulation(MetadataModel): BeforeValidator(lambda v: handle_dropdown_value(v, HYBRID_SIM_TYPES)), ] simulation_type_other: Optional[str] = Field(exclude=True, default=None) - procedure_start: str = "" - procedure_end: str = "" + procedure_start: Optional[str] = None + procedure_end: Optional[str] = None referenced_data: list[ReferencedWork] = [] related_work: list[AssociatedProject] = [] authors: Annotated[list[ProjectUser], BeforeValidator(handle_legacy_authors)] = [] @@ -52,6 +53,34 @@ def handle_other(self): self.simulation_type.name = self.simulation_type_other return self + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "type": self.simulation_type.name, + "title": self.title, + "description": self.description, + } + + fedora_json["creator"] = [ + f"{author.lname}, {author.fname}" for author in self.authors + ] + + fedora_json["identifier"] = self.dois + if self.facility: + fedora_json["contributor"] = self.facility.name + + for referenced_data in self.referenced_data: + reference_mapping = referenced_data.to_fedora_json() + for key in reference_mapping: + fedora_json[key] = fedora_json.get(key, []) + [reference_mapping[key]] + + for related_work in self.related_work: + related_mapping = related_work.to_fedora_json() + for key in related_mapping: + fedora_json[key] = fedora_json.get(key, []) + [related_mapping[key]] + + return fedora_json + class HybridSimGlobalModel(MetadataModel): """Model for hybrid sim global models.""" @@ -67,6 +96,14 @@ class HybridSimGlobalModel(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "global model", + "title": self.title, + "description": self.description, + } + class HybridSimCoordinator(MetadataModel): """Model for coordinators.""" @@ -84,6 +121,14 @@ class HybridSimCoordinator(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "master simulation coordinator", + "title": self.title, + "description": self.description, + } + class HybridSimSimSubstructure(MetadataModel): """Model for simulation substructures.""" @@ -102,6 +147,14 @@ class HybridSimSimSubstructure(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "simulation substructure", + "title": self.title, + "description": self.description, + } + class HybridSimExpSubstructure(MetadataModel): """Model for experimental substructures.""" @@ -119,6 +172,14 @@ class HybridSimExpSubstructure(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "experimental substructure", + "title": self.title, + "description": self.description, + } + class HybridSimCoordinatorOutput(MetadataModel): """Model for coordinator output.""" @@ -136,6 +197,14 @@ class HybridSimCoordinatorOutput(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "coordinator output", + "title": self.title, + "description": self.description, + } + class HybridSimSimOutput(MetadataModel): """Model for coordinator output.""" @@ -154,6 +223,14 @@ class HybridSimSimOutput(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "simulation output", + "title": self.title, + "description": self.description, + } + class HybridSimExpOutput(MetadataModel): """Model for experimental substructure output.""" @@ -172,6 +249,14 @@ class HybridSimExpOutput(MetadataModel): tags: Optional[dict] = Field(default=None, exclude=True) + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "experimental output", + "title": self.title, + "description": self.description, + } + class HybridSimAnalysis(MetadataModel): """Model for hybrid sim analysis entities.""" @@ -190,6 +275,14 @@ class HybridSimAnalysis(MetadataModel): reference: Optional[str] = None referencedoi: Optional[str] = None + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "analysis", + "title": self.title, + "description": self.description, + } + class HybridSimReport(MetadataModel): """Model for hybrid sim reports.""" @@ -204,3 +297,7 @@ class HybridSimReport(MetadataModel): file_objs: list[FileObj] = [] tags: Optional[dict] = Field(default=None, exclude=True) + + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return {"type": "report", "title": self.title, "description": self.description} diff --git a/designsafe/apps/api/projects_v2/schema_models/simulation.py b/designsafe/apps/api/projects_v2/schema_models/simulation.py index a937afb3f0..acf04f1a9c 100644 --- a/designsafe/apps/api/projects_v2/schema_models/simulation.py +++ b/designsafe/apps/api/projects_v2/schema_models/simulation.py @@ -50,6 +50,34 @@ def handle_other(self): self.simulation_type.name = self.simulation_type_other return self + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + fedora_json = { + "type": self.simulation_type.name, + "title": self.title, + "description": self.description, + } + + fedora_json["creator"] = [ + f"{author.lname}, {author.fname}" for author in self.authors + ] + + fedora_json["identifier"] = self.dois + if self.facility: + fedora_json["contributor"] = self.facility.name + + for referenced_data in self.referenced_data: + reference_mapping = referenced_data.to_fedora_json() + for key in reference_mapping: + fedora_json[key] = fedora_json.get(key, []) + [reference_mapping[key]] + + for related_work in self.related_work: + related_mapping = related_work.to_fedora_json() + for key in related_mapping: + fedora_json[key] = fedora_json.get(key, []) + [related_mapping[key]] + + return fedora_json + class SimulationModel(MetadataModel): """Model for a simulation model.""" @@ -69,6 +97,14 @@ class SimulationModel(MetadataModel): file_tags: list[FileTag] = [] file_objs: list[FileObj] = [] + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "simulation model", + "title": self.title, + "description": self.description, + } + class SimulationInput(MetadataModel): """Model for simulation input.""" @@ -85,6 +121,14 @@ class SimulationInput(MetadataModel): file_tags: list[FileTag] = [] file_objs: list[FileObj] = [] + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "simulation input", + "title": self.title, + "description": self.description, + } + class SimulationOutput(MetadataModel): """Model for simulation output.""" @@ -102,6 +146,14 @@ class SimulationOutput(MetadataModel): file_tags: list[FileTag] = [] file_objs: list[FileObj] = [] + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "simulation output", + "title": self.title, + "description": self.description, + } + class SimulationAnalysis(MetadataModel): """Model for simulation analysis.""" @@ -121,6 +173,14 @@ class SimulationAnalysis(MetadataModel): reference: Optional[str] = None referencedoi: Optional[str] = None + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return { + "type": "analysis", + "title": self.title, + "description": self.description, + } + class SimulationReport(MetadataModel): """Model for simulation reports.""" @@ -135,3 +195,7 @@ class SimulationReport(MetadataModel): files: list[str] = [] file_tags: list[FileTag] = [] file_objs: list[FileObj] = [] + + def to_fedora_json(self): + """Metadata representation for the Fedora repository""" + return {"type": "report", "title": self.title, "description": self.description} diff --git a/designsafe/apps/api/projects_v2/tasks.py b/designsafe/apps/api/projects_v2/tasks.py new file mode 100644 index 0000000000..0a280b7390 --- /dev/null +++ b/designsafe/apps/api/projects_v2/tasks.py @@ -0,0 +1,65 @@ +"""Async tasks related to project creation/management.""" + +from celery import shared_task +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.mail import send_mail + +# pylint: disable=unused-import +from designsafe.apps.api.projects_v2.operations.project_system_operations import ( + add_user_to_project_async, + remove_user_from_project_async, +) +from designsafe.apps.api.projects_v2.models.project_metadata import ProjectMetadata +from designsafe.apps.api.projects_v2.operations.project_publish_operations import ( + publish_project_async, + amend_publication_async, +) +from designsafe.apps.api.projects_v2.operations.project_archive_operations import ( + archive_publication_async, +) + + +@shared_task(max_retries=3, default_retry_delay=60) +def alert_sensitive_data(project_id, username): + """ + contact project admins regarding publication of sensitive information + """ + project = ProjectMetadata.get_project_by_id(project_id) + admins = settings.PROJECT_ADMINS_EMAIL + user = get_user_model().objects.get(username=username) + + for admin in admins: + email_body = """ +

    Hello,

    +

    + The following Field Research project has been created with the intent of publishing sensitive information: +
    + {prjID} - {title} +

    +

    + Contact PI: +
    + {name} - {email} +

    +

    + Link to Project: +
    + {url}. +

    + This is a programmatically generated message. Do NOT reply to this message. + """.format( + name=user.get_full_name(), + email=user.email, + title=project.value["title"], + prjID=project_id, + url=f"https://designsafe-ci.org/data/browser/projects/{project_id}", + ) + + send_mail( + "DesignSafe PII Alert", + email_body, + settings.DEFAULT_FROM_EMAIL, + [admin], + html_message=email_body, + ) diff --git a/designsafe/apps/api/projects_v2/tests/schema_integration.py b/designsafe/apps/api/projects_v2/tests/schema_integration.py index 04dc4c0e1b..5f65fc7828 100644 --- a/designsafe/apps/api/projects_v2/tests/schema_integration.py +++ b/designsafe/apps/api/projects_v2/tests/schema_integration.py @@ -11,7 +11,7 @@ from designsafe.apps.api.projects_v2.migration_utils.graph_constructor import ( transform_pub_entities, ) -from designsafe.apps.api.agave import service_account +from designsafe.apps.api.agave import get_service_account_client_v2 as service_account from designsafe.apps.api.publications.operations import listing as list_pubs from designsafe.apps.api.projects_v2.models.project_metadata import ProjectMetadata from designsafe.apps.api.projects_v2.operations.project_publish_operations import ( diff --git a/designsafe/apps/api/projects_v2/urls.py b/designsafe/apps/api/projects_v2/urls.py index 7d398a6328..9a62d775a0 100644 --- a/designsafe/apps/api/projects_v2/urls.py +++ b/designsafe/apps/api/projects_v2/urls.py @@ -22,6 +22,7 @@ path("/preview/", ProjectPreviewView.as_view()), # path("/associations", ProjectsView.as_view), path("entities//", ProjectEntityView.as_view()), + path("/entities/create/", ProjectEntityView.as_view()), path("/entities/ordering/", ProjectEntityOrderView.as_view()), path("/entities/validate/", ProjectEntityValidateView.as_view()), path( diff --git a/designsafe/apps/api/projects_v2/views.py b/designsafe/apps/api/projects_v2/views.py index fa41c2b5aa..539ad4183c 100644 --- a/designsafe/apps/api/projects_v2/views.py +++ b/designsafe/apps/api/projects_v2/views.py @@ -4,27 +4,48 @@ import json import networkx as nx from django.http import HttpRequest, JsonResponse +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django.db import models from designsafe.apps.api.views import BaseApiView, ApiException from designsafe.apps.api.projects_v2.models.project_metadata import ProjectMetadata +from designsafe.apps.api.projects_v2.schema_models.base import BaseProject +from designsafe.apps.api.projects_v2.tasks import alert_sensitive_data +from designsafe.apps.api.projects_v2.migration_utils.graph_constructor import ( + ALLOWED_RELATIONS, +) +from designsafe.apps.api.projects_v2 import constants from designsafe.apps.api.projects_v2.operations.graph_operations import ( reorder_project_nodes, add_node_to_project, remove_nodes_from_project, + initialize_project_graph, + remove_nodes_for_entity, ) from designsafe.apps.api.projects_v2.operations.project_meta_operations import ( patch_metadata, add_file_associations, set_file_associations, + create_entity_metadata, + delete_entity, remove_file_associations, set_file_tags, change_project_type, + create_project_metdata, + get_changed_users, ) from designsafe.apps.api.projects_v2.operations.project_publish_operations import ( add_values_to_tree, validate_entity_selection, ) +from designsafe.apps.api.projects_v2.operations.project_system_operations import ( + increment_workspace_count, + setup_project_file_system, + add_user_to_project_async, + remove_user_from_project_async, +) from designsafe.apps.api.projects_v2.schema_models.base import FileObj +from designsafe.apps.api.decorators import tapis_jwt_login logger = logging.getLogger(__name__) @@ -44,6 +65,7 @@ def get_search_filter(query_string): class ProjectsView(BaseApiView): """View for listing and creating projects""" + @method_decorator(tapis_jwt_login) def get(self, request: HttpRequest): """Return the list of projects for a given user.""" offset = int(request.GET.get("offset", 0)) @@ -71,11 +93,33 @@ def get(self, request: HttpRequest): def post(self, request: HttpRequest): """Create a new project.""" - - + user = request.user + if not request.user.is_authenticated: + raise ApiException("Unauthenticated user", status=401) + req_body = json.loads(request.body) + metadata_value = req_body.get("value", {}) + # Projects are initialized as type None + + # increment project count + prj_number = increment_workspace_count() + # create metadata and graph + metadata_value["projectType"] = "None" + metadata_value["projectId"] = f"PRJ-{prj_number}" + project_meta = create_project_metdata(metadata_value) + initialize_project_graph(project_meta.project_id) + project_users = [user.username for user in project_meta.users.all()] + # create project system + setup_project_file_system(project_uuid=project_meta.uuid, users=project_users) + # add users to system + + return JsonResponse({"projectId": project_meta.project_id}) + + +@method_decorator(csrf_exempt, name="dispatch") class ProjectInstanceView(BaseApiView): """View for listing/updating project entities.""" + @method_decorator(tapis_jwt_login) def get(self, request: HttpRequest, project_id: str): """Return all project metadata for a project ID""" user = request.user @@ -109,7 +153,7 @@ def put(self, request: HttpRequest, project_id: str): raise ApiException("Unauthenticated user", status=401) try: - user.projects.get( + project: ProjectMetadata = user.projects.get( models.Q(uuid=project_id) | models.Q(value__projectId=project_id) ) except ProjectMetadata.DoesNotExist as exc: @@ -118,22 +162,28 @@ def put(self, request: HttpRequest, project_id: str): ) from exc # Get the new value from the request data - new_value = request.data.get("new_value") + req_body = json.loads(request.body) + new_value = req_body.get("value", {}) + sensitive_data_option = req_body.get("sensitiveData", False) + if sensitive_data_option: + logger.debug("PROJECT %s INDICATES SENSITIVE DATA", project_id) + alert_sensitive_data.apply_async([project_id, request.user.username]) # Call the change_project_type function to update the project type updated_project = change_project_type(project_id, new_value) - entities = ProjectMetadata.objects.filter(base_project=updated_project) - return JsonResponse( - { - "updatedProject": updated_project.to_dict(), - "entities": [e.to_dict() for e in entities], - "tree": nx.tree_data( - nx.node_link_graph(updated_project.project_graph.value), "NODE_ROOT" - ), - } + users_to_add, users_to_remove = get_changed_users( + BaseProject.model_validate(project.value), + BaseProject.model_validate(updated_project.value), ) + for user_to_add in users_to_add: + add_user_to_project_async.apply_async([project.uuid, user_to_add]) + for user_to_remove in users_to_remove: + remove_user_from_project_async.apply_async([project_id, user_to_remove]) + + return JsonResponse({"result": "OK"}) + @method_decorator(tapis_jwt_login) def patch(self, request: HttpRequest, project_id: str): """Update a project's root metadata""" user = request.user @@ -150,7 +200,18 @@ def patch(self, request: HttpRequest, project_id: str): ) from exc request_body = json.loads(request.body).get("patchMetadata", {}) - patch_metadata(project.uuid, request_body) + prev_metadata = BaseProject.model_validate(project.value) + updated_project = patch_metadata(project.uuid, request_body) + updated_metadata = BaseProject.model_validate(updated_project.value) + + users_to_add, users_to_remove = get_changed_users( + prev_metadata, updated_metadata + ) + for user_to_add in users_to_add: + add_user_to_project_async.apply_async([project.uuid, user_to_add]) + for user_to_remove in users_to_remove: + remove_user_from_project_async.apply_async([project_id, user_to_remove]) + return JsonResponse({"result": "OK"}) @@ -174,6 +235,47 @@ def patch(self, request: HttpRequest, entity_uuid: str): patch_metadata(entity_uuid, request_body) return JsonResponse({"result": "OK"}) + def delete(self, request: HttpRequest, entity_uuid: str): + """Delete an entity's metadata and remove the entity from the graph""" + user = request.user + if not request.user.is_authenticated: + raise ApiException("Unauthenticated user", status=401) + + entity_meta = ProjectMetadata.objects.get(uuid=entity_uuid) + if user not in entity_meta.base_project.users.all(): + raise ApiException( + "User does not have access to the requested project", status=403 + ) + + remove_nodes_for_entity(entity_meta.project_id, entity_uuid) + delete_entity(entity_uuid) + return JsonResponse({"result": "OK"}) + + def post(self, request: HttpRequest, project_id: str): + """Add a new entity to a project""" + + user = request.user + if not request.user.is_authenticated: + raise ApiException("Unauthenticated user", status=401) + + try: + project: ProjectMetadata = user.projects.get( + models.Q(uuid=project_id) | models.Q(value__projectId=project_id) + ) + except ProjectMetadata.DoesNotExist as exc: + raise ApiException( + "User does not have access to the requested project", status=403 + ) from exc + + req_body = json.loads(request.body) + value = req_body.get("value", {}) + name = req_body.get("name", "") + + new_meta = create_entity_metadata(project.project_id, name, value) + if name in ALLOWED_RELATIONS[constants.PROJECT]: + add_node_to_project(project_id, "NODE_ROOT", new_meta.uuid, name) + return JsonResponse({"result": "OK"}) + class ProjectPreviewView(BaseApiView): """View for generating the Publication Preview""" diff --git a/designsafe/apps/api/projects_v2/views_unit_test.py b/designsafe/apps/api/projects_v2/views_unit_test.py new file mode 100644 index 0000000000..a6c5723458 --- /dev/null +++ b/designsafe/apps/api/projects_v2/views_unit_test.py @@ -0,0 +1,65 @@ +import pytest +import json + + +@pytest.mark.django_db +def test_get_project_instance_unauthed(client, project_with_associations): + _, _, project_uuid = project_with_associations + response = client.get(f"/api/projects/v2/{project_uuid}/") + assert response.status_code == 401 + + +@pytest.mark.django_db +def test_get_project_instance(client, authenticated_user, project_with_associations): + _, _, project_uuid = project_with_associations + response = client.get(f"/api/projects/v2/{project_uuid}/") + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_get_project_instance_with_jwt( + client, regular_user_using_jwt, project_with_associations +): + _, _, project_uuid = project_with_associations + response = client.get(f"/api/projects/v2/{project_uuid}/") + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_patch_project_instance_unauthed(client, project_with_associations): + _, _, project_uuid = project_with_associations + map_entry = { + "name": "Name", + "uuid": "1234", + "path": "/something.hazmapper", + "deployment": "test", + } + patch_data = {"patchMetadata": {"hazmapperMaps": [map_entry]}} + response = client.patch( + f"/api/projects/v2/{project_uuid}/", + data=json.dumps(patch_data), + content_type="application/json", + ) + assert response.status_code == 401 + + +@pytest.mark.django_db +def test_patch_project_instance_with_jwt( + client, regular_user_using_jwt, project_with_associations +): + project, _, project_uuid = project_with_associations + map_entry = { + "name": "Name", + "uuid": "1234", + "path": "/something.hazmapper", + "deployment": "test", + } + patch_data = {"patchMetadata": {"hazmapperMaps": [map_entry]}} + response = client.patch( + f"/api/projects/v2/{project_uuid}/", + data=json.dumps(patch_data), + content_type="application/json", + ) + assert response.status_code == 200 + project.refresh_from_db() + assert [map_entry] == project.value["hazmapperMaps"] diff --git a/designsafe/apps/api/publications/operations.py b/designsafe/apps/api/publications/operations.py index 35a734a42c..5bff9556c2 100644 --- a/designsafe/apps/api/publications/operations.py +++ b/designsafe/apps/api/publications/operations.py @@ -167,7 +167,7 @@ def metrics(project_id, *args, **kwargs): return metrics_meta -def neeslisting(offset=0, limit=100, limit_fields=True, *args): +def neeslisting(offset=0, limit=100, limit_fields=True, q='', *args): client = new_es_client() pub_query = IndexedPublicationLegacy.search(using=client) pub_query = pub_query.extra(from_=offset, size=limit) @@ -178,8 +178,9 @@ def neeslisting(offset=0, limit=100, limit_fields=True, *args): ) res = pub_query.execute() hits = list(map(lambda h: h.to_dict(), res.hits)) - - return {'listing': hits} + if q: + return neessearch(offset=offset, limit=limit, query_string=q) + return {'listing': hits, 'total': res.hits.total.value} def neessearch(offset=0, limit=100, query_string='', limit_fields=True, *args): @@ -203,7 +204,7 @@ def neessearch(offset=0, limit=100, query_string='', limit_fields=True, *args): ) res = pub_query.execute() hits = list(map(lambda h: h.to_dict(), res.hits)) - return {'listing': hits} + return {'listing': hits, 'total': res.hits.total.value} def description(project_id, revision=None, *args): diff --git a/designsafe/apps/api/publications/urls.py b/designsafe/apps/api/publications/urls.py index c2ac84aacd..f5c64b7ad5 100644 --- a/designsafe/apps/api/publications/urls.py +++ b/designsafe/apps/api/publications/urls.py @@ -1,6 +1,7 @@ from django.urls import re_path as url -from designsafe.apps.api.publications.views import PublicationListingView, PublicationDetailView, PublicationDataCiteView +from designsafe.apps.api.publications.views import PublicationListingView, PublicationDetailView, PublicationDataCiteView, PublicationDataCiteEventsView from django.http import JsonResponse +from django.views.decorators.cache import cache_page urlpatterns = [ @@ -8,7 +9,8 @@ # Browsing: # # GET /listing//// - url(r'^data-cite/(?P[ \S]*)$', PublicationDataCiteView.as_view(), name='publication_datacite'), + url(r'^data-cite/events$', cache_page(60*15)(PublicationDataCiteEventsView.as_view()), name='publication_datacite_usage'), + url(r'^data-cite/(?P\S+)$', cache_page(60*15)(PublicationDataCiteView.as_view()), name='publication_datacite'), url(r'^(?P[\w.-]+)/$', PublicationListingView.as_view(), name='publication_listing'), url(r'^(?P[\w.-]+)/(?P[A-Z\-]+-[0-9]+)(v(?P[0-9]+))?/$', PublicationDetailView.as_view(), name='publication_detail'), url(r'^(?P[\w.-]+)/(?P[\w.-]+)/$', PublicationDetailView.as_view(), name='legacy-publication_detail') diff --git a/designsafe/apps/api/publications/views.py b/designsafe/apps/api/publications/views.py index 931c5ebcea..0207358f3b 100644 --- a/designsafe/apps/api/publications/views.py +++ b/designsafe/apps/api/publications/views.py @@ -5,9 +5,11 @@ from requests.exceptions import HTTPError from designsafe.apps.api.publications import operations from designsafe.apps.projects.managers import datacite as DataciteManager +from django.utils.decorators import method_decorator import json import logging -# Create your views here. +import requests + logger = logging.getLogger(__name__) @@ -42,8 +44,31 @@ def get(self, request, operation, project_id, revision=None): """ class PublicationDataCiteView(BaseApiView): def get(self, request, doi): + url = f'https://api.datacite.org/dois/{doi}' + try: - response = DataciteManager.get_doi(doi) - return JsonResponse(response) + response = requests.get(url) + response.raise_for_status() # Raises an HTTPError for bad responses (4xx and 5xx) + return JsonResponse(response.json()) # Assuming the response is JSON except HTTPError as e: return JsonResponse({'message': str(e)}, status=e.response.status_code) + except Exception as e: + return JsonResponse({'message': str(e)}, status=500) + +""" +View for getting DataCite metrics details from publications. +""" +class PublicationDataCiteEventsView(BaseApiView): + def get(self, request): + doi = request.GET.get('doi', '') + source_id = request.GET.get('source-id', 'datacite-usage') + + url = f'https://api.datacite.org/events?source-id={source_id}&doi={doi}' + + try: + response = requests.get(url) + response.raise_for_status() + events = response.json() + return JsonResponse(events) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) diff --git a/designsafe/apps/api/publications_v2/elasticsearch.py b/designsafe/apps/api/publications_v2/elasticsearch.py new file mode 100644 index 0000000000..ca0fa34351 --- /dev/null +++ b/designsafe/apps/api/publications_v2/elasticsearch.py @@ -0,0 +1,29 @@ +"""Elasticsearch model for published works""" + +from elasticsearch_dsl import Document +from elasticsearch.exceptions import NotFoundError +from django.conf import settings +from designsafe.apps.api.publications_v2.models import Publication + + +class IndexedPublication(Document): + """Elasticsearch model for published works""" + + # pylint: disable=too-few-public-methods + class Index: + """Index meta settings""" + + name = settings.ES_INDICES["publications_v2"]["alias"] + + +def index_publication(project_id): + """Util to index a publication by its project ID""" + pub = Publication.objects.get(project_id=project_id) + try: + pub_es = IndexedPublication.get(project_id) + pub_es.update(**pub.tree) + + except NotFoundError: + pub_es = IndexedPublication(**pub.tree) + pub_es.meta["id"] = project_id + pub_es.save() diff --git a/designsafe/apps/api/publications_v2/operations/fedora_graph_operations.py b/designsafe/apps/api/publications_v2/operations/fedora_graph_operations.py new file mode 100644 index 0000000000..3ddd3379f2 --- /dev/null +++ b/designsafe/apps/api/publications_v2/operations/fedora_graph_operations.py @@ -0,0 +1,382 @@ +"""Utils for constructing Fedora metadata definitions from publications""" + +import os +import urllib +import networkx as nx +from fido.fido import Fido +from designsafe.apps.api.publications_v2.models import Publication +from designsafe.apps.api.projects_v2.schema_models import SCHEMA_MAPPING, PATH_SLUGS +from designsafe.apps.api.projects_v2.schema_models.base import BaseProject +from designsafe.apps.api.projects_v2 import constants +from designsafe.libs.fedora.fedora_operations import ( + create_fc_version, + upload_manifest, + fedora_post, + fedora_update, + get_sha1_hash, + get_fido_output, + get_child_paths, + generate_manifest, + PUBLICATIONS_MOUNT_ROOT, +) + +prov_predecessor_mapping = { + # Experimental + constants.EXPERIMENT: {"wasStartedBy": [constants.PROJECT]}, + constants.EXPERIMENT_MODEL_CONFIG: {"wasGeneratedBy": [constants.EXPERIMENT]}, + constants.EXPERIMENT_SENSOR: { + "wasGeneratedBy": [constants.EXPERIMENT], + "wasDerivedFrom": [constants.EXPERIMENT_MODEL_CONFIG], + }, + constants.EXPERIMENT_EVENT: { + "wasGeneratedBy": [constants.EXPERIMENT], + "wasDerivedFrom": [constants.EXPERIMENT_MODEL_CONFIG], + "wasInformedBy": [constants.EXPERIMENT_SENSOR], + }, + constants.EXPERIMENT_REPORT: { + "wasGeneratedBy": [constants.EXPERIMENT], + }, + constants.EXPERIMENT_ANALYSIS: { + "wasGeneratedBy": [constants.EXPERIMENT], + }, + # Hybrid Sim + constants.HYBRID_SIM: {"wasGeneratedBy": [constants.PROJECT]}, + constants.HYBRID_SIM_GLOBAL_MODEL: {"wasGeneratedBy": [constants.HYBRID_SIM]}, + constants.HYBRID_SIM_COORDINATOR: { + "wasGeneratedBy": [constants.HYBRID_SIM], + "wasInfluencedBy": [constants.HYBRID_SIM_GLOBAL_MODEL], + }, + constants.HYBRID_SIM_COORDINATOR_OUTPUT: { + "wasGeneratedBy": [constants.HYBRID_SIM], + "wasInfluencedBy": [constants.HYBRID_SIM_GLOBAL_MODEL], + "wasDerivedFrom": [constants.HYBRID_SIM_COORDINATOR], + }, + constants.HYBRID_SIM_SIM_SUBSTRUCTURE: { + "wasInfluencedBy": [ + constants.HYBRID_SIM_GLOBAL_MODEL, + constants.HYBRID_SIM_COORDINATOR, + ], + "wasGeneratedBy": [constants.HYBRID_SIM], + }, + constants.HYBRID_SIM_SIM_OUTPUT: { + "wasDerivedFrom": [constants.HYBRID_SIM_SIM_SUBSTRUCTURE] + }, + constants.HYBRID_SIM_EXP_SUBSTRUCTURE: { + "wasInfluencedBy": [ + constants.HYBRID_SIM_GLOBAL_MODEL, + constants.HYBRID_SIM_COORDINATOR, + ], + "wasGeneratedBy": [constants.HYBRID_SIM], + }, + constants.HYBRID_SIM_EXP_OUTPUT: { + "wasDerivedFrom": [constants.HYBRID_SIM_EXP_SUBSTRUCTURE] + }, + constants.HYBRID_SIM_ANALYSIS: {"wasGeneratedBy": [constants.HYBRID_SIM]}, + constants.HYBRID_SIM_REPORT: {"wasGenerateBy": [constants.HYBRID_SIM]}, + # Simulation + constants.SIMULATION: {"wasGeneratedBy": [constants.PROJECT]}, + constants.SIMULATION_MODEL: {"wasGeneratedBy": [constants.SIMULATION]}, + constants.SIMULATION_INPUT: { + "wasGeneratedBy": [constants.SIMULATION], + "wasDerivedFrom": [constants.SIMULATION_MODEL], + }, + constants.SIMULATION_OUTPUT: { + "wasGeneratedBy": [constants.SIMULATION], + "wasDerivedFrom": [constants.SIMULATION_MODEL, constants.SIMULATION_INPUT], + }, + constants.SIMULATION_ANALYSIS: {"wasGeneratedBy": [constants.SIMULATION]}, + constants.SIMULATION_REPORT: {"wasGeneratedBy": [constants.SIMULATION]}, + # Field Recon + constants.FIELD_RECON_MISSION: {"wasStartedBy": [constants.PROJECT]}, + constants.FIELD_RECON_REPORT: {"wasStartedBy": [constants.PROJECT]}, + constants.FIELD_RECON_PLANNING: {"wasGeneratedBy": [constants.FIELD_RECON_MISSION]}, + constants.FIELD_RECON_SOCIAL_SCIENCE: { + "wasGeneratedBy": [constants.FIELD_RECON_MISSION] + }, + constants.FIELD_RECON_GEOSCIENCE: { + "wasGeneratedBy": [constants.FIELD_RECON_MISSION] + }, +} + +prov_successor_mapping = { + constants.EXPERIMENT: { + "generated": [ + constants.EXPERIMENT_MODEL_CONFIG, + constants.EXPERIMENT_ANALYSIS, + constants.EXPERIMENT_REPORT, + constants.EXPERIMENT_SENSOR, + constants.EXPERIMENT_EVENT, + ] + }, + constants.HYBRID_SIM: { + "generated": [ + constants.HYBRID_SIM_ANALYSIS, + constants.HYBRID_SIM_REPORT, + constants.HYBRID_SIM_COORDINATOR, + constants.HYBRID_SIM_GLOBAL_MODEL, + constants.HYBRID_SIM_COORDINATOR_OUTPUT, + constants.HYBRID_SIM_SIM_SUBSTRUCTURE, + constants.HYBRID_SIM_SIM_OUTPUT, + constants.HYBRID_SIM_EXP_SUBSTRUCTURE, + constants.HYBRID_SIM_EXP_OUTPUT, + ] + }, + constants.SIMULATION: { + "generated": [ + constants.SIMULATION_ANALYSIS, + constants.SIMULATION_REPORT, + constants.SIMULATION_MODEL, + constants.SIMULATION_INPUT, + constants.SIMULATION_OUTPUT, + ] + }, + constants.FIELD_RECON_MISSION: { + "generated": [ + constants.FIELD_RECON_PLANNING, + constants.FIELD_RECON_SOCIAL_SCIENCE, + constants.FIELD_RECON_GEOSCIENCE, + ] + }, +} + + +def get_node_url_path( + pub_tree: nx.DiGraph, node_id: str, project_id: str, version: str = 1 +): + """Get the path to an entity in Fedora relative to the publication container root.""" + url_path = project_id + if version > 1: + url_path = f"{project_id}v{version}" + + node_path = nx.shortest_path(pub_tree, source="NODE_ROOT", target=node_id) + for path_id in node_path[1:]: + entity_title = pub_tree.nodes[path_id]["value"]["title"] + url_path += f"/{urllib.parse.quote(entity_title)}" + + return url_path + + +def get_predecessor_prov_tags(pub_tree: nx.DiGraph, node_id: str): + """ + Get PROV metadata relating to a node's predecessors. Example return value: + {'wasGeneratedBy': ['Experiment: Particle Image Data (10.17603/ds2-j0b5-5y02)'], + 'wasDerivedFrom': ['Model-config: Culebra, Humacao and Yabucoa'], + 'wasInformedBy': ['Sensor: Particle Image Velocimetry System']} + """ + prov_predecessor_json = {} + node_name = pub_tree.nodes[node_id]["name"] + + node_path = nx.shortest_path(pub_tree, source="NODE_ROOT", target=node_id) + prov_map = prov_predecessor_mapping.get(node_name, {}) + for predecessor_node_id in node_path: + for prov_relation in prov_map: + if pub_tree.nodes[predecessor_node_id]["name"] in prov_map[prov_relation]: + predecessor_node_data = pub_tree.nodes[predecessor_node_id] + predecessor_name = f"{PATH_SLUGS[predecessor_node_data['name']]}: {predecessor_node_data['value']['title']}" + if predecessor_node_data["value"].get("dois"): + predecessor_name += ( + f" ({predecessor_node_data['value']['dois'][0]})" + ) + + prov_predecessor_json[prov_relation] = prov_predecessor_json.get( + prov_relation, [] + ) + [predecessor_name] + + return prov_predecessor_json + + +def get_successor_prov_tags(pub_tree: nx.DiGraph, node_id: str): + """ + Get PROV metadata related to a nodes' successors. Example return value: + {'generated': ['Report: Data Dictionary', + 'Model-config: Culebra, Humacao and Yabucoa', + 'Sensor: Particle Image Velocimetry System', + 'Event: Approach Flow', + 'Event: Culebra Model']} + """ + prov_successor_json = {} + node_name = pub_tree.nodes[node_id]["name"] + prov_map = prov_successor_mapping.get(node_name, {}) + + successors = nx.dfs_preorder_nodes(pub_tree, node_id) + for successor_node_id in successors: + for prov_relation in prov_map: + if pub_tree.nodes[successor_node_id]["name"] in prov_map[prov_relation]: + successor_node_data = pub_tree.nodes[successor_node_id] + successor_name = f"{PATH_SLUGS[successor_node_data['name']]}: {successor_node_data['value']['title']}" + if successor_node_data["value"].get("dois"): + successor_name += f" ({successor_node_data['value']['dois'][0]})" + + prov_successor_json[prov_relation] = prov_successor_json.get( + prov_relation, [] + ) + [successor_name] + + return prov_successor_json + + +def get_project_root_mapping( + project_id, version, pub_tree: nx.DiGraph, node_data: dict +): + """Get Fedora mapping for the project root.""" + + fedora_json = BaseProject.model_validate(node_data["value"]).to_fedora_json() + + publication_date = node_data.get("publicationDate", None) + if publication_date: + fedora_json["available"] = publication_date + + project_mapping = { + "uuid": node_data["uuid"], + "container_path": get_node_url_path(pub_tree, "NODE_ROOT", project_id, version), + "fedora_mapping": fedora_json, + "fileObjs": [], + "fileTags": node_data["value"].get("fileTags", []), + } + return project_mapping + + +def get_fedora_json(project_id: str, version: int = 1): + """ + Returns Fedora mappings and path/file tag information for each entity in a pub. + """ + pub = Publication.objects.get(project_id=project_id) + + pub_tree: nx.DiGraph = nx.node_link_graph(pub.tree) + + pub_root = pub_tree.nodes["NODE_ROOT"] + + fedora_json_mappings = [] + + # Handle type Other + if not pub_root.get("name"): + base_project_node_data = next( + ( + pub_tree.nodes[e] + for e in pub_tree.successors("NODE_ROOT") + if pub_tree.nodes[e]["version"] == version + ), + None, + ) + project_mapping = get_project_root_mapping( + project_id, version, pub_tree, base_project_node_data + ) + + fedora_json_mappings.append(project_mapping) + return fedora_json_mappings + + # Handle non-Other pubs with entity trees + + base_project_mapping = get_project_root_mapping( + project_id, version, pub_tree, pub_tree.nodes["NODE_ROOT"] + ) + base_project_mapping["fedora_mapping"]["available"] = str(pub.created) + + fedora_json_mappings.append(base_project_mapping) + + published_entities_with_version = [ + e + for e in pub_tree.successors("NODE_ROOT") + if pub_tree.nodes[e]["version"] == version + ] + + for entity_node in published_entities_with_version: + dfs_nodes = nx.dfs_preorder_nodes(pub_tree, entity_node) + for dfs_node_id in dfs_nodes: + entity_meta = pub_tree.nodes[dfs_node_id] + + fedora_mapping = ( + SCHEMA_MAPPING[entity_meta["name"]] + .model_validate(entity_meta["value"]) + .to_fedora_json() + ) + fedora_mapping = { + **fedora_mapping, + **get_predecessor_prov_tags(pub_tree, dfs_node_id), + **get_successor_prov_tags(pub_tree, dfs_node_id), + } + + if not fedora_mapping.get("identifier"): + fedora_mapping["identifier"] = entity_meta["uuid"] + + fedora_json_mappings.append( + { + "uuid": entity_meta["uuid"], + "container_path": get_node_url_path( + pub_tree, dfs_node_id, project_id, version + ), + "fedora_mapping": fedora_mapping, + "fileObjs": entity_meta["value"].get("fileObjs", []), + "fileTags": entity_meta["value"].get("fileTags", []), + } + ) + return fedora_json_mappings + + +def generate_manifest_other(project_id, version=1): + """Generate the file manifest for an Other-type project""" + fido_client = Fido() + pub = Publication.objects.get(project_id=project_id) + uuid = pub.tree.nodes[0]["uuid"] + file_tags = pub.value.get("fileTags", []) + + if version and version > 1: + project_id = f"{project_id}v{str(version)}" + manifest = [] + archive_path = PUBLICATIONS_MOUNT_ROOT + + for path in get_child_paths(archive_path): + tags = [ + tag + for tag in file_tags + if tag["path"].strip("/") == os.path.relpath(path, archive_path).strip("/") + ] + + manifest.append( + { + "parent_entity": uuid, + "corral_path": path, + "checksum": get_sha1_hash(path), + "tags": [t["tagName"] for t in tags], + "ffi": get_fido_output(fido_client, path), + } + ) + + return manifest + + +def ingest_pub_fedora(project_id: str, version: int = 1, amend: bool = False): + """ + Ingest a project into Fedora by creating a record in the repo, updating it + with the published metadata, and uploading its file manifest. + """ + pub_meta = Publication.objects.get(project_id=project_id) + project_type = pub_meta.value.get("projectType", "other") + + if project_type == "other": + container_path = project_id + if version and version > 1: + container_path = f"{container_path}v{version}" + fedora_post(container_path) + project_meta = get_fedora_json(project_id, version=version)[0] + if amend: + create_fc_version(project_meta["container_path"]) + fedora_update(project_meta["container_path"], project_meta["fedora_mapping"]) + if not amend: + manifest = generate_manifest_other(project_id, version) + upload_manifest(manifest, project_id, version=version) + + else: + container_path = project_id + if version and version > 1: + container_path = f"{container_path}v{version}" + + walk_result = get_fedora_json(project_id, version=version) + for entity in walk_result: + if amend: + create_fc_version(entity["container_path"]) + fedora_post(entity["container_path"]) + fedora_update(entity["container_path"], entity["fedora_mapping"]) + + if not amend: + manifest = generate_manifest(walk_result, project_id, version) + upload_manifest(manifest, project_id, version) diff --git a/designsafe/apps/api/publications_v2/tasks.py b/designsafe/apps/api/publications_v2/tasks.py new file mode 100644 index 0000000000..a4f4fc258c --- /dev/null +++ b/designsafe/apps/api/publications_v2/tasks.py @@ -0,0 +1,11 @@ +"""Async tasks related to published work""" +from celery import shared_task +from designsafe.apps.api.publications_v2.operations.fedora_graph_operations import ( + ingest_pub_fedora, +) + + +@shared_task() +def ingest_pub_fedora_async(project_id: str, version: int = 1, amend: bool = False): + """async wrapper around Fedora ingest""" + ingest_pub_fedora(project_id, version, amend) diff --git a/designsafe/apps/api/publications_v2/urls.py b/designsafe/apps/api/publications_v2/urls.py index 7684703605..c13b361e6e 100644 --- a/designsafe/apps/api/publications_v2/urls.py +++ b/designsafe/apps/api/publications_v2/urls.py @@ -4,11 +4,17 @@ from designsafe.apps.api.publications_v2.views import ( PublicationListingView, PublicationDetailView, + PublicationPublishView, + PublicationAmendView, + PublicationVersionView, ) urlpatterns = [ path("", PublicationListingView.as_view()), path("/", PublicationListingView.as_view()), + path("publish/", PublicationPublishView.as_view()), + path("amend/", PublicationAmendView.as_view()), + path("version/", PublicationVersionView.as_view()), re_path( r"^(?P[A-Z\-]+-[0-9]+)(v(?P[0-9]+))?/?$", PublicationDetailView.as_view(), diff --git a/designsafe/apps/api/publications_v2/views.py b/designsafe/apps/api/publications_v2/views.py index 96b4f5035a..c442d6aa73 100644 --- a/designsafe/apps/api/publications_v2/views.py +++ b/designsafe/apps/api/publications_v2/views.py @@ -1,14 +1,148 @@ """Views for published data""" import logging +import json import networkx as nx +from django.db import models from django.http import HttpRequest, JsonResponse -from designsafe.apps.api.views import BaseApiView +from designsafe.apps.api.views import BaseApiView, ApiException from designsafe.apps.api.publications_v2.models import Publication +from designsafe.apps.api.publications_v2.elasticsearch import IndexedPublication +from designsafe.apps.api.projects_v2.models.project_metadata import ProjectMetadata +from designsafe.apps.api.projects_v2.operations.project_publish_operations import ( + publish_project_async, + amend_publication_async, +) logger = logging.getLogger(__name__) +def handle_search(query_opts: dict, offset=0, limit=100): + from elasticsearch_dsl import Q + + logger.debug(offset) + + query = IndexedPublication.search() + + if project_type_query := query_opts["project-type"]: + query = query.filter("terms", **{"nodes.value.projectType": project_type_query}) + + if facility_query := query_opts["facility"]: + query = query.filter( + Q("term", **{"nodes.value.facility.id.keyword": facility_query}) + | Q("term", **{"nodes.value.facilities.id.keyword": facility_query}) + ) + if nh_type_query := query_opts["nh-type"]: + query = query.filter( + Q("term", **{"nodes.value.nhTypes.id.keyword": nh_type_query}) + | Q("term", **{"nodes.value.nhTypes": nh_type_query}) + ) + + if pub_year_query := query_opts["pub-year"]: + query = query.filter( + Q( + { + "range": { + "nodes.publicationDate": { + "gte": f"{pub_year_query}||/y", + "lte": f"{pub_year_query}||/y", + "format": "yyyy", + } + } + } + ) + ) + + if nh_year_query := query_opts["nh-year"]: + query = query.filter( + Q( + { + "range": { + "nodes.value.nhEventStart": { + "gte": f"{nh_year_query}||/y", + "lte": f"{nh_year_query}||/y", + "format": "yyyy", + } + } + } + ) + ) + + if experiment_type_query := query_opts["experiment-type"]: + query = query.filter( + Q( + "term", + **{"nodes.value.experimentType.id.keyword": experiment_type_query}, + ) + ) + + if sim_type_query := query_opts["sim-type"]: + query = query.filter( + Q( + "term", + **{"nodes.value.simulationType.id.keyword": sim_type_query}, + ) + ) + + if fr_type_query := query_opts["fr-type"]: + query = query.filter( + Q( + "term", + **{"nodes.value.frTypes.id.keyword": fr_type_query}, + ) + ) + + if hyb_sim_type_query := query_opts["hyb-sim-type"]: + query = query.filter( + Q( + "term", + **{"nodes.value.simulationType.id.keyword": hyb_sim_type_query}, + ) + ) + + if data_type_query := query_opts["data-type"]: + query = query.filter( + Q( + "term", + **{"nodes.value.dataType.id.keyword": data_type_query}, + ) + ) + + if search_string := query_opts["q"]: + qs_query = Q( + "query_string", + query=search_string, + default_operator="AND", + type="cross_fields", + fields=[ + "nodes.value.description", + "nodes.value.keywords", + "nodes.value.title", + "nodes.value.projectId", + "nodes.value.projectType", + "nodes.value.dataType", + "nodes.value.authors", + "nodes.value.authors.fname", + "nodes.value.authors.lname", + "nodes.value.authors.username", + "nodes.value.authors.inst", + ], + ) + term_query = Q({"term": {"nodes.value.projectId.keyword": search_string}}) + query = query.filter(qs_query | term_query) + + hits = ( + query.extra(from_=offset, size=limit) + .sort({"nodes.publicationDate": {"order": "desc"}}) + .source([""]) + .execute() + .hits + ) + returned_ids = [hit.meta.id for hit in hits] + + return returned_ids, hits.total.value + + class PublicationListingView(BaseApiView): """List all publications.""" @@ -17,15 +151,46 @@ def get(self, request: HttpRequest): offset = int(request.GET.get("offset", 0)) limit = int(request.GET.get("limit", 100)) - publications = Publication.objects.defer("tree").order_by("-created")[ - offset : offset + limit - ] - total = Publication.objects.count() + # Search/filter params + query_opts = { + "q": request.GET.get("q", None), + "project-type": request.GET.getlist("project-type", []), + "nh-type": request.GET.get("nh-type", None), + "pub-year": request.GET.get("pub-year", None), + "facility": request.GET.get("facility", None), + "experiment-type": request.GET.get("experiment-type", None), + "sim-type": request.GET.get("sim-type", None), + "fr-type": request.GET.get("fr-type", None), + "nh-year": request.GET.get("nh-year", None), + "hyb-sim-type": request.GET.get("hyb-sim-type", None), + "data-type": request.GET.get("data-type", None), + } + + has_query = any(query_opts.values()) + if has_query: + hits, total = handle_search(query_opts, offset, limit) + publications_query = ( + Publication.objects.filter(project_id__in=hits, is_published=True) + .defer("tree") + .order_by("-created") + ) + publications = publications_query + else: + publications_query = ( + Publication.objects.filter(is_published=True) + .defer("tree") + .order_by("-created") + ) + total = publications_query.count() + publications = publications_query[offset : offset + limit] result = [ { "projectId": pub.value["projectId"], "title": pub.value["title"], "description": pub.value["description"], + "keywords": pub.value["keywords"], + "type": pub.value["projectType"], + "dataTypes": [t["name"] for t in pub.value.get("dataTypes", None)], "pi": next( (user for user in pub.value["users"] if user["role"] == "pi"), None ), @@ -41,8 +206,119 @@ class PublicationDetailView(BaseApiView): def get(self, request: HttpRequest, project_id, version=None): """Returns the tree view and base project metadata for a publication.""" - pub_meta = Publication.objects.get(project_id=project_id) + try: + pub_meta = Publication.objects.get(project_id=project_id) + except Publication.DoesNotExist as exc: + raise ApiException(status=404, message="Publication not found.") from exc + + pub_tree: nx.DiGraph = nx.node_link_graph(pub_meta.tree) + file_tags = [] + for file_tag_arr in [ + node.get("value", {}).get("fileTags", []) + for (_, node) in pub_tree.nodes.data() + ]: + for tag in file_tag_arr: + file_tags.append(tag) + + tree_json = nx.tree_data(pub_tree, "NODE_ROOT") + + return JsonResponse( + {"tree": tree_json, "fileTags": file_tags, "baseProject": pub_meta.value} + ) + + +class PublicationPublishView(BaseApiView): + """view for publishing a project.""" + + def post(self, request: HttpRequest): + """Create a new publication from a project.""" + user = request.user + request_body = json.loads(request.body) + logger.debug(request_body) + + project_id = request_body.get("projectId", None) + entities_to_publish = request_body.get("entityUuids", None) + + if (not project_id) or (not entities_to_publish): + raise ApiException("Missing project ID or entity list.", status=400) + + try: + user.projects.get( + models.Q(uuid=project_id) | models.Q(value__projectId=project_id) + ) + except ProjectMetadata.DoesNotExist as exc: + raise ApiException( + "User does not have access to the requested project", status=403 + ) from exc + + publish_project_async.apply_async([project_id, entities_to_publish]) + logger.debug(project_id) + logger.debug(entities_to_publish) + return JsonResponse({"result": "OK"}) + + +class PublicationVersionView(BaseApiView): + """view for versioning a project.""" + + def post(self, request: HttpRequest): + """Create a new publication from a project.""" + user = request.user + request_body = json.loads(request.body) + logger.debug(request_body) + + project_id = request_body.get("projectId", None) + entities_to_publish = request_body.get("entityUuids", None) + version_info = request_body.get("versionInfo", None) + + if (not project_id) or (not entities_to_publish): + raise ApiException("Missing project ID or entity list.", status=400) + + try: + user.projects.get( + models.Q(uuid=project_id) | models.Q(value__projectId=project_id) + ) + except ProjectMetadata.DoesNotExist as exc: + raise ApiException( + "User does not have access to the requested project", status=403 + ) from exc + + pub_root = Publication.objects.get(project_id=project_id) + pub_tree: nx.DiGraph = nx.node_link_graph(pub_root.tree) + latest_version = max( + pub_tree.nodes[node]["version"] for node in pub_tree.successors("NODE_ROOT") + ) + + publish_project_async.apply_async( + [project_id, entities_to_publish, latest_version + 1, version_info] + ) + logger.debug(project_id) + logger.debug(entities_to_publish) + return JsonResponse({"result": "OK"}) + + +class PublicationAmendView(BaseApiView): + """view for amemding a project.""" + + def post(self, request: HttpRequest): + """Create a new publication from a project.""" + user = request.user + request_body = json.loads(request.body) + logger.debug(request_body) + + project_id = request_body.get("projectId", None) + + if not project_id: + raise ApiException("Missing project ID.", status=400) - tree_json = nx.tree_data(nx.node_link_graph(pub_meta.tree), "NODE_ROOT") + try: + user.projects.get( + models.Q(uuid=project_id) | models.Q(value__projectId=project_id) + ) + except ProjectMetadata.DoesNotExist as exc: + raise ApiException( + "User does not have access to the requested project", status=403 + ) from exc - return JsonResponse({"tree": tree_json, "baseProject": pub_meta.value}) + amend_publication_async.apply_async([project_id]) + logger.debug(project_id) + return JsonResponse({"result": "OK"}) diff --git a/designsafe/apps/api/search/searchmanager/cms.py b/designsafe/apps/api/search/searchmanager/cms.py index 515c5b18ad..410c85745d 100644 --- a/designsafe/apps/api/search/searchmanager/cms.py +++ b/designsafe/apps/api/search/searchmanager/cms.py @@ -5,12 +5,11 @@ import logging -from future.utils import python_2_unicode_compatible from elasticsearch_dsl import Q, Index from django.conf import settings from designsafe.apps.api.search.searchmanager.base import BaseSearchManager -@python_2_unicode_compatible + class CMSSearchManager(BaseSearchManager): """ Search manager handling CMS data. """ diff --git a/designsafe/apps/api/systems/__init__.py b/designsafe/apps/api/systems/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/apps/api/systems/ssh_keys_manager.py b/designsafe/apps/api/systems/ssh_keys_manager.py new file mode 100644 index 0000000000..a47483eeb3 --- /dev/null +++ b/designsafe/apps/api/systems/ssh_keys_manager.py @@ -0,0 +1,146 @@ +""" +.. :module:: apps.accounts.managers.ssh_keys + :synopsis: Manager handling anything pertaining to accounts +""" + +import logging +import paramiko + + +logger = logging.getLogger(__name__) + + +class KeyCannotBeAdded(Exception): + """Key Cannot Be Added Exception + + Exception raised when there is an error adding a public key + to `~/.ssh/authorized_keys` + """ + + def __init__(self, msg, output, error_output, *args, **kwargs): + super().__init__(*args, **kwargs) + self.msg = msg + self.output = output + self.error_output = error_output + + def __str__(self): + return f"{self.msg}: {self.output} \n {self.error_output}" + + +class KeysManager: + # pylint: disable=too-few-public-methods + """Keys Manager + + Class to wrap together any necessary action pertaining to ssh keys + and remote resources. + """ + + def __init__(self, username, password, token): + # pylint: disable=super-init-not-called + """Init""" + self.username = username + self.password = password + self.token = token + + def _tacc_prompt_handler(self, title, instructions, prompt_list): + """TACC Prompt Handler + + This method handles SSH prompts from TACC resources + """ + answers = { + "password": self.password, + "tacc_token_code": self.token, + "tacc_token": self.token, + } + resp = [] + logger.debug("title: %s", title) + logger.debug("instructions: %s", instructions) + logger.debug("list: %s", prompt_list) + for prmpt in prompt_list: + prmpt_str = prmpt[0].lower().strip().replace(" ", "_").replace(":", "") + resp.append(answers[prmpt_str]) + return resp + + def get_transport(self, hostname, port): + """Gets authenticated transport""" + handler = self._tacc_prompt_handler + + trans = paramiko.Transport((hostname, port)) + # trans.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # trans.set_hexdump(True) + trans.use_compression() + # trans.set_keepalive(5) + trans.connect() + trans.auth_interactive_dumb(str(self.username), handler) + return trans + + def _get_pub_key_comment(self, system_id): + """Get Pub Key Comment + + :param str system_id: Agave's system id + + :return str: comment + """ + comment = f"{self.username}@{system_id}" + return comment + + def _get_add_pub_key_command(self, system_id, public_key): + """Get Add Pub Key Command + + :param str system_id: Agave's system id + :param str publick_key: Public Key + + :return str: command + """ + comment = self._get_pub_key_comment(system_id) + string = " ".join([public_key, comment]) + command = ( + 'if [ ! -f "~/.ssh/authorized_keys" ]; then ' + "mkdir -p ~/.ssh/ && touch ~/.ssh/authorized_keys " + "&& chmod 0600 ~/.ssh/authorized_keys; fi && " + 'grep -q -F "{string}" ~/.ssh/authorized_keys || ' + 'echo "{string}" >> ~/.ssh/authorized_keys' + ).format(string=string) + return command + + def add_public_key( + self, system_id, hostname, public_key, port=22, transport=None + ): # pylint: disable=too-many-arguments, arguments-differ + """Adds public key to `authorized_keys` + + :param str sytem_id: System Id + :param str hostname: Hostname + :param str public_key: Public Key + :param int port: Port (optional) + :param transport: Transport object (optional) + """ + if transport is None: + trans = self.get_transport(hostname, port) + else: + trans = transport + channel = trans.open_session() + command = self._get_add_pub_key_command(system_id, public_key) + channel.exec_command(command) + # recv_exit_status blocks until there's an exit status from the + # executed command. + # So, after this we're safe to read stdout and stderr + status = channel.recv_exit_status() + output = channel.makefile() + stderr = channel.makefile_stderr() + output_lines = "" + for line in output.readlines(): + output_lines += line + "\n" + logger.debug(line) + + if status == -1: + logger.info("No response from the server") + elif status == 0: + logger.info(f"Public key added successfully to {hostname}") + elif status > 0: + error_lines = "" + for line in stderr.readlines(): + error_lines += line + "\n" + + raise KeyCannotBeAdded("Error adding public key", output_lines, error_lines) + trans.close() + return output_lines diff --git a/designsafe/apps/api/systems/urls.py b/designsafe/apps/api/systems/urls.py new file mode 100644 index 0000000000..cb1a20ab17 --- /dev/null +++ b/designsafe/apps/api/systems/urls.py @@ -0,0 +1,8 @@ +"""Publication API routes""" + +from django.urls import path +from .views import SystemKeysView + +urlpatterns = [ + path("keys/", SystemKeysView.as_view()), +] diff --git a/designsafe/apps/api/systems/utils.py b/designsafe/apps/api/systems/utils.py new file mode 100644 index 0000000000..984efeba77 --- /dev/null +++ b/designsafe/apps/api/systems/utils.py @@ -0,0 +1,82 @@ +""" +.. :module:: apps.accounts.managers.accounts + :synopsis: Manager handling anything pertaining to accounts +""" + +import logging +from paramiko.ssh_exception import ( + AuthenticationException, + ChannelException, + SSHException, +) +from .ssh_keys_manager import KeysManager, KeyCannotBeAdded + + +logger = logging.getLogger(__name__) + + +# pylint: disable=too-many-arguments +def add_pub_key_to_resource( + user, + password, + token, + system_id, + pub_key, + hostname=None, + port=22, +): + """Add Public Key to Remote Resource + + :param user: Django User object + :param str password: Username's pasword to remote resource + :param str token: TACC's token + :param str system_id: Tapis system's id + :param str hostname: Resource's hostname + :param int port: Port to use for ssh connection + + :raises: :class:`~portal.apps.accounts.managers.` + + """ + success = True + mgr = KeysManager(user, password, token) + message = "add_pub_key_to_resource" + + logger.info(f"Adding public key for user {user.username} on system {system_id}") + try: + if hostname is None: + sys = user.tapis_oauth.client.systems.getSystem(systemId=system_id) + hostname = sys.host + + transport = mgr.get_transport(hostname, port) + message = mgr.add_public_key( + system_id, hostname, pub_key, port=port, transport=transport + ) + status = 200 + + except Exception as base_exc: # pylint: disable=broad-exception-caught + # Catch all exceptions and set a status code for unknown exceptions + success = False + message = str(base_exc) + logger.exception( + message, + extra={"user": user.username}, + ) + + try: + # "Re-throw" exception to get known exception type status codes + raise base_exc + except AuthenticationException: + # Bad password/token + status = 403 # Forbidden + except KeyCannotBeAdded: + # May occur when system is down + message = "KeyCannotBeAdded" # KeyCannnotBeAdded exception does not contain a message? + status = 503 + except (ChannelException, SSHException) as exc: + # cannot ssh to system + message = str( + type(exc) + ) # paramiko exceptions do not contain a string message? + status = 500 # Bad gateway + + return success, message, status diff --git a/designsafe/apps/api/systems/views.py b/designsafe/apps/api/systems/views.py new file mode 100644 index 0000000000..12bbbe9397 --- /dev/null +++ b/designsafe/apps/api/systems/views.py @@ -0,0 +1,56 @@ +""" +.. :module:: designsafe.apps.api.systems.views + :synopsis: Systems views +""" + +import logging +import json +from django.http import JsonResponse +from designsafe.apps.api.views import AuthenticatedApiView +from designsafe.utils.system_access import create_system_credentials +from designsafe.utils.encryption import createKeyPair +from .utils import add_pub_key_to_resource + +logger = logging.getLogger(__name__) + + +class SystemKeysView(AuthenticatedApiView): + """Systems View + + Main view for anything involving a system test + """ + + def post(self, request): + """POST + + :param request: Django's request object + :param str system_id: System id + """ + body = json.loads(request.body) + system_id = body["systemId"] + + logger.info( + f"Resetting credentials for user {request.user.username} on system {system_id}" + ) + (priv_key_str, publ_key_str) = createKeyPair() + + _, result, http_status = add_pub_key_to_resource( + request.user, + password=body["password"], + token=body["token"], + system_id=system_id, + pub_key=publ_key_str, + hostname=body["hostname"], + ) + + create_system_credentials( + request.user.tapis_oauth.client, + request.user.username, + publ_key_str, + priv_key_str, + system_id, + ) + + return JsonResponse( + {"systemId": system_id, "message": result}, status=http_status + ) diff --git a/designsafe/apps/api/tests.py b/designsafe/apps/api/tests.py index 01df8f33fb..1ad34c673e 100644 --- a/designsafe/apps/api/tests.py +++ b/designsafe/apps/api/tests.py @@ -4,7 +4,7 @@ from designsafe.apps.projects.models.agave.experimental import ExperimentalProject, ModelConfig, FileModel -from agavepy.agave import Agave +# from agavepy.agave import Agave import mock import json @@ -17,7 +17,7 @@ # Create your tests here. class ProjectDataModelsTestCase(TestCase): - fixtures = ['user-data.json', 'agave-oauth-token-data.json'] + fixtures = ['user-data.json', 'auth.json'] def setUp(self): user = get_user_model().objects.get(pk=2) diff --git a/designsafe/apps/api/urls.py b/designsafe/apps/api/urls.py index 7af0b1dc91..1c2149757a 100644 --- a/designsafe/apps/api/urls.py +++ b/designsafe/apps/api/urls.py @@ -10,6 +10,8 @@ path("publications/v2", include('designsafe.apps.api.publications_v2.urls')), path("publications/v2/", include('designsafe.apps.api.publications_v2.urls')), + path("systems/", include('designsafe.apps.api.systems.urls')), + url(r'^projects/', include(('designsafe.apps.api.projects.urls', 'designsafe.apps.api.projects'), namespace='ds_projects_api')), diff --git a/designsafe/apps/api/users/utils.py b/designsafe/apps/api/users/utils.py index 0923178d1c..bd51e92e52 100644 --- a/designsafe/apps/api/users/utils.py +++ b/designsafe/apps/api/users/utils.py @@ -1,33 +1,46 @@ +import logging +from pytas.http import TASClient from django.db.models import Q -import logging -import json logger = logging.getLogger(__name__) + +def get_user_data(username): + """Returns user contact information + + : returns: user_data + : rtype: dict + """ + tas_client = TASClient() + user_data = tas_client.get_user(username=username) + return user_data + + def list_to_model_queries(q_comps): query = None if len(q_comps) > 2: - query = Q(first_name__icontains = ' '.join(q_comps[:1])) - query |= Q(first_name__icontains = ' '.join(q_comps[:2])) - query |= Q(last_name__icontains = ' '.join(q_comps[1:])) - query |= Q(last_name__icontains = ' '.join(q_comps[2:])) + query = Q(first_name__icontains=" ".join(q_comps[:1])) + query |= Q(first_name__icontains=" ".join(q_comps[:2])) + query |= Q(last_name__icontains=" ".join(q_comps[1:])) + query |= Q(last_name__icontains=" ".join(q_comps[2:])) else: - query = Q(first_name__icontains = q_comps[0]) - query |= Q(last_name__icontains = q_comps[1]) + query = Q(first_name__icontains=q_comps[0]) + query |= Q(last_name__icontains=q_comps[1]) return query + def q_to_model_queries(q): if not q: return None query = None - if ' ' in q: + if " " in q: q_comps = q.split() query = list_to_model_queries(q_comps) else: - query = Q(email__icontains = q) - query |= Q(first_name__icontains = q) - query |= Q(last_name__icontains = q) + query = Q(email__icontains=q) + query |= Q(first_name__icontains=q) + query |= Q(last_name__icontains=q) return query diff --git a/designsafe/apps/api/users/views.py b/designsafe/apps/api/users/views.py index 792f66181b..0ffa7a833c 100644 --- a/designsafe/apps/api/users/views.py +++ b/designsafe/apps/api/users/views.py @@ -17,9 +17,9 @@ def check_public_availability(username): es_client = new_es_client() - query = Q({'multi_match': {'fields': ['project.value.teamMembers', - 'project.value.coPis', - 'project.value.pi'], + query = Q({'multi_match': {'fields': ['project.value.teamMembers', + 'project.value.coPis', + 'project.value.pi'], 'query': username}}) res = IndexedPublication.search(using=es_client).filter(query).execute() return res.hits.total.value > 0 @@ -50,14 +50,13 @@ def get(self, request): "last_name": u.last_name, "email": u.email, "oauth": { - "access_token": u.agave_oauth.access_token, - "expires_in": u.agave_oauth.expires_in, - "scope": u.agave_oauth.scope, - } + "expires_in": u.tapis_oauth.expires_in, + }, + "isStaff": u.is_staff, } return JsonResponse(out) - return HttpResponse('Unauthorized', status=401) + return JsonResponse({'message': 'Unauthorized'}, status=401) class SearchView(View): @@ -120,7 +119,7 @@ def get(self, request): return JsonResponse(resp, safe=False) else: return HttpResponseNotFound() - + class ProjectUserView(BaseApiView): """View for handling search for project users""" @@ -128,17 +127,17 @@ def get(self, request: HttpRequest): """retrieve a user by their exact TACC username.""" if not request.user.is_authenticated: raise ApiException(message="Authentication required", status=401) - + username_query = request.GET.get("q") user_match = get_user_model().objects.filter(username__iexact=username_query) user_resp = [{"fname": u.first_name, "lname": u.last_name, "inst": u.profile.institution, - "email": u.email, + "email": u.email, "username": u.username} for u in user_match] - + return JsonResponse({"result": user_resp}) - + class PublicView(View): diff --git a/designsafe/apps/api/views.py b/designsafe/apps/api/views.py index 1ee0eb4d0c..d41833c10f 100644 --- a/designsafe/apps/api/views.py +++ b/designsafe/apps/api/views.py @@ -1,57 +1,102 @@ -from django.http.response import HttpResponse, HttpResponseForbidden, JsonResponse +from django.views.decorators.csrf import csrf_exempt from django.views.generic import View -from requests.exceptions import ConnectionError, HTTPError +from django.http import JsonResponse, HttpResponse, Http404 +from django.core.exceptions import PermissionDenied +from django.utils.decorators import method_decorator +from requests.exceptions import HTTPError from .exceptions import ApiException import logging from logging import getLevelName import json +from designsafe.apps.api.decorators import tapis_jwt_login +from tapipy.errors import BaseTapyException logger = logging.getLogger(__name__) class BaseApiView(View): + """Base api view to centralize error logging.""" def dispatch(self, request, *args, **kwargs): """ Dispatch override to centralize error handling. If the error is instance of :class: `ApiException `. - An extra dictionary object will be used when calling `logger.error()`. - This allows to use any information in the `extra` dictionary object on the + An extra dictionary object will be used when calling `logger.error()`. + This allows to use any information in the `extra` dictionary object on the logger output. """ try: - return super(BaseApiView, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) + except (PermissionDenied, Http404) as e: + # log information but re-raise exception to let django handle response + logger.error(e, exc_info=True) + raise e except ApiException as e: - status = e.response.status_code + status = e.response.status_code or 400 message = e.response.reason extra = e.extra - logger.error('{}'.format(message), exc_info=True, extra=extra) - except (ConnectionError, HTTPError) as e: - if e.response: + if status != 404: + logger.error( + "%s: %s", message, e.response.text, exc_info=True, extra=extra + ) + else: + logger.info("Error %s", message, exc_info=True, extra=extra) + return JsonResponse({"message": message}, status=status) + except (ConnectionError, HTTPError, BaseTapyException) as e: + # status code and json content from ConnectionError/HTTPError exceptions + # are used in the returned response. Note: the handling of these two exceptions + # is significant as client-side code make use of these status codes (e.g. error + # responses from tapis are used to determine a tapis storage systems does not exist) + status = 500 + if e.response is not None: status = e.response.status_code - message = e.response.reason - if status not in [403, 404]: - logger.error('%s: %s', message, e.response.text, - exc_info=True, - extra={'username': request.user.username, - 'sessionId': request.session.session_key}) + try: + content = e.response.json() + message = content.get("message", "Unknown Error") + except ValueError: + message = "Unknown Error" + if status in [404, 403]: + logger.warning( + "%s: %s", + message, + e.response.text, + exc_info=True, + extra={ + "username": request.user.username, + "session_key": request.session.session_key, + }, + ) else: - logger.warning('%s: %s', message, e.response.text, - exc_info=True, - extra={'username': request.user.username, - 'sessionId': request.session.session_key}) + logger.error( + "%s: %s", + message, + e.response.text, + exc_info=True, + extra={ + "username": request.user.username, + "session_key": request.session.session_key, + }, + ) else: - logger.error('%s', e, exc_info=True) + logger.error( + e, + exc_info=True, + extra={ + "username": request.user.username, + "session_key": request.session.session_key, + }, + ) message = str(e) - status = 500 - - resp = {'message': message} - - return HttpResponse(json.dumps(resp), - status=status, content_type='application/json') + return JsonResponse({"message": message}, status=status) + except Exception as e: # pylint: disable=broad-except + logger.error(e, exc_info=True) + return JsonResponse({"message": "Something went wrong here..."}, status=500) class AuthenticatedApiView(BaseApiView): + """ + Extends BaseApiView to require authenticated requests + """ def dispatch(self, request, *args, **kwargs): """Returns 401 if user is not authenticated.""" @@ -61,6 +106,20 @@ def dispatch(self, request, *args, **kwargs): return super(AuthenticatedApiView, self).dispatch(request, *args, **kwargs) +class AuthenticatedAllowJwtApiView(AuthenticatedApiView): + """ + Extends AuthenticatedApiView to also allow JWT access in addition to django session cookie + """ + + @method_decorator(csrf_exempt, name="dispatch") + @method_decorator(tapis_jwt_login) + def dispatch(self, request, *args, **kwargs): + """Returns 401 if user is not authenticated like AuthenticatedApiView but allows JWT access.""" + return super(AuthenticatedAllowJwtApiView, self).dispatch( + request, *args, **kwargs + ) + + class LoggerApi(BaseApiView): """ Logger API for capturing logs from the front-end. @@ -80,15 +139,19 @@ def post(self, request): Returns: HTTP 202 """ - log_json = request.body.decode('utf-8') + log_json = request.body.decode("utf-8") log_data = json.loads(log_json) - level = getLevelName(log_data.pop('level', 'INFO')) - name = log_data.pop('name'); - - logger.log(level, '%s: %s', name, json.dumps(log_data), extra={ - 'user': request.user.username, - 'referer': request.META.get('HTTP_REFERER') - }) - return HttpResponse('OK', status=202) - - + level = getLevelName(log_data.pop("level", "INFO")) + name = log_data.pop("name") + + logger.log( + level, + "%s: %s", + name, + json.dumps(log_data), + extra={ + "user": request.user.username, + "referer": request.META.get("HTTP_REFERER"), + }, + ) + return HttpResponse("OK", status=202) diff --git a/designsafe/apps/applications/views.py b/designsafe/apps/applications/views.py index c82237bf6b..455e30d07d 100644 --- a/designsafe/apps/applications/views.py +++ b/designsafe/apps/applications/views.py @@ -1,4 +1,4 @@ -from agavepy.agave import Agave, AgaveException, load_resource +# from agavepy.agave import Agave, AgaveException, load_resource from designsafe.apps.licenses.models import LICENSE_TYPES, get_license_info from designsafe.apps.notifications.views import get_number_unread_notifications from designsafe.libs.common.decorators import profile as profile_fn @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) metrics = logging.getLogger('metrics') -AGAVE_RESOURCES = load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) +# AGAVE_RESOURCES = load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) @login_required diff --git a/designsafe/apps/auth/README.md b/designsafe/apps/auth/README.md index ed0e415cbb..de247e4a59 100644 --- a/designsafe/apps/auth/README.md +++ b/designsafe/apps/auth/README.md @@ -9,27 +9,27 @@ support the various authentication requirements of DesignSafe. Authenticate directly against TACC's TAS Identity Store. This backend is used when authenticating directly to the Django Admin app. An OAuth token will not be obtained when -using this backend, so using Agave/DesignSafe API features will not work. +using this backend, so using Tapis/DesignSafe API features will not work. -### AgaveOAuthBackend +### TapisOAuthBackend -Authenticate using Agave OAuth Webflow (authorization code). See the [Agave Authentication Docs][1] +Authenticate using Tapis OAuth Webflow (authorization code). See the [Tapis Authentication Docs][1] for complete documentation. -#### AgaveTokenRefreshMiddleware +#### TapisTokenRefreshMiddleware -OAuth tokens obtained from Agave are valid for a limited time, usually one hour (3600s). +OAuth tokens obtained from Tapis are valid for a limited time, usually ten days (14400s). The app can automatically refresh the OAuth token as necessary. Add the refresh middleware in `settings.py`. The middleware *must* appear after `django.contrib.sessions.middleware.SessionMiddleware`: ``` -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( ..., 'django.contrib.sessions.middleware.SessionMiddleware', - designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware, + designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware, ..., ) ``` -[1]: http://agaveapi.co/documentation/authorization-guide/#authorization_code_flow \ No newline at end of file +[1]: https://tapis.readthedocs.io/en/latest/technical/authentication.html#authorization-code-grant-generating-tokens-for-users diff --git a/designsafe/apps/auth/backends.py b/designsafe/apps/auth/backends.py index f822bf649a..b668a94b3b 100644 --- a/designsafe/apps/auth/backends.py +++ b/designsafe/apps/auth/backends.py @@ -1,51 +1,57 @@ +"""Auth backends""" + +import logging +import re from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend +from tapipy.tapis import Tapis +from tapipy.errors import BaseTapyException +from designsafe.apps.accounts.models import DesignSafeProfile, NotificationPreferences +from designsafe.apps.api.users.utils import get_user_data +from designsafe.apps.auth.models import TapisOAuthToken from django.contrib.auth.signals import user_logged_out from django.contrib import messages -from django.contrib.auth.backends import ModelBackend from django.core.exceptions import ValidationError from django.dispatch import receiver -from designsafe.apps.accounts.models import DesignSafeProfile, NotificationPreferences -from designsafe.apps.api.agave import get_service_account_client from designsafe.apps.auth.tasks import update_institution_from_tas from pytas.http import TASClient -import logging -import re -import requests -from requests.auth import HTTPBasicAuth + +logger = logging.getLogger(__name__) @receiver(user_logged_out) def on_user_logged_out(sender, request, user, **kwargs): - backend = request.session.get('_auth_user_backend', None) - tas_backend_name = '%s.%s' % (TASBackend.__module__, - TASBackend.__name__) - agave_backend_name = '%s.%s' % (AgaveOAuthBackend.__module__, - AgaveOAuthBackend.__name__) + "Signal processor for user_logged_out" + backend = request.session.get("_auth_user_backend", None) + tas_backend_name = "%s.%s" % (TASBackend.__module__, TASBackend.__name__) + tapis_backend_name = "%s.%s" % ( + TapisOAuthBackend.__module__, + TapisOAuthBackend.__name__, + ) if backend == tas_backend_name: - login_provider = 'TACC' - elif backend == agave_backend_name: - login_provider = 'TACC' - else: - login_provider = 'your authentication provider' - - logger = logging.getLogger(__name__) - logger.debug("attempting call to revoke agave token function: %s", user.agave_oauth.token) - a = AgaveOAuthBackend() - AgaveOAuthBackend.revoke(a,user.agave_oauth) - - logout_message = '

    You are Logged Out!

    ' \ - 'You are now logged out of DesignSafe! However, you may still ' \ - 'be logged in at %s. To ensure security, you should close your ' \ - 'browser to end all authenticated sessions.' % login_provider + login_provider = "TACC" + elif backend == tapis_backend_name: + login_provider = "TACC" + + logger.info( + "Revoking tapis token: %s", TapisOAuthToken().get_masked_token(user.tapis_oauth.access_token) + ) + backend = TapisOAuthBackend() + TapisOAuthBackend.revoke(backend, user.tapis_oauth.access_token) + + logout_message = ( + "

    You are Logged Out!

    " + "You are now logged out of DesignSafe! However, you may still " + f"be logged in at {login_provider}. To ensure security, you should close your " + "browser to end all authenticated sessions." + ) messages.warning(request, logout_message) class TASBackend(ModelBackend): - logger = logging.getLogger(__name__) - def __init__(self): self.tas = TASClient() @@ -56,20 +62,31 @@ def authenticate(self, request, username=None, password=None, **kwargs): if username is not None and password is not None: tas_user = None if request is not None: - self.logger.info('Attempting login via TAS for user "%s" from IP "%s"' % (username, request.META.get('REMOTE_ADDR'))) + self.logger.info( + 'Attempting login via TAS for user "%s" from IP "%s"' + % (username, request.META.get("REMOTE_ADDR")) + ) else: - self.logger.info('Attempting login via TAS for user "%s" from IP "%s"' % (username, 'unknown')) + self.logger.info( + 'Attempting login via TAS for user "%s" from IP "%s"' + % (username, "unknown") + ) try: # Check if this user is valid on the mail server if self.tas.authenticate(username, password): tas_user = self.tas.get_user(username=username) self.logger.info('Login successful for user "%s"' % username) else: - raise ValidationError('Authentication Error', 'Your username or password is incorrect.') + raise ValidationError( + "Authentication Error", + "Your username or password is incorrect.", + ) except Exception as e: self.logger.warning(e.args) - if re.search(r'PendingEmailConfirmation', e.args[1]): - raise ValidationError('Please confirm your email address before logging in.') + if re.search(r"PendingEmailConfirmation", e.args[1]): + raise ValidationError( + "Please confirm your email address before logging in." + ) else: raise ValidationError(e.args[1]) @@ -78,27 +95,30 @@ def authenticate(self, request, username=None, password=None, **kwargs): try: # Check if the user exists in Django's local database user = UserModel.objects.get(username=username) - user.first_name = tas_user['firstName'] - user.last_name = tas_user['lastName'] - user.email = tas_user['email'] + user.first_name = tas_user["firstName"] + user.last_name = tas_user["lastName"] + user.email = tas_user["email"] user.save() except UserModel.DoesNotExist: # Create a user in Django's local database - self.logger.info('Creating local user record for "%s" from TAS Profile' % username) + self.logger.info( + 'Creating local user record for "%s" from TAS Profile' + % username + ) user = UserModel.objects.create_user( username=username, - first_name=tas_user['firstName'], - last_name=tas_user['lastName'], - email=tas_user['email'] - ) + first_name=tas_user["firstName"], + last_name=tas_user["lastName"], + email=tas_user["email"], + ) try: profile = DesignSafeProfile.objects.get(user=user) - profile.institution = tas_user.get('institution', None) + profile.institution = tas_user.get("institution", None) profile.save() except DesignSafeProfile.DoesNotExist: profile = DesignSafeProfile(user=user) - profile.institution = tas_user.get('institution', None) + profile.institution = tas_user.get("institution", None) profile.save() try: @@ -110,72 +130,67 @@ def authenticate(self, request, username=None, password=None, **kwargs): return user -# class CILogonBackend(ModelBackend): - -# def authenticate(self, **kwargs): -# return None - - -class AgaveOAuthBackend(ModelBackend): - - logger = logging.getLogger(__name__) +class TapisOAuthBackend(ModelBackend): def authenticate(self, *args, **kwargs): user = None - if 'backend' in kwargs and kwargs['backend'] == 'agave': - token = kwargs['token'] - base_url = getattr(settings, 'AGAVE_TENANT_BASEURL') + if "backend" in kwargs and kwargs["backend"] == "tapis": + token = kwargs["token"] - self.logger.info('Attempting login via Agave with token "%s"' % - token[:8].ljust(len(token), '-')) + logger.info( + 'Attempting login via Tapis with token "%s"' % TapisOAuthToken().get_masked_token(token) + ) + client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) - # TODO make this into an AgavePy call - response = requests.get('%s/profiles/v2/me' % base_url, - headers={'Authorization': 'Bearer %s' % token}) - if response.status_code >= 200 and response.status_code <= 299: - json_result = response.json() - agave_user = json_result['result'] - username = agave_user['username'] - UserModel = get_user_model() - try: - user = UserModel.objects.get(username=username) - user.first_name = agave_user['first_name'] - user.last_name = agave_user['last_name'] - user.email = agave_user['email'] - user.save() - except UserModel.DoesNotExist: - self.logger.info('Creating local user record for "%s" ' - 'from Agave Profile' % username) - user = UserModel.objects.create_user( - username=username, - first_name=agave_user['first_name'], - last_name=agave_user['last_name'], - email=agave_user['email'] - ) + try: + tapis_user_info = client.authenticator.get_userinfo() + except BaseTapyException as e: + logger.info("Tapis Authentication failed: %s", e.message) + return None - try: - profile = DesignSafeProfile.objects.get(user=user) - except DesignSafeProfile.DoesNotExist: - profile = DesignSafeProfile(user=user) - profile.save() - update_institution_from_tas.apply_async(args=[username], queue='api') + username = tapis_user_info.username - try: - prefs = NotificationPreferences.objects.get(user=user) - except NotificationPreferences.DoesNotExist: - prefs = NotificationPreferences(user=user) - prefs.save() + try: + user_data = get_user_data(username=username) + defaults = { + "first_name": user_data["firstName"], + "last_name": user_data["lastName"], + "email": user_data["email"], + } + except Exception: + logger.exception( + "Error retrieving TAS user profile data for user: %s", username + ) + defaults = { + "first_name": tapis_user_info.given_name, + "last_name": tapis_user_info.last_name, + "email": tapis_user_info.email, + } + + user, created = get_user_model().objects.update_or_create( + username=username, defaults=defaults + ) + + if created: + logger.info( + 'Created local user record for "%s" from TAS Profile', username + ) + + DesignSafeProfile.objects.get_or_create(user=user) + NotificationPreferences.objects.get_or_create(user=user) + + update_institution_from_tas.apply_async(args=[username], queue="api") + + logger.info('Login successful for user "%s"', username) - self.logger.error('Login successful for user "%s"' % username) - else: - self.logger.error('Agave Authentication failed: %s' % response.text) return user - def revoke(self, user): - base_url = getattr(settings, 'AGAVE_TENANT_BASEURL') - self.logger.info("attempting to revoke agave token %s" % user.masked_token) - response = requests.post('{base_url}/revoke'.format(base_url = base_url), - auth=HTTPBasicAuth(settings.AGAVE_CLIENT_KEY, settings.AGAVE_CLIENT_SECRET), - data={'token': user.access_token}) - self.logger.info("revoke response is %s" % response) + def revoke(self, token): + logger.info( + "Attempting to revoke Tapis token %s" % TapisOAuthToken().get_masked_token(token) + ) + + client = Tapis(base_url=settings.TAPIS_TENANT_BASEURL, access_token=token) + response = client.authenticator.revoke_token(token=token) + logger.info("revoke response is %s" % response) diff --git a/designsafe/apps/auth/backends_unit_test.py b/designsafe/apps/auth/backends_unit_test.py new file mode 100644 index 0000000000..f02d3e4680 --- /dev/null +++ b/designsafe/apps/auth/backends_unit_test.py @@ -0,0 +1,97 @@ +import pytest +from django.contrib.auth import get_user_model +from mock import Mock +from designsafe.apps.auth.backends import TapisOAuthBackend +from tapipy.tapis import TapisResult +from tapipy.errors import BaseTapyException + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def user_data_mock(mocker): + mock_user_data = mocker.patch( + "designsafe.apps.auth.backends.get_user_data", + return_value={ + "username": "testuser", + "firstName": "test", + "lastName": "user", + "email": "new@email.com", + }, + ) + return mock_user_data + + +@pytest.fixture() +def tapis_mock(mocker): + tapis_patcher = mocker.patch("designsafe.apps.auth.backends.Tapis") + mock_tapis = Mock() + mock_tapis.authenticator.get_userinfo.return_value = TapisResult( + username="testuser" + ) + tapis_patcher.return_value = mock_tapis + yield tapis_patcher + + +@pytest.fixture() +def update_institution_from_tas_mock(mocker): + yield mocker.patch("designsafe.apps.auth.backends.update_institution_from_tas") + + +# def test_launch_setup_checks(mocker, regular_user, settings): +# mocker.patch("designsafe.apps.auth.views.new_user_setup_check") +# mock_execute = mocker.patch("designsafe.apps.auth.views.execute_setup_steps") +# regular_user.profile.setup_complete = False +# launch_setup_checks(regular_user) +# mock_execute.apply_async.assert_called_with(args=["username"]) + + +def test_bad_backend_params(tapis_mock): + # Test backend authenticate with no backend params + backend = TapisOAuthBackend() + result = backend.authenticate() + assert result is None + + # Test TapisOAuthBackend if params do not indicate tapis + result = backend.authenticate(backend="not_tapis") + assert result is None + + +def test_bad_response_status( + tapis_mock, user_data_mock, update_institution_from_tas_mock +): + """Test that backend failure responses are handled""" + backend = TapisOAuthBackend() + mock_tapis = Mock() + mock_tapis.authenticator.get_userinfo.side_effect = BaseTapyException + tapis_mock.return_value = mock_tapis + result = backend.authenticate(backend="tapis", token="1234") + assert result is None + + +def test_new_user(tapis_mock, user_data_mock, update_institution_from_tas_mock): + """Test that a new user is created and returned""" + backend = TapisOAuthBackend() + result = backend.authenticate(backend="tapis", token="1234") + assert result.username == "testuser" + + +def test_update_existing_user( + tapis_mock, user_data_mock, update_institution_from_tas_mock +): + """Test that an existing user's information is updated with from info from the Tapis backend response""" + backend = TapisOAuthBackend() + + # Create a pre-existing user with the same username + user = get_user_model().objects.create_user( + username="testuser", + first_name="test", + last_name="user", + email="old@email.com", + ) + result = backend.authenticate(backend="tapis", token="1234") + # Result user object should be the same + assert result == user + # Existing user object should be updated + user = get_user_model().objects.get(username="testuser") + assert user.email == "new@email.com" diff --git a/designsafe/apps/auth/context_processors.py b/designsafe/apps/auth/context_processors.py deleted file mode 100644 index b4ec8e65a0..0000000000 --- a/designsafe/apps/auth/context_processors.py +++ /dev/null @@ -1,14 +0,0 @@ -from designsafe.apps.auth.models import AgaveOAuthToken - - -def auth(request): - try: - ag_token = request.user.agave_oauth - context = { - 'agave_ready': ag_token is not None - } - except (AttributeError, AgaveOAuthToken.DoesNotExist): - context = { - 'agave_ready': False - } - return context diff --git a/designsafe/apps/auth/middleware.py b/designsafe/apps/auth/middleware.py index acfc8174a5..10a9e81336 100644 --- a/designsafe/apps/auth/middleware.py +++ b/designsafe/apps/auth/middleware.py @@ -1,36 +1,75 @@ +""" +Auth middleware +""" + +import logging from django.contrib.auth import logout from django.core.exceptions import ObjectDoesNotExist -from requests.exceptions import RequestException, HTTPError -import logging -from django.utils.deprecation import MiddlewareMixin +from django.db import transaction +from django.http import HttpResponseRedirect +from django.urls import reverse +from tapipy.errors import BaseTapyException +from designsafe.apps.auth.models import TapisOAuthToken logger = logging.getLogger(__name__) -class AgaveTokenRefreshMiddleware(MiddlewareMixin): +class TapisTokenRefreshMiddleware: + """Refresh Middleware for a User's Tapis OAuth Token""" - def process_request(self, request): - if request.path != '/logout/' and request.user.is_authenticated: - try: - agave_oauth = request.user.agave_oauth - if agave_oauth.expired: - try: - agave_oauth.client.token.refresh() - except HTTPError: - logger.exception('Agave Token refresh failed; Forcing logout', - extra={'user': request.user.username}) - logout(request) - except ObjectDoesNotExist: - logger.warn('Authenticated user missing Agave API Token', - extra={'user': request.user.username}) - logout(request) - except RequestException: - logger.exception('Agave Token refresh failed. Forcing logout', - extra={'user': request.user.username}) - logout(request) - - def process_response(self, request, response): - if hasattr(request, 'user'): - if request.user.is_authenticated: - response['Authorization'] = 'Bearer ' + request.user.agave_oauth.access_token + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if ( + request.path != reverse("logout") + and request.path != reverse("login") + and not request.path.startswith("/static/") + and request.user.is_authenticated + ): + self.process_request(request) + + response = self.get_response(request) return response + + def process_request(self, request): + """Processes requests to backend and refreshes Tapis Token atomically if token is expired.""" + try: + tapis_oauth = request.user.tapis_oauth + except ObjectDoesNotExist: + logger.warning( + "Authenticated user missing Tapis OAuth Token", + extra={"user": request.user.username}, + ) + logout(request) + return HttpResponseRedirect(reverse("designsafe_auth:login")) + + if not tapis_oauth.expired: + return + + logger.info( + f"Tapis OAuth token expired for user {request.user.username}. Refreshing token" + ) + with transaction.atomic(): + # Get a lock on this user's token row in db. + latest_token = ( + TapisOAuthToken.objects.select_for_update() + .filter(user=request.user) + .first() + ) + if latest_token.expired: + try: + logger.info("Refreshing Tapis OAuth token") + tapis_oauth.refresh_tokens() + except BaseTapyException: + logger.exception( + "Tapis Token refresh failed. Forcing logout", + extra={"user": request.user.username}, + ) + logout(request) + return HttpResponseRedirect(reverse("designsafe_auth:login")) + + else: + logger.info( + "Token updated by another request. Refreshing token from DB." + ) diff --git a/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py new file mode 100644 index 0000000000..29a3f33c85 --- /dev/null +++ b/designsafe/apps/auth/migrations/0003_tapisoauthtoken_delete_agaveoauthtoken.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.6 on 2024-02-28 21:26 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("designsafe_auth", "0002_auto_20160209_0427"), + ] + + operations = [ + migrations.CreateModel( + name="TapisOAuthToken", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("access_token", models.CharField(max_length=2048)), + ("refresh_token", models.CharField(max_length=2048)), + ("expires_in", models.BigIntegerField()), + ("created", models.BigIntegerField()), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="tapis_oauth", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.DeleteModel( + name="AgaveOAuthToken", + ), + ] diff --git a/designsafe/apps/auth/models.py b/designsafe/apps/auth/models.py index 9f57bc9fb8..6db226f3f0 100644 --- a/designsafe/apps/auth/models.py +++ b/designsafe/apps/auth/models.py @@ -1,126 +1,115 @@ -from django.db import models -from django.conf import settings -from agavepy.agave import Agave -from agavepy import agave +"""Auth models +""" + import logging -import six import time -import requests -from requests import HTTPError -# from .signals import * -from designsafe.libs.common.decorators import deprecated +from django.db import models +from django.conf import settings +from tapipy.tapis import Tapis logger = logging.getLogger(__name__) TOKEN_EXPIRY_THRESHOLD = 600 -AGAVE_RESOURCES = agave.load_resource(getattr(settings, 'AGAVE_TENANT_BASEURL')) -class AgaveOAuthToken(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='agave_oauth', on_delete=models.CASCADE) - token_type = models.CharField(max_length=255) - scope = models.CharField(max_length=255) - access_token = models.CharField(max_length=255) - refresh_token = models.CharField(max_length=255) +class TapisOAuthToken(models.Model): + """Represents an Tapis OAuth Token object. + + Use this class to store login details as well as refresh a token. + """ + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, related_name="tapis_oauth", on_delete=models.CASCADE + ) + access_token = models.CharField(max_length=2048) + refresh_token = models.CharField(max_length=2048) expires_in = models.BigIntegerField() created = models.BigIntegerField() - @property - def masked_token(self): - return self.access_token[:8].ljust(len(self.access_token), '-') - @property def expired(self): - current_time = time.time() - return self.created + self.expires_in - current_time - TOKEN_EXPIRY_THRESHOLD <= 0 + """Check if token is expired + + :return: True or False, depending if the token is expired. + :rtype: bool + """ + return self.is_token_expired(self.created, self.expires_in) @property def created_at(self): - """ - Map the agavepy.Token property to model property + """Map the tapipy.Token property to model property + :return: The Epoch timestamp this token was created + :rtype: int """ return self.created_at @created_at.setter def created_at(self, value): - """ - Map the agavepy.Token property to model property - :param value: The Epoch timestamp this token was created + """Map the tapipy.Token property to model property + + :param int value: The Epoch timestamp this token was created """ self.created = value @property def token(self): + """Token dictionary. + + :return: Full token object + :rtype: dict + """ return { - 'access_token': self.access_token, - 'refresh_token': self.refresh_token, - 'token_type': self.token_type, - 'scope': self.scope, - 'created': self.created, - 'expires_in': self.expires_in + "access_token": self.access_token, + "refresh_token": self.refresh_token, + "created": self.created, + "expires_in": self.expires_in, } @property def client(self): - return Agave(api_server=getattr(settings, 'AGAVE_TENANT_BASEURL'), - api_key=getattr(settings, 'AGAVE_CLIENT_KEY'), - api_secret=getattr(settings, 'AGAVE_CLIENT_SECRET'), - token=self.access_token, - resources=AGAVE_RESOURCES, - refresh_token=self.refresh_token, - token_callback=self.update) + """Tapis client to limit one request to Tapis per User. + + :return: Tapis client using refresh token. + :rtype: :class:Tapis + """ + return Tapis( + base_url=getattr(settings, "TAPIS_TENANT_BASEURL"), + client_id=getattr(settings, "TAPIS_CLIENT_ID"), + client_key=getattr(settings, "TAPIS_CLIENT_KEY"), + access_token=self.access_token, + refresh_token=self.refresh_token, + ) def update(self, **kwargs): - for k, v in six.iteritems(kwargs): + """Bulk update model attributes""" + for k, v in kwargs.items(): setattr(self, k, v) self.save() - @deprecated - def refresh(self): - """ - DEPRECATED - :return: - """ - logger.debug('Refreshing Agave OAuth token for user=%s' % self.user.username) - ag = Agave(api_server=getattr(settings, 'AGAVE_TENANT_BASEURL'), - api_key=getattr(settings, 'AGAVE_CLIENT_KEY'), - api_secret=getattr(settings, 'AGAVE_CLIENT_SECRET'), - resources=AGAVE_RESOURCES, - token=self.access_token, - refresh_token=self.refresh_token) + def refresh_tokens(self): + """Refresh and update Tapis OAuth Tokens""" + self.client.refresh_tokens() + self.update( + created=int(time.time()), + access_token=self.client.access_token.access_token, + refresh_token=self.client.refresh_token.refresh_token, + expires_in=self.client.access_token.expires_in().total_seconds(), + ) + + def __str__(self): + access_token_masked = self.access_token[-5:] + refresh_token_masked = self.refresh_token[-5:] + return f"access_token:{access_token_masked} refresh_token:{refresh_token_masked} expires_in:{self.expires_in} created:{self.created}" + + @staticmethod + def is_token_expired(created, expires_in): + """Check if token is expired, with TOKEN_EXPIRY_THRESHOLD buffer.""" current_time = time.time() - ag.token.refresh() - self.created = int(current_time) - self.update(**ag.token.token_info) - logger.debug('Agave OAuth token for user=%s refreshed: %s' % (self.user.username, - self.masked_token)) - - -class AgaveServiceStatus(object): - page_id = getattr(settings, 'AGAVE_STATUSIO_PAGE_ID', '53a1e022814a437c5a000781') - status_io_base_url = getattr(settings, 'STATUSIO_BASE_URL', - 'https://api.status.io/1.0') - status_overall = {} - status = [] - incidents = [] - maintenance = { - 'active': [], - 'upcoming': [], - } - - def __init__(self): - self.update() - - def update(self): - try: - resp = requests.get('%s/status/%s' % (self.status_io_base_url, self.page_id)) - data = resp.json() - if 'result' in data: - for k, v, in six.iteritems(data['result']): - setattr(self, k, v) - else: - raise Exception(data) - except HTTPError: - logger.warning('Agave Service Status update failed') + return created + expires_in - current_time - TOKEN_EXPIRY_THRESHOLD <= 0 + + @staticmethod + def get_masked_token(token): + """Return a token as a masked string""" + return token[:8].ljust(len(token), "-") diff --git a/designsafe/apps/auth/models_unit_test.py b/designsafe/apps/auth/models_unit_test.py new file mode 100644 index 0000000000..cbd9d159c2 --- /dev/null +++ b/designsafe/apps/auth/models_unit_test.py @@ -0,0 +1,46 @@ +import pytest +import time +from datetime import timedelta +from designsafe.apps.auth.models import TapisOAuthToken + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def authenticated_user_with_expired_token(authenticated_user): + authenticated_user.tapis_oauth.expires_in = 0 + authenticated_user.tapis_oauth.save() + yield authenticated_user + + +@pytest.fixture +def authenticated_user_with_valid_token(authenticated_user): + authenticated_user.tapis_oauth.created = time.time() + authenticated_user.tapis_oauth.save() + yield authenticated_user + + +@pytest.fixture() +def tapis_client_mock(mocker): + mock_client = mocker.patch("designsafe.apps.auth.models.TapisOAuthToken.client") + mock_client.access_token.access_token = ("XYZXYZXYZ",) + mock_client.access_token.expires_in.return_value = timedelta(seconds=2000) + yield mock_client + + +def test_valid_user(client, authenticated_user_with_valid_token, tapis_client_mock): + tapis_oauth = ( + TapisOAuthToken.objects.filter(user=authenticated_user_with_valid_token) + .select_for_update() + .get() + ) + assert not tapis_oauth.expired + + +def test_expired_user(client, authenticated_user_with_expired_token, tapis_client_mock): + tapis_oauth = ( + TapisOAuthToken.objects.filter(user=authenticated_user_with_expired_token) + .select_for_update() + .get() + ) + assert tapis_oauth.expired diff --git a/designsafe/apps/auth/tasks.py b/designsafe/apps/auth/tasks.py index 99f07e4738..7ec331e93d 100644 --- a/designsafe/apps/auth/tasks.py +++ b/designsafe/apps/auth/tasks.py @@ -1,15 +1,18 @@ from datetime import datetime, timedelta import requests from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist from django.core.mail import send_mail -from agavepy.agave import Agave, AgaveException +from designsafe.apps.api.agave import get_service_account_client, get_tg458981_client from designsafe.apps.api.tasks import agave_indexer from designsafe.apps.api.notifications.models import Notification from celery import shared_task from django.contrib.auth import get_user_model from pytas.http import TASClient - -from requests import HTTPError +from tapipy.errors import NotFoundError, BaseTapyException +from designsafe.utils.system_access import register_public_key, create_system_credentials +from designsafe.utils.encryption import createKeyPair from django.contrib.auth import get_user_model import logging @@ -17,76 +20,77 @@ logger = logging.getLogger(__name__) -@shared_task(default_retry_delay=30, max_retries=3) -def check_or_create_agave_home_dir(username, systemId): +def get_systems_to_configure(username): + """ Get systems to configure either during startup or for new user """ + + systems = [] + for system in settings.TAPIS_SYSTEMS_TO_CONFIGURE: + system_copy = system.copy() + system_copy['path'] = system_copy['path'].format(username=username) + systems.append(system_copy) + return systems + + +@shared_task(default_retry_delay=30, max_retries=3, queue='onboarding', bind=True) +def check_or_configure_system_and_user_directory(self, username, system_id, path, create_path): try: - # TODO should use OS calls to create directory. - logger.info( - "Checking home directory for user=%s on " - "default storage systemId=%s", - username, - systemId + user_client = get_user_model().objects.get(username=username).tapis_oauth.client + user_client.files.listFiles( + systemId=system_id, path=path ) - ag = Agave(api_server=settings.AGAVE_TENANT_BASEURL, - token=settings.AGAVE_SUPER_TOKEN) - try: - listing_response = ag.files.list( - systemId=systemId, - filePath=username) - logger.info('check home dir response: {}'.format(listing_response)) - - except HTTPError as e: - if e.response.status_code == 404: - logger.info("Creating the home directory for user=%s then going to run setfacl", username) - body = {'action': 'mkdir', 'path': username} - fm_response = ag.files.manage(systemId=systemId, - filePath='', - body=body) - logger.info('mkdir response: {}'.format(fm_response)) - - ds_admin_client = Agave( - api_server=getattr( - settings, - 'AGAVE_TENANT_BASEURL' - ), - token=getattr( - settings, - 'AGAVE_SUPER_TOKEN' - ), + logger.info(f"System Works: " + f"Checked and there is no need to configure system:{system_id} path:{path} for {username}") + return + except ObjectDoesNotExist: + # User is missing; handling email confirmation process where user has not logged in + logger.info(f"New User: " + f"Checked and there is a need to configure system:{system_id} path:{path} for {username} ") + except BaseTapyException as e: + logger.info(f"Unable to list system/files: " + f"Checked and there is a need to configure system:{system_id} path:{path} for {username}: {e}") + + try: + if create_path: + tg458981_client = get_tg458981_client() + try: + # User tg account to check if path exists + tg458981_client.files.listFiles(systemId=system_id, path=path) + logger.info(f"Directory for user={username} on system={system_id}/{path} exists and works. ") + except NotFoundError: + logger.info("Creating the directory for user=%s then going to run setfacl on system=%s path=%s", + username, + system_id, + path) + + tg458981_client.files.mkdir(systemId=system_id, path=path) + tg458981_client.files.setFacl( + systemId=system_id, + path=path, + operation="ADD", + recursionMethod="PHYSICAL", + aclString=f"d:u:{username}:rwX,u:{username}:rwX,d:u:tg458981:rwX,u:tg458981:rwX,d:o::---,o::---,d:m::rwX,m::rwX", + ) + agave_indexer.apply_async( + kwargs={"systemId": system_id, "filePath": path, "recurse": False}, + queue="indexing", ) - if systemId == settings.AGAVE_STORAGE_SYSTEM: - job_body = { - 'parameters': { - 'username': username, - 'directory': 'shared/{}'.format(username) - }, - 'name': f'setfacl mydata for user {username}', - 'appId': 'setfacl_corral3-0.1' - } - elif systemId == settings.AGAVE_WORKING_SYSTEM: - job_body = { - 'parameters': { - 'username': username, - }, - 'name': f'setfacl work for user {username}', - 'appId': 'setfacl_frontera_work-0.1' - } - else: - logger.error('Attempting to set permissions on unsupported system: {}'.format(systemId)) - return - - jobs_response = ds_admin_client.jobs.submit(body=job_body) - logger.info('setfacl response: {}'.format(jobs_response)) - - # add dir to index - logger.info("Indexing the home directory for user=%s", username) - agave_indexer.apply_async(kwargs={'username': username, 'systemId': systemId, 'filePath': username}, queue='indexing') - - except AgaveException: - logger.exception('Failed to create home directory.', + # create keys, push to key service and use as credential for Tapis system + logger.info("Creating credentials for user=%s on system=%s", username, system_id) + (private_key, public_key) = createKeyPair() + register_public_key(username, public_key, system_id) + service_account = get_service_account_client() + create_system_credentials(service_account, + username, + public_key, + private_key, + system_id) + except BaseTapyException as exc: + logger.exception('Failed to configure system (i.e. create directory, set acl, create credentials).', extra={'user': username, - 'systemId': systemId}) + 'systemId': system_id, + 'path': path}) + raise self.retry(exc=exc) @shared_task(default_retry_delay=30, max_retries=3) @@ -97,7 +101,7 @@ def new_user_alert(username): 'Name: ' + user.first_name + ' ' + user.last_name + '\n' + 'Id: ' + str(user.id) + '\n', settings.DEFAULT_FROM_EMAIL, settings.NEW_ACCOUNT_ALERT_EMAILS.split(','),) - + tram_headers = {"tram-services-key": settings.TRAM_SERVICES_KEY} tram_body = {"project_id": settings.TRAM_PROJECT_ID, "email": user.email} diff --git a/designsafe/apps/auth/templates/designsafe/apps/auth/login.html b/designsafe/apps/auth/templates/designsafe/apps/auth/login.html index 6b8e292501..a10dbc2408 100644 --- a/designsafe/apps/auth/templates/designsafe/apps/auth/login.html +++ b/designsafe/apps/auth/templates/designsafe/apps/auth/login.html @@ -23,13 +23,13 @@

    Log in {% endif %} -{% endif %} {% addtoblock "css" %} @@ -52,21 +51,22 @@ {% endaddtoblock %} +{% endif %} -{% addtoblock "js" %} +{% addtoblock "react_assets" %} {% if debug and react_flag %} - - - + + {% else %} - + {% include "react-assets.html" %} {% endif %} {% endaddtoblock %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/designsafe/apps/workspace/tests.py b/designsafe/apps/workspace/tests.py index c1261d9a1c..0e09e88518 100644 --- a/designsafe/apps/workspace/tests.py +++ b/designsafe/apps/workspace/tests.py @@ -1,40 +1,48 @@ import json -import os from mock import patch from django.test import TestCase from .models.app_descriptions import AppDescription +from unittest import skip from django.urls import reverse from django.contrib.auth import get_user_model +@skip("TODOv3: Update apps api with Tapisv3") class AppDescriptionModelTest(TestCase): - fixtures = ['user-data', 'agave-oauth-token-data'] + fixtures = ["user-data", "auth"] def setUp(self): user = get_user_model().objects.get(pk=2) - user.set_password('user/password') + user.set_password("user/password") user.save() def test_string_representation(self): - descriptionModel = AppDescription(appid='TestApp0.1', appdescription='Test description') + descriptionModel = AppDescription( + appid="TestApp0.1", appdescription="Test description" + ) self.assertEqual(str(descriptionModel), descriptionModel.appid) def test_get_app_description(self): - AppDescription.objects.create(appid='TestApp0.1', appdescription='Test description') - self.client.login(username='ds_user', password='user/password') - url = reverse('designsafe_workspace:call_api', args=('description',)) - response = self.client.get(url, {'app_id': 'TestApp0.1'}) - self.assertContains(response, 'TestApp0.1') + AppDescription.objects.create( + appid="TestApp0.1", appdescription="Test description" + ) + self.client.login(username="ds_user", password="user/password") + url = reverse("designsafe_workspace:call_api", args=("description",)) + response = self.client.get(url, {"app_id": "TestApp0.1"}) + self.assertContains(response, "TestApp0.1") +@skip("TODOv3: Update apps api with Tapisv3") class TestAppsApiViews(TestCase): - fixtures = ['user-data', 'agave-oauth-token-data'] + fixtures = ["user-data", "auth"] @classmethod def setUpClass(cls): super(TestAppsApiViews, cls).setUpClass() - cls.mock_client_patcher = patch('designsafe.apps.auth.models.AgaveOAuthToken.client') + cls.mock_client_patcher = patch( + "designsafe.apps.auth.models.TapisOAuthToken.client" + ) cls.mock_client = cls.mock_client_patcher.start() @classmethod @@ -44,26 +52,20 @@ def tearDownClass(cls): def setUp(self): user = get_user_model().objects.get(pk=2) - user.set_password('user/password') + user.set_password("user/password") user.save() def test_apps_list(self): - self.client.login(username='ds_user', password='user/password') + self.client.login(username="ds_user", password="user/password") apps = [ - { - "id": "app-one", - "executionSystem": "stampede2" - }, - { - "id": "app-two", - "executionSystem": "stampede2" - } + {"id": "app-one", "executionSystem": "stampede2"}, + {"id": "app-two", "executionSystem": "stampede2"}, ] - #need to do a return_value on the mock_client because - #the calling signature is something like client = Agave(**kwargs).apps.list() + # need to do a return_value on the mock_client because + # the calling signature is something like client = Agave(**kwargs).apps.list() self.mock_client.apps.list.return_value = apps - url = reverse('designsafe_workspace:call_api', args=('apps',)) + url = reverse("designsafe_workspace:call_api", args=("apps",)) response = self.client.get(url, follow=True) data = response.json() # If the request is sent successfully, then I expect a response to be returned. @@ -72,37 +74,41 @@ def test_apps_list(self): self.assertTrue(data == apps) def test_job_submit_notifications(self): - with open('designsafe/apps/workspace/fixtures/job-submission.json') as f: + with open("designsafe/apps/workspace/fixtures/job-submission.json") as f: job_data = json.load(f) self.mock_client.jobs.submit.return_value = {"status": "ok"} - self.client.login(username='ds_user', password='user/password') + self.client.login(username="ds_user", password="user/password") - url = reverse('designsafe_workspace:call_api', args=('jobs',)) - response = self.client.post(url, json.dumps(job_data), content_type="application/json") + url = reverse("designsafe_workspace:call_api", args=("jobs",)) + response = self.client.post( + url, json.dumps(job_data), content_type="application/json" + ) data = response.json() self.assertTrue(self.mock_client.jobs.submit.called) - self.assertEqual(data['status'], 'ok') + self.assertEqual(data["status"], "ok") self.assertEqual(response.status_code, 200) def test_job_submit_parse_urls(self): - with open('designsafe/apps/workspace/fixtures/job-submission.json') as f: + with open("designsafe/apps/workspace/fixtures/job-submission.json") as f: job_data = json.load(f) # the spaces should get quoted out job_data["inputs"]["workingDirectory"] = "agave://test.system/name with spaces" self.mock_client.jobs.submit.return_value = {"status": "ok"} - self.client.login(username='ds_user', password='user/password') + self.client.login(username="ds_user", password="user/password") - url = reverse('designsafe_workspace:call_api', args=('jobs',)) - response = self.client.post(url, json.dumps(job_data), content_type="application/json") + url = reverse("designsafe_workspace:call_api", args=("jobs",)) + response = self.client.post( + url, json.dumps(job_data), content_type="application/json" + ) self.assertEqual(response.status_code, 200) args, kwargs = self.mock_client.jobs.submit.call_args body = kwargs["body"] input = body["inputs"]["workingDirectory"] - #the spaces should have been quoted + # the spaces should have been quoted self.assertTrue("%20" in input) def test_licensed_apps(self): diff --git a/designsafe/apps/workspace/urls.py b/designsafe/apps/workspace/urls.py index f17951f190..362316ed6a 100644 --- a/designsafe/apps/workspace/urls.py +++ b/designsafe/apps/workspace/urls.py @@ -1,22 +1,8 @@ -from django.urls import re_path as url -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +"""Workspace URLs +""" +from django.urls import re_path from designsafe.apps.workspace import views -# TODO look at linking directly into an app in the workspace - urlpatterns = [ - url(r'^$', views.index, name='index'), - url(r'^api/(?P[a-z]+?)/$', views.call_api, name='call_api'), - url(r'^notification/process/(?P\d+)', views.process_notification, name='process_notification'), + re_path('^', views.WorkspaceView.as_view(), name="workspace"), ] - -def menu_items(**kwargs): - if 'type' in kwargs and kwargs['type'] == 'research_workbench': - return [ - { - 'label': _('Workspace'), - 'url': reverse('designsafe_workspace:index'), - 'children': [] - } - ] diff --git a/designsafe/apps/workspace/views.py b/designsafe/apps/workspace/views.py index 20325b89cf..6c2ab98af8 100644 --- a/designsafe/apps/workspace/views.py +++ b/designsafe/apps/workspace/views.py @@ -1,286 +1,19 @@ -from agavepy.agave import AgaveException, Agave -from django.shortcuts import render, redirect -from django.conf import settings +""" +.. :module: apps.workspace.views + :synopsis: Views to handle Workspace +""" +from django.views.generic.base import TemplateView +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import ensure_csrf_cookie from django.contrib.auth.decorators import login_required -from django.core.serializers.json import DjangoJSONEncoder -from django.urls import reverse -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpResponse -from designsafe.apps.api.notifications.models import Notification -from designsafe.apps.workspace.tasks import JobSubmitError, submit_job -from designsafe.apps.licenses.models import LICENSE_TYPES, get_license_info -from designsafe.libs.common.decorators import profile as profile_fn -from designsafe.apps.api.tasks import index_or_update_project -from designsafe.apps.workspace import utils as WorkspaceUtils -from designsafe.apps.workspace.models.app_descriptions import AppDescription -from requests import HTTPError -from urllib.parse import urlparse -from datetime import datetime -import json -import six -import logging -import urllib.request, urllib.parse, urllib.error -logger = logging.getLogger(__name__) -@login_required -def index(request): - context = { - } - return render(request, 'designsafe/apps/workspace/index.html', context) +@method_decorator(login_required, name="dispatch") +class WorkspaceView(TemplateView): + """Workspace View""" + template_name = 'designsafe/apps/workspace/index.html' - -def _app_license_type(app_id): - app_lic_type = app_id.replace('-{}'.format(app_id.split('-')[-1]), '').upper() - lic_type = next((t for t in LICENSE_TYPES if t in app_lic_type), None) - return lic_type - - -@profile_fn -@login_required -def call_api(request, service): - try: - agave = request.user.agave_oauth.client - if service == 'apps': - app_id = request.GET.get('app_id') - if app_id: - data = agave.apps.get(appId=app_id) - data['exec_sys'] = agave.systems.get(systemId=data['executionSystem']) - lic_type = _app_license_type(app_id) - data['license'] = { - 'type': lic_type - } - if lic_type is not None: - _, license_models = get_license_info() - license_model = [x for x in license_models if x.license_type == lic_type][0] - lic = license_model.objects.filter(user=request.user).first() - data['license']['enabled'] = lic is not None - - else: - - public_only = request.GET.get('publicOnly') - if public_only == 'true': - data = agave.apps.list(publicOnly='true') - else: - data = agave.apps.list() - - elif service == 'monitors': - target = request.GET.get('target') - ds_admin_client = Agave(api_server=getattr(settings, 'AGAVE_TENANT_BASEURL'), token=getattr(settings, 'AGAVE_SUPER_TOKEN')) - data = ds_admin_client.monitors.list(target=target) - - elif service == 'meta': - app_id = request.GET.get('app_id') - if request.method == 'GET': - if app_id: - data = agave.meta.get(appId=app_id) - lic_type = _app_license_type(app_id) - data['license'] = { - 'type': lic_type - } - if lic_type is not None: - _, license_models = get_license_info() - license_model = [x for x in license_models if x.license_type == lic_type][0] - lic = license_model.objects.filter(user=request.user).first() - data['license']['enabled'] = lic is not None - - else: - query = request.GET.get('q') - data = agave.meta.listMetadata(q=query) - elif request.method == 'POST': - meta_post = json.loads(request.body) - meta_uuid = meta_post.get('uuid') - - if meta_uuid: - del meta_post['uuid'] - data = agave.meta.updateMetadata(uuid=meta_uuid, body=meta_post) - index_or_update_project.apply_async(args=[meta_uuid], queue='api') - else: - data = agave.meta.addMetadata(body=meta_post) - elif request.method == 'DELETE': - meta_uuid = request.GET.get('uuid') - if meta_uuid: - data = agave.meta.deleteMetadata(uuid=meta_uuid) - - - # TODO: Need auth on this DELETE business - elif service == 'jobs': - if request.method == 'DELETE': - job_id = request.GET.get('job_id') - data = agave.jobs.delete(jobId=job_id) - elif request.method == 'POST': - job_post = json.loads(request.body) - logger.debug(job_post) - job_id = job_post.get('job_id') - - # cancel job / stop job - if job_id: - data = agave.jobs.manage(jobId=job_id, body='{"action":"stop"}') - - # submit job - elif job_post: - - # cleaning archive path value - if 'archivePath' in job_post: - parsed = urlparse(job_post['archivePath']) - if parsed.path.startswith('/'): - # strip leading '/' - archive_path = parsed.path[1:] - else: - archive_path = parsed.path - - job_post['archivePath'] = archive_path - - if parsed.netloc: - job_post['archiveSystem'] = parsed.netloc - else: - job_post['archivePath'] = \ - '{}/archive/jobs/{}/${{JOB_NAME}}-${{JOB_ID}}'.format( - request.user.username, - datetime.now().strftime('%Y-%m-%d')) - - # check for running licensed apps - lic_type = _app_license_type(job_post['appId']) - if lic_type is not None: - _, license_models = get_license_info() - license_model = [x for x in license_models if x.license_type == lic_type][0] - lic = license_model.objects.filter(user=request.user).first() - job_post['parameters']['_license'] = lic.license_as_str() - - # url encode inputs - if job_post['inputs']: - for key, value in six.iteritems(job_post['inputs']): - if type(value) == list: - inputs = [] - for val in value: - parsed = urlparse(val) - if parsed.scheme: - inputs.append('{}://{}{}'.format( - parsed.scheme, parsed.netloc, urllib.parse.quote(parsed.path))) - else: - inputs.append(urllib.parse.quote(parsed.path)) - job_post['inputs'][key] = inputs - else: - parsed = urlparse(value) - if parsed.scheme: - job_post['inputs'][key] = '{}://{}{}'.format( - parsed.scheme, parsed.netloc, urllib.parse.quote(parsed.path)) - else: - job_post['inputs'][key] = urllib.parse.quote(parsed.path) - - if settings.DEBUG: - wh_base_url = settings.WEBHOOK_POST_URL.strip('/') + '/webhooks/' - jobs_wh_url = settings.WEBHOOK_POST_URL + reverse('designsafe_api:jobs_wh_handler') - else: - wh_base_url = request.build_absolute_uri('/webhooks/') - jobs_wh_url = request.build_absolute_uri(reverse('designsafe_api:jobs_wh_handler')) - - job_post['parameters']['_webhook_base_url'] = wh_base_url - - # Remove any params from job_post that are not in appDef - for param, _ in list(job_post['parameters'].items()): - if not any(p['id'] == param for p in job_post['appDefinition']['parameters']): - del job_post['parameters'][param] - - del job_post['appDefinition'] - - job_post['notifications'] = [ - {'url': jobs_wh_url, - 'event': e} - for e in ["PENDING", "QUEUED", "SUBMITTING", "PROCESSING_INPUTS", "STAGED", "RUNNING", "KILLED", "FAILED", "STOPPED", "FINISHED", "BLOCKED"]] - - try: - data = submit_job(request, request.user.username, job_post) - except JobSubmitError as e: - data = e.json() - logger.error('Failed to submit job {0}'.format(data)) - return HttpResponse(json.dumps(data), - content_type='application/json', - status=e.status_code) - - # list jobs (via POST?) - else: - limit = request.GET.get('limit', 10) - offset = request.GET.get('offset', 0) - data = agave.jobs.list(limit=limit, offset=offset) - - elif request.method == 'GET': - job_id = request.GET.get('job_id') - - # get specific job info - if job_id: - data = agave.jobs.get(jobId=job_id) - q = {"associationIds": job_id} - job_meta = agave.meta.listMetadata(q=json.dumps(q)) - data['_embedded'] = {"metadata": job_meta} - - archive_system_path = '{}/{}'.format(data['archiveSystem'], - data['archivePath']) - data['archiveUrl'] = reverse( - 'designsafe_data:data_depot') - data['archiveUrl'] += 'agave/{}/'.format(archive_system_path) - - # list jobs - else: - limit = request.GET.get('limit', 10) - offset = request.GET.get('offset', 0) - data = agave.jobs.list(limit=limit, offset=offset) - else: - return HttpResponse('Unexpected service: %s' % service, status=400) - - elif service == 'ipynb': - put = json.loads(request.body) - dir_path = put.get('file_path') - system = put.get('system') - data = WorkspaceUtils.setup_identity_file( - request.user.username, - agave, - system, - dir_path - ) - elif service == 'description': - app_id = request.GET.get('app_id') - try: - data = AppDescription.objects.get(appid=app_id).desc_to_dict() - except ObjectDoesNotExist: - return HttpResponse('No description found for {}'.format(app_id), status=200) - else: - return HttpResponse('Unexpected service: %s' % service, status=400) - except HTTPError as e: - logger.exception( - 'Failed to execute %s API call due to HTTPError=%s\n%s', - service, - e, - e.response.content - ) - return HttpResponse(json.dumps(e), - content_type='application/json', - status=400) - except AgaveException as e: - logger.exception('Failed to execute {0} API call due to AgaveException={1}'.format( - service, e)) - return HttpResponse(json.dumps(e), content_type='application/json', - status=400) - except Exception as e: - logger.exception('Failed to execute {0} API call due to Exception={1}'.format( - service, e)) - return HttpResponse( - json.dumps({'status': 'error', 'message': '{}'.format(e)}), - content_type='application/json', status=400) - - return HttpResponse(json.dumps(data, cls=DjangoJSONEncoder), - content_type='application/json') - - -def process_notification(request, pk, **kwargs): - n = Notification.objects.get(pk=pk) - extra = n.extra_content - logger.info('extra: {}'.format(extra)) - archiveSystem = extra['archiveSystem'] - archivePath = extra['archivePath'] - - archive_id = '%s/%s' % (archiveSystem, archivePath) - - target_path = reverse('designsafe_data:data_depot') + 'agave/' + archive_id + '/' - - return redirect(target_path) + @method_decorator(ensure_csrf_cookie) + def dispatch(self, request, *args, **kwargs): + """Overwrite dispatch to ensure csrf cookie""" + return super(WorkspaceView, self).dispatch(request, *args, **kwargs) diff --git a/designsafe/asgi.py b/designsafe/asgi.py index 4916d71d1b..e79c37d877 100644 --- a/designsafe/asgi.py +++ b/designsafe/asgi.py @@ -16,7 +16,6 @@ from django.urls import re_path from designsafe.apps.signals.websocket_consumers import DesignsafeWebsocketConsumer -#from chat.routing import websocket_urlpatterns os.environ.setdefault("DJANGO_SETTINGS_MODULE", "designsafe.settings") websocket_urlpatterns = [ diff --git a/designsafe/conftest.py b/designsafe/conftest.py index 3b327245b1..d32b49dc54 100644 --- a/designsafe/conftest.py +++ b/designsafe/conftest.py @@ -1,44 +1,63 @@ +"""Base User pytest fixtures""" + import pytest +import os +import json +from unittest.mock import patch from django.conf import settings -from designsafe.apps.auth.models import AgaveOAuthToken +from designsafe.apps.auth.models import TapisOAuthToken + @pytest.fixture -def mock_agave_client(mocker): - yield mocker.patch('designsafe.apps.auth.models.AgaveOAuthToken.client', autospec=True) +def mock_tapis_client(mocker): + """Tapis client fixture""" + yield mocker.patch( + "designsafe.apps.auth.models.TapisOAuthToken.client", autospec=True + ) @pytest.fixture -def regular_user(django_user_model, mock_agave_client): - django_user_model.objects.create_user(username="username", - password="password", - first_name="Firstname", - last_name="Lastname", - email="user@user.com") - django_user_model.objects.create_user(username="username2", - password="password2", - first_name="Firstname2", - last_name="Lastname2", - email="user@user.com2") +def regular_user(django_user_model, mock_tapis_client): + """Normal User fixture""" + django_user_model.objects.create_user( + username="username", + password="password", + first_name="Firstname", + last_name="Lastname", + email="user@user.com", + ) user = django_user_model.objects.get(username="username") - token = AgaveOAuthToken.objects.create( + TapisOAuthToken.objects.create( user=user, - token_type="bearer", - scope="default", access_token="1234fsf", refresh_token="123123123", expires_in=14400, - created=1523633447) - token.save() + created=1523633447, + ) yield user +@pytest.fixture +def regular_user_using_jwt(regular_user, client): + """Fixture for regular user who is using jwt for authenticated requests""" + with patch('designsafe.apps.api.decorators.Tapis') as mock_tapis: + # Mock the Tapis's validate_token method within the tapis_jwt_login decorator + mock_validate_token = mock_tapis.return_value.validate_token + mock_validate_token.return_value = {"tapis/username": regular_user.username} + + client.defaults['HTTP_X_TAPIS_TOKEN'] = 'fake_token_string' + + yield client + + @pytest.fixture def project_admin_user(django_user_model): - django_user_model.objects.create_user(username="test_prjadmin", - password="password", - first_name="Project", - last_name="Admin", + django_user_model.objects.create_user( + username="test_prjadmin", + password="password", + first_name="Project", + last_name="Admin", ) user = django_user_model.objects.get(username="test_prjadmin") yield user @@ -48,3 +67,16 @@ def project_admin_user(django_user_model): def authenticated_user(client, regular_user): client.force_login(regular_user) yield regular_user + + +@pytest.fixture +def tapis_tokens_create_mock(): + yield json.load( + open( + os.path.join( + settings.BASE_DIR, + "designsafe/fixtures/tapis/auth/create-tokens-response.json", + ), + "r", + ) + ) diff --git a/designsafe/fixtures/auth.json b/designsafe/fixtures/auth.json new file mode 100644 index 0000000000..b338bfb9e9 --- /dev/null +++ b/designsafe/fixtures/auth.json @@ -0,0 +1,35 @@ +[ + { + "model": "designsafe_auth.tapisoauthtoken", + "pk": 1, + "fields": { + "user": 1, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536692280 + } + }, + { + "model": "designsafe_auth.tapisoauthtoken", + "pk": 2, + "fields": { + "user": 2, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700041 + } + }, + { + "model": "designsafe_auth.tapisoauthtoken", + "pk": 3, + "fields": { + "user": 3, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700084 + } + } +] diff --git a/designsafe/fixtures/tapis/auth/create-tokens-response.json b/designsafe/fixtures/tapis/auth/create-tokens-response.json new file mode 100644 index 0000000000..00191a6106 --- /dev/null +++ b/designsafe/fixtures/tapis/auth/create-tokens-response.json @@ -0,0 +1,20 @@ +{ + "message": "Token created successfully.", + "metadata": {}, + "result": { + "access_token": { + "access_token": "eyJhbGci---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------", + "expires_at": "2024-03-01T03:47:18.611914+00:00", + "expires_in": 14400, + "jti": "108792e6-2a77-41ad-964c-f289cc2198f7" + }, + "refresh_token": { + "expires_at": "2025-02-28T23:47:18.711146+00:00", + "expires_in": 31536000, + "jti": "69992b30-3b3b-477a-ba22-3bd2a8203791", + "refresh_token": "eyJhbGci---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------" + } + }, + "status": "success", + "version": "dev" +} diff --git a/designsafe/fixtures/user-data.json b/designsafe/fixtures/user-data.json new file mode 100644 index 0000000000..5d1cb1c5db --- /dev/null +++ b/designsafe/fixtures/user-data.json @@ -0,0 +1,103 @@ +[ + { + "fields": { + "username": "ds_admin", + "first_name": "DesignSafe", + "last_name": "Admin", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "admin@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" + }, + "model": "auth.user", + "pk": 1 + }, + { + "fields": { + "username": "envision", + "first_name": "DesignSafe", + "last_name": "Admin", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "admin@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" + }, + "model": "auth.user", + "pk": 3 + }, + { + "fields": { + "username": "ds_user", + "first_name": "DesignSafe", + "last_name": "User", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2016-03-01T00:00:00.000Z", + "groups": [], + "user_permissions": [], + "password": "", + "email": "user@designsafe-ci.org", + "date_joined": "2016-03-01T00:00:00.000Z" + }, + "model": "auth.user", + "pk": 2 + }, + { + "fields": { + "user": 2, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJkMGU1YWZiZi05Yzk3LTQyOTMtOTNlMS1jYWIyYzAxY2JhMDAiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJhY2Nlc3MiLCJ0YXBpcy9kZWxlZ2F0aW9uIjpmYWxzZSwidGFwaXMvZGVsZWdhdGlvbl9zdWIiOm51bGwsInRhcGlzL3VzZXJuYW1lIjoidGVzdHVzZXIyMDAiLCJ0YXBpcy9hY2NvdW50X3R5cGUiOiJ1c2VyIiwiZXhwIjoxNjU2MDE5MzM1fQ.2mevJWnoS-nlUNfna17berL1HKCHKaPuX6BGi8RZQTQV2meFRLNhAu8B0nDJvROTqYiHna23N2h_FEgS51kRhpwL8N3zTuguh2cT090GxzCFw1QnI1V2rNK4zZjvxagciJxov8SbaOgta6H6_AUentKi_NFjpYTerPRjCDkuCwYitvGOJdzTUFY7cn8SX6JQvlRkcwQ7I0bfC5JN5m5Q0trPD5r2-VDIElI5JVY_isMMT9O5-lT1HTIN1BCYoOnLPgza6vkZeWdArsW9bcvpMANjDlK3mWFtc1fEybN6O3c9RaxRj8GO8zNoyngNH7h6DXeEGdsVJcrt9VWI-nW8iA", + "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwYTYzNTAxOS1mNTllLTQxMjItOGUwNi0zZmRkYTNmMzYzNWEiLCJpc3MiOiJodHRwczovL2Rldi5kZXZlbG9wLnRhcGlzLmlvL3YzL3Rva2VucyIsInN1YiI6InRlc3R1c2VyMjAwQGRldiIsInRhcGlzL2luaXRpYWxfdHRsIjo2MDAsInRhcGlzL3RlbmFudF9pZCI6ImRldiIsInRhcGlzL3Rva2VuX3R5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNjU2MDE5MzM1LCJ0YXBpcy9hY2Nlc3NfdG9rZW4iOnsianRpIjoiZDBlNWFmYmYtOWM5Ny00MjkzLTkzZTEtY2FiMmMwMWNiYTAwIiwiaXNzIjoiaHR0cHM6Ly9kZXYuZGV2ZWxvcC50YXBpcy5pby92My90b2tlbnMiLCJzdWIiOiJ0ZXN0dXNlcjIwMEBkZXYiLCJ0YXBpcy90ZW5hbnRfaWQiOiJkZXYiLCJ0YXBpcy90b2tlbl90eXBlIjoiYWNjZXNzIiwidGFwaXMvZGVsZWdhdGlvbiI6ZmFsc2UsInRhcGlzL2RlbGVnYXRpb25fc3ViIjpudWxsLCJ0YXBpcy91c2VybmFtZSI6InRlc3R1c2VyMjAwIiwidGFwaXMvYWNjb3VudF90eXBlIjoidXNlciIsInR0bCI6NjAwfX0.WI9vfN6SPNJwDR9uOaJ16quGzyKl-RWoaDwbOaQa1gpSQoutw8lBqsifzUb0WEJ9fqg8ZWAwbuu-IJikXTiwOiUqWy-09yHxNtCFpBARpY-jurMe20HbDCSlPGICpf8Bend-3tMSnf5c9JyuAgbVx1fnqSjhY3V7yiTVzCur-mOWqI47TiflDnddPscyQj7HBawwadinSiSwQKbnXw2FNkRIdKRrCEOaecKaZ-Hb69vHbi-A3D-HP80nhZzuQW8vzg0L_3cyGOh_Y-8qu22_21UfJwS_nWEizjrs9WTU5hCGpn2Da8U035gk01eC4S9J_WIhZjUhBRneB14QfgTNvg", + "expires_in": 1325391984000, + "created": 1536700084 + }, + "model": "designsafe_auth.tapisoauthtoken", + "pk": 1 + }, + { + "fields": { + "nickname": "Test Token", + "user": 2, + "created": "2016-09-06T00:00:00.000Z" + }, + "model": "token_access.token", + "pk": "5da84493fa0037de0945631d1f9df5c00cdcac49" + }, + { + "model": "designsafe_accounts.designsafeprofile", + "pk": 5610, + "fields": { + "user": 2, + "ethnicity": "Asian", + "gender": "Male", + "agree_to_account_limit": "2020-07-02T23:41:19.342Z", + "bio": null, + "website": null, + "orcid_id": null, + "professional_level": null, + "update_required": true, + "last_updated": "2020-07-02T23:41:19.343Z", + "nh_interests": [], + "nh_technical_domains": [], + "research_activities": [] + } + }, + { + "fields": { + "announcements": true, + "user": 2 + }, + "model": "designsafe_accounts.notificationpreferences", + "pk": 1 + } +] diff --git a/designsafe/libs/elasticsearch/docs.py b/designsafe/libs/elasticsearch/docs.py index 2dad2365d0..13e426ce41 100644 --- a/designsafe/libs/elasticsearch/docs.py +++ b/designsafe/libs/elasticsearch/docs.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES different doc types. """ -from future.utils import python_2_unicode_compatible + import logging import json from django.conf import settings diff --git a/designsafe/libs/elasticsearch/docs/base.py b/designsafe/libs/elasticsearch/docs/base.py index c50d85f8f2..1b422da64a 100644 --- a/designsafe/libs/elasticsearch/docs/base.py +++ b/designsafe/libs/elasticsearch/docs/base.py @@ -1,8 +1,8 @@ -from future.utils import python_2_unicode_compatible + import logging -@python_2_unicode_compatible + class BaseESResource(object): """Base class used to represent an Elastic Search resource. @@ -14,7 +14,7 @@ class BaseESResource(object): """ def __init__(self, wrapped_doc=None, **kwargs): self._wrap(wrapped_doc, **kwargs) - + def to_dict(self): """Return wrapped doc as dict""" return self._wrapped.to_dict() @@ -32,7 +32,7 @@ def __getattr__(self, name): """ _wrapped = object.__getattribute__(self, '_wrapped') if _wrapped and hasattr(_wrapped, name): - return getattr(_wrapped, name) + return getattr(_wrapped, name) else: return object.__getattribute__(self, name) @@ -43,4 +43,4 @@ def __setattr__(self, name, value): return else: object.__setattr__(self, name, value) - return \ No newline at end of file + return diff --git a/designsafe/libs/elasticsearch/docs/files.py b/designsafe/libs/elasticsearch/docs/files.py index ffdb04e193..c61e585cea 100644 --- a/designsafe/libs/elasticsearch/docs/files.py +++ b/designsafe/libs/elasticsearch/docs/files.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES ``files`` doc type. """ -from future.utils import python_2_unicode_compatible + import logging import os from django.conf import settings @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) #pylint: enable=invalid-name -@python_2_unicode_compatible + class BaseESFile(BaseESResource): """Wrapper class for Elastic Search indexed file. @@ -61,25 +61,25 @@ def _index_cls(cls, reindex): def children(self, limit=100): """ - Yield all children (i.e. documents whose basePath matches self.path) by + Yield all children (i.e. documents whose basePath matches self.path) by paginating with the search_after api. """ res, search_after = self._index_cls(self._reindex).children( self.username, self.system, - self.path, + self.path, limit=limit) for doc in res: yield BaseESFile(self.username, wrapped_doc=doc) while not len(res) < limit: # If the number or results doesn't match the limit, we're done paginating. - # Retrieve the sort key from the last element then use + # Retrieve the sort key from the last element then use # search_after to get the next page of results res, search_after = self._index_cls(self._reindex).children( self.username, self.system, - self.path, + self.path, limit=limit, search_after=search_after) for doc in res: @@ -101,4 +101,4 @@ def delete(self): for child in children: if child.path != self.path: child.delete() - self._wrapped.delete() \ No newline at end of file + self._wrapped.delete() diff --git a/designsafe/libs/elasticsearch/docs/publication_legacy.py b/designsafe/libs/elasticsearch/docs/publication_legacy.py index 1255100ff2..80185aa833 100644 --- a/designsafe/libs/elasticsearch/docs/publication_legacy.py +++ b/designsafe/libs/elasticsearch/docs/publication_legacy.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES ``files`` doc type. """ -from future.utils import python_2_unicode_compatible + import logging import os import zipfile @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) #pylint: enable=invalid-name -@python_2_unicode_compatible + class BaseESPublicationLegacy(BaseESResource): """Wrapper class for Elastic Search indexed NEES publication. @@ -67,10 +67,10 @@ def to_file(self): publication_dict = self.to_dict() project_dict = {} - for key in ['deleted', 'description', 'endDate', 'facility', 'name', + for key in ['deleted', 'description', 'endDate', 'facility', 'name', 'organization', 'pis', 'project', 'projectPath', 'publications', 'startDate', 'system', 'title', 'sponsor']: - + if key in publication_dict: project_dict[key] = publication_dict[key] @@ -97,5 +97,5 @@ def to_file(self): 'experiments': experiments, 'project': project_dict }} - + return dict_obj diff --git a/designsafe/libs/elasticsearch/docs/publications.py b/designsafe/libs/elasticsearch/docs/publications.py index 396eb1a32d..e822f11487 100644 --- a/designsafe/libs/elasticsearch/docs/publications.py +++ b/designsafe/libs/elasticsearch/docs/publications.py @@ -5,7 +5,7 @@ """ import logging -from future.utils import python_2_unicode_compatible + from designsafe.apps.data.models.elasticsearch import IndexedPublication from designsafe.libs.elasticsearch.docs.base import BaseESResource from designsafe.libs.elasticsearch.exceptions import DocumentNotFound @@ -16,7 +16,7 @@ # pylint: enable=invalid-name -@python_2_unicode_compatible + class BaseESPublication(BaseESResource): """Wrapper class for Elastic Search indexed publication. @@ -157,7 +157,7 @@ def to_file(self): except: dict_obj['meta']['piLabel'] = '({pi})'.format(pi=pi) return dict_obj - + def entity_keys(self, publishable=False): """Type specific keys for publication""" diff --git a/designsafe/libs/elasticsearch/indices.py b/designsafe/libs/elasticsearch/indices.py index 8e51a1ba3d..2ae93abe64 100644 --- a/designsafe/libs/elasticsearch/indices.py +++ b/designsafe/libs/elasticsearch/indices.py @@ -3,7 +3,7 @@ :synopsis: Wrapper classes for ES different doc types. """ -from future.utils import python_2_unicode_compatible + import logging import json import six diff --git a/designsafe/libs/elasticsearch/utils.py b/designsafe/libs/elasticsearch/utils.py index c97412663f..3504460691 100644 --- a/designsafe/libs/elasticsearch/utils.py +++ b/designsafe/libs/elasticsearch/utils.py @@ -1,4 +1,4 @@ -from future.utils import python_2_unicode_compatible + import urllib.request, urllib.parse, urllib.error from elasticsearch import Elasticsearch import logging @@ -154,10 +154,24 @@ def iterate_level(client, system, path, limit=100): offset = 0 while True: - page = client.files.list(systemId=system, - filePath=urllib.parse.quote(path), + _page = client.files.listFiles(systemId=system, + path=urllib.parse.quote(path), offset=offset, limit=limit) + + page = [{ + 'system': system, + 'type': 'dir' if f.type == 'dir' else 'file', + 'format': 'folder' if f.type == 'dir' else 'raw', + 'mimeType': f.mimeType, + 'path': f"/{f.path}", + 'name': f.name, + 'length': f.size, + 'lastModified': f.lastModified, + '_links': { + 'self': {'href': f.url} + }} for f in _page] + yield from page offset += limit if len(page) != limit: @@ -165,7 +179,7 @@ def iterate_level(client, system, path, limit=100): break # pylint: disable=too-many-locals -@python_2_unicode_compatible + def walk_levels(client, system, path, bottom_up=False, ignore_hidden=False, paths_to_ignore=None): """Walk a pth in an Agave storgae system. @@ -298,14 +312,14 @@ def index_level(path, folders, files, systemId, reindex=False): logger.debug(children_paths) delete_recursive(hit.system, hit.path) -@python_2_unicode_compatible + def repair_path(name, path): if not path.endswith(name): path = path + '/' + name path = path.strip('/') return '/{path}'.format(path=path) -@python_2_unicode_compatible + def repair_paths(limit=1000): from designsafe.apps.data.models.elasticsearch import IndexedFile from elasticsearch import Elasticsearch diff --git a/designsafe/libs/fedora/fedora_operations.py b/designsafe/libs/fedora/fedora_operations.py index a2eeb82d94..9c406dd6e3 100644 --- a/designsafe/libs/fedora/fedora_operations.py +++ b/designsafe/libs/fedora/fedora_operations.py @@ -28,6 +28,9 @@ "abstract": { "@id": "http://purl.org/dc/elements/1.1/abstract" }, + "accessRights": { + "@id": "http://purl.org/dc/elements/1.1/accessRights" + }, "available": { "@id": "http://purl.org/dc/elements/1.1/available" }, @@ -690,10 +693,10 @@ def get_child_paths(dir_path): def generate_manifest(walk_result, project_id, version=None): fido_client = Fido() - if version: + if version and version > 1: project_id = '{}v{}'.format(project_id, str(version)) manifest = [] - archive_path = os.path.join(PUBLICATIONS_MOUNT_ROOT, project_id) + archive_path = PUBLICATIONS_MOUNT_ROOT for entity in walk_result: file_objs = entity['fileObjs'] file_tags = entity.get('fileTags', []) @@ -756,7 +759,7 @@ def generate_manifest_experimental(project_id, version=None): def upload_manifest(manifest_dict, project_id, version=None): - if version: + if version and version > 1: project_id = '{}v{}'.format(project_id, str(version)) fedora_root = parse.urljoin(settings.FEDORA_URL, PUBLICATIONS_CONTAINER) project_root = os.path.join(fedora_root, project_id) diff --git a/designsafe/libs/mongo/load_ttc_grants.py b/designsafe/libs/mongo/load_ttc_grants.py index e47ed15055..8ec7b293f1 100644 --- a/designsafe/libs/mongo/load_ttc_grants.py +++ b/designsafe/libs/mongo/load_ttc_grants.py @@ -51,16 +51,12 @@ def get_ttc_grants(self, query=None, sort=None): #set sorting option final_sort = [] - if sort == "Start Date Descending": - final_sort = [('StartDate', DESCENDING)] - elif sort == "Start Date Ascending": - final_sort = [('StartDate', ASCENDING)] - elif sort == "End Date Descending": + if sort == "End Date Descending": final_sort = [('EndDate', DESCENDING)] elif sort == "End Date Ascending": final_sort = [('EndDate', ASCENDING)] else: - final_sort = [('StartDate', DESCENDING)] + final_sort = [('EndDate', DESCENDING)] mongo_db = self._mc[getattr(settings, 'MONGO_DB', 'scheduler')] cursor = mongo_db.ttc_grant.find(query).sort(final_sort) @@ -84,3 +80,19 @@ def get_ttc_categories(self): results = list(cursor) for category in results: yield category['name'] + + def get_ttc_hazard_types(self): + """Get unique hazard type from the ttc grants db""" + mongo_db = self._mc[getattr(settings, 'MONGO_DB', 'scheduler')] + cursor = mongo_db.ttc_grant.distinct('Hazard') + results = list(cursor) + for hazard_type in results: + yield hazard_type + + def get_ttc_grant_types(self): + """Get unique grant type from the ttc grants db""" + mongo_db = self._mc[getattr(settings, 'MONGO_DB', 'scheduler')] + cursor = mongo_db.ttc_grant.distinct('Type') + results = list(cursor) + for grant_type in results: + yield grant_type \ No newline at end of file diff --git a/designsafe/libs/tapis/serializers.py b/designsafe/libs/tapis/serializers.py new file mode 100644 index 0000000000..51c31874df --- /dev/null +++ b/designsafe/libs/tapis/serializers.py @@ -0,0 +1,41 @@ +""" +.. module: libs.tapis.serializers + :synopsis: Serialize a Tapis object into a dict. +""" + +import logging +import json +from tapipy.tapis import TapisResult + +logger = logging.getLogger(__name__) + + +class BaseTapisResultSerializer(json.JSONEncoder): + """Class to serialize a Tapis response object""" + + def _serialize(self, obj): + if isinstance(obj, TapisResult): + _wrapped = vars(obj) + for key, value in _wrapped.items(): + if isinstance(value, TapisResult): + _wrapped[key] = self._serialize(value) + elif isinstance(value, list): + for index, item in enumerate(value): + value[index] = self._serialize(item) + elif isinstance(value, dict): + for n_key, n_value in value.items(): + value[n_key] = self._serialize(n_value) + return _wrapped + + if isinstance(obj, list): + for index, item in enumerate(obj): + obj[index] = self._serialize(item) + elif isinstance(obj, dict): + for key, value in obj.items(): + obj[key] = self._serialize(value) + return obj + + def default(self, o): + if isinstance(o, (TapisResult, list, dict)): + return self._serialize(o) + return super().default(o) diff --git a/designsafe/middleware.py b/designsafe/middleware.py index d9deca554f..0f2a5e7387 100644 --- a/designsafe/middleware.py +++ b/designsafe/middleware.py @@ -13,10 +13,11 @@ from termsandconditions.middleware import (TermsAndConditionsRedirectMiddleware, is_path_protected) from termsandconditions.models import TermsAndConditions -from django.shortcuts import redirect +from django.shortcuts import redirect, render from django.urls import reverse from django.utils.deprecation import MiddlewareMixin from designsafe.apps.notifications.models import SiteMessage +from designsafe.apps.api.utils import get_client_ip logger = logging.getLogger(__name__) @@ -73,6 +74,30 @@ def process_request(self, request): messages.warning(request, message.message) +class MaintenanceMiddleware: + """Redirect to a maintenance page if the DJANGO_MAINTENANCE setting is toggled.""" + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Code to be executed for each request before + # the view (and later middleware) are called. + show_maintenance = getattr(settings, "DJANGO_MAINTENANCE", False) + client_ip = get_client_ip(request) + if not show_maintenance: + return self.get_response(request) + + # Allow access behind the maint page for staff members or behind TACC VPN + if getattr(request.user, "is_staff") or client_ip.startswith(settings.STAFF_VPN_IP_PREFIX): + return self.get_response(request) + + # Non-staff users see the maint page instead of the page they requested + if not request.path.startswith('/static'): + return render(request, 'maintenance.html') + + return self.get_response(request) + + class RequestProfilingMiddleware(MiddlewareMixin): """Middleware to run cProfiler on each request""" diff --git a/designsafe/settings/celery_settings.py b/designsafe/settings/celery_settings.py index a374b7984c..feb8d51b96 100644 --- a/designsafe/settings/celery_settings.py +++ b/designsafe/settings/celery_settings.py @@ -42,6 +42,8 @@ Queue('files', Exchange('io'), routing_key='io.files'), #Use to queue tasks which mainly call external APIs Queue('api', Exchange('api'), routing_key='api.agave'), + # Use to queue tasks which handle user onboarding + Queue('onboarding', Exchange('onboarding'), routing_key='onboarding'), ) CELERY_TASK_DEFAULT_QUEUE = 'default' CELERY_TASK_DEFAULT_EXCHANGE = 'default' diff --git a/designsafe/settings/common_settings.py b/designsafe/settings/common_settings.py index e37b4cd889..606d9f0c7c 100644 --- a/designsafe/settings/common_settings.py +++ b/designsafe/settings/common_settings.py @@ -36,6 +36,7 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' +DJANGO_MAINTENANCE = os.environ.get("DJANGO_MAINTENANCE", 'False') == 'True' RENDER_REACT = os.environ.get('RENDER_REACT', 'False') == 'True' ALLOWED_HOSTS = ['*'] @@ -117,12 +118,12 @@ ) AUTHENTICATION_BACKENDS = ( - 'designsafe.apps.auth.backends.AgaveOAuthBackend', + 'designsafe.apps.auth.backends.TapisOAuthBackend', 'designsafe.apps.auth.backends.TASBackend', 'django.contrib.auth.backends.ModelBackend', ) -LOGIN_REDIRECT_URL = os.environ.get('LOGIN_REDIRECT_URL', '/account/') +LOGIN_REDIRECT_URL = os.environ.get('LOGIN_REDIRECT_URL', '/dashboard/') LOGOUT_REDIRECT_URL = os.environ.get('LOGOUT_REDIRECT_URL', '/auth/logged-out/') CACHES = { @@ -140,7 +141,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'designsafe.apps.token_access.middleware.TokenAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware', + 'designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -152,6 +153,7 @@ 'designsafe.middleware.DesignSafeTermsMiddleware', 'designsafe.middleware.DesignsafeProfileUpdateMiddleware', 'designsafe.middleware.SiteMessageMiddleware', + 'designsafe.middleware.MaintenanceMiddleware' ) ROOT_URLCONF = 'designsafe.urls' @@ -179,7 +181,6 @@ 'designsafe.context_processors.site_verification', 'designsafe.context_processors.debug', 'designsafe.context_processors.messages', - 'designsafe.apps.auth.context_processors.auth', 'designsafe.apps.cms_plugins.context_processors.cms_section', ], }, @@ -203,7 +204,6 @@ # https://docs.djangoproject.com/en/1.8/ref/settings/#databases if os.environ.get('DATABASE_HOST'): - # mysql connection DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', @@ -231,7 +231,7 @@ ALDRYN_SEARCH_REGISTER_APPHOOK = True from designsafe.settings.nees_settings import NEES_USER_DATABASE -#if NEES_USER_DATABASE['NAME']: +# if NEES_USER_DATABASE['NAME']: # DATABASES['nees_users'] = NEES_USER_DATABASE @@ -348,13 +348,13 @@ 'allowedContent': True } -#MIGRATION_MODULES = { +# MIGRATION_MODULES = { # 'djangocms_file': 'djangocms_file.migrations_django', # 'djangocms_googlemap': 'djangocms_googlemap.migrations_django', # 'djangocms_picture': 'djangocms_picture.migrations_django', # 'djangocms_video': 'djangocms_video.migrations_django', # 'djangocms_style': 'djangocms_style.migrations_django', -#} +# } LOGIN_URL = os.environ.get('LOGIN_URL', '/login/') @@ -497,6 +497,19 @@ TRAM_SERVICES_KEY = os.environ.get('TRAM_SERVICES_KEY', None) TRAM_PROJECT_ID = os.environ.get('TRAM_PROJECT_ID', None) +TAS_CLIENT_KEY = os.environ.get('TAS_CLIENT_KEY', None) +TAS_CLIENT_SECRET = os.environ.get('TAS_CLIENT_SECRET', None) +TAS_URL = os.environ.get('TAS_URL', None) + +# Allocations to exclude +# +ALLOCATIONS_TO_EXCLUDE = ( + os.environ.get("ALLOCATIONS_TO_EXCLUDE", "").split(",") + if os.environ.get("ALLOCATIONS_TO_EXCLUDE") + else ["DesignSafe-DCV"] +) + + ### # Agave Integration # @@ -530,6 +543,26 @@ AGAVE_USER_STORE_ID = os.environ.get('AGAVE_USER_STORE_ID', 'TACC') AGAVE_USE_SANDBOX = os.environ.get('AGAVE_USE_SANDBOX', 'False').lower() == 'true' +TAPIS_SYSTEMS_TO_CONFIGURE = [ + {"system_id": AGAVE_STORAGE_SYSTEM, "path": "{username}", "create_path": True}, + {"system_id": AGAVE_WORKING_SYSTEM, "path": "{username}", "create_path": True}, + {"system_id": "cloud.data", "path": "/ ", "create_path": False}, +] + +# Tapis Client Configuration +PORTAL_ADMIN_USERNAME = os.environ.get('PORTAL_ADMIN_USERNAME') +TAPIS_TENANT_BASEURL = os.environ.get('TAPIS_TENANT_BASEURL') +TAPIS_CLIENT_ID = os.environ.get('TAPIS_CLIENT_ID') +TAPIS_CLIENT_KEY = os.environ.get('TAPIS_CLIENT_KEY') +TAPIS_ADMIN_JWT = os.environ.get('TAPIS_ADMIN_JWT') +TAPIS_TG458981_JWT = os.environ.get('TAPIS_TG458981_JWT') + +KEY_SERVICE_TOKEN = os.environ.get('KEY_SERVICE_TOKEN') + +PORTAL_NAMESPACE = 'DESIGNSAFE' + +PORTAL_JOB_NOTIFICATION_STATES = ["PENDING", "STAGING_INPUTS", "RUNNING", "ARCHIVING", "BLOCKED", "PAUSED", "FINISHED", "CANCELLED", "FAILED"] + DS_ADMIN_USERNAME = os.environ.get('DS_ADMIN_USERNAME') DS_ADMIN_PASSWORD = os.environ.get('DS_ADMIN_PASSWORD') @@ -557,6 +590,8 @@ } } +PROJECT_STORAGE_SYSTEM_CREDENTIALS = json.loads(os.environ.get('PROJECT_SYSTEM_STORAGE_CREDENTIALS', '{}')) + PUBLISHED_SYSTEM = 'designsafe.storage.published' COMMUNITY_SYSTEM = 'designsafe.storage.community' NEES_PUBLIC_SYSTEM = 'nees.public' @@ -568,7 +603,7 @@ RECAPTCHA_PRIVATE_KEY= os.environ.get('DJANGOCMS_FORMS_RECAPTCHA_SECRET_KEY') NOCAPTCHA = True -#FOR RAPID UPLOADS +# FOR RAPID UPLOADS DESIGNSAFE_UPLOAD_PATH = '/corral-repl/tacc/NHERI/uploads' DESIGNSAFE_PROJECTS_PATH = os.environ.get('DESIGNSAFE_PROJECTS_PATH', '/corral-repl/tacc/NHERI/projects/') DESIGNSAFE_PUBLISHED_PATH = os.environ.get('DESIGNSAFE_PUBLISHED_PATH', '/corral-repl/tacc/NHERI/published/') @@ -587,7 +622,6 @@ from designsafe.settings.external_resource_settings import * from designsafe.settings.elasticsearch_settings import * from designsafe.settings.rt_settings import * - from designsafe.settings.external_resource_secrets import * from designsafe.settings.nco_mongo import * except ImportError: pass @@ -669,3 +703,8 @@ FEDORA_USERNAME = os.environ.get('FEDORA_USERNAME') FEDORA_PASSWORD = os.environ.get('FEDORA_PASSWORD') FEDORA_CONTAINER= os.environ.get('FEDORA_CONTAINER', 'designsafe-publications-dev') + +CSRF_TRUSTED_ORIGINS = [f"https://{os.environ.get('SESSION_COOKIE_DOMAIN')}"] +WEBHOOK_POST_URL = os.environ.get('WEBHOOK_POST_URL', '') + +STAFF_VPN_IP_PREFIX = os.environ.get("STAFF_VPN_IP_PREFIX", "129.114") diff --git a/designsafe/settings/elasticsearch_settings.py b/designsafe/settings/elasticsearch_settings.py index 4d04f8d688..ab3613d66a 100644 --- a/designsafe/settings/elasticsearch_settings.py +++ b/designsafe/settings/elasticsearch_settings.py @@ -43,6 +43,11 @@ 'document': 'designsafe.apps.data.models.elasticsearch.IndexedPublication', 'kwargs': {'index.mapping.total_fields.limit': 3000} }, + 'publications_v2': { + 'alias': ES_INDEX_PREFIX.format('publications_v2'), + 'document': 'designsafe.apps.api.publications_v2.elasticsearch.IndexedPublication', + 'kwargs': {} + }, 'web_content': { 'alias': ES_INDEX_PREFIX.format('web-content'), 'document': 'designsafe.apps.data.models.elasticsearch.IndexedCMSPage', diff --git a/designsafe/settings/external_resource_settings.py b/designsafe/settings/external_resource_settings.py index 79785be4c1..02fdb17164 100644 --- a/designsafe/settings/external_resource_settings.py +++ b/designsafe/settings/external_resource_settings.py @@ -26,3 +26,5 @@ 'user_property': 'user_id', 'credentials_property': 'credential' } +GOOGLE_OAUTH2_CLIENT_SECRET = os.environ.get("GOOGLE_OAUTH2_CLIENT_SECRET", "CHANGE_ME") +GOOGLE_OAUTH2_CLIENT_ID = os.environ.get("GOOGLE_OAUTH2_CLIENT_ID", "CHANGE_ME") \ No newline at end of file diff --git a/designsafe/settings/test_settings.py b/designsafe/settings/test_settings.py index e2b3a73388..7e2ba4681b 100644 --- a/designsafe/settings/test_settings.py +++ b/designsafe/settings/test_settings.py @@ -133,7 +133,7 @@ 'django.contrib.sessions.middleware.SessionMiddleware', 'designsafe.apps.token_access.middleware.TokenAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware', + 'designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -169,7 +169,6 @@ 'designsafe.context_processors.site_verification', 'designsafe.context_processors.debug', 'designsafe.context_processors.messages', - 'designsafe.apps.auth.context_processors.auth', 'designsafe.apps.cms_plugins.context_processors.cms_section', ], }, @@ -237,6 +236,10 @@ MEDIA_ROOT = '/srv/www/designsafe/media/' MEDIA_URL = '/media/' +FIXTURE_DIRS = [ + os.path.join(BASE_DIR, 'designsafe', 'fixtures'), +] + ##### # @@ -447,6 +450,7 @@ AGAVE_TOKEN_SESSION_ID = os.environ.get('AGAVE_TOKEN_SESSION_ID', 'agave_token') AGAVE_SUPER_TOKEN = os.environ.get('AGAVE_SUPER_TOKEN') AGAVE_STORAGE_SYSTEM = os.environ.get('AGAVE_STORAGE_SYSTEM') +AGAVE_WORKING_SYSTEM = os.environ.get('AGAVE_WORKING_SYSTEM') AGAVE_JWT_PUBKEY = os.environ.get('AGAVE_JWT_PUBKEY') AGAVE_JWT_ISSUER = os.environ.get('AGAVE_JWT_ISSUER') @@ -531,7 +535,7 @@ # No token refreshes during testing MIDDLEWARE= [c for c in MIDDLEWARE if c != - 'designsafe.apps.auth.middleware.AgaveTokenRefreshMiddleware'] + 'designsafe.apps.auth.middleware.TapisTokenRefreshMiddleware'] STATIC_ROOT = os.path.join(BASE_DIR, 'static') MEDIA_ROOT = os.path.join(BASE_DIR, '.media') @@ -543,6 +547,23 @@ AGAVE_CLIENT_SECRET = 'example_com_client_secret' AGAVE_SUPER_TOKEN = 'example_com_client_token' AGAVE_STORAGE_SYSTEM = 'storage.example.com' +AGAVE_WORKING_SYSTEM = 'storage.example.work' + +TAPIS_SYSTEMS_TO_CONFIGURE = [ + {"system_id": AGAVE_STORAGE_SYSTEM, "path": "{username}", "create_path": True}, + {"system_id": AGAVE_WORKING_SYSTEM, "path": "{username}", "create_path": True}, + {"system_id": "cloud.data", "path": "/ ", "create_path": False}, +] + +# Tapis Client Configuration +PORTAL_ADMIN_USERNAME = '' +TAPIS_TENANT_BASEURL = 'https://designsafe.tapis.io' +TAPIS_CLIENT_ID = 'client_id' +TAPIS_CLIENT_KEY = 'client_key' +TAPIS_ADMIN_JWT = 'admin_jwt' +TAPIS_TG458981_JWT = 'tg_jwt' + +KEY_SERVICE_TOKEN = '' MIGRATION_MODULES = { 'data': None, @@ -671,6 +692,11 @@ 'alias': ES_INDEX_PREFIX.format('publications'), 'document': 'designsafe.apps.data.models.elasticsearch.IndexedPublication', 'kwargs': {'index.mapping.total_fields.limit': 3000} + }, + 'publications_v2': { + 'alias': ES_INDEX_PREFIX.format('publications_v2'), + 'document': 'designsafe.apps.api.publications_v2.elasticsearch.IndexedPublication', + 'kwargs': {} }, 'web_content': { 'alias': ES_INDEX_PREFIX.format('web-content'), @@ -741,3 +767,8 @@ ('LOGIN', 'Login/Registration'), ('OTHER', 'Other'), ) + +FEDORA_URL = '' +FEDORA_USERNAME = '' +FEDORA_PASSWORD = '' +FEDORA_CONTAINER= 'designsafe-publications-dev' diff --git a/designsafe/sitemaps.py b/designsafe/sitemaps.py index 39bc9d1b1d..ee02d2b6b7 100644 --- a/designsafe/sitemaps.py +++ b/designsafe/sitemaps.py @@ -37,6 +37,7 @@ from django.urls import reverse from designsafe.apps.api.publications.operations import listing as list_publications, neeslisting as list_nees from designsafe.apps.api.agave import get_service_account_client +from designsafe.apps.api.publications_v2.models import Publication # imported urlpatterns from apps from designsafe import urls # from designsafe import urls not working? @@ -168,23 +169,19 @@ def get_urls(self, site=None, **kwargs): return super(ProjectSitemap, self).get_urls(site=site, **kwargs) def items(self): - client = get_service_account_client() projPath = [] # pefm - PublicElasticFileManager to grab public projects - count = 0 - while True: - projects = list_publications(offset=count, limit=200, limit_fields=False) - for proj in projects['listing']: - subpath = { - 'root' : reverse('designsafe_data:data_depot'), - 'project' : proj['project']['value']['projectId'], - 'system' : 'designsafe.storage.published' - } - projPath.append('{root}public/{system}/{project}'.format(**subpath)) - if len(projects['listing']) < 200: - break - count += 200 + + projects = Publication.objects.all() + for proj in projects: + subpath = { + 'root' : reverse('designsafe_data:data_depot'), + 'project' : proj.project_id, + 'system' : 'designsafe.storage.published' + } + projPath.append('{root}public/{system}/{project}'.format(**subpath)) + count = 0 while True: diff --git a/designsafe/static/scripts/dashboard/components/dashboard/dashboard.component.html b/designsafe/static/scripts/dashboard/components/dashboard/dashboard.component.html index 26e0d6cc2d..45340a215d 100644 --- a/designsafe/static/scripts/dashboard/components/dashboard/dashboard.component.html +++ b/designsafe/static/scripts/dashboard/components/dashboard/dashboard.component.html @@ -1,5 +1,5 @@
    -
    + +
    @@ -33,7 +33,7 @@
    -
    +
    Quick Links
    @@ -47,7 +47,7 @@
    -
    + -
    +
    +
    +
    +

    My Tickets

    + Create New +
    + + +
    + +
    +
    +
    +

    Notifications {{$ctrl.notification_count}}

    @@ -104,23 +121,8 @@

    Notifications {{$ctrl.

    -
    -
    -

    My Tickets

    - Create New -
    - -
    - -
    -
    - -
    +
    diff --git a/designsafe/static/scripts/nco/components/modals/ttc-abstract-modal.template.html b/designsafe/static/scripts/nco/components/modals/ttc-abstract-modal.template.html index e09079d022..77850a9a29 100644 --- a/designsafe/static/scripts/nco/components/modals/ttc-abstract-modal.template.html +++ b/designsafe/static/scripts/nco/components/modals/ttc-abstract-modal.template.html @@ -19,8 +19,15 @@
    Grant Org: {{$ctrl.grant.Org}}
    -
    NHERI Facility: {{grant.NheriFacility}}
    +
    NHERI Facility: {{$ctrl.grant.NheriFacility}}
    NHERI Facility: No Facility Listed
    +
    Hazard Type: {{$ctrl.grant.Hazard}}
    +
    Grant Type: {{$ctrl.grant.Type}}
    +
    Keywords: +
      +
    • {{keyword}}
    • +
    +

    Abstract: diff --git a/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.component.js b/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.component.js index 0a975a1017..7ccb92b1b2 100644 --- a/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.component.js +++ b/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.component.js @@ -10,14 +10,15 @@ class NcoTtcGrantsCtrl { $onInit() { this.initialParams = { - sort: 'Start Date Descending', + sort: 'End Date Descending', }; // default intitial sorting - this.selectedSort = 'Start Date Descending'; + this.selectedSort = 'End Date Descending'; this._ui = { grantsLoading: true, facilitiesLoading: false, - categoriesLoading: false, + grantTypesLoading: false, + hazardTypesLoading: false, }; this.loadGrants(this.initialParams) @@ -39,18 +40,25 @@ class NcoTtcGrantsCtrl { this._ui.facilitiesLoading = false; }); - this.loadCategories({}) - .then((resp) => { - return resp; - }, (err) => { - this._ui.categoriesError = err.message; - }).finally( () => { - this._ui.categoriesLoading = false; - }); + this.loadGrantTypes({}) + .then((resp) => { + return resp; + }, (err) => { + this._ui.grantTypesError = err.message; + }).finally( () => { + this._ui.grantTypesLoading = false; + }); + + this.loadHazardTypes({}) + .then((resp) => { + return resp; + }, (err) => { + this._ui.hazardTypesError = err.message; + }).finally( () => { + this._ui.hazardTypesLoading = false; + }); this.sortOptions = [ - "Start Date Descending", - "Start Date Ascending", "End Date Descending", "End Date Ascending", ]; @@ -86,19 +94,33 @@ class NcoTtcGrantsCtrl { }); } - loadCategories(){ - this._ui.categoriesLoading = true; - return this.$http.get('/nco/api/ttc_categories') + loadGrantTypes(){ + this._ui.grantTypesLoading = true; + return this.$http.get('/nco/api/ttc_grant_types') + .then((resp) => { + this.grantTypes = _.map( + resp.data.response, + ); + return this.grantTypes; + }, (err) => { + this._ui.grantTypesError = err.message; + }).finally ( () => { + this._ui.grantTypesLoading = false; + }); + } + + loadHazardTypes(){ + this._ui.hazardTypesLoading = true; + return this.$http.get('/nco/api/ttc_hazard_types') .then((resp) => { - this.categoriesList = _.map( + this.hazardTypes = _.map( resp.data.response, ); - console.log(this.categoriesList); - return this.categoriesList; + return this.hazardTypes; }, (err) => { - this._ui.categoriesError = err.message; + this._ui.hazardTypesError = err.message; }).finally ( () => { - this._ui.categoriesLoading = false; + this._ui.hazardTypesLoading = false; }); } @@ -116,8 +138,10 @@ class NcoTtcGrantsCtrl { filterSearch(){ var params = { facility: this.selectedFacility, - category: this.selectedCategory, sort: this.selectedSort, + hazard_type: this.selectedHazardType, + grant_type: this.selectedGrantType, + text_search: this.textSearch, }; this.loadGrants(params); } diff --git a/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.template.html b/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.template.html index 6b1ae1a713..88f6818a46 100644 --- a/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.template.html +++ b/designsafe/static/scripts/nco/components/nco-ttc-grants/nco-ttc-grants.template.html @@ -2,27 +2,46 @@
    Please make your selections and click the Search button to change the listing.
    @@ -37,7 +56,6 @@ Award Number Title - Start Date End Date PI / Co-PI Grant Org @@ -54,7 +72,6 @@ {{grant.Title}} - {{grant.StartDate.$date | date}} {{grant.EndDate.$date | date}} {{grant.PiName}} / {{grant.CoPiNames}} diff --git a/designsafe/static/scripts/nco/styles/nco_ttc_grants_styles.css b/designsafe/static/scripts/nco/styles/nco_ttc_grants_styles.css index 045389cd05..4cee5a0b4c 100644 --- a/designsafe/static/scripts/nco/styles/nco_ttc_grants_styles.css +++ b/designsafe/static/scripts/nco/styles/nco_ttc_grants_styles.css @@ -24,12 +24,12 @@ .ttc-search { display: block; - padding: 10px; + padding: 5px; } -.ttc-facility-filter, .ttc-sort-filter, .ttc-category-filter { +.filter-group { display: inline; - padding: 10px; + padding: 5px; } .ttc-row { diff --git a/designsafe/static/scripts/ng-designsafe/components/notification-badge/notification-badge.component.html b/designsafe/static/scripts/ng-designsafe/components/notification-badge/notification-badge.component.html index 78c64a3402..bd706a3cba 100644 --- a/designsafe/static/scripts/ng-designsafe/components/notification-badge/notification-badge.component.html +++ b/designsafe/static/scripts/ng-designsafe/components/notification-badge/notification-badge.component.html @@ -9,7 +9,7 @@
    - \ No newline at end of file diff --git a/designsafe/static/scripts/ng-designsafe/controllers/notifications.js b/designsafe/static/scripts/ng-designsafe/controllers/notifications.js index b0c3b6477c..0fd8875307 100644 --- a/designsafe/static/scripts/ng-designsafe/controllers/notifications.js +++ b/designsafe/static/scripts/ng-designsafe/controllers/notifications.js @@ -40,12 +40,13 @@ export function NotificationBadgeCtrl( $scope.data.unread = 0; } - for (var i=0; i < $scope.data.notifications.length; i++){ - if ($scope.data.notifications[i]['event_type'] == 'job') { - $scope.data.notifications[i]['action_link']=$scope.data.notifications[i]['action_link']=`/rw/workspace/notification/process/${$scope.data.notifications[i]['pk']}`; - } else if ($scope.data.notifications[i]['event_type'] == 'data_depot') { - $scope.data.notifications[i]['action_link']=$scope.data.notifications[i]['action_link']=`/rw/workspace/notification/process/${$scope.data.notifications[i]['pk']}`; - } + for (var i = 0; i < $scope.data.notifications.length; i++) { + const notification = $scope.data.notifications[i]; + if (notification['event_type'] == 'job') { + notification['action_link'] = `/rw/workspace/history`; + } else if (notification['event_type'] == 'data_depot') { + notification['action_link'] = `/data/browser`; + } } }); }; diff --git a/designsafe/static/scripts/ng-designsafe/providers/notifications-provider.js b/designsafe/static/scripts/ng-designsafe/providers/notifications-provider.js index f20142cd4c..a320a02a27 100644 --- a/designsafe/static/scripts/ng-designsafe/providers/notifications-provider.js +++ b/designsafe/static/scripts/ng-designsafe/providers/notifications-provider.js @@ -25,7 +25,6 @@ function NotificationService( * @return {string} url */ function renderLink(msg) { - console.log('rendering link?') const eventType = msg.event_type.toLowerCase(); let url = ''; if (typeof processors[eventType] !== 'undefined' && @@ -34,10 +33,10 @@ function NotificationService( url = processors[eventType].renderLink(msg); } if (msg.status != 'ERROR') { - if (msg.event_type == 'job') { - url=`/rw/workspace/notification/process/${msg.pk}` + if (msg.event_type === 'job') { + url = `/rw/workspace/history`; } else if (msg.event_type == 'data_depot') { - url=`/rw/workspace/notification/process/${msg.pk}` + url = `/data/browser`; } } return url; @@ -120,6 +119,10 @@ function NotificationService( * @param {Object} msg */ function processToastr(e, msg) { + if (msg.event_type === 'job' || msg.event_type ==='WEB' || msg.event_type === 'interactive_session_ready') { + return; + } + try { // msg.extra = JSON.parse(msg.extra); msg.extra = (typeof msg.extra === 'string') ? JSON.parse(msg.extra) : msg.extra; diff --git a/designsafe/static/scripts/notifications/app.js b/designsafe/static/scripts/notifications/app.js index 6c4b1964d0..f2d025ff0b 100644 --- a/designsafe/static/scripts/notifications/app.js +++ b/designsafe/static/scripts/notifications/app.js @@ -31,14 +31,12 @@ angular.module('designsafe').controller('NotificationListCtrl', ['$scope','$root $scope.data.pagination.total = resp.total; $scope.data.notifications = resp.notifs; - for (var i=0; i < $scope.data.notifications.length; i++){ - // $scope.data.notifications[i] = angular.fromJson($scope.data.notifications[i]); - // $scope.data.notifications[i]['fields']['extra'] = angular.fromJson($scope.data.notifications[i]['fields']['extra']); - // $scope.data.notifications[i]['datetime'] = Date($scope.data.notifications[i]['datetime']); - if ($scope.data.notifications[i]['event_type'] == 'job') { - $scope.data.notifications[i]['action_link']=`/rw/workspace/notification/process/${$scope.data.notifications[i]['pk']}`; - } else if ($scope.data.notifications[i]['event_type'] == 'data_depot') { - $scope.data.notifications[i]['action_link']=`/rw/workspace/notification/process/${$scope.data.notifications[i]['pk']}`; + for (var i = 0; i < $scope.data.notifications.length; i++) { + const notification = $scope.data.notifications[i]; + if (notification['event_type'] == 'job') { + notification['action_link'] = `/rw/workspace/history`; + } else if (notification['event_type'] == 'data_depot') { + notification['action_link'] = '/data/browser'; } } diff --git a/designsafe/static/styles/main.css b/designsafe/static/styles/main.css index 55244fa236..131c1d87b8 100644 --- a/designsafe/static/styles/main.css +++ b/designsafe/static/styles/main.css @@ -944,7 +944,7 @@ li .popover.right { flex-direction: column; flex-grow: 1; flex-shrink: 0; - flex-basis: auto; + flex-basis: 0; } .o-site__body > .container-fluid { diff --git a/designsafe/static/styles/ng-designsafe.css b/designsafe/static/styles/ng-designsafe.css index b555c25527..94ac89369b 100644 --- a/designsafe/static/styles/ng-designsafe.css +++ b/designsafe/static/styles/ng-designsafe.css @@ -1074,3 +1074,8 @@ i[class^="icon-ls-pre/post"]:before, :root { color-scheme: only light !important; } + +.html-app-container { + padding: 30px !important; + margin-top: 20px !important; +} diff --git a/designsafe/static/styles/variables.css b/designsafe/static/styles/variables.css index 6cb9e46c01..9c9c81e540 100644 --- a/designsafe/static/styles/variables.css +++ b/designsafe/static/styles/variables.css @@ -9,8 +9,8 @@ --global-color-primary--light: #c6c6c6; --global-color-primary--normal: #afafaf; --global-color-primary--dark: #707070; - --global-color-primary--x-dark: #484848; /* ¹ */ - --global-color-primary--xx-dark: #222222; /* ¹ */ + --global-color-primary--x-dark: #484848; + --global-color-primary--xx-dark: #222222; /* Space */ --global-space--above-breadcrumbs: 35px; diff --git a/designsafe/static/vendor/bootstrap-ds/css/bootstrap.css b/designsafe/static/vendor/bootstrap-ds/css/bootstrap.css index b389ce5759..f743a452a2 100755 --- a/designsafe/static/vendor/bootstrap-ds/css/bootstrap.css +++ b/designsafe/static/vendor/bootstrap-ds/css/bootstrap.css @@ -2853,7 +2853,7 @@ input[type="search"] { .pub-info-modal-label { vertical-align: top; display: inline-block; - width:30%; + width:40%; } .pub-info-modal-heading { border-bottom: darkgrey; @@ -2863,7 +2863,7 @@ input[type="search"] { } .pub-info-modal-data { display: inline-block; - width: 68%; + width: 60%; font-weight:bold; } .pub-info-modal-body { diff --git a/designsafe/templates/403.html b/designsafe/templates/403.html index 739d944d59..d6616a9096 100644 --- a/designsafe/templates/403.html +++ b/designsafe/templates/403.html @@ -5,7 +5,7 @@

    You do not have permission to access the requested resource.

    You do not have permission to access the requested resource. If you feel this - is in error, please submit a ticket + is in error, please submit a ticket and we will investigate how to get this resolved.

    diff --git a/designsafe/templates/404.html b/designsafe/templates/404.html index 7262b5dab2..369c8fdb37 100644 --- a/designsafe/templates/404.html +++ b/designsafe/templates/404.html @@ -5,7 +5,7 @@

    The resource you requested isn't here.

    If you followed a link from another DesignSafe-CI page, please - submit a ticket + submit a ticket to let us know so we can get the link corrected! If you followed a link from elsewhere on the web, we would also like to know so we can get that link redirected properly! diff --git a/designsafe/templates/500.html b/designsafe/templates/500.html index b0059a9a5c..ee74b25cd6 100644 --- a/designsafe/templates/500.html +++ b/designsafe/templates/500.html @@ -8,7 +8,7 @@

    An unexpected error occurred!

    If you continue to receive this error, please - submit a ticket. + submit a ticket.

    {% endblock content %} diff --git a/designsafe/templates/base.j2 b/designsafe/templates/base.j2 index 664eaf05f5..d1c2875e4c 100644 --- a/designsafe/templates/base.j2 +++ b/designsafe/templates/base.j2 @@ -38,6 +38,7 @@ {% block styles %}{% endblock %} {% render_block "css" %} + {% render_block "react_assets" %} {% recaptcha_init 'en' %} @@ -147,6 +148,7 @@ firstName: "{{ request.user.first_name }}", lastName: "{{ request.user.last_name }}", email: "{{ request.user.email }}", + institution: "{{ request.user.profile.institution }}" }; diff --git a/designsafe/templates/includes/header.html b/designsafe/templates/includes/header.html index 78bf801c58..9a7df976d6 100644 --- a/designsafe/templates/includes/header.html +++ b/designsafe/templates/includes/header.html @@ -36,15 +36,7 @@ {% if user.is_authenticated %} - - {% if not agave_ready %} -   - API Session Not Available. Click for details. - {% endif %} - - +
    Welcome, {{ user.first_name }}!
    +{% endblock content %} diff --git a/designsafe/urls.py b/designsafe/urls.py index 0c7e5adb86..da65ce37f4 100644 --- a/designsafe/urls.py +++ b/designsafe/urls.py @@ -25,17 +25,18 @@ from django.conf import settings from django.urls import include, re_path as url from django.conf.urls.static import static +from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin from django.views.generic import RedirectView, TemplateView from django.urls import reverse, path -from django.http import HttpResponse, HttpResponseRedirect -from designsafe.apps.auth.views import login_options as des_login_options +from django.http import HttpResponseRedirect +from designsafe.apps.auth.views import tapis_oauth as login from django.contrib.auth.views import LogoutView as des_logout from designsafe.views import project_version as des_version, redirect_old_nees from impersonate import views as impersonate_views # sitemap - classes must be imported and added to sitemap dictionary -from django.contrib.sitemaps.views import sitemap +from django.contrib.sitemaps.views import sitemap, index from designsafe.sitemaps import StaticViewSitemap, DynamicViewSitemap, HomeSitemap, ProjectSitemap, SubSitemap, DesignSafeCMSSitemap from designsafe import views @@ -74,8 +75,18 @@ ), path("admin/", admin.site.urls), - # sitemap - url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), + path( + "sitemap.xml", + index, + {"sitemaps": sitemaps}, + name="django.contrib.sitemaps.views.index", + ), + path( + "sitemap-
    .xml", + sitemap, + {"sitemaps": sitemaps}, + name="django.contrib.sitemaps.views.sitemap", + ), # terms-and-conditions url(r'^terms/', include('termsandconditions.urls')), @@ -90,6 +101,7 @@ url(r'^data/', include(('designsafe.apps.data.urls', 'designsafe.apps.data'), namespace='designsafe_data')), url(r'^rw/workspace/', include(('designsafe.apps.workspace.urls', 'designsafe.apps.workspace'), namespace='designsafe_workspace')), + path('api/workspace/', include('designsafe.apps.workspace.api.urls', namespace='workspace_api')), url(r'^notifications/', include(('designsafe.apps.notifications.urls', 'designsafe.apps.notifications'), namespace='designsafe_notifications')), url(r'^search/', include(('designsafe.apps.search.urls', 'designsafe.apps.search'), @@ -148,14 +160,14 @@ # auth url(r'^auth/', include(('designsafe.apps.auth.urls', 'designsafe.apps.auth'), namespace='designsafe_auth')), - url(r'^login/$', des_login_options, name='login'), + url(r'^login/$', login, name='login'), url(r'^logout/$', des_logout.as_view(), name='logout'), # help url(r'^help/', include(('designsafe.apps.djangoRT.urls', 'designsafe.apps.djangoRT'), namespace='djangoRT')), # webhooks - url(r'^webhooks/', include('designsafe.webhooks')), + path('webhooks/', include('designsafe.apps.webhooks.urls', namespace='webhooks')), # version check url(r'^version/', des_version), @@ -170,4 +182,8 @@ url(r'^', include('cms.urls')), ] if settings.DEBUG: - urlpatterns + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + # https://docs.djangoproject.com/en/4.2/howto/static-files/#serving-files-uploaded-by-a-user-during-development + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + # https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#static-file-development-view + urlpatterns += staticfiles_urlpatterns() diff --git a/designsafe/utils/__init__.py b/designsafe/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/designsafe/utils/encryption.py b/designsafe/utils/encryption.py new file mode 100644 index 0000000000..cfb39904fe --- /dev/null +++ b/designsafe/utils/encryption.py @@ -0,0 +1,104 @@ +""" +.. :module:: designsafe.utils.encryption + :synopsis: Utilities to handle encryption and ssh keys +""" + +import logging +import base64 +from Crypto.PublicKey import RSA +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto import Random +from django.conf import settings + +# pylint: disable=invalid-name +logger = logging.getLogger(__name__) +# pylint: enable=invalid-name + + +def createKeyPair(): # pylint: disable=invalid-name + """Create private and public keys""" + private_key = create_private_key() + priv_key_str = export_key(private_key, "PEM") + public_key = create_public_key(private_key) + publ_key_str = export_key(public_key, "OpenSSH") + + return priv_key_str, publ_key_str + + +def create_private_key(bits=2048): + """Creates a brand new RSA key + + :param int bits: Key bits + """ + key = RSA.generate(bits) + return key + + +def create_public_key(key): + """Returns public key + + :param key: RSA key + """ + pub_key = key.publickey() + return pub_key + + +def export_key(key, format="PEM"): # pylint: disable=redefined-builtin + """Exports private key + + :param key: RSA key + :param str format: Format to export key + + .. note:: + Use `format='PEM'` for exporting private keys + and `format='OpenSSH' for exporting public keys + """ + return key.exportKey(format).decode("utf-8") + + +def encrypt(raw): + """Encrypts string using AES + + :param str raw: raw string to encrypt + + .. note:: + Shamelessly copied from: + https://stackoverflow.com/questions/42568262/how-to-encrypt-text-with-a-password-in-python/44212550#44212550 + """ + source = raw.encode("utf-8") + # Use hash to make sure size is appropiate + key = SHA256.new(str.encode(settings.SECRET_KEY)).digest() + # pylint: disable=invalid-name + IV = Random.new().read(AES.block_size) + # pylint: enable=invalid-name + encryptor = AES.new(key, AES.MODE_CBC, IV) + # calculate needed padding + padding = AES.block_size - len(source) % AES.block_size + source += bytes([padding]) * padding + # store the IV at the beginning and encrypt + data = IV + encryptor.encrypt(source) + return base64.b64encode(data).decode("utf-8") + + +def decrypt(raw): + """Decrypts a base64 encoded string + + :param source: base64 encoded string + """ + source = base64.b64decode(raw.encode("utf-8")) + # use SHA-256 over our key to get a proper-sized AES key + key = SHA256.new(str.encode(settings.SECRET_KEY)).digest() + # extract the IV from the beginning + # pylint: disable=invalid-name + IV = source[: AES.block_size] + # pylint: enable=invalid-name + decryptor = AES.new(key, AES.MODE_CBC, IV) + # decrypt + data = decryptor.decrypt(source[AES.block_size :]) + # pick the padding value from the end; + padding = data[-1] + if data[-padding:] != bytes([padding]) * padding: + raise ValueError("Invalid padding...") + # remove the padding + return data[:-padding].decode("utf-8") diff --git a/designsafe/utils/system_access.py b/designsafe/utils/system_access.py new file mode 100644 index 0000000000..286301b051 --- /dev/null +++ b/designsafe/utils/system_access.py @@ -0,0 +1,45 @@ +""" +.. :module:: designsafe.utils.system_access + :synopsis: Utilities to register keys with key service and with Tapis +""" +import logging +import requests +from django.conf import settings + + +logger = logging.getLogger(__name__) + + +def create_system_credentials( # pylint: disable=too-many-arguments + client, + username, + public_key, + private_key, + system_id, + skipCredentialCheck=False, # pylint: disable=invalid-name +) -> int: + """ + Set an RSA key pair as the user's auth credential on a Tapis system. + """ + logger.info(f"Creating user credential for {username} on Tapis system {system_id}") + data = {"privateKey": private_key, "publicKey": public_key} + client.systems.createUserCredential( + systemId=system_id, + userName=username, + skipCredentialCheck=skipCredentialCheck, + **data, + ) + + +def register_public_key( + username, publicKey, system_id # pylint: disable=invalid-name +) -> int: + """ + Push a public key to the Key Service API. + """ + url = "https://api.tacc.utexas.edu/keys/v2/" + username + headers = {"Authorization": f"Bearer {settings.KEY_SERVICE_TOKEN}"} + data = {"key_value": publicKey, "tags": [{"name": "system", "value": system_id}]} + response = requests.post(url, json=data, headers=headers, timeout=60) + response.raise_for_status() + return response.status_code diff --git a/designsafe/webhooks.py b/designsafe/webhooks.py deleted file mode 100644 index 70c5578f15..0000000000 --- a/designsafe/webhooks.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -DesignSafe-CI Webhook URLs -""" -from django.conf import settings -from django.urls import include, re_path as url -from designsafe.apps.notifications import views -from designsafe.apps.box_integration import webhooks - -urlpatterns = [ - url(r'^$', views.generic_webhook_handler, name='interactive_wh_handler'), - url(r'^box/$', webhooks.box_webhook), -] diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml deleted file mode 100644 index 8d326e212e..0000000000 --- a/docker-compose-dev.yml +++ /dev/null @@ -1,182 +0,0 @@ -# This compose file is useful for debugging with ipdb - - -version: "2" -services: - redis: - image: redis - - rabbitmq: - image: rabbitmq - - elasticsearch: - image: elasticsearch:5.5.1 - volumes: - - ./data/elasticsearch:/data - ports: - - 9200:9200 - - mysql: - image: mysql:5.6 - ports: - - 3306:3306 - volumes: - - ./data/mysql:/docker-entrypoint-initdb.d - environment: - MYSQL_ROOT_PASSWORD: DS_DEV - MYSQL_DATABASE: DS_DEV - MYSQL_USER: DS_DEV - MYSQL_PASSWORD: DS_DEV - - memcached: - image: memcached - - uwsgi: - build: . - env_file: designsafe.env - volumes: - - .:/portal - environment: - - DJANGO_SETTINGS_MODULE=designsafe.http_debug_settings - - DATABASE_HOST=mysql - - DATABASE_NAME=DS_DEV - - DATABASE_PORT=3306 - - DATABASE_USER=DS_DEV - - DATABASE_PASSWORD=DS_DEV - - DS_LOCAL_DEV=True - - WS_BACKEND_HOST=redis - - WS_BACKEND_DB=0 - - WS_BACKEND_PORT=6379 - - CELERY_BROKER_URL_HOST=rabbitmq - - CELERY_BROKER_URL_PORT=5672 - - CELERY_BROKER_URL_VHOST= - - CELERY_BROKER_URL_USERNAME=guest - - CELERY_BROKER_URL_PWD=guest - - CELERY_BROKER_URL_PROTOCOL=amqp:// - - CELERY_RESULT_BACKEND_HOST=redis - - CELERY_RESULT_BACKEND_PORT=6379 - links: - - redis - ports: - - 9000:9000 - command: ./bin/run-uwsgi.sh - - - django: - stdin_open: true - tty: true - build: . - env_file: designsafe.env - environment: - - DJANGO_SETTINGS_MODULE=designsafe.http_debug_settings - - DATABASE_HOST=mysql - - DATABASE_NAME=DS_DEV - - DATABASE_PORT=3306 - - DATABASE_USER=DS_DEV - - DATABASE_PASSWORD=DS_DEV - - DS_LOCAL_DEV=True - - WS_BACKEND_HOST=redis - - WS_BACKEND_DB=0 - - WS_BACKEND_PORT=6379 - - CELERY_BROKER_URL_HOST=rabbitmq - - CELERY_BROKER_URL_PORT=5672 - - CELERY_BROKER_URL_VHOST= - - CELERY_BROKER_URL_USERNAME=guest - - CELERY_BROKER_URL_PWD=guest - - CELERY_BROKER_URL_PROTOCOL=amqp:// - - CELERY_RESULT_BACKEND_HOST=redis - - CELERY_RESULT_BACKEND_PORT=6379 - links: - - redis:redis - - memcached:memcached - - elasticsearch:elasticsearch - - mysql:mysql - - worker:worker - - rabbitmq:rabbitmq - volumes: - - .:/srv/www/designsafe - - /corral-repl/tacc/NHERI:/corral-repl/tacc/NHERI - - /var/www/designsafe-ci.org/static - - /var/www/designsafe-ci.org/media - ports: - - 8000:8000 - - 5555:5555 - dns: - - 8.8.8.8 - - 8.8.4.4 - #command: /usr/local/bin/uwsgi --ini /portal/conf/uwsgi.ini - command: ./bin/run-django.sh - - nginx: - image: nginx - volumes: - - ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf - - ./conf/nginx/gzip.conf:/etc/nginx/gzip.conf - - ./conf/nginx/dummy.crt:/etc/ssl/dummy.crt - - ./conf/nginx/dummy.key:/etc/ssl/dummy.key - - ./conf/nginx/dhparam.pem:/etc/ssl/dhparam.pem - volumes_from: - - django - links: - - django:django - ports: - - 80:80 - - 443:443 - - # devnginx: - # image: nginx - # volumes: - # - ./conf/nginx.conf:/etc/nginx/nginx.conf - # - ./conf/gzip.conf:/etc/nginx/gzip.conf - # - ./conf/dummy.crt:/etc/ssl/dummy.crt - # - ./conf/dummy.key:/etc/ssl/dummy.key - # - ./conf/dhparam.pem:/etc/ssl/dhparam.pem - # volumes_from: - # - django - # ports: - # - 80:80 - # - 443:443 - # external_links: - # - portal_django_run_1:django - - - worker: - build: . - env_file: designsafe.env - environment: - - DJANGO_SETTINGS_MODULE=designsafe.http_debug_settings - - DATABASE_HOST=mysql - - DATABASE_NAME=DS_DEV - - DATABASE_PORT=3306 - - DATABASE_USER=DS_DEV - - DATABASE_PASSWORD=DS_DEV - - DS_LOCAL_DEV=True - - WS_BACKEND_HOST=redis - - WS_BACKEND_DB=0 - - WS_BACKEND_PORT=6379 - - BROKER_URL_PROTOCOL=amqp:// - - CELERY_BROKER_URL_HOST=rabbitmq - - CELERY_BROKER_URL_PORT=5672 - - CELERY_BROKER_URL_VHOST= - - CELERY_BROKER_URL_USERNAME=guest - - CELERY_BROKER_URL_PWD=guest - - CELERY_BROKER_URL_PROTOCOL=amqp:// - - CELERY_RESULT_BACKEND_HOST=redis - - CELERY_RESULT_BACKEND_PORT=6379 - links: - - redis:redis - - memcached:memcached - - elasticsearch:elasticsearch - - mysql:mysql - - rabbitmq:rabbitmq - volumes: - - .:/portal - - /corral-repl/tacc/NHERI:/corral-repl/tacc/NHERI - dns: - - 8.8.8.8 - - 8.8.4.4 - command: ./bin/run-celery.sh - depends_on: - - rabbitmq - - redis - - memcached diff --git a/poetry.lock b/poetry.lock index b530c7ac6b..ba80888ae4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "agavepy" version = "1.0.0a12" description = "SDK for TACC Tapis (formerly Agave)" -category = "main" optional = false python-versions = "*" files = [ @@ -30,24 +29,22 @@ websocket-client = ">=0.57.0" [[package]] name = "amqp" -version = "5.1.1" +version = "5.2.0" description = "Low-level AMQP client for Python (fork of amqplib)." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "amqp-5.1.1-py3-none-any.whl", hash = "sha256:6f0956d2c23d8fa6e7691934d8c3930eadb44972cbbd1a7ae3a520f735d43359"}, - {file = "amqp-5.1.1.tar.gz", hash = "sha256:2c1b13fecc0893e946c65cbd5f36427861cffa4ea2201d8f6fca22e2a373b5e2"}, + {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, + {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, ] [package.dependencies] -vine = ">=5.0.0" +vine = ">=5.0.0,<6.0.0" [[package]] name = "annotated-types" version = "0.6.0" description = "Reusable constraint types to use with typing.Annotated" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -55,23 +52,10 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[[package]] -name = "appnope" -version = "0.1.3" -description = "Disable App Nap on macOS >= 10.9" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, - {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, -] - [[package]] name = "arrow" version = "1.3.0" description = "Better dates & times for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -85,18 +69,17 @@ types-python-dateutil = ">=2.8.10" [package.extras] doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (>=1.0.0,<2.0.0)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (>=3.0.0,<4.0.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] [[package]] name = "asgiref" -version = "3.7.2" +version = "3.8.1" description = "ASGI specs, helper code, and adapters" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [package.extras] @@ -106,7 +89,6 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] name = "astroid" version = "2.15.8" description = "An abstract syntax tree for Python with inference support." -category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -122,7 +104,6 @@ wrapt = {version = ">=1.14,<2", markers = "python_version >= \"3.11\""} name = "asttokens" version = "2.4.1" description = "Annotate AST trees with source code positions" -category = "main" optional = false python-versions = "*" files = [ @@ -141,7 +122,6 @@ test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -149,11 +129,20 @@ files = [ {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + [[package]] name = "attrdict" version = "2.0.1" description = "A dict with attribute-style access" -category = "main" optional = false python-versions = "*" files = [] @@ -170,28 +159,27 @@ resolved_reference = "83b779ee82d5b0e33be695d398162b8f2430ff33" [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "autobahn" version = "23.6.2" description = "WebSocket client & server library, WAMP real-time framework" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -220,7 +208,6 @@ xbr = ["base58 (>=2.1.0)", "bitarray (>=2.7.5)", "cbor2 (>=5.2.0)", "click (>=8. name = "automat" version = "22.10.0" description = "Self-service finite-state machines for the programmer on the go." -category = "main" optional = false python-versions = "*" files = [ @@ -236,74 +223,85 @@ six = "*" visualize = ["Twisted (>=16.1.1)", "graphviz (>0.5.1)"] [[package]] -name = "backcall" -version = "0.2.0" -description = "Specifications for callback functions passed in to an API" -category = "main" +name = "bcrypt" +version = "4.1.2" +description = "Modern password hashing for your software and your servers" optional = false -python-versions = "*" -files = [ - {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.2" -description = "Screen-scraping library" -category = "main" -optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7" files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, + {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, + {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, + {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, + {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, + {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, + {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, ] -[package.dependencies] -soupsieve = ">1.2" - [package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] [[package]] name = "billiard" -version = "4.1.0" +version = "4.2.0" description = "Python multiprocessing fork with improvements and bugfixes" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "billiard-4.1.0-py3-none-any.whl", hash = "sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a"}, - {file = "billiard-4.1.0.tar.gz", hash = "sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5"}, + {file = "billiard-4.2.0-py3-none-any.whl", hash = "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d"}, + {file = "billiard-4.2.0.tar.gz", hash = "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c"}, ] [[package]] name = "black" -version = "23.10.1" +version = "23.12.1" description = "The uncompromising code formatter." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, - {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, - {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, - {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, - {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, - {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, - {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, - {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, - {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, - {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, - {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, - {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] @@ -315,7 +313,7 @@ platformdirs = ">=2" [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -323,7 +321,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "boxsdk" version = "2.14.0" description = "Official Box Python SDK" -category = "main" optional = false python-versions = "*" files = [ @@ -346,42 +343,40 @@ test = ["bottle", "coverage (<5.0)", "jsonpatch", "mock (>=2.0.0,<4.0.0)", "pyco [[package]] name = "cachetools" -version = "5.3.2" +version = "5.3.3" description = "Extensible memoizing collections and decorators" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, - {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, ] [[package]] name = "celery" -version = "5.3.4" +version = "5.3.6" description = "Distributed Task Queue." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "celery-5.3.4-py3-none-any.whl", hash = "sha256:1e6ed40af72695464ce98ca2c201ad0ef8fd192246f6c9eac8bba343b980ad34"}, - {file = "celery-5.3.4.tar.gz", hash = "sha256:9023df6a8962da79eb30c0c84d5f4863d9793a466354cc931d7f72423996de28"}, + {file = "celery-5.3.6-py3-none-any.whl", hash = "sha256:9da4ea0118d232ce97dff5ed4974587fb1c0ff5c10042eb15278487cdd27d1af"}, + {file = "celery-5.3.6.tar.gz", hash = "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9"}, ] [package.dependencies] -billiard = ">=4.1.0,<5.0" +billiard = ">=4.2.0,<5.0" click = ">=8.1.2,<9.0" click-didyoumean = ">=0.3.0" click-plugins = ">=1.1.1" click-repl = ">=0.2.0" -kombu = ">=5.3.2,<6.0" +kombu = ">=5.3.4,<6.0" python-dateutil = ">=2.8.2" tzdata = ">=2022.7" -vine = ">=5.0.0,<6.0" +vine = ">=5.1.0,<6.0" [package.extras] arangodb = ["pyArango (>=2.0.2)"] -auth = ["cryptography (==41.0.3)"] +auth = ["cryptography (==41.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] cassandra = ["cassandra-driver (>=3.25.0,<4)"] @@ -391,44 +386,42 @@ couchbase = ["couchbase (>=3.0.0)"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] -elasticsearch = ["elasticsearch (<8.0)"] +elasticsearch = ["elastic-transport (<=8.10.0)", "elasticsearch (<=8.11.0)"] eventlet = ["eventlet (>=0.32.0)"] gevent = ["gevent (>=1.5.0)"] librabbitmq = ["librabbitmq (>=2.0.0)"] memcache = ["pylibmc (==1.6.3)"] mongodb = ["pymongo[srv] (>=4.0.2)"] -msgpack = ["msgpack (==1.0.5)"] +msgpack = ["msgpack (==1.0.7)"] pymemcache = ["python-memcached (==1.59)"] pyro = ["pyro4 (==4.82)"] pytest = ["pytest-celery (==0.0.0)"] -redis = ["redis (>=4.5.2,!=4.5.5,<5.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.4)"] +solar = ["ephem (==4.1.5)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.0)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard (==0.21.0)"] +zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "cffi" version = "1.16.0" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -493,7 +486,6 @@ pycparser = "*" name = "channels" version = "4.0.0" description = "Brings async, event-driven capabilities to Django 3.2 and up." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -511,131 +503,139 @@ tests = ["async-timeout", "coverage (>=4.5,<5.0)", "pytest", "pytest-asyncio", " [[package]] name = "channels-redis" -version = "4.1.0" +version = "4.2.0" description = "Redis-backed ASGI channel layer implementation" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "channels_redis-4.1.0-py3-none-any.whl", hash = "sha256:3696f5b9fe367ea495d402ba83d7c3c99e8ca0e1354ff8d913535976ed0abf73"}, - {file = "channels_redis-4.1.0.tar.gz", hash = "sha256:6bd4f75f4ab4a7db17cee495593ace886d7e914c66f8214a1f247ff6659c073a"}, + {file = "channels_redis-4.2.0-py3-none-any.whl", hash = "sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4"}, + {file = "channels_redis-4.2.0.tar.gz", hash = "sha256:01c26c4d5d3a203f104bba9e5585c0305a70df390d21792386586068162027fd"}, ] [package.dependencies] asgiref = ">=3.2.10,<4" channels = "*" msgpack = ">=1.0,<2.0" -redis = ">=4.5.3" +redis = ">=4.6" [package.extras] cryptography = ["cryptography (>=1.3.0)"] tests = ["async-timeout", "cryptography (>=1.3.0)", "pytest", "pytest-asyncio", "pytest-timeout"] +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + [[package]] name = "charset-normalizer" -version = "3.3.1" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.1.tar.gz", hash = "sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win32.whl", hash = "sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f"}, - {file = "charset_normalizer-3.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win32.whl", hash = "sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8"}, - {file = "charset_normalizer-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win32.whl", hash = "sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61"}, - {file = "charset_normalizer-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win32.whl", hash = "sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9"}, - {file = "charset_normalizer-3.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win32.whl", hash = "sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb"}, - {file = "charset_normalizer-3.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win32.whl", hash = "sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4"}, - {file = "charset_normalizer-3.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727"}, - {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -648,14 +648,13 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "click-didyoumean" -version = "0.3.0" +version = "0.3.1" description = "Enables git-like *did-you-mean* feature in click" -category = "main" optional = false -python-versions = ">=3.6.2,<4.0.0" +python-versions = ">=3.6.2" files = [ - {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, - {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, ] [package.dependencies] @@ -665,7 +664,6 @@ click = ">=7" name = "click-plugins" version = "1.1.1" description = "An extension module for click to enable registering CLI commands via setuptools entry-points." -category = "main" optional = false python-versions = "*" files = [ @@ -683,7 +681,6 @@ dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] name = "click-repl" version = "0.3.0" description = "REPL plugin for Click" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -702,7 +699,6 @@ testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] name = "cloudpickle" version = "3.0.0" description = "Pickler class to extend the standard pickle.Pickler functionality" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -714,7 +710,6 @@ files = [ name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -724,76 +719,74 @@ files = [ [[package]] name = "constantly" -version = "15.1.0" +version = "23.10.4" description = "Symbolic constants in Python" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, - {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, + {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, + {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, ] [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.4" description = "Code coverage measurement for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [package.extras] @@ -801,35 +794,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.5" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -849,7 +841,6 @@ test-randomorder = ["pytest-randomly"] name = "cssselect2" version = "0.7.0" description = "CSS selectors for Python ElementTree" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -869,7 +860,6 @@ test = ["flake8", "isort", "pytest"] name = "curlify" version = "2.2.1" description = "Library to convert python requests object to curl command." -category = "main" optional = false python-versions = "*" files = [ @@ -881,14 +871,13 @@ requests = "*" [[package]] name = "daphne" -version = "4.0.0" +version = "4.1.0" description = "Django ASGI (HTTP/WebSocket) server" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "daphne-4.0.0-py3-none-any.whl", hash = "sha256:a288ece46012b6b719c37150be67c69ebfca0793a8521bf821533bad983179b2"}, - {file = "daphne-4.0.0.tar.gz", hash = "sha256:cce9afc8f49a4f15d4270b8cfb0e0fe811b770a5cc795474e97e4da287497666"}, + {file = "daphne-4.1.0-py3-none-any.whl", hash = "sha256:7228cd6a3ca5a9b11c9a1c1c0414dab1bfb4ddc55ff234b545db8d71f6c24938"}, + {file = "daphne-4.1.0.tar.gz", hash = "sha256:882fab39d0b90c6b2709b38116c95f660b6cf236600115dd7c13161fb98b3448"}, ] [package.dependencies] @@ -901,37 +890,39 @@ tests = ["django", "hypothesis", "pytest", "pytest-asyncio"] [[package]] name = "debugpy" -version = "1.8.0" +version = "1.8.1" description = "An implementation of the Debug Adapter Protocol for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7fb95ca78f7ac43393cd0e0f2b6deda438ec7c5e47fa5d38553340897d2fbdfb"}, - {file = "debugpy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada"}, - {file = "debugpy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:a8b7a2fd27cd9f3553ac112f356ad4ca93338feadd8910277aff71ab24d8775f"}, - {file = "debugpy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d9de202f5d42e62f932507ee8b21e30d49aae7e46d5b1dd5c908db1d7068637"}, - {file = "debugpy-1.8.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ef54404365fae8d45cf450d0544ee40cefbcb9cb85ea7afe89a963c27028261e"}, - {file = "debugpy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60009b132c91951354f54363f8ebdf7457aeb150e84abba5ae251b8e9f29a8a6"}, - {file = "debugpy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:8cd0197141eb9e8a4566794550cfdcdb8b3db0818bdf8c49a8e8f8053e56e38b"}, - {file = "debugpy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a64093656c4c64dc6a438e11d59369875d200bd5abb8f9b26c1f5f723622e153"}, - {file = "debugpy-1.8.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:b05a6b503ed520ad58c8dc682749113d2fd9f41ffd45daec16e558ca884008cd"}, - {file = "debugpy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c6fb41c98ec51dd010d7ed650accfd07a87fe5e93eca9d5f584d0578f28f35f"}, - {file = "debugpy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:46ab6780159eeabb43c1495d9c84cf85d62975e48b6ec21ee10c95767c0590aa"}, - {file = "debugpy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:bdc5ef99d14b9c0fcb35351b4fbfc06ac0ee576aeab6b2511702e5a648a2e595"}, - {file = "debugpy-1.8.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:61eab4a4c8b6125d41a34bad4e5fe3d2cc145caecd63c3fe953be4cc53e65bf8"}, - {file = "debugpy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:125b9a637e013f9faac0a3d6a82bd17c8b5d2c875fb6b7e2772c5aba6d082332"}, - {file = "debugpy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:57161629133113c97b387382045649a2b985a348f0c9366e22217c87b68b73c6"}, - {file = "debugpy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:e3412f9faa9ade82aa64a50b602544efcba848c91384e9f93497a458767e6926"}, - {file = "debugpy-1.8.0-py2.py3-none-any.whl", hash = "sha256:9c9b0ac1ce2a42888199df1a1906e45e6f3c9555497643a85e0bf2406e3ffbc4"}, - {file = "debugpy-1.8.0.zip", hash = "sha256:12af2c55b419521e33d5fb21bd022df0b5eb267c3e178f1d374a63a2a6bdccd0"}, + {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, + {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, + {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, + {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, + {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, + {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, + {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, + {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, + {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, + {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, + {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, + {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, + {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, + {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, + {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, + {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, + {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, + {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, + {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, + {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, + {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, + {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, ] [[package]] name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -941,29 +932,28 @@ files = [ [[package]] name = "dill" -version = "0.3.7" +version = "0.3.8" description = "serialize all of Python" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"}, - {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"}, + {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, + {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, ] [package.extras] graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "django" -version = "4.2.6" +version = "4.2.11" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.6-py3-none-any.whl", hash = "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215"}, - {file = "Django-4.2.6.tar.gz", hash = "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f"}, + {file = "Django-4.2.11-py3-none-any.whl", hash = "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"}, + {file = "Django-4.2.11.tar.gz", hash = "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4"}, ] [package.dependencies] @@ -977,14 +967,13 @@ bcrypt = ["bcrypt"] [[package]] name = "django-appconf" -version = "1.0.5" +version = "1.0.6" description = "A helper class for handling configuration defaults of packaged apps gracefully." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "django-appconf-1.0.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"}, - {file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"}, + {file = "django-appconf-1.0.6.tar.gz", hash = "sha256:cfe87ea827c4ee04b9a70fab90b86d704cb02f2981f89da8423cb0fabf88efbf"}, + {file = "django_appconf-1.0.6-py3-none-any.whl", hash = "sha256:c3ae442fba1ff7ec830412c5184b17169a7a1e71cf0864a4c3f93cf4c98a1993"}, ] [package.dependencies] @@ -992,25 +981,22 @@ django = "*" [[package]] name = "django-bootstrap3" -version = "23.4" +version = "23.6" description = "Bootstrap 3 for Django" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "django_bootstrap3-23.4-py3-none-any.whl", hash = "sha256:fc54b9afc6e0d33b9e2ac039dd022996ee95fc19bdf8320f4fd01ec611143ee3"}, - {file = "django_bootstrap3-23.4.tar.gz", hash = "sha256:975e6017bb25b29a86416c4fbac6020f15bfd36d66861f42a20dd4ccfdab435d"}, + {file = "django-bootstrap3-23.6.tar.gz", hash = "sha256:f8563b2641bcad3a8626beda979ff697c8375002cbf906fbd49f4be97b0f8a54"}, + {file = "django_bootstrap3-23.6-py3-none-any.whl", hash = "sha256:ba1334104c390ca9dc5b985a8d8ec45fab2c6401e4abb8d3a47d3b225614c3d9"}, ] [package.dependencies] -beautifulsoup4 = ">=4.8.0" -django = ">=3.2" +Django = ">=3.2" [[package]] name = "django-classy-tags" version = "4.1.0" description = "Class based template tags for Django" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1023,22 +1009,21 @@ django = ">=3.2" [[package]] name = "django-cms" -version = "3.11.4" +version = "3.11.5" description = "Lean enterprise content management powered by Django." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "django-cms-3.11.4.tar.gz", hash = "sha256:58ff8bda97f0012fe365eaa542f1c91a310c6b774030ef5d6c2c3caaabf3cf31"}, - {file = "django_cms-3.11.4-py2.py3-none-any.whl", hash = "sha256:ed27c0d547695b796483d5de5dcfe4b9cedcd73b688c788f32c0175425b7017a"}, + {file = "django-cms-3.11.5.tar.gz", hash = "sha256:6bcddcd036860fcaff780525504a76f77d6b87b54b8c9157cf46c8c513158d74"}, + {file = "django_cms-3.11.5-py2.py3-none-any.whl", hash = "sha256:3d8c974ff51036c60273c77fa7928c855ab34b24955079fc1e12bdee7274f8cf"}, ] [package.dependencies] -Django = ">=3.2,<5.0" +Django = ">=3.2" django-classy-tags = ">=0.7.2" django-formtools = ">=2.1" django-sekizai = ">=0.7" -django-treebeard = ">=4.3" +django-treebeard = ">=4.3,<4.5 || >4.5" djangocms-admin-style = ">=1.2" packaging = "*" @@ -1046,7 +1031,6 @@ packaging = "*" name = "django-filer" version = "2.2.6" description = "A file management application for django that makes handling of files and images a breeze." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1065,7 +1049,6 @@ Unidecode = ">=0.04,<1.2" name = "django-formtools" version = "2.2" description = "A set of high-level abstractions for Django forms" -category = "main" optional = false python-versions = "*" files = [ @@ -1080,7 +1063,6 @@ Django = ">=1.11" name = "django-haystack" version = "3.2.1" description = "Pluggable search for Django." -category = "main" optional = false python-versions = "*" files = [ @@ -1097,7 +1079,6 @@ elasticsearch = ["elasticsearch (>=5,<8)"] name = "django-impersonate" version = "1.9.1" description = "Django app to allow superusers to impersonate other users." -category = "main" optional = false python-versions = "*" files = [ @@ -1108,7 +1089,6 @@ files = [ name = "django-ipware" version = "1.2.0" description = "A Django utility application that returns client's real IP address" -category = "main" optional = false python-versions = "*" files = [ @@ -1117,14 +1097,13 @@ files = [ [[package]] name = "django-js-asset" -version = "2.1.0" +version = "2.2.0" description = "script tag with additional attributes for django.forms.Media" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "django_js_asset-2.1.0-py3-none-any.whl", hash = "sha256:36a3a4dd6e9efc895fb127d13126020f6ec1ec9469ad42878d42143f22495d90"}, - {file = "django_js_asset-2.1.0.tar.gz", hash = "sha256:be6f69ae5c4865617aa7726c48eddb64089a1e7d4ea7d22a35a3beb8282020f6"}, + {file = "django_js_asset-2.2.0-py3-none-any.whl", hash = "sha256:7ef3e858e13d06f10799b56eea62b1e76706f42cf4e709be4e13356bc0ae30d8"}, + {file = "django_js_asset-2.2.0.tar.gz", hash = "sha256:0c57a82cae2317e83951d956110ce847f58ff0cdc24e314dbc18b35033917e94"}, ] [package.dependencies] @@ -1135,14 +1114,13 @@ tests = ["coverage"] [[package]] name = "django-mptt" -version = "0.15.0" +version = "0.16.0" description = "Utilities for implementing Modified Preorder Tree Traversal with your Django Models and working with trees of Model instances." -category = "main" optional = false python-versions = ">=3.9" files = [ - {file = "django_mptt-0.15.0-py3-none-any.whl", hash = "sha256:2574c014b102a41c642be5921f59356b8437fc83c9866d97644c6a0c5dc8ccc1"}, - {file = "django_mptt-0.15.0.tar.gz", hash = "sha256:0df19d5a55f34e73df58ee03fbe0d91807493de4bd3a09f6eb00fc62920035d9"}, + {file = "django_mptt-0.16.0-py3-none-any.whl", hash = "sha256:8716849ba3318d94e2e100ed0923a05c1ffdf8195f8472b690dbaf737d2af3b5"}, + {file = "django_mptt-0.16.0.tar.gz", hash = "sha256:56c9606bf0b329b5f5afd55dd8bfd073612ea1d5999b10903b09de62bee84c8e"}, ] [package.dependencies] @@ -1155,7 +1133,6 @@ tests = ["coverage[toml]", "mock-django"] name = "django-polymorphic" version = "3.1.0" description = "Seamless polymorphic inheritance for Django models" -category = "main" optional = false python-versions = "*" files = [ @@ -1170,7 +1147,6 @@ Django = ">=2.1" name = "django-recaptcha" version = "3.0.0" description = "Django recaptcha form field/widget app." -category = "main" optional = false python-versions = "*" files = [ @@ -1184,8 +1160,7 @@ django = "*" [[package]] name = "django-recaptcha2" version = "1.4.1" -description = "Django reCaptcha v2 field/widget" -category = "main" +description = "" optional = false python-versions = "*" files = [] @@ -1204,7 +1179,6 @@ resolved_reference = "1b7942af0032f1e6ba368e0028c48e3c7cfa0588" name = "django-sekizai" version = "2.0.0" description = "Django Sekizai" -category = "main" optional = false python-versions = "*" files = [ @@ -1220,7 +1194,6 @@ django-classy-tags = ">=1" name = "django-select2" version = "6.3.1" description = "Select2 option fields for Django." -category = "main" optional = false python-versions = "*" files = [ @@ -1235,7 +1208,6 @@ django-appconf = ">=0.6.0" name = "django-termsandconditions" version = "2.0.12" description = "Django app that enables users to accept terms and conditions of a site." -category = "main" optional = false python-versions = ">=3.7.2,<4.0" files = [ @@ -1248,14 +1220,13 @@ Django = ">2.2" [[package]] name = "django-treebeard" -version = "4.7" +version = "4.7.1" description = "Efficient tree implementations for Django" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "django-treebeard-4.7.tar.gz", hash = "sha256:c751a3f924158c288fea89afc25a7151979faf01bf11fdc7be3b858099dfa56d"}, - {file = "django_treebeard-4.7-py3-none-any.whl", hash = "sha256:787117995ff985d98e6c2b241ef6b9d37fe8ff7051cd7535c283616a0b5b2645"}, + {file = "django-treebeard-4.7.1.tar.gz", hash = "sha256:846e462904b437155f76e04907ba4e48480716855f88b898df4122bdcfbd6e98"}, + {file = "django_treebeard-4.7.1-py3-none-any.whl", hash = "sha256:995c7120153ab999898fe3043bbdcd8a0fc77cc106eb94de7350e9d02c885135"}, ] [package.dependencies] @@ -1263,14 +1234,13 @@ Django = ">=3.2" [[package]] name = "djangocms-admin-style" -version = "3.2.6" +version = "3.2.7" description = "Adds pretty CSS styles for the django CMS admin interface." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "djangocms-admin-style-3.2.6.tar.gz", hash = "sha256:f093c65c92db09713726d71280ecea602d5f348c9ad3b02281f7af245a0598db"}, - {file = "djangocms_admin_style-3.2.6-py3-none-any.whl", hash = "sha256:17b59c24d649771ad243cd050114b2acaac8f6d8eecf4539a8dc7740d590f075"}, + {file = "djangocms-admin-style-3.2.7.tar.gz", hash = "sha256:a870a42a34f96588dd8dee4d7cb0c3a428cf2ef0fba085ae0ad26928ea9e83b2"}, + {file = "djangocms_admin_style-3.2.7-py3-none-any.whl", hash = "sha256:6870de9e9877c52998923c4c989bb14e4c6b3fb2587d7850548e5e8e8399cbdd"}, ] [package.dependencies] @@ -1280,7 +1250,6 @@ Django = "*" name = "djangocms-attributes-field" version = "3.0.0" description = "Adds attributes to Django models." -category = "main" optional = false python-versions = "*" files = [ @@ -1295,7 +1264,6 @@ django-cms = ">=3.7" name = "djangocms-cascade" version = "0.16.3" description = "Collection of extendible plugins for django-CMS to create and edit widgets in a simple manner" -category = "main" optional = false python-versions = "*" files = [] @@ -1317,7 +1285,6 @@ resolved_reference = "9e9f9e3088a0fcfdc1b27ad7e08b68390aa6a570" name = "djangocms-file" version = "3.0.0" description = "Adds file plugin to django CMS" -category = "main" optional = false python-versions = "*" files = [ @@ -1334,7 +1301,6 @@ djangocms-attributes-field = ">=1" name = "djangocms-forms-maintained" version = "202206141440" description = "The easiest and most flexible Django CMS Form builder w/ ReCaptcha v2 support!" -category = "main" optional = false python-versions = "*" files = [] @@ -1361,7 +1327,6 @@ resolved_reference = "63ead9288c2ea65139124698bffc0ad01d182afa" name = "djangocms-googlemap" version = "2.0.0" description = "Adds Google Maps plugins to django CMS." -category = "main" optional = false python-versions = "*" files = [ @@ -1377,7 +1342,6 @@ django-filer = ">=1.7" name = "djangocms-picture" version = "3.0.0" description = "Adds an image plugin to django CMS" -category = "main" optional = false python-versions = "*" files = [ @@ -1395,7 +1359,6 @@ easy-thumbnails = "*" name = "djangocms-snippet" version = "3.0.0" description = "Adds snippet plugin to django CMS." -category = "main" optional = false python-versions = "*" files = [ @@ -1410,7 +1373,6 @@ django-cms = ">=3.7" name = "djangocms-style" version = "3.0.0" description = "Adds style plugin to django CMS" -category = "main" optional = false python-versions = "*" files = [ @@ -1424,14 +1386,13 @@ djangocms-attributes-field = ">=1" [[package]] name = "djangocms-text-ckeditor" -version = "5.1.4" +version = "5.1.5" description = "Text Plugin for django CMS with CKEditor support" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "djangocms-text-ckeditor-5.1.4.tar.gz", hash = "sha256:07674ab6fa49e3249d0f23194877d5538df2705a6e9251312913851ac2d9aa5d"}, - {file = "djangocms_text_ckeditor-5.1.4-py3-none-any.whl", hash = "sha256:05c07685a0720acedab96edca034cd57e479903513d948ed03c74e0979f04b08"}, + {file = "djangocms-text-ckeditor-5.1.5.tar.gz", hash = "sha256:eca45b3393879c61bb69d3c23df14a5fd4bef1f2ad66dc36a5bf7bfe06c6b7c3"}, + {file = "djangocms_text_ckeditor-5.1.5-py3-none-any.whl", hash = "sha256:d3e7ac79ca04bbb8c20de313ae1a4c9f0e4a79479b0f3b7a7b196c4f0b34ab29"}, ] [package.dependencies] @@ -1444,7 +1405,6 @@ Pillow = "*" name = "djangocms-video" version = "3.0.0" description = "Adds video plugin to django CMS." -category = "main" optional = false python-versions = "*" files = [ @@ -1461,7 +1421,6 @@ djangocms-attributes-field = ">=1" name = "dropbox" version = "10.6.0" description = "Official Dropbox API Client" -category = "main" optional = false python-versions = "*" files = [ @@ -1478,7 +1437,6 @@ six = ">=1.12.0" name = "easy-thumbnails" version = "2.8.5" description = "Easy thumbnails for Django" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1499,7 +1457,6 @@ svg = ["reportlab", "svglib"] name = "elasticsearch" version = "7.17.9" description = "Python client for Elasticsearch" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" files = [ @@ -1521,7 +1478,6 @@ requests = ["requests (>=2.4.0,<3.0.0)"] name = "elasticsearch-dsl" version = "7.4.1" description = "Python client for Elasticsearch" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1541,7 +1497,6 @@ develop = ["coverage (<5.0.0)", "mock", "pytest (>=3.0.0)", "pytest-cov", "pytes name = "et-xmlfile" version = "1.1.0" description = "An implementation of lxml.xmlfile for the standard library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1551,14 +1506,13 @@ files = [ [[package]] name = "executing" -version = "2.0.0" +version = "2.0.1" description = "Get the currently executing AST node of a frame, and other information" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "executing-2.0.0-py2.py3-none-any.whl", hash = "sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657"}, - {file = "executing-2.0.0.tar.gz", hash = "sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08"}, + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, ] [package.extras] @@ -1568,7 +1522,6 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth name = "exifread" version = "2.3.2" description = "Read Exif metadata from tiff and jpeg files." -category = "main" optional = false python-versions = "*" files = [ @@ -1578,30 +1531,30 @@ files = [ [[package]] name = "future" -version = "0.18.3" +version = "1.0.0" description = "Clean single-source support for Python 3 and 2" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ - {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, ] [[package]] name = "google-api-core" -version = "2.12.0" +version = "2.18.0" description = "Google API client core library" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-core-2.12.0.tar.gz", hash = "sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553"}, - {file = "google_api_core-2.12.0-py3-none-any.whl", hash = "sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160"}, + {file = "google-api-core-2.18.0.tar.gz", hash = "sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9"}, + {file = "google_api_core-2.18.0-py3-none-any.whl", hash = "sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6"}, ] [package.dependencies] google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" +proto-plus = ">=1.22.3,<2.0.0dev" protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" @@ -1612,18 +1565,17 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.105.0" +version = "2.123.0" description = "Google API Client Library for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-python-client-2.105.0.tar.gz", hash = "sha256:0a8b32cfc2d9b3c1868ae6faef7ee1ab9c89a6cec30be709ea9c97f9a3e5902d"}, - {file = "google_api_python_client-2.105.0-py2.py3-none-any.whl", hash = "sha256:571ce7c41e53415e385aab5a955725f71780550683ffcb71596f5809677d40b7"}, + {file = "google-api-python-client-2.123.0.tar.gz", hash = "sha256:a17226b02f71de581afe045437b441844110a9cd91580b73549d41108cf1b9f0"}, + {file = "google_api_python_client-2.123.0-py2.py3-none-any.whl", hash = "sha256:1c2bcaa846acf5bac4d6f244d8373d4de9de73d64eb6e77b56767ab4cf681419"}, ] [package.dependencies] -google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0.dev0" +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" google-auth = ">=1.19.0,<3.0.0.dev0" google-auth-httplib2 = ">=0.1.0" httplib2 = ">=0.15.0,<1.dev0" @@ -1631,14 +1583,13 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.23.3" +version = "2.29.0" description = "Google Authentication Library" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.23.3.tar.gz", hash = "sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3"}, - {file = "google_auth-2.23.3-py2.py3-none-any.whl", hash = "sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda"}, + {file = "google-auth-2.29.0.tar.gz", hash = "sha256:672dff332d073227550ffc7457868ac4218d6c500b155fe6cc17d2b13602c360"}, + {file = "google_auth-2.29.0-py2.py3-none-any.whl", hash = "sha256:d452ad095688cd52bae0ad6fafe027f6a6d6f560e810fec20914e17a09526415"}, ] [package.dependencies] @@ -1657,7 +1608,6 @@ requests = ["requests (>=2.20.0,<3.0.0.dev0)"] name = "google-auth-httplib2" version = "0.1.1" description = "Google Authentication Library: httplib2 transport" -category = "main" optional = false python-versions = "*" files = [ @@ -1673,7 +1623,6 @@ httplib2 = ">=0.19.0" name = "google-auth-oauthlib" version = "0.4.6" description = "Google Authentication Library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1690,14 +1639,13 @@ tool = ["click (>=6.0.0)"] [[package]] name = "googleapis-common-protos" -version = "1.61.0" +version = "1.63.0" description = "Common protobufs used in Google APIs" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.61.0.tar.gz", hash = "sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b"}, - {file = "googleapis_common_protos-1.61.0-py2.py3-none-any.whl", hash = "sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0"}, + {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"}, + {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, ] [package.dependencies] @@ -1710,7 +1658,6 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] name = "hashids" version = "1.3.1" description = "Implements the hashids algorithm in python. For more information, visit http://hashids.org/" -category = "main" optional = false python-versions = ">=2.7" files = [ @@ -1725,7 +1672,6 @@ test = ["pytest (>=2.1.0)"] name = "html5lib" version = "1.1" description = "HTML parser based on the WHATWG HTML specification" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1747,7 +1693,6 @@ lxml = ["lxml"] name = "httplib2" version = "0.22.0" description = "A comprehensive HTTP client library." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1762,7 +1707,6 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 name = "hyperlink" version = "21.0.0" description = "A featureful, immutable, and correct URL for Python." -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1775,21 +1719,19 @@ idna = ">=2.5" [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "importlib-resources" version = "5.13.0" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1805,7 +1747,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", name = "incremental" version = "22.10.0" description = "\"A small library that versions your Python projects.\"" -category = "main" optional = false python-versions = "*" files = [ @@ -1821,7 +1762,6 @@ scripts = ["click (>=6.0)", "twisted (>=16.4.0)"] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1833,7 +1773,6 @@ files = [ name = "ipdb" version = "0.13.13" description = "IPython-enabled pdb" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1847,66 +1786,71 @@ ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} [[package]] name = "ipython" -version = "8.16.1" +version = "8.22.2" description = "IPython: Productive Interactive Computing" -category = "main" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "ipython-8.16.1-py3-none-any.whl", hash = "sha256:0852469d4d579d9cd613c220af7bf0c9cc251813e12be647cb9d463939db9b1e"}, - {file = "ipython-8.16.1.tar.gz", hash = "sha256:ad52f58fca8f9f848e256c629eff888efc0528c12fe0f8ec14f33205f23ef938"}, + {file = "ipython-8.22.2-py3-none-any.whl", hash = "sha256:3c86f284c8f3d8f2b6c662f885c4889a91df7cd52056fd02b7d8d6195d7f56e9"}, + {file = "ipython-8.22.2.tar.gz", hash = "sha256:2dcaad9049f9056f1fef63514f176c7d41f930daa78d05b82a176202818f2c14"}, ] [package.dependencies] -appnope = {version = "*", markers = "sys_platform == \"darwin\""} -backcall = "*" colorama = {version = "*", markers = "sys_platform == \"win32\""} decorator = "*" jedi = ">=0.16" matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} -pickleshare = "*" -prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" stack-data = "*" -traitlets = ">=5" +traitlets = ">=5.13.0" [package.extras] -all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] notebook = ["ipywidgets", "notebook"] parallel = ["ipyparallel"] qtconsole = ["qtconsole"] -test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] -test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] +test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = "*" +files = [ + {file = "isodate-0.6.1-py2.py3-none-any.whl", hash = "sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96"}, + {file = "isodate-0.6.1.tar.gz", hash = "sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"}, +] + +[package.dependencies] +six = "*" [[package]] name = "isort" -version = "5.12.0" +version = "5.13.2" description = "A Python utility / library to sort Python imports." -category = "main" optional = false python-versions = ">=3.8.0" files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] [package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] +colors = ["colorama (>=0.4.6)"] [[package]] name = "jedi" version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1924,14 +1868,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1944,7 +1887,6 @@ i18n = ["Babel (>=2.7)"] name = "jsonfield" version = "3.1.0" description = "A reusable Django field that allows you to store validated JSON in your model." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1957,31 +1899,64 @@ Django = ">=2.2" [[package]] name = "jsonpickle" -version = "3.0.2" +version = "3.0.3" description = "Python library for serializing any arbitrary object graph into JSON" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "jsonpickle-3.0.2-py3-none-any.whl", hash = "sha256:4a8442d97ca3f77978afa58068768dba7bff2dbabe79a9647bc3cdafd4ef019f"}, - {file = "jsonpickle-3.0.2.tar.gz", hash = "sha256:e37abba4bfb3ca4a4647d28bb9f4706436f7b46c8a8333b4a718abafa8e46b37"}, + {file = "jsonpickle-3.0.3-py3-none-any.whl", hash = "sha256:e8d6dcc58f6722bea0321cd328fbda81c582461185688a535df02be0f699afb4"}, + {file = "jsonpickle-3.0.3.tar.gz", hash = "sha256:5691f44495327858ab3a95b9c440a79b41e35421be1a6e09a47b6c9b9421fd06"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] +testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff", "scikit-learn", "simplejson", "sqlalchemy", "ujson"] + +[[package]] +name = "jsonschema" +version = "4.17.3" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"}, + {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"}, ] +[package.dependencies] +attrs = ">=17.4.0" +pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" + [package.extras] -docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"] -testing-libs = ["simplejson", "ujson"] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-spec" +version = "0.1.6" +description = "JSONSchema Spec with object-oriented paths" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "jsonschema_spec-0.1.6-py3-none-any.whl", hash = "sha256:f2206d18c89d1824c1f775ba14ed039743b41a9167bd2c5bdb774b66b3ca0bbf"}, + {file = "jsonschema_spec-0.1.6.tar.gz", hash = "sha256:90215863b56e212086641956b20127ccbf6d8a3a38343dad01d6a74d19482f76"}, +] + +[package.dependencies] +jsonschema = ">=4.0.0,<4.18.0" +pathable = ">=0.4.1,<0.5.0" +PyYAML = ">=5.1" +requests = ">=2.31.0,<3.0.0" [[package]] name = "kombu" -version = "5.3.2" +version = "5.3.6" description = "Messaging library for Python." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "kombu-5.3.2-py3-none-any.whl", hash = "sha256:b753c9cfc9b1e976e637a7cbc1a65d446a22e45546cd996ea28f932082b7dc9e"}, - {file = "kombu-5.3.2.tar.gz", hash = "sha256:0ba213f630a2cb2772728aef56ac6883dc3a2f13435e10048f6e97d48506dbbd"}, + {file = "kombu-5.3.6-py3-none-any.whl", hash = "sha256:49f1e62b12369045de2662f62cc584e7df83481a513db83b01f87b5b9785e378"}, + {file = "kombu-5.3.6.tar.gz", hash = "sha256:f3da5b570a147a5da8280180aa80b03807283d63ea5081fcdb510d18242431d9"}, ] [package.dependencies] @@ -1991,14 +1966,14 @@ vine = "*" [package.extras] azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] -confluentkafka = ["confluent-kafka (==2.1.1)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2"] librabbitmq = ["librabbitmq (>=2.0.0)"] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack"] pyro = ["pyro4"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=4.5.2)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] @@ -2007,223 +1982,216 @@ zookeeper = ["kazoo (>=2.8.0)"] [[package]] name = "lazy-object-proxy" -version = "1.9.0" +version = "1.10.0" description = "A fast and thorough lazy object proxy." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, - {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, - {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, - {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, - {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, - {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, + {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab7004cf2e59f7c2e4345604a3e6ea0d92ac44e1c2375527d56492014e690c3"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0d2fc424e54c70c4bc06787e4072c4f3b1aa2f897dfdc34ce1013cf3ceef05"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e2adb09778797da09d2b5ebdbceebf7dd32e2c96f79da9052b2e87b6ea495895"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1f711e2c6dcd4edd372cf5dec5c5a30d23bba06ee012093267b3376c079ec83"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win32.whl", hash = "sha256:76a095cfe6045c7d0ca77db9934e8f7b71b14645f0094ffcd842349ada5c5fb9"}, + {file = "lazy_object_proxy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4f87d4ed9064b2628da63830986c3d2dca7501e6018347798313fcf028e2fd4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fec03caabbc6b59ea4a638bee5fce7117be8e99a4103d9d5ad77f15d6f81020c"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c83f957782cbbe8136bee26416686a6ae998c7b6191711a04da776dc9e47d4"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009e6bb1f1935a62889ddc8541514b6a9e1fcf302667dcb049a0be5c8f613e56"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75fc59fc450050b1b3c203c35020bc41bd2695ed692a392924c6ce180c6f1dc9"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:782e2c9b2aab1708ffb07d4bf377d12901d7a1d99e5e410d648d892f8967ab1f"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win32.whl", hash = "sha256:edb45bb8278574710e68a6b021599a10ce730d156e5b254941754a9cc0b17d03"}, + {file = "lazy_object_proxy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:e271058822765ad5e3bca7f05f2ace0de58a3f4e62045a8c90a0dfd2f8ad8cc6"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e98c8af98d5707dcdecc9ab0863c0ea6e88545d42ca7c3feffb6b4d1e370c7ba"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:952c81d415b9b80ea261d2372d2a4a2332a3890c2b83e0535f263ddfe43f0d43"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b39d3a151309efc8cc48675918891b865bdf742a8616a337cb0090791a0de9"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e221060b701e2aa2ea991542900dd13907a5c90fa80e199dbf5a03359019e7a3"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92f09ff65ecff3108e56526f9e2481b8116c0b9e1425325e13245abfd79bdb1b"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win32.whl", hash = "sha256:3ad54b9ddbe20ae9f7c1b29e52f123120772b06dbb18ec6be9101369d63a4074"}, + {file = "lazy_object_proxy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:127a789c75151db6af398b8972178afe6bda7d6f68730c057fbbc2e96b08d282"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4ed0518a14dd26092614412936920ad081a424bdcb54cc13349a8e2c6d106a"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ad9e6ed739285919aa9661a5bbed0aaf410aa60231373c5579c6b4801bd883c"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc0a92c02fa1ca1e84fc60fa258458e5bf89d90a1ddaeb8ed9cc3147f417255"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0aefc7591920bbd360d57ea03c995cebc204b424524a5bd78406f6e1b8b2a5d8"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5faf03a7d8942bb4476e3b62fd0f4cf94eaf4618e304a19865abf89a35c0bbee"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win32.whl", hash = "sha256:e333e2324307a7b5d86adfa835bb500ee70bfcd1447384a822e96495796b0ca4"}, + {file = "lazy_object_proxy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:cb73507defd385b7705c599a94474b1d5222a508e502553ef94114a143ec6696"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366c32fe5355ef5fc8a232c5436f4cc66e9d3e8967c01fb2e6302fd6627e3d94"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2297f08f08a2bb0d32a4265e98a006643cd7233fb7983032bd61ac7a02956b3b"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18dd842b49456aaa9a7cf535b04ca4571a302ff72ed8740d06b5adcd41fe0757"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:217138197c170a2a74ca0e05bddcd5f1796c735c37d0eee33e43259b192aa424"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a3a87cf1e133e5b1994144c12ca4aa3d9698517fe1e2ca82977781b16955658"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win32.whl", hash = "sha256:30b339b2a743c5288405aa79a69e706a06e02958eab31859f7f3c04980853b70"}, + {file = "lazy_object_proxy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:a899b10e17743683b293a729d3a11f2f399e8a90c73b089e29f5d0fe3509f0dd"}, + {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, ] [[package]] name = "lxml" -version = "4.9.3" +version = "5.1.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -files = [ - {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, - {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, - {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, - {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, - {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, - {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, - {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, - {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, - {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, - {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, - {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, - {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, - {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, - {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, - {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, - {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, - {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, - {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, - {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, - {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, - {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, - {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, - {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, - {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, - {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, - {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, - {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, - {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, - {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, - {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, - {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, - {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, - {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, + {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, + {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, + {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, + {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, + {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, + {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, + {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, + {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, + {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, + {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, + {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, + {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, + {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, + {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, + {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, + {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, + {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.35)"] +source = ["Cython (>=3.0.7)"] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {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"}, ] [[package]] name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2238,7 +2206,6 @@ traitlets = "*" name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2250,7 +2217,6 @@ files = [ name = "mock" version = "4.0.3" description = "Rolling backport of unittest.mock for all Pythons" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2263,77 +2229,86 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest (<5.4)", "pytest-cov"] +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + [[package]] name = "msgpack" -version = "1.0.7" +version = "1.0.8" description = "MessagePack serializer" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, - {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, - {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, - {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, - {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, - {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, - {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, - {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, - {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, - {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, - {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, - {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2345,7 +2320,6 @@ files = [ name = "networkx" version = "3.2.1" description = "Python package for creating and manipulating graphs and networks" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2364,7 +2338,6 @@ test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] name = "oauth2client" version = "4.1.3" description = "OAuth 2.0 client library" -category = "main" optional = false python-versions = "*" files = [ @@ -2383,7 +2356,6 @@ six = ">=1.6.1" name = "oauthlib" version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2398,20 +2370,90 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "olefile" -version = "0.46" +version = "0.47" description = "Python package to parse, read and write Microsoft OLE2 files (Structured Storage or Compound Document, Microsoft Office)" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f"}, + {file = "olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "openapi-core" +version = "0.16.0" +description = "client-side and server-side support for the OpenAPI Specification v3" +optional = false +python-versions = ">=3.7.0,<4.0.0" files = [ - {file = "olefile-0.46.zip", hash = "sha256:133b031eaf8fd2c9399b78b8bc5b8fcbe4c31e85295749bb17a87cba8f3c3964"}, + {file = "openapi-core-0.16.0.tar.gz", hash = "sha256:5db8fa034e5c262de865cab5f2344995c52f1ba0386182c0be584d02f0282c6a"}, + {file = "openapi_core-0.16.0-py3-none-any.whl", hash = "sha256:4331f528f5a74c7a3963f37b2ad73c54e3dd477276354fd6b7188d2352fd7e8e"}, ] +[package.dependencies] +isodate = "*" +jsonschema-spec = ">=0.1.1,<0.2.0" +more-itertools = "*" +openapi-schema-validator = ">=0.3.0,<0.4.0" +openapi-spec-validator = ">=0.5.0,<0.6.0" +parse = "*" +pathable = ">=0.4.0,<0.5.0" +typing-extensions = ">=4.3.0,<5.0.0" +werkzeug = "*" + +[package.extras] +django = ["django (>=3.0)"] +falcon = ["falcon (>=3.0)"] +flask = ["flask"] +requests = ["requests"] + +[[package]] +name = "openapi-schema-validator" +version = "0.3.4" +description = "OpenAPI schema validation for Python" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi-schema-validator-0.3.4.tar.gz", hash = "sha256:7cf27585dd7970b7257cefe48e1a3a10d4e34421831bdb472d96967433bc27bd"}, + {file = "openapi_schema_validator-0.3.4-py3-none-any.whl", hash = "sha256:34fbd14b7501abe25e64d7b4624a9db02cde1a578d285b3da6f34b290cdf0b3a"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +jsonschema = ">=4.0.0,<5.0.0" + +[package.extras] +isodate = ["isodate"] +rfc3339-validator = ["rfc3339-validator"] +strict-rfc3339 = ["strict-rfc3339"] + +[[package]] +name = "openapi-spec-validator" +version = "0.5.4" +description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "openapi_spec_validator-0.5.4-py3-none-any.whl", hash = "sha256:96be4258fdccc89d3da094738e19d56b94956914b93a22de795b9dd220cb4c7c"}, + {file = "openapi_spec_validator-0.5.4.tar.gz", hash = "sha256:68654e81cc56c71392dba31bf55d11e1c03c99458bebcb0018959a7134e104da"}, +] + +[package.dependencies] +jsonschema = ">=4.0.0,<5.0.0" +jsonschema-spec = ">=0.1.1,<0.2.0" +lazy-object-proxy = ">=1.7.1,<2.0.0" +openapi-schema-validator = ">=0.3.2,<0.5" + +[package.extras] +requests = ["requests"] + [[package]] name = "openpyxl" version = "3.1.2" description = "A Python library to read/write Excel 2010 xlsx/xlsm files" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2426,7 +2468,6 @@ et-xmlfile = "*" name = "opf-fido" version = "1.4.1" description = "Format Identification for Digital Objects (FIDO)." -category = "main" optional = false python-versions = "*" files = [ @@ -2445,21 +2486,51 @@ testing = ["pytest"] [[package]] name = "packaging" -version = "23.2" +version = "24.0" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "paramiko" +version = "3.4.0" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"}, + {file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=2.0)"] + +[[package]] +name = "parse" +version = "1.20.1" +description = "parse() is the opposite of format()" +optional = false +python-versions = "*" +files = [ + {file = "parse-1.20.1-py2.py3-none-any.whl", hash = "sha256:76ddd5214255ae711db4c512be636151fbabaa948c6f30115aecc440422ca82c"}, + {file = "parse-1.20.1.tar.gz", hash = "sha256:09002ca350ad42e76629995f71f7b518670bcf93548bdde3684fd55d2be51975"}, ] [[package]] name = "parso" version = "0.8.3" description = "A Python Parser" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2471,23 +2542,32 @@ files = [ qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["docopt", "pytest (<6.0.0)"] +[[package]] +name = "pathable" +version = "0.4.3" +description = "Object-oriented paths" +optional = false +python-versions = ">=3.7.0,<4.0.0" +files = [ + {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, + {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, +] + [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "petname" version = "2.6" description = "Generate human-readable, random object names" -category = "main" optional = false python-versions = "*" files = [ @@ -2496,125 +2576,127 @@ files = [ [[package]] name = "pexpect" -version = "4.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." -category = "main" optional = false python-versions = "*" files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] [package.dependencies] ptyprocess = ">=0.5" -[[package]] -name = "pickleshare" -version = "0.7.5" -description = "Tiny 'shelve'-like database with concurrency support" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] - [[package]] name = "pillow" -version = "10.1.0" +version = "10.2.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "Pillow-10.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106"}, - {file = "Pillow-10.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593"}, - {file = "Pillow-10.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db"}, - {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f"}, - {file = "Pillow-10.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818"}, - {file = "Pillow-10.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57"}, - {file = "Pillow-10.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7"}, - {file = "Pillow-10.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172"}, - {file = "Pillow-10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061"}, - {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262"}, - {file = "Pillow-10.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992"}, - {file = "Pillow-10.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a"}, - {file = "Pillow-10.1.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b"}, - {file = "Pillow-10.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de"}, - {file = "Pillow-10.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651"}, - {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b"}, - {file = "Pillow-10.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f"}, - {file = "Pillow-10.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996"}, - {file = "Pillow-10.1.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793"}, - {file = "Pillow-10.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01"}, - {file = "Pillow-10.1.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d"}, - {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80"}, - {file = "Pillow-10.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212"}, - {file = "Pillow-10.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14"}, - {file = "Pillow-10.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099"}, - {file = "Pillow-10.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34"}, - {file = "Pillow-10.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd"}, - {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28"}, - {file = "Pillow-10.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2"}, - {file = "Pillow-10.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4"}, - {file = "Pillow-10.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b"}, - {file = "Pillow-10.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f"}, - {file = "Pillow-10.1.0.tar.gz", hash = "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +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)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -2625,7 +2707,6 @@ testing = ["pytest", "pytest-benchmark"] name = "ply" version = "3.11" description = "Python Lex & Yacc" -category = "main" optional = false python-versions = "*" files = [ @@ -2635,52 +2716,64 @@ files = [ [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.43" description = "Library for building powerful interactive command lines in Python" -category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, ] [package.dependencies] wcwidth = "*" +[[package]] +name = "proto-plus" +version = "1.23.0" +description = "Beautiful, Pythonic protocol buffers." +optional = false +python-versions = ">=3.6" +files = [ + {file = "proto-plus-1.23.0.tar.gz", hash = "sha256:89075171ef11988b3fa157f5dbd8b9cf09d65fffee97e29ce403cd8defba19d2"}, + {file = "proto_plus-1.23.0-py3-none-any.whl", hash = "sha256:a829c79e619e1cf632de091013a4173deed13a55f326ef84f05af6f50ff4c82c"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<5.0.0dev" + +[package.extras] +testing = ["google-api-core[grpc] (>=1.31.5)"] + [[package]] name = "protobuf" -version = "4.24.4" +version = "4.25.3" description = "" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "protobuf-4.24.4-cp310-abi3-win32.whl", hash = "sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb"}, - {file = "protobuf-4.24.4-cp310-abi3-win_amd64.whl", hash = "sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085"}, - {file = "protobuf-4.24.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4"}, - {file = "protobuf-4.24.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e"}, - {file = "protobuf-4.24.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9"}, - {file = "protobuf-4.24.4-cp37-cp37m-win32.whl", hash = "sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46"}, - {file = "protobuf-4.24.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37"}, - {file = "protobuf-4.24.4-cp38-cp38-win32.whl", hash = "sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe"}, - {file = "protobuf-4.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b"}, - {file = "protobuf-4.24.4-cp39-cp39-win32.whl", hash = "sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd"}, - {file = "protobuf-4.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b"}, - {file = "protobuf-4.24.4-py3-none-any.whl", hash = "sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92"}, - {file = "protobuf-4.24.4.tar.gz", hash = "sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667"}, + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, ] [[package]] name = "psycopg" -version = "3.1.12" +version = "3.1.18" description = "PostgreSQL database adapter for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "psycopg-3.1.12-py3-none-any.whl", hash = "sha256:8ec5230d6a7eb654b4fb3cf2d3eda8871d68f24807b934790504467f1deee9f8"}, - {file = "psycopg-3.1.12.tar.gz", hash = "sha256:cec7ad2bc6a8510e56c45746c631cf9394148bdc8a9a11fd8cf8554ce129ae78"}, + {file = "psycopg-3.1.18-py3-none-any.whl", hash = "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e"}, + {file = "psycopg-3.1.18.tar.gz", hash = "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b"}, ] [package.dependencies] @@ -2688,9 +2781,9 @@ typing-extensions = ">=4.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} [package.extras] -binary = ["psycopg-binary (==3.1.12)"] -c = ["psycopg-c (==3.1.12)"] -dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +binary = ["psycopg-binary (==3.1.18)"] +c = ["psycopg-c (==3.1.18)"] +dev = ["black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] pool = ["psycopg-pool"] test = ["anyio (>=3.6.2,<4.0)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] @@ -2699,7 +2792,6 @@ test = ["anyio (>=3.6.2,<4.0)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6 name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "main" optional = false python-versions = "*" files = [ @@ -2711,7 +2803,6 @@ files = [ name = "pure-eval" version = "0.2.2" description = "Safely evaluate AST nodes without side effects" -category = "main" optional = false python-versions = "*" files = [ @@ -2726,7 +2817,6 @@ tests = ["pytest"] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2736,36 +2826,33 @@ files = [ [[package]] name = "pyasn1" -version = "0.5.0" +version = "0.6.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, - {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, + {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, + {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, ] [[package]] name = "pyasn1-modules" -version = "0.3.0" +version = "0.4.0" description = "A collection of ASN.1-based protocols modules" -category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, + {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, + {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" +pyasn1 = ">=0.4.6,<0.7.0" [[package]] name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -2773,21 +2860,61 @@ files = [ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +[[package]] +name = "pycryptodome" +version = "3.20.0" +description = "Cryptographic library for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, + {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, + {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, + {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, +] + [[package]] name = "pydantic" -version = "2.5.0" +version = "2.6.4" description = "Data validation using Python type hints" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-2.5.0-py3-none-any.whl", hash = "sha256:7ce6e766c456ad026fe5712f7bcf036efc34bd5d107b3e669ef7ea01b3a9050c"}, - {file = "pydantic-2.5.0.tar.gz", hash = "sha256:69bd6fb62d2d04b7055f59a396993486a2ee586c43a0b89231ce0000de07627c"}, + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.14.1" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -2795,113 +2922,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.1" +version = "2.16.3" description = "" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.14.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:812beca1dcb2b722cccc7e9c620bd972cbc323321194ec2725eab3222e6ac573"}, - {file = "pydantic_core-2.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2ccdc53cb88e51c7d47d74c59630d7be844428f6b8d463055ffad6f0392d8da"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd937733bf2fe7d6a8bf208c12741f1f730b7bf5636033877767a75093c29b8a"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:581bb606a31749a00796f5257947a0968182d7fe91e1dada41f06aeb6bfbc91a"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aadf74a40a7ae49c3c1aa7d32334fe94f4f968e21dd948e301bb4ed431fb2412"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b89821a2c77cc1b8f2c1fc3aacd6a3ecc5df8f7e518dc3f18aef8c4dcf66003d"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49ee28d65f506b2858a60745cc974ed005298ebab12693646b97641dd7c99c35"}, - {file = "pydantic_core-2.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97246f896b4df7fd84caa8a75a67abb95f94bc0b547665bf0889e3262b060399"}, - {file = "pydantic_core-2.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1185548665bc61bbab0dc78f10c8eafa0db0aa1e920fe9a451b77782b10a65cc"}, - {file = "pydantic_core-2.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2a7d08b39fac97540fba785fce3b21ee01a81f081a07a4d031efd791da6666f9"}, - {file = "pydantic_core-2.14.1-cp310-none-win32.whl", hash = "sha256:0a8c8daf4e3aa3aeb98e3638fc3d58a359738f3d12590b2474c6bb64031a0764"}, - {file = "pydantic_core-2.14.1-cp310-none-win_amd64.whl", hash = "sha256:4f0788699a92d604f348e9c1ac5e97e304e97127ba8325c7d0af88dcc7d35bd3"}, - {file = "pydantic_core-2.14.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:2be018a84995b6be1bbd40d6064395dbf71592a981169cf154c0885637f5f54a"}, - {file = "pydantic_core-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc3227408808ba7df8e95eb1d8389f4ba2203bed8240b308de1d7ae66d828f24"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42d5d0e9bbb50481a049bd0203224b339d4db04006b78564df2b782e2fd16ebc"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc6a4ea9f88a810cb65ccae14404da846e2a02dd5c0ad21dee712ff69d142638"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d312ad20e3c6d179cb97c42232b53111bcd8dcdd5c1136083db9d6bdd489bc73"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:679cc4e184f213c8227862e57340d12fd4d4d19dc0e3ddb0f653f86f01e90f94"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101df420e954966868b8bc992aefed5fa71dd1f2755104da62ee247abab28e2f"}, - {file = "pydantic_core-2.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c964c0cc443d6c08a2347c0e5c1fc2d85a272dc66c1a6f3cde4fc4843882ada4"}, - {file = "pydantic_core-2.14.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8276bbab68a9dbe721da92d19cbc061f76655248fe24fb63969d0c3e0e5755e7"}, - {file = "pydantic_core-2.14.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12163197fec7c95751a3c71b36dcc1909eed9959f011ffc79cc8170a6a74c826"}, - {file = "pydantic_core-2.14.1-cp311-none-win32.whl", hash = "sha256:b8ff0302518dcd001bd722bbe342919c29e5066c7eda86828fe08cdc112668b8"}, - {file = "pydantic_core-2.14.1-cp311-none-win_amd64.whl", hash = "sha256:59fa83873223f856d898452c6162a390af4297756f6ba38493a67533387d85d9"}, - {file = "pydantic_core-2.14.1-cp311-none-win_arm64.whl", hash = "sha256:798590d38c9381f07c48d13af1f1ef337cebf76ee452fcec5deb04aceced51c7"}, - {file = "pydantic_core-2.14.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:587d75aec9ae50d0d63788cec38bf13c5128b3fc1411aa4b9398ebac884ab179"}, - {file = "pydantic_core-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26242e3593d4929123615bd9365dd86ef79b7b0592d64a96cd11fd83c69c9f34"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5879ac4791508d8f0eb7dec71ff8521855180688dac0c55f8c99fc4d1a939845"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad9ea86f5fc50f1b62c31184767fe0cacaa13b54fe57d38898c3776d30602411"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:102ac85a775e77821943ae38da9634ddd774b37a8d407181b4f7b05cdfb36b55"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2459cc06572730e079ec1e694e8f68c99d977b40d98748ae72ff11ef21a56b0b"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:217dcbfaf429a9b8f1d54eb380908b9c778e78f31378283b30ba463c21e89d5d"}, - {file = "pydantic_core-2.14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d59e0d7cdfe8ed1d4fcd28aad09625c715dc18976c7067e37d8a11b06f4be3e"}, - {file = "pydantic_core-2.14.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e2be646a5155d408e68b560c0553e8a83dc7b9f90ec6e5a2fc3ff216719385db"}, - {file = "pydantic_core-2.14.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ffba979801e3931a19cd30ed2049450820effe8f152aaa317e2fd93795d318d7"}, - {file = "pydantic_core-2.14.1-cp312-none-win32.whl", hash = "sha256:132b40e479cb5cebbbb681f77aaceabbc8355df16c9124cff1d4060ada83cde2"}, - {file = "pydantic_core-2.14.1-cp312-none-win_amd64.whl", hash = "sha256:744b807fe2733b6da3b53e8ad93e8b3ea3ee3dfc3abece4dd2824cc1f39aa343"}, - {file = "pydantic_core-2.14.1-cp312-none-win_arm64.whl", hash = "sha256:24ba48f9d0b8d64fc5e42e1600366c3d7db701201294989aebdaca23110c02ab"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:ba55d73a2df4771b211d0bcdea8b79454980a81ed34a1d77a19ddcc81f98c895"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e905014815687d88cbb14bbc0496420526cf20d49f20606537d87646b70f1046"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:443dc5eede7fa76b2370213e0abe881eb17c96f7d694501853c11d5d56916602"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abae6fd5504e5e438e4f6f739f8364fd9ff5a5cdca897e68363e2318af90bc28"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9486e27bb3f137f33e2315be2baa0b0b983dae9e2f5f5395240178ad8e644728"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69df82892ff00491d673b1929538efb8c8d68f534fdc6cb7fd3ac8a5852b9034"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:184ff7b30c3f60e1b775378c060099285fd4b5249271046c9005f8b247b39377"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3d5b2a4b3c10cad0615670cab99059441ff42e92cf793a0336f4bc611e895204"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:871c641a83719caaa856a11dcc61c5e5b35b0db888e1a0d338fe67ce744575e2"}, - {file = "pydantic_core-2.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1e7208946ea9b27a8cef13822c339d4ae96e45952cc01fc4a91c7f1cb0ae2861"}, - {file = "pydantic_core-2.14.1-cp37-none-win32.whl", hash = "sha256:b4ff385a525017f5adf6066d7f9fb309f99ade725dcf17ed623dc7dce1f85d9f"}, - {file = "pydantic_core-2.14.1-cp37-none-win_amd64.whl", hash = "sha256:c7411cd06afeb263182e38c6ca5b4f5fe4f20d91466ad7db0cd6af453a02edec"}, - {file = "pydantic_core-2.14.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:2871daf5b2823bf77bf7d3d43825e5d904030c155affdf84b21a00a2e00821d2"}, - {file = "pydantic_core-2.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7977e261cac5f99873dc2c6f044315d09b19a71c4246560e1e67593889a90978"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5a111f9158555582deadd202a60bd7803b6c68f406391b7cf6905adf0af6811"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac417312bf6b7a0223ba73fb12e26b2854c93bf5b1911f7afef6d24c379b22aa"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c36987f5eb2a7856b5f5feacc3be206b4d1852a6ce799f6799dd9ffb0cba56ae"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6e98227eb02623d57e1fd061788837834b68bb995a869565211b9abf3de4bf4"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023b6d7ec4e97890b28eb2ee24413e69a6d48de4e8b75123957edd5432f4eeb3"}, - {file = "pydantic_core-2.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6015beb28deb5306049ecf2519a59627e9e050892927850a884df6d5672f8c7d"}, - {file = "pydantic_core-2.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3f48d4afd973abbd65266ac24b24de1591116880efc7729caf6b6b94a9654c9e"}, - {file = "pydantic_core-2.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:28734bcfb8fc5b03293dec5eb5ea73b32ff767f6ef79a31f6e41dad2f5470270"}, - {file = "pydantic_core-2.14.1-cp38-none-win32.whl", hash = "sha256:3303113fdfaca927ef11e0c5f109e2ec196c404f9d7ba5f8ddb63cdf287ea159"}, - {file = "pydantic_core-2.14.1-cp38-none-win_amd64.whl", hash = "sha256:144f2c1d5579108b6ed1193fcc9926124bd4142b0f7020a7744980d1235c8a40"}, - {file = "pydantic_core-2.14.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:893bf4fb9bfb9c4639bc12f3de323325ada4c6d60e478d5cded65453e9364890"}, - {file = "pydantic_core-2.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:052d8731aaf844f91fe4cd3faf28983b109a5865b3a256ec550b80a5689ead87"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1c6ecb53e4b907ee8486f453dd940b8cbb509946e2b671e3bf807d310a96fc"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:94cf6d0274eb899d39189144dcf52814c67f9b0fd196f211420d9aac793df2da"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36c3bf96f803e207a80dbcb633d82b98ff02a9faa76dd446e969424dec8e2b9f"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb290491f1f0786a7da4585250f1feee200fc17ff64855bdd7c42fb54526fa29"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6590ed9d13eb51b28ea17ddcc6c8dbd6050b4eb589d497105f0e13339f223b72"}, - {file = "pydantic_core-2.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:69cd74e55a5326d920e7b46daa2d81c2bdb8bcf588eafb2330d981297b742ddc"}, - {file = "pydantic_core-2.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d965bdb50725a805b083f5f58d05669a85705f50a6a864e31b545c589290ee31"}, - {file = "pydantic_core-2.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca942a2dc066ca5e04c27feaa8dfb9d353ddad14c6641660c565149186095343"}, - {file = "pydantic_core-2.14.1-cp39-none-win32.whl", hash = "sha256:72c2ef3787c3b577e5d6225d73a77167b942d12cef3c1fbd5e74e55b7f881c36"}, - {file = "pydantic_core-2.14.1-cp39-none-win_amd64.whl", hash = "sha256:55713d155da1e508083c4b08d0b1ad2c3054f68b8ef7eb3d3864822e456f0bb5"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:53efe03cc383a83660cfdda6a3cb40ee31372cedea0fde0b2a2e55e838873ab6"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f523e116879bc6714e61d447ce934676473b068069dce6563ea040381dc7a257"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85bb66d661be51b2cba9ca06759264b3469d2dbb53c3e6effb3f05fec6322be6"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f53a3ccdc30234cb4342cec541e3e6ed87799c7ca552f0b5f44e3967a5fed526"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1bfb63821ada76719ffcd703fc40dd57962e0d8c253e3c565252e6de6d3e0bc6"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e2c689439f262c29cf3fcd5364da1e64d8600facecf9eabea8643b8755d2f0de"}, - {file = "pydantic_core-2.14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a15f6e5588f7afb7f6fc4b0f4ff064749e515d34f34c666ed6e37933873d8ad8"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:f1a30eef060e21af22c7d23349f1028de0611f522941c80efa51c05a63142c62"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16f4a7e1ec6b3ea98a1e108a2739710cd659d68b33fbbeaba066202cab69c7b6"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd80a2d383940eec3db6a5b59d1820f947317acc5c75482ff8d79bf700f8ad6a"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a68a36d71c7f638dda6c9e6b67f6aabf3fa1471b198d246457bfdc7c777cdeb7"}, - {file = "pydantic_core-2.14.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ebc79120e105e4bcd7865f369e3b9dbabb0d492d221e1a7f62a3e8e292550278"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c8c466facec2ccdf025b0b1455b18f2c3d574d5f64d24df905d3d7b8f05d5f4e"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b91b5ec423e88caa16777094c4b2b97f11453283e7a837e5e5e1b886abba1251"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130e49aa0cb316f743bc7792c36aefa39fc2221312f1d4b333b19edbdd71f2b1"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f483467c046f549572f8aca3b7128829e09ae3a9fe933ea421f7cb7c58120edb"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dee4682bd7947afc682d342a8d65ad1834583132383f8e801601a8698cb8d17a"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8d927d042c0ef04607ee7822828b208ab045867d20477ec6593d612156798547"}, - {file = "pydantic_core-2.14.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5a1570875eb0d1479fb2270ed80c88c231aaaf68b0c3f114f35e7fb610435e4f"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cb2fd3ab67558eb16aecfb4f2db4febb4d37dc74e6b8613dc2e7160fb58158a9"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7991f25b98038252363a03e6a9fe92e60fe390fda2631d238dc3b0e396632f8"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b45b7be9f99991405ecd6f6172fb6798908a8097106ae78d5cc5cc15121bad9"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:51506e7652a2ef1d1cf763c4b51b972ff4568d1dddc96ca83931a6941f5e6389"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:66dc0e63349ec39c1ea66622aa5c2c1f84382112afd3ab2fa0cca4fb01f7db39"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:8e17f0c3ba4cb07faa0038a59ce162de584ed48ba645c8d05a5de1e40d4c21e7"}, - {file = "pydantic_core-2.14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d983222223f63e323a5f497f5b85e211557a5d8fb670dc88f343784502b466ba"}, - {file = "pydantic_core-2.14.1.tar.gz", hash = "sha256:0d82a6ee815388a362885186e431fac84c7a06623bc136f508e9f88261d8cadb"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -2909,24 +3013,23 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" version = "1.7.1" description = "JSON Web Token implementation in Python" -category = "main" optional = false python-versions = "*" files = [ @@ -2943,7 +3046,6 @@ test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner name = "pylint" version = "2.17.7" description = "python code static checker" -category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -2968,7 +3070,6 @@ testutils = ["gitpython (>3)"] name = "pylint-django" version = "2.5.5" description = "A Pylint plugin to help Pylint understand the Django web framework" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -2987,7 +3088,6 @@ with-django = ["Django (>=2.2)"] name = "pylint-plugin-utils" version = "0.8.2" description = "Utilities and helpers for writing Pylint plugins" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -3002,7 +3102,6 @@ pylint = ">=1.7" name = "pymemcache" version = "4.0.0" description = "A comprehensive, fast, pure Python memcached client" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3014,7 +3113,6 @@ files = [ name = "pymongo" version = "3.13.0" description = "Python driver for MongoDB " -category = "main" optional = false python-versions = "*" files = [ @@ -3139,45 +3237,109 @@ srv = ["dnspython (>=1.16.0,<1.17.0)"] tls = ["ipaddress"] zstd = ["zstandard"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pyopenssl" -version = "23.3.0" +version = "24.1.0" description = "Python wrapper module around the OpenSSL library" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pyOpenSSL-23.3.0-py3-none-any.whl", hash = "sha256:6756834481d9ed5470f4a9393455154bc92fe7a64b7bc6ee2c804e78c52099b2"}, - {file = "pyOpenSSL-23.3.0.tar.gz", hash = "sha256:6b2cba5cc46e822750ec3e5a81ee12819850b11303630d575e98108a079c2b12"}, + {file = "pyOpenSSL-24.1.0-py3-none-any.whl", hash = "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad"}, + {file = "pyOpenSSL-24.1.0.tar.gz", hash = "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f"}, ] [package.dependencies] -cryptography = ">=41.0.5,<42" +cryptography = ">=41.0.5,<43" [package.extras] docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] -test = ["flaky", "pretend", "pytest (>=3.0.1)"] +test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] [[package]] name = "pyparsing" -version = "3.1.1" +version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyrsistent" +version = "0.20.0" +description = "Persistent/Functional/Immutable data structures" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34"}, + {file = "pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b"}, + {file = "pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f"}, + {file = "pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7"}, + {file = "pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a"}, + {file = "pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224"}, + {file = "pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656"}, + {file = "pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee"}, + {file = "pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3"}, + {file = "pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d"}, + {file = "pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174"}, + {file = "pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d"}, + {file = "pyrsistent-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:79ed12ba79935adaac1664fd7e0e585a22caa539dfc9b7c7c6d5ebf91fb89054"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f920385a11207dc372a028b3f1e1038bb244b3ec38d448e6d8e43c6b3ba20e98"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5c2d012671b7391803263419e31b5c7c21e7c95c8760d7fc35602353dee714"}, + {file = "pyrsistent-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef3992833fbd686ee783590639f4b8343a57f1f75de8633749d984dc0eb16c86"}, + {file = "pyrsistent-0.20.0-cp38-cp38-win32.whl", hash = "sha256:881bbea27bbd32d37eb24dd320a5e745a2a5b092a17f6debc1349252fac85423"}, + {file = "pyrsistent-0.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:6d270ec9dd33cdb13f4d62c95c1a5a50e6b7cdd86302b494217137f760495b9d"}, + {file = "pyrsistent-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca52d1ceae015859d16aded12584c59eb3825f7b50c6cfd621d4231a6cc624ce"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b318ca24db0f0518630e8b6f3831e9cba78f099ed5c1d65ffe3e023003043ba0"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed2c3216a605dc9a6ea50c7e84c82906e3684c4e80d2908208f662a6cbf9022"}, + {file = "pyrsistent-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e14c95c16211d166f59c6611533d0dacce2e25de0f76e4c140fde250997b3ca"}, + {file = "pyrsistent-0.20.0-cp39-cp39-win32.whl", hash = "sha256:f058a615031eea4ef94ead6456f5ec2026c19fb5bd6bfe86e9665c4158cf802f"}, + {file = "pyrsistent-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:58b8f6366e152092194ae68fefe18b9f0b4f89227dfd86a07770c3d86097aebf"}, + {file = "pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b"}, + {file = "pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4"}, +] + [[package]] name = "pytas" version = "1.7.0" description = "Python package for TAS integration" -category = "main" optional = false python-versions = "*" files = [] @@ -3194,14 +3356,13 @@ resolved_reference = "1e2e4e85b895cc381677a4c9d8e1fa7d8c7b86bc" [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -3217,7 +3378,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-asyncio" version = "0.14.0" description = "Pytest support for asyncio." -category = "main" optional = false python-versions = ">= 3.5" files = [ @@ -3235,7 +3395,6 @@ testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=5.7.1)"] name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3253,18 +3412,17 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-django" -version = "4.5.2" +version = "4.8.0" description = "A Django plugin for pytest." -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, - {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, + {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, + {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, ] [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=7.0.0" [package.extras] docs = ["sphinx", "sphinx-rtd-theme"] @@ -3272,32 +3430,30 @@ testing = ["Django", "django-configurations (>=2.0)"] [[package]] name = "pytest-mock" -version = "3.12.0" +version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, - {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, ] [package.dependencies] -pytest = ">=5.0" +pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -3305,14 +3461,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] @@ -3322,7 +3477,6 @@ cli = ["click (>=5.0)"] name = "python-magic" version = "0.4.27" description = "File type identification using libmagic" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -3332,21 +3486,19 @@ files = [ [[package]] name = "pytz" -version = "2023.3.post1" +version = "2023.4" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2023.4-py2.py3-none-any.whl", hash = "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a"}, + {file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"}, ] [[package]] name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3355,6 +3507,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -3362,8 +3515,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -3380,6 +3541,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -3387,6 +3549,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -3394,18 +3557,17 @@ files = [ [[package]] name = "redis" -version = "5.0.1" +version = "5.0.3" description = "Python client for Redis database and key-value store" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "redis-5.0.1-py3-none-any.whl", hash = "sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"}, - {file = "redis-5.0.1.tar.gz", hash = "sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f"}, + {file = "redis-5.0.3-py3-none-any.whl", hash = "sha256:5da9b8fe9e1254293756c16c008e8620b3d15fcc6dde6babde9541850e72a32d"}, + {file = "redis-5.0.3.tar.gz", hash = "sha256:4973bae7444c0fbed64a06b87446f79361cb7e4ec1538c022d696ed7a5015580"}, ] [package.dependencies] -async-timeout = {version = ">=4.0.2", markers = "python_full_version <= \"3.11.2\""} +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] hiredis = ["hiredis (>=1.0.0)"] @@ -3413,17 +3575,17 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "reportlab" -version = "4.0.6" +version = "4.1.0" description = "The Reportlab Toolkit" -category = "main" optional = false python-versions = ">=3.7,<4" files = [ - {file = "reportlab-4.0.6-py3-none-any.whl", hash = "sha256:ec062675202eb76f6100ed44da64f38ed3c7feb5016cf4fe7f17ce35423ab14a"}, - {file = "reportlab-4.0.6.tar.gz", hash = "sha256:069aa35da7c882921f419f6e26327e14dac1d9d0adeb40b584cdadd974d99fc0"}, + {file = "reportlab-4.1.0-py3-none-any.whl", hash = "sha256:28a40d5000afbd8ccae15a47f7abe2841768461354bede1a9d42841132997c98"}, + {file = "reportlab-4.1.0.tar.gz", hash = "sha256:3a99faf412691159c068b3ff01c15307ce2fd2cf6b860199434874e002040a84"}, ] [package.dependencies] +chardet = "*" pillow = ">=9.0.0" [package.extras] @@ -3435,7 +3597,6 @@ renderpm = ["rl-renderPM (>=4.0.3,<4.1)"] name = "requests" version = "2.31.0" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3455,34 +3616,30 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-mock" -version = "1.11.0" +version = "1.12.1" description = "Mock out responses from the requests package" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, - {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, + {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, + {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, ] [package.dependencies] -requests = ">=2.3,<3" -six = "*" +requests = ">=2.22,<3" [package.extras] fixture = ["fixtures"] -test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] [[package]] name = "requests-oauthlib" -version = "1.3.1" +version = "2.0.0" description = "OAuthlib authentication support for Requests." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.4" files = [ - {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"}, - {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"}, + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, ] [package.dependencies] @@ -3496,7 +3653,6 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] name = "requests-toolbelt" version = "0.10.1" description = "A utility belt for advanced users of python-requests" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3511,7 +3667,6 @@ requests = ">=2.0.1,<3.0.0" name = "rsa" version = "4.9" description = "Pure-Python RSA implementation" -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -3526,7 +3681,6 @@ pyasn1 = ">=0.1.3" name = "rt" version = "1.0.13" description = "Python interface to Request Tracker API" -category = "main" optional = false python-versions = "*" files = [ @@ -3539,14 +3693,13 @@ six = "*" [[package]] name = "service-identity" -version = "23.1.0" +version = "24.1.0" description = "Service identity verification for pyOpenSSL & cryptography." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "service_identity-23.1.0-py3-none-any.whl", hash = "sha256:87415a691d52fcad954a500cb81f424d0273f8e7e3ee7d766128f4575080f383"}, - {file = "service_identity-23.1.0.tar.gz", hash = "sha256:ecb33cd96307755041e978ab14f8b14e13b40f1fbd525a4dc78f46d2b986431d"}, + {file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"}, + {file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"}, ] [package.dependencies] @@ -3556,7 +3709,7 @@ pyasn1 = "*" pyasn1-modules = "*" [package.extras] -dev = ["pyopenssl", "service-identity[docs,idna,mypy,tests]"] +dev = ["pyopenssl", "service-identity[idna,mypy,tests]"] docs = ["furo", "myst-parser", "pyopenssl", "sphinx", "sphinx-notfound-page"] idna = ["idna"] mypy = ["idna", "mypy", "types-pyopenssl"] @@ -3564,26 +3717,24 @@ tests = ["coverage[toml] (>=5.0.2)", "pytest"] [[package]] name = "setuptools" -version = "68.2.2" +version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3591,23 +3742,10 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - [[package]] name = "sqlparse" version = "0.4.4" description = "A non-validating SQL parser." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3624,7 +3762,6 @@ test = ["pytest", "pytest-cov"] name = "stack-data" version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" -category = "main" optional = false python-versions = "*" files = [ @@ -3644,7 +3781,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] name = "stone" version = "3.3.1" description = "Stone is an interface description language (IDL) for APIs." -category = "main" optional = false python-versions = "*" files = [ @@ -3661,7 +3797,6 @@ six = ">=1.12.0" name = "svglib" version = "1.5.1" description = "A pure-Python library for reading and converting SVG" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3676,14 +3811,13 @@ tinycss2 = ">=0.6.0" [[package]] name = "tablib" -version = "3.5.0" +version = "3.6.0" description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "tablib-3.5.0-py3-none-any.whl", hash = "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9"}, - {file = "tablib-3.5.0.tar.gz", hash = "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33"}, + {file = "tablib-3.6.0-py3-none-any.whl", hash = "sha256:c403ed093d438b6162236efb106463d76c37a66283565b8c5c4631010c7e8b6d"}, + {file = "tablib-3.6.0.tar.gz", hash = "sha256:414cb1922ae14af267ddd93163687dac6db74220360c5e0bd91f9a4479a9a649"}, ] [package.dependencies] @@ -3691,20 +3825,45 @@ openpyxl = {version = ">=2.6.0", optional = true, markers = "extra == \"xlsx\""} pyyaml = {version = "*", optional = true, markers = "extra == \"yaml\""} [package.extras] -all = ["markuppy", "odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"] +all = ["odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"] cli = ["tabulate"] -html = ["markuppy"] ods = ["odfpy"] pandas = ["pandas"] xls = ["xlrd", "xlwt"] xlsx = ["openpyxl (>=2.6.0)"] yaml = ["pyyaml"] +[[package]] +name = "tapipy" +version = "1.6.3" +description = "Python lib for interacting with an instance of the Tapis API Framework" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "tapipy-1.6.3-py3-none-any.whl", hash = "sha256:2166bc0ec7d16c45cf0c85a874a30d8d0a2721830d286cb9ff774ad060e733ad"}, + {file = "tapipy-1.6.3.tar.gz", hash = "sha256:edb5e6a57a92e030d3ac3db50e5b167a2014ff33a30c0f1d1e51f9cfed808eb9"}, +] + +[package.dependencies] +atomicwrites = ">=1.4.0,<2.0.0" +certifi = ">=2020.11.8" +cloudpickle = ">=1.6.0" +cryptography = ">=3.3.2" +jsonschema = ">=3.2.0" +openapi_core = "0.16.0" +openapi_spec_validator = ">=0.5.0,<0.6.0" +PyJWT = ">=1.7.1" +python_dateutil = ">=2.5.3,<3.0.0" +pyyaml = ">=5.4" +requests = ">=2.20.0,<3.0.0" +setuptools = ">=21.0.0" +six = ">=1.10,<2.0" +urllib3 = ">=1.26.5,<2.0.0" + [[package]] name = "tinycss2" version = "1.2.1" description = "A tiny CSS parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3723,7 +3882,6 @@ test = ["flake8", "isort", "pytest"] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3733,42 +3891,39 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.1" +version = "0.12.4" description = "Style preserving TOML library" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.1-py3-none-any.whl", hash = "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"}, - {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, ] [[package]] name = "traitlets" -version = "5.12.0" +version = "5.14.2" description = "Traitlets Python configuration system" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "traitlets-5.12.0-py3-none-any.whl", hash = "sha256:81539f07f7aebcde2e4b5ab76727f53eabf18ad155c6ed7979a681411602fa47"}, - {file = "traitlets-5.12.0.tar.gz", hash = "sha256:833273bf645d8ce31dcb613c56999e2e055b1ffe6d09168a164bcd91c36d5d35"}, + {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, + {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.6.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] [[package]] name = "twisted" -version = "23.8.0" +version = "24.3.0" description = "An asynchronous networking framework written in Python" -category = "main" optional = false -python-versions = ">=3.7.1" +python-versions = ">=3.8.0" files = [ - {file = "twisted-23.8.0-py3-none-any.whl", hash = "sha256:b8bdba145de120ffb36c20e6e071cce984e89fba798611ed0704216fb7f884cd"}, - {file = "twisted-23.8.0.tar.gz", hash = "sha256:3c73360add17336a622c0d811c2a2ce29866b6e59b1125fd6509b17252098a24"}, + {file = "twisted-24.3.0-py3-none-any.whl", hash = "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63"}, + {file = "twisted-24.3.0.tar.gz", hash = "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae"}, ] [package.dependencies] @@ -3781,19 +3936,18 @@ incremental = ">=22.10.0" pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""} service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} twisted-iocpsupport = {version = ">=1.0.2,<2", markers = "platform_system == \"Windows\""} -typing-extensions = ">=3.10.0" +typing-extensions = ">=4.2.0" zope-interface = ">=5" [package.extras] -all-non-platform = ["twisted[conch,contextvars,http2,serial,test,tls]", "twisted[conch,contextvars,http2,serial,test,tls]"] +all-non-platform = ["twisted[conch,http2,serial,test,tls]", "twisted[conch,http2,serial,test,tls]"] conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"] -contextvars = ["contextvars (>=2.4,<3)"] dev = ["coverage (>=6b1,<7)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "twisted[dev-release]", "twistedchecker (>=0.7,<1.0)"] -dev-release = ["pydoctor (>=23.4.0,<23.5.0)", "pydoctor (>=23.4.0,<23.5.0)", "readthedocs-sphinx-ext (>=2.2,<3.0)", "readthedocs-sphinx-ext (>=2.2,<3.0)", "sphinx (>=5,<7)", "sphinx (>=5,<7)", "sphinx-rtd-theme (>=1.2,<2.0)", "sphinx-rtd-theme (>=1.2,<2.0)", "towncrier (>=22.12,<23.0)", "towncrier (>=22.12,<23.0)", "urllib3 (<2)", "urllib3 (<2)"] +dev-release = ["pydoctor (>=23.9.0,<23.10.0)", "pydoctor (>=23.9.0,<23.10.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"] gtk-platform = ["pygobject", "pygobject", "twisted[all-non-platform]", "twisted[all-non-platform]"] http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] macos-platform = ["pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "twisted[all-non-platform]", "twisted[all-non-platform]"] -mypy = ["mypy (==0.981)", "mypy-extensions (==0.4.3)", "mypy-zope (==0.3.11)", "twisted[all-non-platform,dev]", "types-pyopenssl", "types-setuptools"] +mypy = ["mypy (>=1.8,<2.0)", "mypy-zope (>=1.0.3,<1.1.0)", "twisted[all-non-platform,dev]", "types-pyopenssl", "types-setuptools"] osx-platform = ["twisted[macos-platform]", "twisted[macos-platform]"] serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] @@ -3804,7 +3958,6 @@ windows-platform = ["pywin32 (!=226)", "pywin32 (!=226)", "twisted[all-non-platf name = "twisted-iocpsupport" version = "1.0.4" description = "An extension for use in the twisted I/O Completion Ports reactor." -category = "main" optional = false python-versions = "*" files = [ @@ -3833,7 +3986,6 @@ files = [ name = "txaio" version = "23.1.1" description = "Compatibility API between asyncio/Twisted/Trollius" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3848,45 +4000,41 @@ twisted = ["twisted (>=20.3.0)", "zope.interface (>=5.2.0)"] [[package]] name = "types-python-dateutil" -version = "2.8.19.14" +version = "2.9.0.20240316" description = "Typing stubs for python-dateutil" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, ] [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "tzdata" -version = "2023.3" +version = "2024.1" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] name = "unidecode" version = "1.1.2" description = "ASCII transliterations of Unicode text" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3898,7 +4046,6 @@ files = [ name = "uritemplate" version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3910,7 +4057,6 @@ files = [ name = "urllib3" version = "1.26.18" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -3925,20 +4071,18 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "uwsgi" -version = "2.0.22" +version = "2.0.24" description = "The uWSGI server" -category = "main" optional = false python-versions = "*" files = [ - {file = "uwsgi-2.0.22.tar.gz", hash = "sha256:4cc4727258671ac5fa17ab422155e9aaef8a2008ebb86e4404b66deaae965db2"}, + {file = "uwsgi-2.0.24.tar.gz", hash = "sha256:77b6dd5cd633f4ae87ee393f7701f617736815499407376e78f3d16467523afe"}, ] [[package]] name = "uwsgitop" version = "0.11" description = "uWSGI top-like interface" -category = "main" optional = false python-versions = "*" files = [ @@ -3947,33 +4091,30 @@ files = [ [[package]] name = "vine" -version = "5.0.0" -description = "Promises, promises, promises." -category = "main" +version = "5.1.0" +description = "Python promises." optional = false python-versions = ">=3.6" files = [ - {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, - {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, ] [[package]] name = "wcwidth" -version = "0.2.8" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" -category = "main" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.8-py2.py3-none-any.whl", hash = "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704"}, - {file = "wcwidth-0.2.8.tar.gz", hash = "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [[package]] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" files = [ @@ -3983,14 +4124,13 @@ files = [ [[package]] name = "websocket-client" -version = "1.6.4" +version = "1.7.0" description = "WebSocket client for Python with low level API options" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"}, - {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"}, + {file = "websocket-client-1.7.0.tar.gz", hash = "sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6"}, + {file = "websocket_client-1.7.0-py3-none-any.whl", hash = "sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588"}, ] [package.extras] @@ -3998,146 +4138,156 @@ docs = ["Sphinx (>=6.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] +[[package]] +name = "werkzeug" +version = "3.0.1" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, + {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + [[package]] name = "wrapt" -version = "1.15.0" +version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [[package]] name = "zope-interface" -version = "6.1" +version = "6.2" description = "Interfaces for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, - {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, - {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, - {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, - {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, - {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, - {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, - {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, - {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, - {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, + {file = "zope.interface-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab"}, + {file = "zope.interface-6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e"}, + {file = "zope.interface-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797"}, + {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f"}, + {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328"}, + {file = "zope.interface-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd"}, + {file = "zope.interface-6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037"}, + {file = "zope.interface-6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c"}, + {file = "zope.interface-6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85"}, + {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad"}, + {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac"}, + {file = "zope.interface-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe"}, + {file = "zope.interface-6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48"}, + {file = "zope.interface-6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef"}, + {file = "zope.interface-6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce"}, + {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000"}, + {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b"}, + {file = "zope.interface-6.2-cp312-cp312-win_amd64.whl", hash = "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe"}, + {file = "zope.interface-6.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"}, + {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5"}, + {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000"}, + {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b"}, + {file = "zope.interface-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550"}, + {file = "zope.interface-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532"}, + {file = "zope.interface-6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e"}, + {file = "zope.interface-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f"}, + {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd"}, + {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3"}, + {file = "zope.interface-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0"}, + {file = "zope.interface-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099"}, + {file = "zope.interface-6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d"}, + {file = "zope.interface-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70"}, + {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f"}, + {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a"}, + {file = "zope.interface-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1"}, + {file = "zope.interface-6.2.tar.gz", hash = "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565"}, ] [package.dependencies] setuptools = "*" [package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx_rtd_theme"] test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "520a4dd93287162675b6fb3d8706fc0034342db00718f0345e115313b3bd5bde" +content-hash = "9d7e6b8a5c0cb11bb91a77d503d3412fdde7a0857cdf88693cae7a4438055f8d" diff --git a/portal b/portal deleted file mode 160000 index 1fa9569263..0000000000 --- a/portal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1fa956926318b083d66d933c1b71e76f90a091e7 diff --git a/pyproject.toml b/pyproject.toml index 6ef6a610da..addd58476b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "designsafe" version = "6.0" description = "" authors = ["DesignSafe-CI "] +readme = "README.md" [tool.poetry.dependencies] python = "^3.11" @@ -11,7 +12,6 @@ pytas = {git = "https://bitbucket.org/taccaci/pytas.git", tag = "v1.7.0"} attrdict = { git = "https://github.com/DesignSafe-CI/AttrDict", rev = "83b779ee82d5b0e33be695d398162b8f2430ff33" } Django = "^4.2" daphne = "^4.0.0" -debugpy = "^1.8.0" channels = "^4.0.0" channels-redis = "^4.1.0" cryptography = "^41.0.4" @@ -40,6 +40,7 @@ ipdb = "^0.13.13" elasticsearch = "^7.5.1" elasticsearch-dsl = "^7.1.0" dropbox = "10.6.0" +django-recaptcha = "3.0.0" django-recaptcha2 = { git = "https://github.com/DesignSafe-CI/django-recaptcha2", tag = "v1.4.1--compat-django4" } djangocms-snippet = "3.0.0" django-bootstrap3 = "^23.4" @@ -57,17 +58,7 @@ jsonpickle = "^3.0.2" stone = "^3.2.1" pymemcache = "^4.0.0" django-ipware = "^1.1.6" -pytest = "^7.4.2" -pytest-django = "^4.5.2" -pytest-asyncio = "^0.14.0" pytz = "^2023.3" -black = "^23.10.0" -pylint = "^2.12.2" -pylint-django = "2.5.5" -mock = "^4.0.3" -requests-mock = "^1.9.3" -pytest-mock = "^3.6.1" -pytest-cov = "^2.10.0" ExifRead = "^2.3.2" opf-fido = "1.4.1" asgiref = "^3.7.2" @@ -75,7 +66,23 @@ django-select2 = "6.3.1" djangocms-admin-style = "~3.2.6" pydantic = "^2.5.0" networkx = "^3.2.1" +tapipy = "^1.6.1" +pycryptodome = "^3.20.0" +paramiko = "^3.4.0" + +[tool.poetry.group.dev.dependencies] +black = "^23.10.0" +debugpy = "^1.8.0" +mock = "^4.0.3" +pylint = "^2.12.2" +pylint-django = "2.5.5" +pytest = "^7.4.2" +pytest-asyncio = "^0.14.0" +pytest-django = "^4.5.2" +pytest-cov = "^2.10.0" +pytest-mock = "^3.6.1" +requests-mock = "^1.12.1" [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fea69f2701..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,168 +0,0 @@ -agavepy==1.0.0a12 -amqp==5.1.1; python_version >= "3.7" -appnope==0.1.3; sys_platform == "darwin" and python_version >= "3.4" -arrow==1.2.3; python_version >= "3.6" -asgiref==3.6.0; python_version >= "3.7" -astroid==2.14.2; python_full_version >= "3.7.2" -async-timeout==4.0.2; python_version >= "3.7" -atomicwrites==1.4.1; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5") or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5") -attrdict==2.0.1 -attrs==22.2.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -backcall==0.2.0; python_version >= "3.4" -billiard==3.6.4.0; python_version >= "3.7" -black==21.12b0; python_full_version >= "3.6.2" -boxsdk==2.14.0 -cached-property==1.5.2; python_version < "3.8" and python_version >= "3.7" -cachetools==5.3.0; python_version >= "3.7" and python_version < "4.0" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") -celery==5.2.7; python_version >= "3.7" -certifi==2022.12.7; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.4.0" and python_version < "4" and python_version >= "3.7" -cffi==1.15.1; python_version >= "2.7" and python_full_version < "3.0.0" and platform_python_implementation == "CPython" and sys_platform == "win32" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_version > "3.5") or platform_python_implementation == "CPython" and sys_platform == "win32" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_version > "3.5") and python_full_version >= "3.5.0" -charset-normalizer==3.0.1; python_version >= "3.7" and python_version < "4" -click-didyoumean==0.3.0; python_full_version >= "3.6.2" and python_full_version < "4.0.0" and python_version >= "3.7" -click-plugins==1.1.1; python_version >= "3.7" -click-repl==0.2.0; python_version >= "3.7" -click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" and python_full_version < "4.0.0" -cloudpickle==2.2.1; python_version >= "3.6" -colorama==0.4.6; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.7.2" and platform_system == "Windows" and (python_version >= "3.4" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.4" and python_full_version >= "3.7.0") and (python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5") or sys_platform == "win32" and python_version >= "3.7" and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.5") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5") and python_full_version >= "3.7.0") -coverage==7.1.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -cryptography==2.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -curlify==2.2.1 -decorator==5.1.1; python_version >= "3.5" -dill==0.3.6 -django-appconf==1.0.5; python_version >= "3.6" -django-bootstrap3==11.1.0 -django-classy-tags==3.0.1; python_version >= "3.7" -django-cms==3.8.0 -django-entangled==0.5.3 -django-filer==1.7.1 -django-formtools==2.2 -django-haystack==2.8.1 -django-impersonate==1.5.1 -django-ipware==1.2.0 -django-js-asset==2.0.0; python_version >= "3.6" -django-mptt==0.14.0; python_version >= "3.6" -django-polymorphic==2.1.2 -django-recaptcha2==1.4.1 -django-recaptcha==3.0.0 -django-sekizai==1.1.0 -django-select2==6.3.1 -django-termsandconditions==1.2.15 -django-treebeard==4.5.1; python_version >= "3.6" -django-websocket-redis==0.6.0 -django==2.2.28; python_version >= "3.5" -djangocms-admin-style==3.2.3; python_version >= "3.7" -djangocms-attributes-field==2.0.0 -djangocms-cascade==1.3.7 -djangocms-file==2.4.0 -djangocms-forms-maintained @ git+https://github.com/avryhof/djangocms-forms@d8a69efd2f447ee2f940c7b6f5b8b088c9cb79ed -djangocms-googlemap==1.4.0 -djangocms-picture==2.4.0 -djangocms-snippet==2.3.0 -djangocms-style==2.3.0 -djangocms-text-ckeditor==4.0.0 -djangocms-video==2.3.0 -dropbox==10.6.0 -easy-thumbnails==2.8.5; python_version >= "3.6" -elasticsearch-dsl==7.4.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") -elasticsearch==7.17.9; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0" and python_version < "4") -et-xmlfile==1.1.0; python_version >= "3.7" -exifread==2.3.2 -future==0.18.3; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" -gevent==22.10.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_version > "3.5" -google-api-core==2.11.0; python_version >= "3.7" -google-api-python-client==2.77.0; python_version >= "3.7" -google-auth-httplib2==0.1.0 -google-auth-oauthlib==0.4.6; python_version >= "3.6" -google-auth==2.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" -googleapis-common-protos==1.58.0; python_version >= "3.7" -greenlet==2.0.2; python_version >= "2.7" and python_full_version < "3.0.0" and platform_python_implementation == "CPython" or python_version > "3.5" and python_full_version < "3.0.0" and platform_python_implementation == "CPython" or python_version > "3.5" and platform_python_implementation == "CPython" and python_full_version >= "3.5.0" -hashids==1.3.1; python_version >= "2.7" -html5lib==1.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -httplib2==0.21.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -idna==3.4; python_version >= "3.7" and python_version < "4" -importlib-metadata==2.1.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -importlib-resources==5.10.2; python_version >= "3.7" -ipdb==0.12.3; python_version >= "2.7" -ipython==6.5.0; python_version >= "3.3" -isort==5.11.5; python_full_version >= "3.7.2" -jedi==0.18.2; python_version >= "3.6" -jinja2==3.1.2; python_version >= "3.7" -jsonfield==3.1.0; python_version >= "3.6" -jsonpickle==0.9.6 -kombu==5.2.4; python_version >= "3.7" -lazy-object-proxy==1.9.0; python_version >= "3.7" and python_full_version >= "3.7.2" -markupsafe==2.1.2; python_version >= "3.7" -mccabe==0.7.0; python_version >= "3.6" and python_full_version >= "3.7.2" -mock==4.0.3; python_version >= "3.6" -more-itertools==9.0.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -mypy-extensions==1.0.0; python_version >= "3.5" and python_full_version >= "3.6.2" -mysqlclient==1.4.6 -oauth2client==4.1.3 -oauthlib==3.2.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -olefile==0.46; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" -openpyxl==3.1.1; python_version >= "3.7" -opf-fido==1.6.1 -packaging==23.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -parso==0.8.3; python_version >= "3.6" -pathspec==0.11.0; python_version >= "3.7" and python_full_version >= "3.6.2" -petname==2.6 -pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.4" -pickleshare==0.7.5; python_version >= "3.4" -pillow==9.4.0; python_version >= "3.7" -platformdirs==3.0.0; python_version >= "3.7" and python_full_version >= "3.7.2" -pluggy==0.13.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -ply==3.11 -prompt-toolkit==1.0.18; python_version >= "3.7" -protobuf==4.21.12; python_version >= "3.7" -ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.3" -py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -pyasn1-modules==0.2.8; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" -pyasn1==0.4.8; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") or python_full_version >= "3.6.0" and python_version >= "3.7" and python_version < "4" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") -pycparser==2.21; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -pygments==2.14.0; python_version >= "3.6" -pyjwt==1.7.1 -pylint==2.16.2; python_full_version >= "3.7.2" -pymongo==3.13.0 -pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" -pytest-asyncio==0.14.0; python_version >= "3.5" -pytest-cov==2.12.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -pytest-django==3.10.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") -pytest-mock==3.10.0; python_version >= "3.7" -pytest==5.4.3; python_version >= "3.5" -python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -python-dotenv==0.21.1; python_version >= "3.7" -python-magic==0.4.27; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -python-memcached==1.59 -pytz==2022.7.1; python_version >= "3.7" -pyyaml==6.0; python_version >= "3.7" -redis==4.5.1; python_version >= "3.7" -requests-mock==1.10.0 -requests-oauthlib==1.3.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -requests-toolbelt==0.10.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" -requests==2.28.2; python_version >= "3.7" and python_version < "4" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") -rsa==4.9; python_version >= "3.6" and python_version < "4" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7") -rt==1.0.13 -simplegeneric==0.8.1; python_version >= "3.4" -six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7" -sqlparse==0.4.3; python_version >= "3.6" -stone==3.3.1 -tablib==3.3.0; python_version >= "3.7" -toml==0.10.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -tomli==1.2.3; python_version >= "3.6" and python_full_version >= "3.7.2" and python_version < "3.11" -tomlkit==0.11.6; python_version >= "3.6" and python_full_version >= "3.7.2" -traitlets==5.9.0; python_version >= "3.7" -typed-ast==1.5.4; python_version < "3.8" and implementation_name == "cpython" and python_full_version >= "3.7.2" and python_version >= "3.6" -typing-extensions==4.4.0 -unidecode==1.1.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" -uritemplate==4.1.1; python_version >= "3.7" -urllib3==1.26.14; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" -uwsgi==2.0.21 -uwsgitop==0.11 -vine==5.0.0; python_version >= "3.7" -wcwidth==0.2.6; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -webencodings==0.5.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -websocket-client==1.5.1; python_version >= "3.7" -wrapt==1.14.1 -zipp==3.13.0; python_version < "3.8" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_version < "3.8" and python_version >= "3.7" and python_full_version >= "3.5.0") and python_full_version >= "3.6.2" -zope.event==4.6; python_version >= "2.7" and python_full_version < "3.0.0" or python_version > "3.5" -zope.interface==5.5.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_version > "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version > "3.5"