diff --git a/client/package.json b/client/package.json index e406e1b..0310b35 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "dive-dsa", - "version": "1.10.18", + "version": "1.10.19", "author": { "name": "Kitware, Inc.", "email": "Bryon.Lewis@kitware.com" @@ -29,8 +29,6 @@ "@flatten-js/interval-tree": "^1.0.11", "@girder/components": "^3.2.0", "@mdi/font": "^6.2.95", - "@sentry/browser": "^5.24.2", - "@sentry/integrations": "^5.24.2", "axios": "^1.6.7", "color-hash": "^1.0.3", "core-js": "^3.22.2", @@ -50,7 +48,6 @@ "vuex": "^3.0.1" }, "devDependencies": { - "@sentry/webpack-plugin": "^1.18.3", "@types/axios": "^0.14.0", "@types/body-parser": "^1.19.0", "@types/color-hash": "^1.0.0", diff --git a/client/platform/web-girder/api/divemetadata.service.ts b/client/platform/web-girder/api/divemetadata.service.ts index eb91087..bb056a3 100644 --- a/client/platform/web-girder/api/divemetadata.service.ts +++ b/client/platform/web-girder/api/divemetadata.service.ts @@ -1,4 +1,5 @@ import girderRest from 'platform/web-girder/plugins/girder'; +import { GirderModelBase } from 'vue-girder-slicer-cli-ui/dist/girderTypes'; import { StringKeyObject } from 'vue-media-annotator/BaseAnnotation'; export interface MetadataFilterItem { @@ -35,6 +36,7 @@ export interface DIVEMetadataFilterValueResults { created: string; root: string; metadataKeys: Record; + unlocked: string[]; } export interface DIVEMetadataResults { @@ -73,8 +75,102 @@ function createDiveMetadataClone(folder: string, filters: DIVEMetadataFilter, de }); } +export interface createDiveMetadataResponse { + 'results': string, + 'errors': string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 'metadataKeys': any[]; + 'folderId': string; + +} + +function createDiveMetadataFolder( + parentFolder: string, + name: string, + rootFolderId: string, + categoricalLimit = 50, + displayConfig = { + display: ['DIVE_DatasetId', 'DIVE_Name'], + }, + ffprobeMetadata = { + import: true, keys: ['width', 'height', 'display_aspect_ratio'], + }, +) { + return girderRest.post(`dive_metadata/create_metadata_folder/${parentFolder}`, null, { + params: { + name, rootFolderId, categoricalLimit, displayConfig, ffprobeMetadata, + }, + }); +} + +function modifyDiveMetadataPermission(rootMetadataFolder: string, key: string, unlocked: boolean) { + return girderRest.patch(`dive_metadata/${rootMetadataFolder}/modify_key_permission`, null, { + params: { + key, unlocked, + }, + }); +} + +function addDiveMetadataKey(rootMetadataFolder: string, key: string, category: 'numerical' | 'categorical' | 'search' | 'boolean', unlocked = false, valueList: string[] = [], defaultValue?: number | string | boolean) { + const values = valueList.length ? valueList : undefined; + return girderRest.put(`dive_metadata/${rootMetadataFolder}/add_key`, null, { + params: { + key, category, unlocked, values, default_value: defaultValue, + }, + }); +} + +function deleteDiveMetadataKey(rootMetadataFolder:string, key: string) { + return girderRest.delete(`dive_metadata/${rootMetadataFolder}/delete_key`, { + params: { + key, + }, + }); +} + +function deleteDiveDatasetMetadataKey(diveDatasetId: string, key: string) { + return girderRest.delete(`dive_metadata/${diveDatasetId}`, { params: { key } }); +} +function setDiveDatasetMetadataKey(diveDatasetId: string, key: string, updateValue?: number | string | boolean) { + const value = updateValue === undefined ? null : updateValue; + return girderRest.patch(`dive_metadata/${diveDatasetId}`, null, { + params: { + key, value, + }, + }); +} + +async function updateDiveMetadataDisplay(folderId: string, key: string, state: 'display' | 'hidden' | 'none') { + const resp = await girderRest.get(`folder/${folderId}`); + const DIVEMetadataFilter = resp.data.meta.DIVEMetadataFilter as {display: string[], hide: string[], categoricalLimit: number}; + if (DIVEMetadataFilter) { + const { display } = DIVEMetadataFilter; + const { hide } = DIVEMetadataFilter; + if (display.includes(key)) { + display.splice(display.findIndex((item) => item === key), 1); + } + if (hide.includes(key)) { + hide.splice(hide.findIndex((item) => item === key), 1); + } + if (state === 'display') { + display.push(key); + } + if (state === 'hidden') { + hide.push(key); + } + await girderRest.put(`folder/${folderId}/metadata`, { DIVEMetadataFilter }); + } +} + export { getMetadataFilterValues, filterDiveMetadata, createDiveMetadataClone, + createDiveMetadataFolder, + modifyDiveMetadataPermission, + addDiveMetadataKey, + deleteDiveMetadataKey, + deleteDiveDatasetMetadataKey, + setDiveDatasetMetadataKey, + updateDiveMetadataDisplay, }; diff --git a/client/platform/web-girder/api/girder.service.ts b/client/platform/web-girder/api/girder.service.ts index cbd6418..9c3720b 100644 --- a/client/platform/web-girder/api/girder.service.ts +++ b/client/platform/web-girder/api/girder.service.ts @@ -27,6 +27,22 @@ function getFolder(folderId: string) { return girderRest.get(`folder/${folderId}`); } +export interface AccessType { + flags: string[]; + id: string; + level: number; + login: string; + name: string; +} +export interface FolderAccessType { + groups: AccessType[]; + users: AccessType[]; +} + +function getFolderAccess(folderId: string) { + return girderRest.get(`folder/${folderId}/access`); +} + function setUsePrivateQueue(userId: string, value = false) { return girderRest.put<{ user_private_queue_enabled: boolean; @@ -42,4 +58,5 @@ export { getItemsInFolder, getFolder, setUsePrivateQueue, + getFolderAccess, }; diff --git a/client/platform/web-girder/main.ts b/client/platform/web-girder/main.ts index 51be4ab..669b284 100644 --- a/client/platform/web-girder/main.ts +++ b/client/platform/web-girder/main.ts @@ -1,7 +1,5 @@ import Vue from 'vue'; import VueGtag from 'vue-gtag'; -import { init as SentryInit } from '@sentry/browser'; -import { Vue as SentryVue } from '@sentry/integrations'; import registerNotifications from 'vue-media-annotator/notificatonBus'; import promptService from 'dive-common/vue-utilities/prompt-service'; @@ -22,15 +20,6 @@ if ( process.env.NODE_ENV === 'production' && window.location.hostname !== 'localhost' ) { - SentryInit({ - dsn: process.env.VUE_APP_SENTRY_DSN, - integrations: [ - new SentryVue({ Vue, logErrors: true }), - ], - release: process.env.VUE_APP_GIT_HASH, - environment: (window.location.hostname === 'viame.kitware.com') - ? 'production' : 'development', - }); Vue.use(VueGtag, { config: { id: process.env.VUE_APP_GTAG }, }, router); diff --git a/client/platform/web-girder/router.ts b/client/platform/web-girder/router.ts index 786c372..78dda48 100644 --- a/client/platform/web-girder/router.ts +++ b/client/platform/web-girder/router.ts @@ -12,6 +12,7 @@ import DataShared from './views/DataShared.vue'; import DataBrowser from './views/DataBrowser.vue'; import Summary from './views/Summary.vue'; import DIVEMetadataSearchVue from './views/DIVEMetadataSearch.vue'; +import DiveMetadataEditVue from './views/DIVEMetadataEdit.vue'; Vue.use(Router); @@ -88,6 +89,22 @@ const router = new Router({ }, beforeEnter, }, + { + path: '/metadata-edit/:id/', + name: 'metadata-edit', + component: DiveMetadataEditVue, + props: (route) => { + if (route.query.filter) { + return { + id: route.params.id, // Map route parameter to prop + }; + } + return { + id: route.params.id, + }; + }, + beforeEnter, + }, { path: '', component: Home, diff --git a/client/platform/web-girder/views/CreateDIVEMetadata.vue b/client/platform/web-girder/views/CreateDIVEMetadata.vue new file mode 100644 index 0000000..f3b5067 --- /dev/null +++ b/client/platform/web-girder/views/CreateDIVEMetadata.vue @@ -0,0 +1,204 @@ + + + diff --git a/client/platform/web-girder/views/DIVEMetadataEdit.vue b/client/platform/web-girder/views/DIVEMetadataEdit.vue new file mode 100644 index 0000000..042f5f7 --- /dev/null +++ b/client/platform/web-girder/views/DIVEMetadataEdit.vue @@ -0,0 +1,334 @@ + + + + + diff --git a/client/platform/web-girder/views/DIVEMetadataEditKey.vue b/client/platform/web-girder/views/DIVEMetadataEditKey.vue new file mode 100644 index 0000000..9833079 --- /dev/null +++ b/client/platform/web-girder/views/DIVEMetadataEditKey.vue @@ -0,0 +1,85 @@ + + + diff --git a/client/platform/web-girder/views/DIVEMetadataFilter.vue b/client/platform/web-girder/views/DIVEMetadataFilter.vue index 169f7ae..f7cc52a 100644 --- a/client/platform/web-girder/views/DIVEMetadataFilter.vue +++ b/client/platform/web-girder/views/DIVEMetadataFilter.vue @@ -43,6 +43,10 @@ export default defineComponent({ default: () => {}, }, + ownerAdmin: { + type: Boolean, + default: false, + }, }, setup(props, { emit }) { const { prompt } = usePrompt(); @@ -94,6 +98,7 @@ export default defineComponent({ const getFilters = async () => { const filterData = await getMetadataFilterValues(props.id); filters.value = filterData.data.metadataKeys; + emit('filter-data', filterData.data); }; onBeforeMount(async () => { await getFilters(); @@ -236,6 +241,24 @@ export default defineComponent({ Advanced Filters + + + mdi-pencil + + Edit Filters + + Filtered:{{ filtered }} / {{ count }} = ref([]); + const unlockedMap: Ref> = ref({}); const displayConfig: Ref = ref({ display: [], hide: [], categoricalLimit: 50 }); const totalPages = ref(0); const currentPage = ref(0); const count = ref(0); const filtered = ref(0); + const girderRest = useGirderRest(); const filters: Ref = ref(props.filter || {}); const locationStore = { _id: props.id, @@ -39,7 +48,8 @@ export default defineComponent({ }; const currentFilter: Ref = ref(props.filter || {}); - + const isOwnerAdmin = ref(false); + const prompt = usePrompt(); const processFilteredMetadataResults = (data: DIVEMetadataResults) => { folderList.value = data.pageResults; totalPages.value = data.totalPages; @@ -53,9 +63,25 @@ export default defineComponent({ const getFolderInfo = async (id: string) => { const folder = (await getFolder(id)).data; + try { + const access = (await getFolderAccess(id)).data; + const accessMap: Record = {}; + access.users.forEach((item) => { + accessMap[item.id] = item; + }); + if (accessMap[girderRest.user._id] && accessMap[girderRest.user._id].level === 2) { + isOwnerAdmin.value = true; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + console.warn('Cannot Access Folder assuming not an owner'); + } if (folder.meta.DIVEMetadata) { displayConfig.value = folder.meta.DIVEMetadataFilter; } + if (folder.creatorId === girderRest.user._id || girderRest.user.admin) { + isOwnerAdmin.value = true; + } }; const updateURLParams = () => { @@ -74,7 +100,7 @@ export default defineComponent({ const storedSortVal = ref('filename'); const storedSortDir = ref(1); - const updateFilter = async ({ filter, sortVal, sortDir } : { filter?:DIVEMetadataFilter, sortVal?: string, sortDir?: number}) => { + const updateFilter = async ({ filter, sortVal, sortDir }: { filter?: DIVEMetadataFilter, sortVal?: string, sortDir?: number }) => { if (filter) { filters.value = filter; currentPage.value = 0; @@ -111,6 +137,32 @@ export default defineComponent({ return advancedList; }; const openClone = ref(false); + + const setFilterData = (data: DIVEMetadataFilterValueResults) => { + //get unlock fields and their data types: + const { unlocked } = data; + unlockedMap.value = {}; + if (!unlocked) { + return; + } + unlocked.forEach((item) => { + if (data.metadataKeys[item]) { + unlockedMap.value[item] = data.metadataKeys[item]; + } + }); + }; + + const updateDiveMetadataKeyVal = async (id: string, key: string, val: boolean | number | string) => { + try { + await setDiveDatasetMetadataKey(id, key, val); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + prompt.prompt({ + title: 'Error Setting Data', + text: err.response.data.message, + }); + } + }; return { totalPages, count, @@ -123,9 +175,13 @@ export default defineComponent({ displayConfig, getAdvanced, filters, + isOwnerAdmin, //Cloning openClone, currentFilter, + setFilterData, + unlockedMap, + updateDiveMetadataKeyVal, }; }, }); @@ -141,36 +197,25 @@ export default defineComponent({ :count="count" :filtered="filtered" :display-config="displayConfig" + :owner-admin="isOwnerAdmin" @update:currentPage="changePage($event)" @updateFilters="updateFilter($event)" + @filter-data="setFilterData($event)" > - +
{{ item.filename }}
@@ -199,7 +240,15 @@ export default defineComponent({ {{ display }}: -
+
+ +
+
{{ item.metadata[display] }}
@@ -212,7 +261,15 @@ export default defineComponent({ {{ dataKey }}: -
+
+ +
+
{{ data }}
@@ -228,9 +285,10 @@ export default defineComponent({ diff --git a/client/platform/web-girder/views/Home.vue b/client/platform/web-girder/views/Home.vue index 83482e0..dd3b3fc 100644 --- a/client/platform/web-girder/views/Home.vue +++ b/client/platform/web-girder/views/Home.vue @@ -19,6 +19,7 @@ import ShareTab from './ShareTab.vue'; import DataShared from './DataShared.vue'; import { useStore } from '../store/types'; import eventBus from '../eventBus'; +import CreateDIVEMetadata from './CreateDIVEMetadata.vue'; const buttonOptions = { block: true, @@ -45,6 +46,7 @@ export default defineComponent({ Upload, DataShared, ShareTab, + CreateDIVEMetadata, }, // everything below needs to be refactored to composition-api inject: ['girderRest'], @@ -155,6 +157,11 @@ export default defineComponent({ v-bind="{ buttonOptions, menuOptions }" :dataset-ids="locationInputs" /> + +

-This is the documentation site for DIVE-DSA (DIVE - Digital Slide Archive), which is a fork based on [**DIVE**](https://github.com/Kitware/dive) a [**free and open-source**](https://www.kitware.com/open-philosophy/) annotation and analysis platform for web and desktop built by [Kitware](https://kitware.com). This fork eliminates DIVE's integration with VIAME and provides some enhanced annotation features when compared to base DIVE. It is also meant to be integrated in with [Digial Slide Archive](https://digitalslidearchive.github.io/digital_slide_archive/). +This is the documentation site for DIVE-DSA (DIVE - Digital Slide Archive), which is a fork based on [**DIVE**](https://github.com/Kitware/dive) a [**free and open-source**](https://www.kitware.com/open-philosophy/) annotation and analysis platform for web and desktop built by [Kitware](https://kitware.com). This fork eliminates DIVE's integration with VIAME and provides some enhanced annotation features when compared to base DIVE. It is also meant to be integrated in with [Digital Slide Archive](https://digitalslidearchive.github.io/digital_slide_archive/). ![Home](images/Banner.png) diff --git a/server/dive_server/crud_dataset.py b/server/dive_server/crud_dataset.py index ecb071c..ccf77a6 100644 --- a/server/dive_server/crud_dataset.py +++ b/server/dive_server/crud_dataset.py @@ -180,7 +180,10 @@ def get_task_defaults( def get_recursive_datasets( - dsFolder: types.GirderModel, user: types.GirderUserModel, datasetList: List[types.GirderModel], limit: int = -1 + dsFolder: types.GirderModel, + user: types.GirderUserModel, + datasetList: List[types.GirderModel], + limit: int = -1, ): subFolders = list(Folder().childFolders(dsFolder, 'folder', user)) for child in subFolders: diff --git a/server/dive_server/views_dataset.py b/server/dive_server/views_dataset.py index 032b496..73eaba3 100644 --- a/server/dive_server/views_dataset.py +++ b/server/dive_server/views_dataset.py @@ -446,7 +446,9 @@ def get_configuration(self, folder): str(folderParentId), level=AccessType.READ, user=user, force=True ) childFolders = list( - Folder().childFolders(folderParent, folderParentType, sort=[['lowerName', 1]], user=user) + Folder().childFolders( + folderParent, folderParentType, sort=[['lowerName', 1]], user=user + ) ) for index, item in enumerate(childFolders): if item.get('_id') == folder.get('_id'): @@ -463,7 +465,6 @@ def get_configuration(self, folder): if index + 1 < len(childFolders): counter = 1 while index + counter < len(childFolders): - print(childFolders[index + counter].get('meta', {})) if childFolders[index + counter].get('meta', {}).get('annotate', False): next = childFolders[index + counter] break @@ -487,7 +488,6 @@ def get_configuration(self, folder): 'hierarchy': hierarchy, 'metadata': combinedConfiguration, } - print(returnVal) return json.dumps(returnVal, indent=4) @access.user @@ -512,7 +512,8 @@ def get_task_defaults(self, folder): @access.public(scope=TokenScope.DATA_READ, cookie=True) @autoDescribeRoute( - Description("Get a Recursive list of all DIVE Datasets within a parent folder").modelParam( + Description("Get a Recursive list of all DIVE Datasets within a parent folder") + .modelParam( "id", level=AccessType.READ, **DatasetModelParam, @@ -524,7 +525,6 @@ def get_task_defaults(self, folder): default=-1, required=False, ) - ) def get_recursive(self, folder, limit): datasetList = [] diff --git a/server/dive_server/views_metadata.py b/server/dive_server/views_metadata.py index 76ab390..270e3b7 100644 --- a/server/dive_server/views_metadata.py +++ b/server/dive_server/views_metadata.py @@ -114,10 +114,23 @@ def __init__(self, resourceName): ), self.filter_folder, ) + self.route("POST", ("create_metadata_folder", ":id"), self.create_metadata_folder) self.route("POST", (':id', "clone_filter"), self.clone_filter) self.route("GET", (':id', 'metadata_keys'), self.get_metadata_keys) self.route("GET", (':id', 'metadata_filter_values'), self.get_metadata_filter) - self.route("DELETE", (':rootId',), self.delete_metadata) + self.route( + "DELETE", + ( + ':rootId', + 'root_metadata', + ), + self.delete_metadata, + ) + self.route("DELETE", (':rootId', 'delete_key'), self.delete_metadata_key) + self.route("PUT", (':root', 'add_key'), self.add_metadata_key) + self.route("PATCH", (':root', 'modify_key_permission'), self.modify_key_permission) + self.route("PATCH", (':divedataset',), self.set_key_value) + self.route("DELETE", (':divedataset',), self.delete_key_value) @access.user @autoDescribeRoute( @@ -187,7 +200,15 @@ def __init__(self, resourceName): ) ) def process_metadata( - self, folder, sibling_path, fileType, matcher, path_key, displayConfig, ffprobeMetadata, categoricalLimit + self, + folder, + sibling_path, + fileType, + matcher, + path_key, + displayConfig, + ffprobeMetadata, + categoricalLimit, ): # Process the current folder for the specified fileType using the matcher to generate DIVE_Metadata # make sure the folder is set to a DIVE Metadata folder using DIVE_METADATA = True @@ -259,12 +280,22 @@ def process_metadata( if modified_path: if modified_path == resource_path: item['pathMatches'] = True - if ffprobeMetadata.get('import', False): # Add in ffprobe metadata to the system - ffmetadata = datasetFolder.get('meta', {}).get('ffprobe_info', {}) + # add in DIVE Keys: + item['DIVE_DatasetId'] = str(datasetFolder['_id']) + item['DIVE_Name'] = datasetFolder['lowerName'] + datasetFolder.get('name') + if ffprobeMetadata.get( + 'import', False + ): # Add in ffprobe metadata to the system + ffmetadata = datasetFolder.get('meta', {}).get( + 'ffprobe_info', {} + ) ffkeys = ffprobeMetadata.get('keys', []) for ffMetadataKey in ffkeys: if ffmetadata.get(ffMetadataKey, False): - item[f'ffprobe_{ffMetadataKey}'] = ffmetadata.get(ffMetadataKey, False) + item[f'ffprobe_{ffMetadataKey}'] = ffmetadata.get( + ffMetadataKey, False + ) DIVE_Metadata().createMetadata( datasetFolder, folder, user, item ) @@ -336,6 +367,144 @@ def process_metadata( "metadataKeys": metadataKeys, } + @access.user + @autoDescribeRoute( + Description("Processing a folder and any children folder that have a specified format") + .modelParam( + "id", + description="Parent Folder of where to add the new metadata folder", + model=Folder, + level=AccessType.WRITE, + ) + .param( + "name", + description="Metadata Folder Name", + paramType="formData", + dataType="string", + default='New Metadata Folder Name', + required=False, + ) + .param( + "rootFolderId", + "Root folder to search for all files underneath and crate metadata entries for all of them", + paramType="formData", + dataType="string", + required=True, + ) + .jsonParam( + "displayConfig", + "List of Main Display Keys for the metadata and keys to hide from the filter", + required=True, + default={ + "display": ['DIVE_DatasetId', 'DIVE_Name'], + "hide": [""], + }, + ) + .jsonParam( + "ffprobeMetadata", + "List Metadata keys to extract from the ffprobe metadata from videos. Setting 'import' to 'true' will import the data", + required=True, + default={ + "import": True, + "keys": ["width", "height", "display_aspect_ratio"], + }, + ) + .param( + "categoricalLimit", + "Above this number make a field a search field instead of a dropdown", + paramType="formData", + dataType="integer", + default=50, + ) + ) + def create_metadata_folder( + self, folder, name, rootFolderId, displayConfig, ffprobeMetadata, categoricalLimit + ): + # Process the current folder for the specified fileType using the matcher to generate DIVE_Metadata + # make sure the folder is set to a DIVE Metadata folder using DIVE_METADATA = True + user = self.getCurrentUser() + + base_folder = Folder().createFolder(folder, name) + data = None + errorLog = [] + added = 0 + metadataKeys = {} + datasetList = [] + rootFolder = Folder().load( + rootFolderId, + level=AccessType.WRITE, + user=user, + force=True, + ) + crud_dataset.get_recursive_datasets(rootFolder, user, datasetList) + + for item in datasetList: + data = {} + data['DIVE_DatasetId'] = str(item['_id']) + data['DIVE_Name'] = str(item['lowerName']) + if ffprobeMetadata.get('import', False): # Add in ffprobe metadata to the system + ffmetadata = item.get('meta', {}).get('ffprobe_info', {}) + ffkeys = ffprobeMetadata.get('keys', []) + for ffMetadataKey in ffkeys: + if ffmetadata.get(ffMetadataKey, False): + data[f'ffprobe_{ffMetadataKey}'] = ffmetadata.get(ffMetadataKey, False) + DIVE_Metadata().createMetadata(item, base_folder, user, data) + for key in data.keys(): + if key not in metadataKeys.keys() and data[key] is not None: + datatype = python_to_javascript_type(type(data[key])) + metadataKeys[key] = {"type": datatype, "set": set(), "count": 0} + if data[key] is None: + continue # we skip null values for processing + if metadataKeys[key]['type'] == 'string': + metadataKeys[key]['set'].add(data[key]) + metadataKeys[key]['count'] += 1 + if metadataKeys[key]['type'] == 'array': + for arrayitem in data[key]: + if python_to_javascript_type(type(arrayitem)) == 'string': + metadataKeys[key]['set'].add(arrayitem) + metadataKeys[key]['count'] += 1 + if metadataKeys[key]['type'] == 'number': + if 'range' not in metadataKeys[key].keys(): + metadataKeys[key]['range'] = {"min": data[key], "max": data[key]} + metadataKeys[key]['range'] = { + "min": min(data[key], metadataKeys[key]["range"]["min"]), + "max": max(data[key], metadataKeys[key]["range"]["max"]), + } + + # now we need to determine what is categorical vs what is a search field + for key in metadataKeys.keys(): + item = metadataKeys[key] + metadataKeys[key]["unique"] = len(item["set"]) + if item["type"] in ['string', 'array'] and ( + item["unique"] < categoricalLimit + or (item["count"] <= len(item["set"]) and len(item["set"]) < categoricalLimit) + ): + metadataKeys[key]["category"] = "categorical" + metadataKeys[key]['set'] = list(metadataKeys[key]['set']) + elif item["type"] == 'string': + metadataKeys[key]["category"] = "search" + del metadataKeys[key]['set'] + elif item["type"] == 'number': + metadataKeys[key]["category"] = "numerical" + del metadataKeys[key]['set'] + else: + del metadataKeys[key]['set'] + DIVE_MetadataKeys().createMetadataKeys(base_folder, user, metadataKeys) + # add metadata to root folder for + base_folder['meta'][DIVEMetadataMarker] = True + displayConfig['categoricalLimit'] = categoricalLimit + if displayConfig.get('hide', False) is False: + displayConfig['hide'] = [""] + base_folder['meta'][DIVEMetadataFilter] = displayConfig + Folder().save(base_folder) + + return { + "results": f"added {added} folders", + "errors": errorLog, + "metadataKeys": metadataKeys, + "folderId": str(base_folder['_id']) + } + @access.user @autoDescribeRoute( Description( @@ -357,6 +526,20 @@ def get_metadata_keys( query=query, user=user, ) + keys = metadata_key['metadataKeys'] + for key in keys: + item = keys[key] + if item.get('category', False): + if item['category'] == 'numerical': + if ( + item['range'] + and item['range']['min'] == float('inf') + or item['range']['max'] == float('-inf') + ): + item['range'] = {'min': 0, 'max': 0} + if metadata_key.get('unlocked', False) is False: + metadata_key['unlocked'] = [] + DIVE_MetadataKeys().initialize_updated_data(folder, None) return metadata_key @access.user @@ -510,6 +693,9 @@ def get_filter_query(self, folder, user, filters): def get_metadata_filter(self, folder, keys=None): # Create initial Meva State based off of information query = {'root': str(folder['_id'])} + found = DIVE_MetadataKeys().findOne(query=query) + unlocked = found['unlocked'] + print(f'UNLOCKED: {unlocked}') metadata_items = list( DIVE_Metadata().find( query, @@ -527,11 +713,11 @@ def get_metadata_filter(self, folder, keys=None): for key in item['metadata'].keys(): if keys is None and key not in results.keys(): results[key] = set() - if item['metadata'].get(key, None) is not None and not isinstance( + if (item['metadata'].get(key, None) is not None or key in unlocked) and not isinstance( item['metadata'][key], list ): results[key].add(item['metadata'][key]) - elif item['metadata'].get(key, None) is not None and isinstance( + elif (item['metadata'].get(key, None) is not None or key in unlocked) and isinstance( item['metadata'][key], list ): for array_item in item['metadata'][key]: @@ -544,24 +730,212 @@ def get_metadata_filter(self, folder, keys=None): @access.user @autoDescribeRoute( - Description("Delete Folder VideoState").modelParam( - "rootId", + Description("Delete Folder Metadata").modelParam( + "root", description="FolderId to get state from", model=Folder, level=AccessType.READ, - destName="rootId", + destName="root", ) ) - def delete_metadata(self, rootId): + def delete_metadata(self, root): user = self.getCurrentUser() - query = {"root": str(rootId["_id"])} + query = {"root": str(root["_id"])} found = DIVE_Metadata().findOne(query=query, user=user) if found: DIVE_Metadata().removeWithQuery(query) DIVE_MetadataKeys().removeWithQuery(query) - rootId = Folder().setMetadata( - rootId, {DIVEMetadataMarker: None, DIVEMetadataFilter: None} - ) - Folder().save(rootId) + root = Folder().setMetadata(root, {DIVEMetadataMarker: None, DIVEMetadataFilter: None}) + Folder().save(root) else: raise RestException('Could not find a state to delete') + + @access.user + @autoDescribeRoute( + Description("Delete Metadata Key from Metadata Folder") + .modelParam( + "rootId", + description="Root metadata FolderId", + model=Folder, + level=AccessType.READ, + destName="rootId", + ) + .param( + "key", + "Metadata key to remove", + required=True, + ) + ) + def delete_metadata_key(self, rootId, key): + user = self.getCurrentUser() + query = {"root": str(rootId["_id"]), "owner": str(user['_id'])} + found = DIVE_MetadataKeys().findOne(query=query, user=user) + print(found) + if found: + DIVE_MetadataKeys().deleteKey(rootId, user, key) + Folder().save(rootId) + else: + raise RestException( + f'Could not find Metadata for FolderId: {rootId["_id"]} to delete key.' + ) + + @autoDescribeRoute( + Description("Add Metadata Key to Metdata Folder") + .modelParam( + "root", + description="Root metadata FolderId", + model=Folder, + level=AccessType.WRITE, + destName="root", + ) + .param( + "key", + "Metadata key to add", + required=False, + ) + .param( + "category", + "type of metadata to add", + enum=['numerical', 'categorical', 'search', 'boolean'], + required=True, + default='numerical', + ) + .param( + "unlocked", + "If this value for each metadata item should be modified by regular users", + dataType='boolean', + required=True, + default=False, + ) + .jsonParam( + "values", + "List of values, either numbers for numerical category or string for categorical, for search this field isn't required. I.E ['key1', 'key2'] or [0, 20]", + required=False, + default=[], + ) + .param( + "default_value", + "If this value for each metadata item should be modified by regular users", + required=False, + default=None, + ) + + ) + def add_metadata_key(self, root, key, category, unlocked, values=[], default_value=None): # noqa: B006 + user = self.getCurrentUser() + query = {"root": str(root["_id"]), "owner": str(user['_id'])} + found = DIVE_MetadataKeys().findOne(query=query) + if found: + info = {"count": 0, "category": category} + if category == 'categorical' and values and len(values) > 0: + info['set'] = list(set(values)) + if category == 'numerical': + info['range'] = {'min': float('inf'), 'max': float('-inf')} + if info.get('set', None) is None: + info['set'] = [] + DIVE_MetadataKeys().addKey(root, user, key, info, unlocked) + Folder().save(root) + else: + raise RestException(f'Could not find for FolderId: {root["_id"]} to delete key.') + query = {"root": str(root["_id"])} + existing_data = DIVE_Metadata().find(query) + for item in existing_data: + diveDatasetFolder = Folder().load(item['DIVEDataset'], level=AccessType.WRITE, user=user, force=True) + DIVE_Metadata().updateKey(diveDatasetFolder, root, user, key, default_value) + + @autoDescribeRoute( + Description("Add Metadata Key to Metdata Folder") + .modelParam( + "root", + description="Root metadata FolderId", + model=Folder, + level=AccessType.READ, + destName="root", + ) + .param( + "key", + "Metadata key to add", + required=False, + ) + .param( + "unlocked", + "If this value for each metadata item should be modified by regular users", + dataType='boolean', + required=True, + default=False, + ) + ) + def modify_key_permission(self, root, key, unlocked): + user = self.getCurrentUser() + query = {"root": str(root["_id"])} + found = DIVE_MetadataKeys().findOne(query=query, owner=str(user['_id'])) + if found: + if found.get('owner', False) is False: + DIVE_MetadataKeys().initialize_updated_data(root, user) + DIVE_MetadataKeys().modifyKeyPermission(root, user, key, unlocked) + Folder().save(root) + else: + raise RestException( + f'Could not find Metadata for FolderId: {root["_id"]} to delete key.' + ) + + @access.user + @autoDescribeRoute( + Description("Set MetadataKey value for a folder") + .modelParam( + "divedataset", + description="The folder to set the key on", + model=Folder, + level=AccessType.WRITE, + destName="divedataset", + ) + .param( + "key", + "Metadata key to add", + required=False, + ) + .param( + "value", + "Value to set the key to, empty is a None value", + required=True, + default=None, + ) + ) + def set_key_value(self, divedataset, key, value): + user = self.getCurrentUser() + query = {"DIVEDataset": str(divedataset["_id"])} + found = DIVE_Metadata().findOne(query=query, user=user, level=AccessType.WRITE) + if found: + rootId = found['root'] + rootFolder = Folder().load(rootId, user=user, level=AccessType.WRITE) + categoricalLimit = ( + rootFolder['meta'].get(DIVEMetadataFilter, {}).get('categoricalLimit', 50) + ) + DIVE_Metadata().updateKey(divedataset, rootId, user, key, value, categoricalLimit) + else: + raise RestException(f'Could not find for FolderId: {divedataset["_id"]} to modify key-value: {key} - {value}.') + + @autoDescribeRoute( + Description("Delete a key from a specific DIVE Dataset") + .modelParam( + "divedataset", + description="The folder to delete the key from", + model=Folder, + level=AccessType.WRITE, + destName="divedataset", + ) + .param( + "key", + "Metadata key to delete", + required=False, + ) + ) + def delete_key_value(self, divedataset, key): + user = self.getCurrentUser() + query = {"DIVEDataset": str(divedataset["_id"])} + found = DIVE_Metadata().findOne(query=query, user=user) + if found: + rootId = found['root'] + DIVE_Metadata().deleteKey(divedataset, rootId, user, key) + else: + raise RestException(f'Could not find for FolderId: {divedataset["_id"]} to delete key.') diff --git a/server/dive_tasks/tasks.py b/server/dive_tasks/tasks.py index 277929b..d4d6506 100644 --- a/server/dive_tasks/tasks.py +++ b/server/dive_tasks/tasks.py @@ -141,7 +141,9 @@ def convert_video( videostream = list(filter(lambda x: x["codec_type"] == "video", jsoninfo["streams"])) multiple_video_streams = None if len(videostream) != 1: - multiple_video_streams = "More than One video stream found, defaulting to the first stream" + multiple_video_streams = ( + "More than One video stream found, defaulting to the first stream" + ) # Extract average framerate avgFpsString: str = videostream[0]["avg_frame_rate"] @@ -189,7 +191,6 @@ def convert_video( constants.FPSMarker: newAnnotationFps, constants.MarkForPostProcess: False, "ffprobe_info": ffprobe_info, - }, ) return diff --git a/server/dive_utils/metadata/models.py b/server/dive_utils/metadata/models.py index 83a1d19..30853b4 100644 --- a/server/dive_utils/metadata/models.py +++ b/server/dive_utils/metadata/models.py @@ -39,7 +39,7 @@ def initialize(self): ] ) - def createMetadata(self, folder, root, owner, metadata, created_date=None): + def createMetadata(self, folder, root, owner, metadata, created_date=None): # noqa: B006 existing = self.findOne({'DIVEDataset': str(folder['_id'])}) if not existing: if created_date is None: @@ -53,11 +53,13 @@ def createMetadata(self, folder, root, owner, metadata, created_date=None): root=str(root['_id']), metadata=metadata, created=created, + owner=str(owner['_id']), ) else: existing['metadata'] = metadata existing['filename'] = str(folder['name']) existing['root'] = str(root['_id']) + existing['owner'] = str(owner['_id']) existing = self.save(existing) return existing @@ -65,9 +67,44 @@ def validate(self, doc): if not doc.get('DIVEDataset') or not isinstance(doc['DIVEDataset'], str): raise ValidationException('DIVEDataset must be a string') if 'root' not in doc or not isinstance(doc['root'], str): - raise ValidationException('owner must be a string') + raise ValidationException('root must be a string') return doc + def updateKey(self, folder, root, owner, key, value, categoricalLimit=50): + existing = self.findOne({'DIVEDataset': str(folder['_id'])}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folder["_id"]} found') + query = {'root': existing['root']} + metadataKeys = DIVE_MetadataKeys().findOne( + query=query, + owner=str(owner['_id']), + ) + if not metadataKeys: + raise Exception(f'Could not find the root metadataKeys with folderId: {folder["_id"]}') + if key not in metadataKeys['unlocked']: + raise Exception(f'Key {key} is not unlocked for this metadata and cannot be modified') + if metadataKeys['metadataKeys'][key]['category'] == 'numerical': + existing['metadata'][key] = float(value) + else: + existing['metadata'][key] = value + self.save(existing) + # now we need to update the metadataKey + DIVE_MetadataKeys().updateKeyValue(existing['root'], owner, key, value, categoricalLimit) + + def deleteKey(self, folder, root, owner, key): + existing = self.findOne({'DIVEDataset': str(folder['_id'])}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folder["_id"]} found') + query = {'root': existing['root']} + metadataKeys = DIVE_MetadataKeys().findOne( + query=query, + owner=str(owner['_id']), + ) + if not metadataKeys: + raise Exception(f'Could not find the root metadataKeys with folderId: {folder["_id"]}') + del existing['metadata'][key] + self.save(existing) + class DIVE_MetadataKeys(Model): # This is NOT an access controlled model; it is expected that all endpoints @@ -91,6 +128,7 @@ def initialize(self): self.ensureIndices( [ 'root', + 'owner', ( [ ('created', SortDir.ASCENDING), @@ -100,6 +138,17 @@ def initialize(self): ] ) + def initialize_updated_data(self, folder, user): + existing = self.findOne({'root': str(folder['_id'])}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folder["_id"]} found') + else: + if existing.get('unlocked', False) is False: + existing['unlocked'] = [] + if user is not None: + existing['owner'] = str(user['_id']) + self.save(existing) + def createMetadataKeys(self, root, owner, metadataKeys, created_date=None): existing = self.findOne({'root': str(root['_id'])}) if not existing: @@ -111,10 +160,13 @@ def createMetadataKeys(self, root, owner, metadataKeys, created_date=None): existing = dict( root=str(root['_id']), metadataKeys=metadataKeys, + unlocked=[], created=created, + owner=str(owner['_id']), ) else: existing['metadataKeys'] = metadataKeys + existing['owner'] = str(owner['_id']) self.save(existing) return existing @@ -122,3 +174,85 @@ def validate(self, doc): if 'root' not in doc or not isinstance(doc['root'], str): raise ValidationException('owner must be a string') return doc + + def modifyKeyPermission(self, folder, owner, key, unlocked): + existing = self.findOne({'root': str(folder['_id'])}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folder["_id"]} found') + if owner['_id'] and existing['owner'] != str(owner['_id']): + raise Exception('Only the Owner can modify key permissions') + elif existing: + if key not in existing['metadataKeys'].keys(): + raise Exception(f'Key: {key} not in the metadata keys to modify permission') + else: + if not existing.get('unlocked', False): + existing['unlocked'] = [] + self.save(existing) + if unlocked and key not in existing.get('unlocked', {}): + existing['unlocked'].append(key) + self.save(existing) + if not unlocked and key in existing['unlocked']: + existing['unlocked'].remove(key) + self.save(existing) + + def addKey( + self, + folder, + owner, + key, + info={"set": set(), "count": 0, "category": "categorical"}, # noqa: B006 + unlocked=True, + ): + # info is {"type": datatype, "set": set(), "count": 0} may include range: {min: number, max: number} + existing = self.findOne({'root': str(folder['_id'])}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folder["_id"]} found') + if owner['_id'] and existing['owner'] != str(owner['_id']): + raise Exception('Only the Owner can modify key permissions') + elif existing: + if key in existing['metadataKeys'].keys(): + raise Exception(f'Key: {key} already exists in the dataset and cannot be added') + else: + existing["metadataKeys"][key] = info + if unlocked and key not in existing.get('unlocked', {}): + existing['unlocked'].append(key) + self.save(existing) + + def deleteKey(self, folder, owner, key): + existing = self.findOne({'root': str(folder['_id'])}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folder["_id"]} found') + if owner['_id'] and existing['owner'] != str(owner['_id']): + raise Exception('Only the Owner can modify key permissions') + elif existing: + if key in existing['unlocked']: + existing['unlocked'].remove(key) + if key in existing['metadataKeys'].keys(): + del existing['metadataKeys'][key] + self.save(existing) + else: + raise Exception(f'Key: {key} not found in the current metdata') + + def updateKeyValue(self, folderId, owner, key, value, categoricalLimit): + existing = self.findOne({'root': folderId}) + if not existing: + raise Exception(f'Note MetadataKeys with folderId: {folderId} not found') + if key not in existing['metadataKeys'].keys(): + raise Exception(f'Key: {key} is not in the metadata') + keyData = existing['metadataKeys'][key] + category = keyData['category'] + keyDataSet = set(keyData['set']) + if category == 'categorical': + if len(keyData['set']) + 1 < categoricalLimit: + keyDataSet.add(value) + else: + keyData['category'] = 'search' + del keyData['set'] + if category == 'numerical' and keyData.get('range', False): + range = keyData['range'] + range['min'] = min(float(value), float(range['min'])) + range['max'] = max(float(value), float(range['max'])) + keyData['range'] = range + keyData['set'] = list(keyDataSet) + existing['metadataKeys'][key] = keyData + self.save(existing) diff --git a/server/poetry.lock b/server/poetry.lock index a2ca2b4..06fae99 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -681,6 +681,26 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + [[package]] name = "docker" version = "7.1.0" @@ -855,12 +875,12 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "girder" -version = "3.2.3" +version = "3.2.6" description = "Web-based data management platform" optional = false python-versions = ">=3.8" files = [ - {file = "girder-3.2.3.tar.gz", hash = "sha256:6f1d31c26237bc5b6f4010692248231c7c9a4d2a75701ec761a41d6e75de8fcb"}, + {file = "girder-3.2.6.tar.gz", hash = "sha256:22d1dd4d5741328036c3032d2aa609f6a735343950437bf24aefaef72c92e2af"}, ] [package.dependencies] @@ -875,7 +895,7 @@ jsonschema = "*" Mako = "*" passlib = {version = "*", extras = ["bcrypt", "totp"]} psutil = "*" -pymongo = ">=3.6,<4" +pymongo = ">=4" pyOpenSSL = "*" python-dateutil = "*" pytz = "*" @@ -888,12 +908,12 @@ sftp = ["paramiko"] [[package]] name = "girder-client" -version = "3.2.3" +version = "3.2.6" description = "Python client for interacting with Girder servers" optional = false python-versions = ">=3.8" files = [ - {file = "girder-client-3.2.3.tar.gz", hash = "sha256:18433114045d597082301829a7e20451335260c226f01fd2997e3247a2876556"}, + {file = "girder_client-3.2.6.tar.gz", hash = "sha256:9a3e8d103a2e653a458ebfd6136b073222f804620b276ade47404df1487637bc"}, ] [package.dependencies] @@ -904,12 +924,12 @@ requests_toolbelt = "*" [[package]] name = "girder-jobs" -version = "3.2.3" +version = "3.2.6" description = "A general purpose plugin for managing offline jobs." optional = false python-versions = ">=3.8" files = [ - {file = "girder-jobs-3.2.3.tar.gz", hash = "sha256:14a5a55138477dee260a0f22aac83ffb4daf622a62816a3f937750a5428c5d8d"}, + {file = "girder_jobs-3.2.6.tar.gz", hash = "sha256:ab5bdcc0937604e09a6a1f7f052456ac6649f32ef32f08c5a42c48a151c2e9c1"}, ] [package.dependencies] @@ -965,12 +985,12 @@ girder = ["girder (>=3.0.1,<5)", "girder-jobs (>=3.0.1,<5)"] [[package]] name = "girder-worker-utils" -version = "0.9.0" +version = "0.9.1" description = "Helper utilities for the Girder Worker" optional = false python-versions = ">=3.8" files = [ - {file = "girder-worker-utils-0.9.0.tar.gz", hash = "sha256:ee1a2d9470d623d14db9cecd82e6c8f1d4f612fc8e15d6725ee23b5fd60a8fb4"}, + {file = "girder_worker_utils-0.9.1.tar.gz", hash = "sha256:9e72a4f28458cd67f8ce5b7b4f68159630dac5cc74501f5ae62ee954a8ad12a1"}, ] [package.dependencies] @@ -1882,130 +1902,83 @@ email = ["email-validator (>=1.0.3)"] [[package]] name = "pymongo" -version = "3.13.0" +version = "4.9.1" description = "Python driver for MongoDB " optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pymongo-3.13.0-cp27-cp27m-macosx_10_14_intel.whl", hash = "sha256:3ad3a3df830f7df7e0856c2bdb54d19f5bf188bd7420985e18643b8e4d2a075f"}, - {file = "pymongo-3.13.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b96e0e9d2d48948240b510bac81614458fc10adcd3a93240c2fd96448b4efd35"}, - {file = "pymongo-3.13.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f592b202d77923498b32ddc5b376e5fa9ba280d3e16ed56cb8c932fe6d6a478"}, - {file = "pymongo-3.13.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:851f2bb52b5cb2f4711171ca925e0e05344a8452972a748a8a8ffdda1e1d72a7"}, - {file = "pymongo-3.13.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1c9d23f62a3fa7523d849c4942acc0d9ff7081ebc00c808ee7cfdc070df0687f"}, - {file = "pymongo-3.13.0-cp27-cp27m-win32.whl", hash = "sha256:a17b81f22398e3e0f72bdf938e98c810286994b2bcc0a125cd5ad8fd4ea54ad7"}, - {file = "pymongo-3.13.0-cp27-cp27m-win_amd64.whl", hash = "sha256:4f6dd55dab77adf60b445c11f426ee5cdfa1b86f6d54cb937bfcbf09572333ab"}, - {file = "pymongo-3.13.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:776f90bf2252f90a4ae838e7917638894c6356bef7265f424592e2fd1f577d05"}, - {file = "pymongo-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:50b99f4d3eee6f03778fe841d6f470e6c18e744dc665156da6da3bc6e65b398d"}, - {file = "pymongo-3.13.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50a81b2d9f188c7909e0a1084fa969bb92a788076809c437ac1ae80393f46df9"}, - {file = "pymongo-3.13.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c7c45a8a1a752002b0a7c81ab3a4c5e3b6f67f9826b16fbe3943f5329f565f24"}, - {file = "pymongo-3.13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1037097708498bdc85f23c8798a5c46c7bce432d77d23608ff14e0d831f1a971"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux1_i686.whl", hash = "sha256:b5b733694e7df22d5c049581acfc487695a6ff813322318bed8dd66f79978636"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d7c91747ec8dde51440dd594603158cc98abb3f7df84b2ed8a836f138285e4fb"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:f4175fcdddf764d371ee52ec4505a40facee2533e84abf2953cda86d050cfa1f"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:93d4e9a02c17813b34e4bd9f6fbf07310c140c8f74341537c24d07c1cdeb24d1"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:3b261d593f2563299062733ae003a925420a86ff4ddda68a69097d67204e43f3"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:172db03182a22e9002157b262c1ea3b0045c73d4ff465adc152ce5b4b0e7b8d4"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09de3bfc995ae8cb955abb0c9ae963c134dba1b5622be3bcc527b89b0fd4091c"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0379447587ee4b8f983ba183202496e86c0358f47c45612619d634d1fcd82bd"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30245a8747dc90019a3c9ad9df987e0280a3ea632ad36227cde7d1d8dcba0830"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6fddf6a7b91da044f202771a38e71bbb9bf42720a406b26b25fe2256e7102"}, - {file = "pymongo-3.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5831a377d15a626fbec10890ffebc4c6abcd37e4126737932cd780a171eabdc1"}, - {file = "pymongo-3.13.0-cp310-cp310-win32.whl", hash = "sha256:944249aa83dee314420c37d0f40c30a8f6dc4a3877566017b87062e53af449f4"}, - {file = "pymongo-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea8824ebc9a1a5c8269e8f1e3989b5a6bec876726e2f3c33ebd036cb488277f0"}, - {file = "pymongo-3.13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bdd34c57b4da51a7961beb33645646d197e41f8517801dc76b37c1441e7a4e10"}, - {file = "pymongo-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f9cc42a162faa241c82e117ac85734ae9f14343dc2df1c90c6b2181f791b22"}, - {file = "pymongo-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a82a1c10f5608e6494913faa169e213d703194bfca0aa710901f303be212414"}, - {file = "pymongo-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8927f22ef6a16229da7f18944deac8605bdc2c0858be5184259f2f7ce7fd4459"}, - {file = "pymongo-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6f8191a282ef77e526f8f8f63753a437e4aa4bc78f5edd8b6b6ed0eaebd5363"}, - {file = "pymongo-3.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d9ed67c987bf9ac2ac684590ba3d2599cdfb0f331ee3db607f9684469b3b59d"}, - {file = "pymongo-3.13.0-cp311-cp311-win32.whl", hash = "sha256:e8f6979664ff477cd61b06bf8aba206df7b2334209815ab3b1019931dab643d6"}, - {file = "pymongo-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:174fd1000e896d0dfbc7f6d7e6a1992a4868796c7dec31679e38218c78d6a942"}, - {file = "pymongo-3.13.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:d1ee773fb72ba024e7e3bb6ea8907fe52bccafcb5184aaced6bad995bd30ea20"}, - {file = "pymongo-3.13.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:28565e3dbd69fe5fe35a210067064dbb6ed5abe997079f653c19c873c3896fe6"}, - {file = "pymongo-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:5c1db7d366004d6c699eb08c716a63ae0a3e946d061cbebea65d7ce361950265"}, - {file = "pymongo-3.13.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e1956f3338c10308e2f99c2c9ff46ae412035cbcd7aaa76c39ccdb806854a247"}, - {file = "pymongo-3.13.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:10f0fddc1d63ba3d4a4bffcc7720184c1b7efd570726ad5e2f55818da320239f"}, - {file = "pymongo-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:570ae3365b23d4fd8c669cb57613b1a90b2757e993588d3370ef90945dbeec4b"}, - {file = "pymongo-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:79f777eaf3f5b2c6d81f9ef00d87837001d7063302503bbcbfdbf3e9bc27c96f"}, - {file = "pymongo-3.13.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:d42eb29ba314adfd9c11234b4b646f61b0448bf9b00f14db4b317e6e4b947e77"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:e5e87c0eb774561c546f979342a8ff36ebee153c60a0b6c6b03ba989ceb9538c"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0f2c5a5984599a88d087a15859860579b825098b473d8c843f1979a83d159f2e"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:59c98e86c5e861032b71e6e5b65f23e6afaacea6e82483b66f1191a5021a7b4f"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:70b67390e27e58876853efbb87e43c85252de2515e2887f7dd901b4fa3d21973"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:42ba8606492d76e6f9e4c7a458ed4bc712603be393259a52450345f0945da2cf"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:0e5536994cf2d8488c6fd9dea71df3c4dbb3e0d2ba5e695da06d9142a29a0969"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:fe8194f107f0fa3cabd14e9e809f174eca335993c1db72d1e74e0f496e7afe1f"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d593d50815771f517d3ac4367ff716e3f3c78edae51d98e1e25791459f8848ff"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5136ebe8da6a1604998a8eb96be55935aa5f7129c41cc7bddc400d48e8df43be"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a424bdedfd84454d2905a861e0d4bb947cc5bd024fdeb3600c1a97d2be0f4255"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5161167b3840e9c84c80f2534ea6a099f51749d5673b662a3dd248be17c3208"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644470442beaf969df99c4e00367a817eee05f0bba5d888f1ba6fe97b5e1c102"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2406df90b2335371706c59b7d79e9633b81ed2a7ecd48c1faf8584552bdf2d90"}, - {file = "pymongo-3.13.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:222591b828de10ac90064047b5d4916953f38c38b155009c4b8b5e0d33117c2b"}, - {file = "pymongo-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:7cb987b199fa223ad78eebaa9fbc183d5a5944bfe568a9d6f617316ca1c1f32f"}, - {file = "pymongo-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6cbb73d9fc2282677e2b7a137d13da987bd0b13abd88ed27bba5534c226db06"}, - {file = "pymongo-3.13.0-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:b1223b826acbef07a7f5eb9bf37247b0b580119916dca9eae19d92b1290f5855"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:398fb86d374dc351a4abc2e24cd15e5e14b2127f6d90ce0df3fdf2adcc55ac1b"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:9c3d07ea19cd2856d9943dce37e75d69ecbb5baf93c3e4c82f73b6075c481292"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:2943d739715f265a2983ac43747595b6af3312d0a370614040959fd293763adf"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:c3b70ed82f20d18d22eafc9bda0ea656605071762f7d31f3c5afc35c59d3393b"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:7ec2bb598847569ae34292f580842d37619eea3e546005042f485e15710180d5"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:8cc37b437cba909bef06499dadd91a39c15c14225e8d8c7870020049f8a549fe"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:65a063970e15a4f338f14b820561cf6cdaf2839691ac0adb2474ddff9d0b8b0b"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02f0e1a75d3bc0e16c7e15daf9c56185642be055e425f3b34888fc6eb1b22401"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e74b9c2aca2734c7f49f00fe68d6830a30d26df60e2ace7fe40ccb92087b94"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24e954be35ad4537840f20bbc8d75320ae647d3cb4fab12cb8fcd2d55f408e76"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a149377d1ff766fd618500798d0d94637f66d0ae222bb6d28f41f3e15c626297"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61660710b054ae52c8fc10368e91d74719eb05554b631d7f8ca93d21d2bff2e6"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bbc0d27dfef7689285e54f2e0a224f0c7cd9d5c46d2638fabad5500b951c92f"}, - {file = "pymongo-3.13.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9b2ed9c3b30f11cd4a3fbfc22167af7987b01b444215c2463265153fe7cf66d6"}, - {file = "pymongo-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:1c2c5e2b00e2fadcd590c0b2e293d71215e98ed1cb635cfca2be4998d197e534"}, - {file = "pymongo-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:32eac95bbb030b2376ffd897376c6f870222a3457f01a9ce466b9057876132f8"}, - {file = "pymongo-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a796ef39dadf9d73af05d24937644d386495e43a7d13617aa3651d836da542c8"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b6793baf4639c72a500698a49e9250b293e17ae1faf11ac1699d8141194786fe"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:80d8576b04d0824f63bf803190359c0d3bcb6e7fa63fefbd4bc0ceaa7faae38c"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:db2e11507fe9cc2a722be21ccc62c1b1295398fe9724c1f14900cdc7166fc0d7"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:b01ce58eec5edeededf1992d2dce63fb8565e437be12d6f139d75b15614c4d08"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:d1a19d6c5098f1f4e11430cd74621699453cbc534dd7ade9167e582f50814b19"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:7219b1a726ced3bacecabef9bd114529bbb69477901373e800d7d0140baadc95"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:2dae3b353a10c3767e0aa1c1492f2af388f1012b08117695ab3fd1f219e5814e"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12721d926d43d33dd3318e58dce9b0250e8a9c6e1093fa8e09f4805193ff4b43"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6af0a4b17faf26779d5caee8542a4f2cba040cea27d3bffc476cbc6ccbd4c8ee"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09b9d0f5a445c7e0ddcc021b09835aa6556f0166afc498f57dfdd72cdf6f02ad"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db5b4f8ad8607a3d612da1d4c89a84e4cf5c88f98b46365820d9babe5884ba45"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dbf5fecf653c152edb75a35a8b15dfdc4549473484ee768aeb12c97983cead"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34cd48df7e1fc69222f296d8f69e3957eb7c6b5aa0709d3467184880ed7538c0"}, - {file = "pymongo-3.13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c8f755ff1f4ab4ca790d1d6d3229006100b301475948021b6b2757822e0d6c97"}, - {file = "pymongo-3.13.0-cp38-cp38-win32.whl", hash = "sha256:b0746d0d4535f56bbaa63a8f6da362f330804d578e66e126b226eebe76c2bf00"}, - {file = "pymongo-3.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ad0515abb132f52ce9d8abd1a29681a1e65dba7b7fe13ea01e1a8db5715bf80"}, - {file = "pymongo-3.13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3c5cb6c93c94df76a879bad4b89db0104b01806d17c2b803c1316ba50962b6d6"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2e0854170813238f0c3131050c67cb1fb1ade75c93bf6cd156c1bd9a16095528"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1410faa51ce835cc1234c99ec42e98ab4f3c6f50d92d86a2d4f6e11c97ee7a4e"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d7910135f5de1c5c3578e61d6f4b087715b15e365f11d4fa51a9cee92988b2bd"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:028175dd8d2979a889153a2308e8e500b3df7d9e3fd1c33ca7fdeadf61cc87a2"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:2bfc39276c0e6d07c95bd1088b5003f049e986e089509f7dbd68bb7a4b1e65ac"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:4092b660ec720d44d3ca81074280dc25c7a3718df1b6c0fe9fe36ac6ed2833e4"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:5bdeb71a610a7b801416268e500e716d0fe693fb10d809e17f0fb3dac5be5a34"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3bca8e76f5c00ed2bb4325e0e383a547d71595926d5275d7c88175aaf7435e"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c7cab8155f430ca460a6fc7ae8a705b34f3e279a57adb5f900eb81943ec777c"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a32f3dfcca4a4816373bdb6256c18c78974ebb3430e7da988516cd95b2bd6e4"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ed2788a6ec68743e2040ab1d16573d7d9f6e7333e45070ce9268cbc93d148c"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e61a536ffed84d10376c21c13a6ed1ebefb61989a844952547c229d6aeedf3"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0665412dce26b2318092a33bd2d2327d487c4490cfcde158d6946d39b1e28d78"}, - {file = "pymongo-3.13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:64ed1a5ce5e5926727eb0f87c698c4d9a7a9f7b0953683a65e9ce2b7cc5f8e91"}, - {file = "pymongo-3.13.0-cp39-cp39-win32.whl", hash = "sha256:7593cb1214185a0c5b43b96effc51ce82ddc933298ee36db7dc2bd45d61b4adc"}, - {file = "pymongo-3.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:3cfc9bc1e8b5667bc1f3dbe46d2f85b3f24ff7533893bdc1203058012db2c046"}, - {file = "pymongo-3.13.0.tar.gz", hash = "sha256:e22d6cf5802cd09b674c307cc9e03870b8c37c503ebec3d25b86f2ce8c535dc7"}, -] + {file = "pymongo-4.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc3d070d746ab79e9b393a5c236df20e56607389af2b79bf1bfe9a841117558e"}, + {file = "pymongo-4.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe709d05654c12fc513617c8d5c8d05b7e9cf1d5d94ada68add4e89530c867d2"}, + {file = "pymongo-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa4493f304b33c5d2ecee3055c98889ac6724d56f5f922d47420a45d0d4099c9"}, + {file = "pymongo-4.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8e8b8deba6a4bff3dd5421071083219521c74d2acae0322de5c06f1a66c56af"}, + {file = "pymongo-4.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3645aff8419ca60f9ccd08966b2f6b0d78053f9f98a814d025426f1d874c19a"}, + {file = "pymongo-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51dbc6251c6783dfcc7d657c346986d8bad7210989b2fe15de16db5204a8e7ae"}, + {file = "pymongo-4.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d7aa9cc2d92e73bdb036c578ba019da94ea165eb147e691cd910a6fab7ce3b7"}, + {file = "pymongo-4.9.1-cp310-cp310-win32.whl", hash = "sha256:8b632e01617f2608880f7b9926f54a5f5ebb51631996e0540fff7fc7980663c9"}, + {file = "pymongo-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:f05e34d401be871d7c87cb10727d49315444e4ded07ff876a595e4c23b7436da"}, + {file = "pymongo-4.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bb3d5282278594753089dc7da48bfae4a7f337a2dd4d397eabb591c649e58d0"}, + {file = "pymongo-4.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f0d5258bc85a4e6b5bcae8160628168e71ec4625a58ceb53327c3280a0b6914"}, + {file = "pymongo-4.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96462fb2175f740701d229f52018ea6e4adc4148c4112e6628bb359dd534a3df"}, + {file = "pymongo-4.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:286fb275267f0293364ba579f6354452599161f1902ad411061c7f744ab88328"}, + {file = "pymongo-4.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cddb51cead9700c4dccc916952bc0321b8d766bf782d374bfa0e93ef47c1d20"}, + {file = "pymongo-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d79f20f9c7cbc1c708fb80b648b6fbd3220fd3437a9bd6017c1eb592e03b361"}, + {file = "pymongo-4.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd3352eaf578f8e9bdea7a5692910eedad1e8680f60726fc70e99c8af51a5449"}, + {file = "pymongo-4.9.1-cp311-cp311-win32.whl", hash = "sha256:ea3f0196e7c311b9944a609ac175bd91ab97952164a1246716fdd38d53ca3bcc"}, + {file = "pymongo-4.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4c793db8457c856f333f396798470b9bfe405e17c307d581532c74cec70150c"}, + {file = "pymongo-4.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:47b4896544095d172c366dd4d4ea1da6b0ab1a77d8416897cc1801e2421b1e67"}, + {file = "pymongo-4.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbb1c7dfcf6c44e9e1928290631c7603817991cdf570691c9e15fca594918435"}, + {file = "pymongo-4.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7689da1d1b444284e4ea9ab2eb64a15307b6b795918c0f3cd7774dd1d8a7556"}, + {file = "pymongo-4.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f962d74201c772555f7a78792fed820a5ea76db5c7ee6cf43748e411b44e430"}, + {file = "pymongo-4.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08fbab69f3fb6f8088c81f4c4a8abd84a99c132034f5e27e47f894bbcb6bf439"}, + {file = "pymongo-4.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4327c0d9bd616b8289691360f2d4a09a72fe35479795832eae0d4ff78af53923"}, + {file = "pymongo-4.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34e4993ae78be56f9e27a141168a1ab78253576fa3e893fa335a719ce204c3ef"}, + {file = "pymongo-4.9.1-cp312-cp312-win32.whl", hash = "sha256:e1f346811d4a2369f88ab7a6f886fa9c3bbc9ed4e4f4a3becca8717a73d465cb"}, + {file = "pymongo-4.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:a2b12c74cfd90147babb77f9728646bcedfdbd2bd2a5b4130a00e3a0af1a3d34"}, + {file = "pymongo-4.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a40ea8bc9cffb61c5c9c426c430d22235e085e610ee81ae075ddf51f12f76236"}, + {file = "pymongo-4.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75d5974f874acdb2f125bdbe785045b23a39ecce1d3143dd5712800c7b6d25eb"}, + {file = "pymongo-4.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f23a046531030318622414f21198e232cf93c5640da9a80b45596a059c8cc090"}, + {file = "pymongo-4.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91b1a92214c3912af5467f77c2f6435cd76f6de64c70cba7bb4ee43eba7f459e"}, + {file = "pymongo-4.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a846423c4535428f69a90a1451df3718bc59f0c4ab685b9e96d3071951e0be4"}, + {file = "pymongo-4.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d476d91a5c9e6c37bc8ec3fb294e1c01d95736ccf01a59bb1540fe2f710f826e"}, + {file = "pymongo-4.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:172d8ba0f567e351a18765db23dab7dbcfdffd91a8788d90d46b350f80a40781"}, + {file = "pymongo-4.9.1-cp313-cp313-win32.whl", hash = "sha256:95418e334629440f70fe5ceeefc6cbbd50defb566901c8d68179ffbaec8d5f01"}, + {file = "pymongo-4.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:1dfd2aa30174d36a3ef1dae4ee4c89710c2d65cac52ce6e13f17c710edbd61cf"}, + {file = "pymongo-4.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c4204fad54830a3173a5c939cd052d0561fba03dba7e0ff6852fd631f3314aa4"}, + {file = "pymongo-4.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:375765ec81b1f0a26d08928afea0c3dff897c36080a090be53fc7b70cc51d497"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d1b959a3dda0775d9111622ee47ad47772aed3a9da2e7d5f2f513fa68175dea"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42c19d2b094cdd0ead7dbb38860bbe8268c140334ce55d8b39204ddb4ebd4904"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1fac1def9e9073f1c80198c99f0ec39c2528236c8912d96d7fd3b0237f4c523a"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b347052d510989d1f52b8553b31297f21cf74bd9f6aed71ee84e563492f4ff17"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b4b961fce213f2bcdc92268f85111a3668c61b9b4d4e7ece27dce3a137cfcbd"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a0b10cf51ec14a487c94709d294c00e1fb6a0a4c38cdc3acfb2ced5ef60972a0"}, + {file = "pymongo-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:679b8d55854da7c7fdb82aa5e092ab4de0144daf6758defed8ab00ff9ce05360"}, + {file = "pymongo-4.9.1-cp38-cp38-win32.whl", hash = "sha256:432ad395d2233056b042ccc73234e7136aa65d944d6bd8b5138394bd38aaff79"}, + {file = "pymongo-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9fbe9fad27619ac4cfda5df0ade26a99906da7dfe7b01deddc25997eb1804e4c"}, + {file = "pymongo-4.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:99b611ff75b5d9e17183dcf9584a7b04f9db07e51a162f23ea05e485e0735c0a"}, + {file = "pymongo-4.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8089003a99127f917bdbeec177d41cef019cda8ec70534c1018cb60aacd23c2a"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d78adf25967c06298c7e488f4cfab79a390fc32c2b1d428613976f99031603d"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56877cfcdf7dfc5c6408e4551ec0d6d65ebbca4d744a0bc90400f09ef6bbcc8a"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d2efe559d0d96bc0b74b3ff76701ad6f6e1a65f6581b573dcacc29158131c8"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f838f613e74b4dad8ace0d90f42346005bece4eda5bf6d389cfadb8322d39316"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db5b299e11284f8d82ce2983d8e19fcc28f98f902a179709ef1982b4cca6f8b8"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b23211c031b45d0f32de83ab7d77f9c26f1025c2d2c91463a5d8594a16103655"}, + {file = "pymongo-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:687cf70e096381bc65b4273a6a9319617618f7ace65caffc356e1099c4a68511"}, + {file = "pymongo-4.9.1-cp39-cp39-win32.whl", hash = "sha256:e02b03e3815b80a63e773e4c32aed3cf5633d406f376477be74550295c211256"}, + {file = "pymongo-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:0492ef43f3342354cf581712e431621c221f60c877ebded84e3f3e53b71bbbe0"}, + {file = "pymongo-4.9.1.tar.gz", hash = "sha256:b7f2d34390acf60e229c30037d1473fcf69f4536cd7f48f6f78c0c931c61c505"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" [package.extras] -aws = ["pymongo-auth-aws (<2.0.0)"] -encryption = ["pymongocrypt (>=1.1.0,<2.0.0)"] -gssapi = ["pykerberos"] -ocsp = ["certifi", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] +docs = ["furo (==2023.9.10)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<3)", "sphinxcontrib-shellcheck (>=1,<2)"] +encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.10.0,<2.0.0)"] +gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] +ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] snappy = ["python-snappy"] -srv = ["dnspython (>=1.16.0,<1.17.0)"] -tls = ["ipaddress"] +test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"] zstd = ["zstandard"] [[package]] @@ -2803,4 +2776,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "d94d1fca38eaed435a1677a86b93ba204f7c27e630e0080893718346a1e45369" +content-hash = "770af67d8a67b57c204ba2bb896fadb12d40e6b7b28b9dd134af31c46165d576" diff --git a/server/pyproject.toml b/server/pyproject.toml index 58ca267..664fa41 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -49,10 +49,11 @@ packages = [ python = ">=3.8,<3.12" cheroot = ">=8.4.5" click = "^8.1.3" -girder = ">=3.2.3" -girder_jobs = ">=3.2.3" +girder = ">=3.2.6" +girder_jobs = ">=3.2.6" +girder_client = ">=3.2.6" girder_worker = ">=0.10.3" -girder_worker_utils = ">=0.9.0" +girder_worker_utils = ">=0.9.1" pydantic = "1.10.13" pyrabbit2 = "1.0.7" typing-extensions = "^4.2.0"