diff --git a/RELEASE.rst b/RELEASE.rst index 81c08e184d..e938bf123a 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,18 @@ Release Notes ============= +Version 0.63.6 +-------------- + +- Facet counts and aggregations for Vector search (#3210) +- Program Unenrollment (#3203) +- Update dependency Django to v4.2.30 [SECURITY] (#3212) +- Update dependency cryptography to v46.0.7 [SECURITY] (#3211) +- Update dependency requests to v2.33.0 [SECURITY] (#3214) +- Filtering for similarity endpoints (#3204) +- fix: minor improvements on video collection pages (#3205) +- Update dependency litellm to v1.83.0 [SECURITY] (#3213) + Version 0.63.5 (Released April 16, 2026) -------------- diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 368c911b52..8e0eb0472a 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -11532,6 +11532,7 @@ export const VectorContentFilesSearchApiAxiosParamCreator = function ( /** * Vector Search for content * @summary Content File Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `key` - Key * `course_number` - Course Number * `platform` - Platform * `offered_by` - Offered By * `file_extension` - File Extension * `content_feature_type` - Content Feature Type * `run_readable_id` - Run Readable Id * `resource_readable_id` - Resource Readable Id * `run_title` - Run Title * `edx_module_id` - Edx Module Id * `content_type` - Content Type * `description` - Description * `title` - Title * `url` - Url * `file_type` - File Type * `summary` - Summary * `flashcards` - Flashcards * `checksum` - Checksum * @param {string} [collection_name] Manually specify the name of the Qdrant collection to query * @param {Array} [file_extension] The extension of the content file. * @param {string} [group_by] The attribute to group results by @@ -11552,6 +11553,7 @@ export const VectorContentFilesSearchApiAxiosParamCreator = function ( * @throws {RequiredError} */ vectorContentFilesSearchRetrieve: async ( + aggregations?: Array, collection_name?: string, file_extension?: Array, group_by?: string, @@ -11586,6 +11588,10 @@ export const VectorContentFilesSearchApiAxiosParamCreator = function ( const localVarHeaderParameter = {} as any const localVarQueryParameter = {} as any + if (aggregations) { + localVarQueryParameter["aggregations"] = aggregations + } + if (collection_name !== undefined) { localVarQueryParameter["collection_name"] = collection_name } @@ -11680,6 +11686,7 @@ export const VectorContentFilesSearchApiFp = function ( /** * Vector Search for content * @summary Content File Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `key` - Key * `course_number` - Course Number * `platform` - Platform * `offered_by` - Offered By * `file_extension` - File Extension * `content_feature_type` - Content Feature Type * `run_readable_id` - Run Readable Id * `resource_readable_id` - Resource Readable Id * `run_title` - Run Title * `edx_module_id` - Edx Module Id * `content_type` - Content Type * `description` - Description * `title` - Title * `url` - Url * `file_type` - File Type * `summary` - Summary * `flashcards` - Flashcards * `checksum` - Checksum * @param {string} [collection_name] Manually specify the name of the Qdrant collection to query * @param {Array} [file_extension] The extension of the content file. * @param {string} [group_by] The attribute to group results by @@ -11700,6 +11707,7 @@ export const VectorContentFilesSearchApiFp = function ( * @throws {RequiredError} */ async vectorContentFilesSearchRetrieve( + aggregations?: Array, collection_name?: string, file_extension?: Array, group_by?: string, @@ -11725,6 +11733,7 @@ export const VectorContentFilesSearchApiFp = function ( > { const localVarAxiosArgs = await localVarAxiosParamCreator.vectorContentFilesSearchRetrieve( + aggregations, collection_name, file_extension, group_by, @@ -11783,6 +11792,7 @@ export const VectorContentFilesSearchApiFactory = function ( ): AxiosPromise { return localVarFp .vectorContentFilesSearchRetrieve( + requestParameters.aggregations, requestParameters.collection_name, requestParameters.file_extension, requestParameters.group_by, @@ -11812,6 +11822,13 @@ export const VectorContentFilesSearchApiFactory = function ( * @interface VectorContentFilesSearchApiVectorContentFilesSearchRetrieveRequest */ export interface VectorContentFilesSearchApiVectorContentFilesSearchRetrieveRequest { + /** + * aggregations for facet counts * `key` - Key * `course_number` - Course Number * `platform` - Platform * `offered_by` - Offered By * `file_extension` - File Extension * `content_feature_type` - Content Feature Type * `run_readable_id` - Run Readable Id * `resource_readable_id` - Resource Readable Id * `run_title` - Run Title * `edx_module_id` - Edx Module Id * `content_type` - Content Type * `description` - Description * `title` - Title * `url` - Url * `file_type` - File Type * `summary` - Summary * `flashcards` - Flashcards * `checksum` - Checksum + * @type {Array<'key' | 'course_number' | 'platform' | 'offered_by' | 'file_extension' | 'content_feature_type' | 'run_readable_id' | 'resource_readable_id' | 'run_title' | 'edx_module_id' | 'content_type' | 'description' | 'title' | 'url' | 'file_type' | 'summary' | 'flashcards' | 'checksum'>} + * @memberof VectorContentFilesSearchApiVectorContentFilesSearchRetrieve + */ + readonly aggregations?: Array + /** * Manually specify the name of the Qdrant collection to query * @type {string} @@ -11946,6 +11963,7 @@ export class VectorContentFilesSearchApi extends BaseAPI { ) { return VectorContentFilesSearchApiFp(this.configuration) .vectorContentFilesSearchRetrieve( + requestParameters.aggregations, requestParameters.collection_name, requestParameters.file_extension, requestParameters.group_by, @@ -11968,6 +11986,31 @@ export class VectorContentFilesSearchApi extends BaseAPI { } } +/** + * @export + */ +export const VectorContentFilesSearchRetrieveAggregationsEnum = { + Key: "key", + CourseNumber: "course_number", + Platform: "platform", + OfferedBy: "offered_by", + FileExtension: "file_extension", + ContentFeatureType: "content_feature_type", + RunReadableId: "run_readable_id", + ResourceReadableId: "resource_readable_id", + RunTitle: "run_title", + EdxModuleId: "edx_module_id", + ContentType: "content_type", + Description: "description", + Title: "title", + Url: "url", + FileType: "file_type", + Summary: "summary", + Flashcards: "flashcards", + Checksum: "checksum", +} as const +export type VectorContentFilesSearchRetrieveAggregationsEnum = + (typeof VectorContentFilesSearchRetrieveAggregationsEnum)[keyof typeof VectorContentFilesSearchRetrieveAggregationsEnum] /** * @export */ @@ -11991,6 +12034,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( /** * Vector Search for learning resources * @summary Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `readable_id` - Readable Id * `resource_type` - Resource Type * `certification` - Certification * `certification_type` - Certification Type * `professional` - Professional * `free` - Free * `course_feature` - Course Feature * `topic` - Topic * `ocw_topic` - Ocw Topic * `level` - Level * `department` - Department * `platform` - Platform * `offered_by` - Offered By * `delivery` - Delivery * `title` - Title * `url` - Url * `resource_type_group` - Resource Type Group * `resource_category` - Resource Category * `published` - Published * @param {boolean | null} [certification] True if the learning resource offers a certificate * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ @@ -12005,6 +12049,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service * @param {boolean | null} [professional] + * @param {boolean} [published] If the resource is published. We default to True unless passed in * @param {string} [q] The search text * @param {string} [readable_id] The readable id of the resource * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document @@ -12016,6 +12061,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( * @throws {RequiredError} */ vectorLearningResourcesSearchRetrieve: async ( + aggregations?: Array, certification?: boolean | null, certification_type?: Array, course_feature?: Array, @@ -12030,6 +12076,7 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( offset?: number, platform?: Array, professional?: boolean | null, + published?: boolean, q?: string, readable_id?: string, resource_type?: Array, @@ -12055,6 +12102,10 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( const localVarHeaderParameter = {} as any const localVarQueryParameter = {} as any + if (aggregations) { + localVarQueryParameter["aggregations"] = aggregations + } + if (certification !== undefined) { localVarQueryParameter["certification"] = certification } @@ -12111,6 +12162,10 @@ export const VectorLearningResourcesSearchApiAxiosParamCreator = function ( localVarQueryParameter["professional"] = professional } + if (published !== undefined) { + localVarQueryParameter["published"] = published + } + if (q !== undefined) { localVarQueryParameter["q"] = q } @@ -12169,6 +12224,7 @@ export const VectorLearningResourcesSearchApiFp = function ( /** * Vector Search for learning resources * @summary Vector Search + * @param {Array} [aggregations] aggregations for facet counts * `readable_id` - Readable Id * `resource_type` - Resource Type * `certification` - Certification * `certification_type` - Certification Type * `professional` - Professional * `free` - Free * `course_feature` - Course Feature * `topic` - Topic * `ocw_topic` - Ocw Topic * `level` - Level * `department` - Department * `platform` - Platform * `offered_by` - Offered By * `delivery` - Delivery * `title` - Title * `url` - Url * `resource_type_group` - Resource Type Group * `resource_category` - Resource Category * `published` - Published * @param {boolean | null} [certification] True if the learning resource offers a certificate * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ @@ -12183,6 +12239,7 @@ export const VectorLearningResourcesSearchApiFp = function ( * @param {number} [offset] The initial index from which to return the results * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service * @param {boolean | null} [professional] + * @param {boolean} [published] If the resource is published. We default to True unless passed in * @param {string} [q] The search text * @param {string} [readable_id] The readable id of the resource * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document @@ -12194,6 +12251,7 @@ export const VectorLearningResourcesSearchApiFp = function ( * @throws {RequiredError} */ async vectorLearningResourcesSearchRetrieve( + aggregations?: Array, certification?: boolean | null, certification_type?: Array, course_feature?: Array, @@ -12208,6 +12266,7 @@ export const VectorLearningResourcesSearchApiFp = function ( offset?: number, platform?: Array, professional?: boolean | null, + published?: boolean, q?: string, readable_id?: string, resource_type?: Array, @@ -12224,6 +12283,7 @@ export const VectorLearningResourcesSearchApiFp = function ( > { const localVarAxiosArgs = await localVarAxiosParamCreator.vectorLearningResourcesSearchRetrieve( + aggregations, certification, certification_type, course_feature, @@ -12238,6 +12298,7 @@ export const VectorLearningResourcesSearchApiFp = function ( offset, platform, professional, + published, q, readable_id, resource_type, @@ -12287,6 +12348,7 @@ export const VectorLearningResourcesSearchApiFactory = function ( ): AxiosPromise { return localVarFp .vectorLearningResourcesSearchRetrieve( + requestParameters.aggregations, requestParameters.certification, requestParameters.certification_type, requestParameters.course_feature, @@ -12301,6 +12363,7 @@ export const VectorLearningResourcesSearchApiFactory = function ( requestParameters.offset, requestParameters.platform, requestParameters.professional, + requestParameters.published, requestParameters.q, requestParameters.readable_id, requestParameters.resource_type, @@ -12321,6 +12384,13 @@ export const VectorLearningResourcesSearchApiFactory = function ( * @interface VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieveRequest */ export interface VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieveRequest { + /** + * aggregations for facet counts * `readable_id` - Readable Id * `resource_type` - Resource Type * `certification` - Certification * `certification_type` - Certification Type * `professional` - Professional * `free` - Free * `course_feature` - Course Feature * `topic` - Topic * `ocw_topic` - Ocw Topic * `level` - Level * `department` - Department * `platform` - Platform * `offered_by` - Offered By * `delivery` - Delivery * `title` - Title * `url` - Url * `resource_type_group` - Resource Type Group * `resource_category` - Resource Category * `published` - Published + * @type {Array<'readable_id' | 'resource_type' | 'certification' | 'certification_type' | 'professional' | 'free' | 'course_feature' | 'topic' | 'ocw_topic' | 'level' | 'department' | 'platform' | 'offered_by' | 'delivery' | 'title' | 'url' | 'resource_type_group' | 'resource_category' | 'published'>} + * @memberof VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieve + */ + readonly aggregations?: Array + /** * True if the learning resource offers a certificate * @type {boolean} @@ -12419,6 +12489,13 @@ export interface VectorLearningResourcesSearchApiVectorLearningResourcesSearchRe */ readonly professional?: boolean | null + /** + * If the resource is published. We default to True unless passed in + * @type {boolean} + * @memberof VectorLearningResourcesSearchApiVectorLearningResourcesSearchRetrieve + */ + readonly published?: boolean + /** * The search text * @type {string} @@ -12490,6 +12567,7 @@ export class VectorLearningResourcesSearchApi extends BaseAPI { ) { return VectorLearningResourcesSearchApiFp(this.configuration) .vectorLearningResourcesSearchRetrieve( + requestParameters.aggregations, requestParameters.certification, requestParameters.certification_type, requestParameters.course_feature, @@ -12504,6 +12582,7 @@ export class VectorLearningResourcesSearchApi extends BaseAPI { requestParameters.offset, requestParameters.platform, requestParameters.professional, + requestParameters.published, requestParameters.q, requestParameters.readable_id, requestParameters.resource_type, @@ -12517,6 +12596,32 @@ export class VectorLearningResourcesSearchApi extends BaseAPI { } } +/** + * @export + */ +export const VectorLearningResourcesSearchRetrieveAggregationsEnum = { + ReadableId: "readable_id", + ResourceType: "resource_type", + Certification: "certification", + CertificationType: "certification_type", + Professional: "professional", + Free: "free", + CourseFeature: "course_feature", + Topic: "topic", + OcwTopic: "ocw_topic", + Level: "level", + Department: "department", + Platform: "platform", + OfferedBy: "offered_by", + Delivery: "delivery", + Title: "title", + Url: "url", + ResourceTypeGroup: "resource_type_group", + ResourceCategory: "resource_category", + Published: "published", +} as const +export type VectorLearningResourcesSearchRetrieveAggregationsEnum = + (typeof VectorLearningResourcesSearchRetrieveAggregationsEnum)[keyof typeof VectorLearningResourcesSearchRetrieveAggregationsEnum] /** * @export */ diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index ca87060fc9..392613c715 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -16443,47 +16443,43 @@ export const LearningResourcesApiAxiosParamCreator = function ( } }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ learningResourcesSimilarList: async ( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesSimilarListSortbyEnum, topic?: Array, options: RawAxiosRequestConfig = {}, ): Promise => { @@ -16540,6 +16536,10 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["limit"] = limit } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -16552,14 +16552,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["professional"] = professional } - if (readable_id) { - localVarQueryParameter["readable_id"] = readable_id - } - - if (resource_id) { - localVarQueryParameter["resource_id"] = resource_id - } - if (resource_type) { localVarQueryParameter["resource_type"] = resource_type } @@ -16568,10 +16560,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["resource_type_group"] = resource_type_group } - if (sortby !== undefined) { - localVarQueryParameter["sortby"] = sortby - } - if (topic) { localVarQueryParameter["topic"] = topic } @@ -16799,47 +16787,43 @@ export const LearningResourcesApiAxiosParamCreator = function ( } }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesVectorSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ learningResourcesVectorSimilarList: async ( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesVectorSimilarListSortbyEnum, topic?: Array, options: RawAxiosRequestConfig = {}, ): Promise => { @@ -16897,6 +16881,10 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["limit"] = limit } + if (ocw_topic) { + localVarQueryParameter["ocw_topic"] = ocw_topic + } + if (offered_by) { localVarQueryParameter["offered_by"] = offered_by } @@ -16909,14 +16897,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["professional"] = professional } - if (readable_id) { - localVarQueryParameter["readable_id"] = readable_id - } - - if (resource_id) { - localVarQueryParameter["resource_id"] = resource_id - } - if (resource_type) { localVarQueryParameter["resource_type"] = resource_type } @@ -16925,10 +16905,6 @@ export const LearningResourcesApiAxiosParamCreator = function ( localVarQueryParameter["resource_type_group"] = resource_type_group } - if (sortby !== undefined) { - localVarQueryParameter["sortby"] = sortby - } - if (topic) { localVarQueryParameter["topic"] = topic } @@ -17287,47 +17263,43 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { )(axios, operationBasePath || basePath) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ async learningResourcesSimilarList( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesSimilarListSortbyEnum, topic?: Array, options?: RawAxiosRequestConfig, ): Promise< @@ -17347,14 +17319,12 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { free, level, limit, + ocw_topic, offered_by, platform, professional, - readable_id, - resource_id, resource_type, resource_type_group, - sortby, topic, options, ) @@ -17497,47 +17467,43 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { )(axios, operationBasePath || basePath) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {number} id - * @param {boolean} [certification] - * @param {Array} [certification_type] The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @param {Array} [course_feature] Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features - * @param {Array>} [delivery] The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @param {Array} [department] The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @param {boolean} [free] The course/program is offered for free - * @param {Array} [level] The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory + * @param {boolean | null} [certification] True if the learning resource offers a certificate + * @param {Array} [certification_type] The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ + * @param {Array} [delivery] The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [free] + * @param {Array} [level] * @param {number} [limit] - * @param {Array} [offered_by] The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @param {Array} [platform] The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @param {boolean} [professional] - * @param {Array} [readable_id] A unique text identifier for the resources - * @param {Array} [resource_id] Comma-separated list of learning resource IDs - * @param {Array} [resource_type] The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @param {Array} [resource_type_group] The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @param {LearningResourcesVectorSimilarListSortbyEnum} [sortby] Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @param {Array} [topic] Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * @param {Array} [ocw_topic] The ocw topic name. + * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @param {Array} [platform] The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @param {boolean | null} [professional] + * @param {Array} [resource_type] The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @param {Array} [resource_type_group] The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @param {Array} [topic] The topic name. To see a list of options go to api/v1/topics/ * @param {*} [options] Override http request option. * @throws {RequiredError} */ async learningResourcesVectorSimilarList( id: number, - certification?: boolean, + certification?: boolean | null, certification_type?: Array, course_feature?: Array, - delivery?: Array>, + delivery?: Array, department?: Array, - free?: boolean, + free?: boolean | null, level?: Array, limit?: number, + ocw_topic?: Array, offered_by?: Array, platform?: Array, - professional?: boolean, - readable_id?: Array, - resource_id?: Array, + professional?: boolean | null, resource_type?: Array, resource_type_group?: Array, - sortby?: LearningResourcesVectorSimilarListSortbyEnum, topic?: Array, options?: RawAxiosRequestConfig, ): Promise< @@ -17557,14 +17523,12 @@ export const LearningResourcesApiFp = function (configuration?: Configuration) { free, level, limit, + ocw_topic, offered_by, platform, professional, - readable_id, - resource_id, resource_type, resource_type_group, - sortby, topic, options, ) @@ -17751,7 +17715,7 @@ export const LearningResourcesApiFactory = function ( .then((request) => request(axios, basePath)) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {LearningResourcesApiLearningResourcesSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -17772,14 +17736,12 @@ export const LearningResourcesApiFactory = function ( requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -17841,7 +17803,7 @@ export const LearningResourcesApiFactory = function ( .then((request) => request(axios, basePath)) }, /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {LearningResourcesApiLearningResourcesVectorSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -17862,14 +17824,12 @@ export const LearningResourcesApiFactory = function ( requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -18214,50 +18174,50 @@ export interface LearningResourcesApiLearningResourcesSimilarListRequest { readonly id: number /** - * + * True if the learning resource offers a certificate * @type {boolean} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly certification?: boolean + readonly certification?: boolean | null /** - * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly certification_type?: Array /** - * Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features + * The course feature. Possible options are at api/v1/course_features/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly course_feature?: Array /** - * The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @type {Array>} + * The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @type {Array<'online' | 'hybrid' | 'in_person' | 'offline'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly delivery?: Array> + readonly delivery?: Array /** - * The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @type {Array<'1' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '2' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} + * The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @type {Array<'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly department?: Array /** - * The course/program is offered for free + * * @type {boolean} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly free?: boolean + readonly free?: boolean | null /** - * The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory - * @type {Array<'advanced' | 'graduate' | 'high_school' | 'intermediate' | 'introductory' | 'noncredit' | 'undergraduate'>} + * + * @type {Array<'undergraduate' | 'graduate' | 'high_school' | 'noncredit' | 'advanced' | 'intermediate' | 'introductory'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly level?: Array @@ -18270,63 +18230,49 @@ export interface LearningResourcesApiLearningResourcesSimilarListRequest { readonly limit?: number /** - * The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @type {Array<'bootcamps' | 'climate' | 'mitpe' | 'mitx' | 'ocw' | 'see' | 'xpro'>} - * @memberof LearningResourcesApiLearningResourcesSimilarList - */ - readonly offered_by?: Array - - /** - * The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @type {Array<'bootcamps' | 'canvas' | 'climate' | 'csail' | 'ctl' | 'edx' | 'emeritus' | 'globalalumni' | 'mitpe' | 'mitxonline' | 'ocw' | 'oll' | 'ovs' | 'podcast' | 'scc' | 'see' | 'simplilearn' | 'susskind' | 'whu' | 'xpro' | 'youtube'>} + * The ocw topic name. + * @type {Array} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly platform?: Array + readonly ocw_topic?: Array /** - * - * @type {boolean} + * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see' | 'climate'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly professional?: boolean + readonly offered_by?: Array /** - * A unique text identifier for the resources - * @type {Array} + * The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @type {Array<'edx' | 'ocw' | 'oll' | 'mitxonline' | 'bootcamps' | 'xpro' | 'csail' | 'mitpe' | 'see' | 'scc' | 'ctl' | 'whu' | 'susskind' | 'globalalumni' | 'simplilearn' | 'emeritus' | 'podcast' | 'youtube' | 'canvas' | 'climate' | 'ovs'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly readable_id?: Array + readonly platform?: Array /** - * Comma-separated list of learning resource IDs - * @type {Array} + * + * @type {boolean} * @memberof LearningResourcesApiLearningResourcesSimilarList */ - readonly resource_id?: Array + readonly professional?: boolean | null /** - * The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @type {Array<'course' | 'document' | 'learning_path' | 'podcast' | 'podcast_episode' | 'program' | 'video' | 'video_playlist'>} + * The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @type {Array<'course' | 'program' | 'learning_path' | 'podcast' | 'podcast_episode' | 'video' | 'video_playlist' | 'document'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly resource_type?: Array /** - * The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @type {Array<'course' | 'learning_material' | 'program'>} + * The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @type {Array<'course' | 'program' | 'learning_material'>} * @memberof LearningResourcesApiLearningResourcesSimilarList */ readonly resource_type_group?: Array /** - * Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @type {'-id' | '-last_modified' | '-mitcoursenumber' | '-readable_id' | '-start_date' | '-views' | 'id' | 'last_modified' | 'mitcoursenumber' | 'new' | 'readable_id' | 'start_date' | 'upcoming' | 'views'} - * @memberof LearningResourcesApiLearningResourcesSimilarList - */ - readonly sortby?: LearningResourcesSimilarListSortbyEnum - - /** - * Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * The topic name. To see a list of options go to api/v1/topics/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesSimilarList */ @@ -18508,52 +18454,50 @@ export interface LearningResourcesApiLearningResourcesVectorSimilarListRequest { readonly id: number /** - * + * True if the learning resource offers a certificate * @type {boolean} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly certification?: boolean + readonly certification?: boolean | null /** - * The type of certification offered * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate - * @type {Array<'completion' | 'micromasters' | 'none' | 'professional'>} + * The type of certificate * `micromasters` - MicroMasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate + * @type {Array<'micromasters' | 'professional' | 'completion' | 'none'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly certification_type?: Array /** - * Content feature for the resources. Load the \'api/v1/course_features\' endpoint for a list of course features + * The course feature. Possible options are at api/v1/course_features/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly course_feature?: Array /** - * The delivery of course/program resources * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline - * @type {Array>} + * The delivery options in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person * `offline` - Offline + * @type {Array<'online' | 'hybrid' | 'in_person' | 'offline'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly delivery?: Array< - Array - > + readonly delivery?: Array /** - * The department that offers learning resources * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies - * @type {Array<'1' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '2' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} + * The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `SP` - Special Programs * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @type {Array<'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' | '14' | '15' | '16' | '17' | '18' | '20' | '21A' | '21G' | '21H' | '21L' | '21M' | '22' | '24' | 'CC' | 'CMS-W' | 'EC' | 'ES' | 'ESD' | 'HST' | 'IDS' | 'MAS' | 'PE' | 'SP' | 'STS' | 'WGS'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly department?: Array /** - * The course/program is offered for free + * * @type {boolean} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly free?: boolean + readonly free?: boolean | null /** - * The academic level of the resources * `undergraduate` - Undergraduate * `graduate` - Graduate * `high_school` - High School * `noncredit` - Non-Credit * `advanced` - Advanced * `intermediate` - Intermediate * `introductory` - Introductory - * @type {Array<'advanced' | 'graduate' | 'high_school' | 'intermediate' | 'introductory' | 'noncredit' | 'undergraduate'>} + * + * @type {Array<'undergraduate' | 'graduate' | 'high_school' | 'noncredit' | 'advanced' | 'intermediate' | 'introductory'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly level?: Array @@ -18566,63 +18510,49 @@ export interface LearningResourcesApiLearningResourcesVectorSimilarListRequest { readonly limit?: number /** - * The organization that offers a learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate - * @type {Array<'bootcamps' | 'climate' | 'mitpe' | 'mitx' | 'ocw' | 'see' | 'xpro'>} - * @memberof LearningResourcesApiLearningResourcesVectorSimilarList - */ - readonly offered_by?: Array - - /** - * The platform on which learning resources are offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service - * @type {Array<'bootcamps' | 'canvas' | 'climate' | 'csail' | 'ctl' | 'edx' | 'emeritus' | 'globalalumni' | 'mitpe' | 'mitxonline' | 'ocw' | 'oll' | 'ovs' | 'podcast' | 'scc' | 'see' | 'simplilearn' | 'susskind' | 'whu' | 'xpro' | 'youtube'>} + * The ocw topic name. + * @type {Array} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly platform?: Array + readonly ocw_topic?: Array /** - * - * @type {boolean} + * The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `climate` - MIT Climate + * @type {Array<'mitx' | 'ocw' | 'bootcamps' | 'xpro' | 'mitpe' | 'see' | 'climate'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly professional?: boolean + readonly offered_by?: Array /** - * A unique text identifier for the resources - * @type {Array} + * The platform on which the learning resource is offered * `edx` - edX * `ocw` - MIT OpenCourseWare * `oll` - Open Learning Library * `mitxonline` - MITx Online * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `csail` - CSAIL * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * `scc` - Schwarzman College of Computing * `ctl` - Center for Transportation & Logistics * `whu` - WHU * `susskind` - Susskind * `globalalumni` - Global Alumni * `simplilearn` - Simplilearn * `emeritus` - Emeritus * `podcast` - Podcast * `youtube` - YouTube * `canvas` - Canvas * `climate` - MIT Climate * `ovs` - ODL Video Service + * @type {Array<'edx' | 'ocw' | 'oll' | 'mitxonline' | 'bootcamps' | 'xpro' | 'csail' | 'mitpe' | 'see' | 'scc' | 'ctl' | 'whu' | 'susskind' | 'globalalumni' | 'simplilearn' | 'emeritus' | 'podcast' | 'youtube' | 'canvas' | 'climate' | 'ovs'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly readable_id?: Array + readonly platform?: Array /** - * Comma-separated list of learning resource IDs - * @type {Array} + * + * @type {boolean} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ - readonly resource_id?: Array + readonly professional?: boolean | null /** - * The type of learning resource * `course` - Course * `program` - Program * `learning_path` - Learning Path * `podcast` - Podcast * `podcast_episode` - Podcast Episode * `video` - Video * `video_playlist` - Video Playlist * `document` - Document - * @type {Array<'course' | 'document' | 'learning_path' | 'podcast' | 'podcast_episode' | 'program' | 'video' | 'video_playlist'>} + * The type of learning resource * `course` - course * `program` - program * `learning_path` - learning path * `podcast` - podcast * `podcast_episode` - podcast episode * `video` - video * `video_playlist` - video playlist * `document` - document + * @type {Array<'course' | 'program' | 'learning_path' | 'podcast' | 'podcast_episode' | 'video' | 'video_playlist' | 'document'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly resource_type?: Array /** - * The resource type group of the learning resources * `course` - Course * `program` - Program * `learning_material` - Learning Material - * @type {Array<'course' | 'learning_material' | 'program'>} + * The category of learning resource * `course` - Course * `program` - Program * `learning_material` - Learning Material + * @type {Array<'course' | 'program' | 'learning_material'>} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ readonly resource_type_group?: Array /** - * Sort By * `id` - Object ID ascending * `-id` - Object ID descending * `readable_id` - Readable ID ascending * `-readable_id` - Readable ID descending * `last_modified` - Last Modified Date ascending * `-last_modified` - Last Modified Date descending * `new` - Newest resources first * `start_date` - Start Date ascending * `-start_date` - Start Date descending * `mitcoursenumber` - MIT course number ascending * `-mitcoursenumber` - MIT course number descending * `views` - Popularity ascending * `-views` - Popularity descending * `upcoming` - Next start date ascending - * @type {'-id' | '-last_modified' | '-mitcoursenumber' | '-readable_id' | '-start_date' | '-views' | 'id' | 'last_modified' | 'mitcoursenumber' | 'new' | 'readable_id' | 'start_date' | 'upcoming' | 'views'} - * @memberof LearningResourcesApiLearningResourcesVectorSimilarList - */ - readonly sortby?: LearningResourcesVectorSimilarListSortbyEnum - - /** - * Topics covered by the resources. Load the \'/api/v1/topics\' endpoint for a list of topics + * The topic name. To see a list of options go to api/v1/topics/ * @type {Array} * @memberof LearningResourcesApiLearningResourcesVectorSimilarList */ @@ -18806,7 +18736,7 @@ export class LearningResourcesApi extends BaseAPI { } /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources * @param {LearningResourcesApiLearningResourcesSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -18828,14 +18758,12 @@ export class LearningResourcesApi extends BaseAPI { requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -18902,7 +18830,7 @@ export class LearningResourcesApi extends BaseAPI { } /** - * Fetch similar learning resources Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter + * Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource Returns: QuerySet of similar LearningResource for the resource matching the id parameter * @summary Get similar resources using vector embeddings * @param {LearningResourcesApiLearningResourcesVectorSimilarListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. @@ -18924,14 +18852,12 @@ export class LearningResourcesApi extends BaseAPI { requestParameters.free, requestParameters.level, requestParameters.limit, + requestParameters.ocw_topic, requestParameters.offered_by, requestParameters.platform, requestParameters.professional, - requestParameters.readable_id, - requestParameters.resource_id, requestParameters.resource_type, requestParameters.resource_type_group, - requestParameters.sortby, requestParameters.topic, options, ) @@ -19153,10 +19079,10 @@ export type LearningResourcesListSortbyEnum = * @export */ export const LearningResourcesSimilarListCertificationTypeEnum = { - Completion: "completion", Micromasters: "micromasters", - None: "none", Professional: "professional", + Completion: "completion", + None: "none", } as const export type LearningResourcesSimilarListCertificationTypeEnum = (typeof LearningResourcesSimilarListCertificationTypeEnum)[keyof typeof LearningResourcesSimilarListCertificationTypeEnum] @@ -19176,6 +19102,14 @@ export type LearningResourcesSimilarListDeliveryEnum = */ export const LearningResourcesSimilarListDepartmentEnum = { _1: "1", + _2: "2", + _3: "3", + _4: "4", + _5: "5", + _6: "6", + _7: "7", + _8: "8", + _9: "9", _10: "10", _11: "11", _12: "12", @@ -19184,7 +19118,6 @@ export const LearningResourcesSimilarListDepartmentEnum = { _16: "16", _17: "17", _18: "18", - _2: "2", _20: "20", _21A: "21A", _21G: "21G", @@ -19193,13 +19126,6 @@ export const LearningResourcesSimilarListDepartmentEnum = { _21M: "21M", _22: "22", _24: "24", - _3: "3", - _4: "4", - _5: "5", - _6: "6", - _7: "7", - _8: "8", - _9: "9", Cc: "CC", CmsW: "CMS-W", Ec: "EC", @@ -19219,13 +19145,13 @@ export type LearningResourcesSimilarListDepartmentEnum = * @export */ export const LearningResourcesSimilarListLevelEnum = { - Advanced: "advanced", + Undergraduate: "undergraduate", Graduate: "graduate", HighSchool: "high_school", + Noncredit: "noncredit", + Advanced: "advanced", Intermediate: "intermediate", Introductory: "introductory", - Noncredit: "noncredit", - Undergraduate: "undergraduate", } as const export type LearningResourcesSimilarListLevelEnum = (typeof LearningResourcesSimilarListLevelEnum)[keyof typeof LearningResourcesSimilarListLevelEnum] @@ -19233,13 +19159,13 @@ export type LearningResourcesSimilarListLevelEnum = * @export */ export const LearningResourcesSimilarListOfferedByEnum = { - Bootcamps: "bootcamps", - Climate: "climate", - Mitpe: "mitpe", Mitx: "mitx", Ocw: "ocw", - See: "see", + Bootcamps: "bootcamps", Xpro: "xpro", + Mitpe: "mitpe", + See: "see", + Climate: "climate", } as const export type LearningResourcesSimilarListOfferedByEnum = (typeof LearningResourcesSimilarListOfferedByEnum)[keyof typeof LearningResourcesSimilarListOfferedByEnum] @@ -19247,27 +19173,27 @@ export type LearningResourcesSimilarListOfferedByEnum = * @export */ export const LearningResourcesSimilarListPlatformEnum = { - Bootcamps: "bootcamps", - Canvas: "canvas", - Climate: "climate", - Csail: "csail", - Ctl: "ctl", Edx: "edx", - Emeritus: "emeritus", - Globalalumni: "globalalumni", - Mitpe: "mitpe", - Mitxonline: "mitxonline", Ocw: "ocw", Oll: "oll", - Ovs: "ovs", - Podcast: "podcast", - Scc: "scc", + Mitxonline: "mitxonline", + Bootcamps: "bootcamps", + Xpro: "xpro", + Csail: "csail", + Mitpe: "mitpe", See: "see", - Simplilearn: "simplilearn", - Susskind: "susskind", + Scc: "scc", + Ctl: "ctl", Whu: "whu", - Xpro: "xpro", + Susskind: "susskind", + Globalalumni: "globalalumni", + Simplilearn: "simplilearn", + Emeritus: "emeritus", + Podcast: "podcast", Youtube: "youtube", + Canvas: "canvas", + Climate: "climate", + Ovs: "ovs", } as const export type LearningResourcesSimilarListPlatformEnum = (typeof LearningResourcesSimilarListPlatformEnum)[keyof typeof LearningResourcesSimilarListPlatformEnum] @@ -19276,13 +19202,13 @@ export type LearningResourcesSimilarListPlatformEnum = */ export const LearningResourcesSimilarListResourceTypeEnum = { Course: "course", - Document: "document", + Program: "program", LearningPath: "learning_path", Podcast: "podcast", PodcastEpisode: "podcast_episode", - Program: "program", Video: "video", VideoPlaylist: "video_playlist", + Document: "document", } as const export type LearningResourcesSimilarListResourceTypeEnum = (typeof LearningResourcesSimilarListResourceTypeEnum)[keyof typeof LearningResourcesSimilarListResourceTypeEnum] @@ -19291,35 +19217,14 @@ export type LearningResourcesSimilarListResourceTypeEnum = */ export const LearningResourcesSimilarListResourceTypeGroupEnum = { Course: "course", - LearningMaterial: "learning_material", Program: "program", + LearningMaterial: "learning_material", } as const export type LearningResourcesSimilarListResourceTypeGroupEnum = (typeof LearningResourcesSimilarListResourceTypeGroupEnum)[keyof typeof LearningResourcesSimilarListResourceTypeGroupEnum] /** * @export */ -export const LearningResourcesSimilarListSortbyEnum = { - Id: "-id", - LastModified: "-last_modified", - Mitcoursenumber: "-mitcoursenumber", - ReadableId: "-readable_id", - StartDate: "-start_date", - Views: "-views", - Id2: "id", - LastModified2: "last_modified", - Mitcoursenumber2: "mitcoursenumber", - New: "new", - ReadableId2: "readable_id", - StartDate2: "start_date", - Upcoming: "upcoming", - Views2: "views", -} as const -export type LearningResourcesSimilarListSortbyEnum = - (typeof LearningResourcesSimilarListSortbyEnum)[keyof typeof LearningResourcesSimilarListSortbyEnum] -/** - * @export - */ export const LearningResourcesSummaryListCertificationTypeEnum = { Completion: "completion", Micromasters: "micromasters", @@ -19489,10 +19394,10 @@ export type LearningResourcesSummaryListSortbyEnum = * @export */ export const LearningResourcesVectorSimilarListCertificationTypeEnum = { - Completion: "completion", Micromasters: "micromasters", - None: "none", Professional: "professional", + Completion: "completion", + None: "none", } as const export type LearningResourcesVectorSimilarListCertificationTypeEnum = (typeof LearningResourcesVectorSimilarListCertificationTypeEnum)[keyof typeof LearningResourcesVectorSimilarListCertificationTypeEnum] @@ -19512,6 +19417,14 @@ export type LearningResourcesVectorSimilarListDeliveryEnum = */ export const LearningResourcesVectorSimilarListDepartmentEnum = { _1: "1", + _2: "2", + _3: "3", + _4: "4", + _5: "5", + _6: "6", + _7: "7", + _8: "8", + _9: "9", _10: "10", _11: "11", _12: "12", @@ -19520,7 +19433,6 @@ export const LearningResourcesVectorSimilarListDepartmentEnum = { _16: "16", _17: "17", _18: "18", - _2: "2", _20: "20", _21A: "21A", _21G: "21G", @@ -19529,13 +19441,6 @@ export const LearningResourcesVectorSimilarListDepartmentEnum = { _21M: "21M", _22: "22", _24: "24", - _3: "3", - _4: "4", - _5: "5", - _6: "6", - _7: "7", - _8: "8", - _9: "9", Cc: "CC", CmsW: "CMS-W", Ec: "EC", @@ -19555,13 +19460,13 @@ export type LearningResourcesVectorSimilarListDepartmentEnum = * @export */ export const LearningResourcesVectorSimilarListLevelEnum = { - Advanced: "advanced", + Undergraduate: "undergraduate", Graduate: "graduate", HighSchool: "high_school", + Noncredit: "noncredit", + Advanced: "advanced", Intermediate: "intermediate", Introductory: "introductory", - Noncredit: "noncredit", - Undergraduate: "undergraduate", } as const export type LearningResourcesVectorSimilarListLevelEnum = (typeof LearningResourcesVectorSimilarListLevelEnum)[keyof typeof LearningResourcesVectorSimilarListLevelEnum] @@ -19569,13 +19474,13 @@ export type LearningResourcesVectorSimilarListLevelEnum = * @export */ export const LearningResourcesVectorSimilarListOfferedByEnum = { - Bootcamps: "bootcamps", - Climate: "climate", - Mitpe: "mitpe", Mitx: "mitx", Ocw: "ocw", - See: "see", + Bootcamps: "bootcamps", Xpro: "xpro", + Mitpe: "mitpe", + See: "see", + Climate: "climate", } as const export type LearningResourcesVectorSimilarListOfferedByEnum = (typeof LearningResourcesVectorSimilarListOfferedByEnum)[keyof typeof LearningResourcesVectorSimilarListOfferedByEnum] @@ -19583,27 +19488,27 @@ export type LearningResourcesVectorSimilarListOfferedByEnum = * @export */ export const LearningResourcesVectorSimilarListPlatformEnum = { - Bootcamps: "bootcamps", - Canvas: "canvas", - Climate: "climate", - Csail: "csail", - Ctl: "ctl", Edx: "edx", - Emeritus: "emeritus", - Globalalumni: "globalalumni", - Mitpe: "mitpe", - Mitxonline: "mitxonline", Ocw: "ocw", Oll: "oll", - Ovs: "ovs", - Podcast: "podcast", - Scc: "scc", + Mitxonline: "mitxonline", + Bootcamps: "bootcamps", + Xpro: "xpro", + Csail: "csail", + Mitpe: "mitpe", See: "see", - Simplilearn: "simplilearn", - Susskind: "susskind", + Scc: "scc", + Ctl: "ctl", Whu: "whu", - Xpro: "xpro", + Susskind: "susskind", + Globalalumni: "globalalumni", + Simplilearn: "simplilearn", + Emeritus: "emeritus", + Podcast: "podcast", Youtube: "youtube", + Canvas: "canvas", + Climate: "climate", + Ovs: "ovs", } as const export type LearningResourcesVectorSimilarListPlatformEnum = (typeof LearningResourcesVectorSimilarListPlatformEnum)[keyof typeof LearningResourcesVectorSimilarListPlatformEnum] @@ -19612,13 +19517,13 @@ export type LearningResourcesVectorSimilarListPlatformEnum = */ export const LearningResourcesVectorSimilarListResourceTypeEnum = { Course: "course", - Document: "document", + Program: "program", LearningPath: "learning_path", Podcast: "podcast", PodcastEpisode: "podcast_episode", - Program: "program", Video: "video", VideoPlaylist: "video_playlist", + Document: "document", } as const export type LearningResourcesVectorSimilarListResourceTypeEnum = (typeof LearningResourcesVectorSimilarListResourceTypeEnum)[keyof typeof LearningResourcesVectorSimilarListResourceTypeEnum] @@ -19627,32 +19532,11 @@ export type LearningResourcesVectorSimilarListResourceTypeEnum = */ export const LearningResourcesVectorSimilarListResourceTypeGroupEnum = { Course: "course", - LearningMaterial: "learning_material", Program: "program", + LearningMaterial: "learning_material", } as const export type LearningResourcesVectorSimilarListResourceTypeGroupEnum = (typeof LearningResourcesVectorSimilarListResourceTypeGroupEnum)[keyof typeof LearningResourcesVectorSimilarListResourceTypeGroupEnum] -/** - * @export - */ -export const LearningResourcesVectorSimilarListSortbyEnum = { - Id: "-id", - LastModified: "-last_modified", - Mitcoursenumber: "-mitcoursenumber", - ReadableId: "-readable_id", - StartDate: "-start_date", - Views: "-views", - Id2: "id", - LastModified2: "last_modified", - Mitcoursenumber2: "mitcoursenumber", - New: "new", - ReadableId2: "readable_id", - StartDate2: "start_date", - Upcoming: "upcoming", - Views2: "views", -} as const -export type LearningResourcesVectorSimilarListSortbyEnum = - (typeof LearningResourcesVectorSimilarListSortbyEnum)[keyof typeof LearningResourcesVectorSimilarListSortbyEnum] /** * LearningResourcesSearchApi - axios parameter creator diff --git a/frontends/api/src/mitxonline/hooks/enrollment/index.ts b/frontends/api/src/mitxonline/hooks/enrollment/index.ts index 97398dfef6..81fa057658 100644 --- a/frontends/api/src/mitxonline/hooks/enrollment/index.ts +++ b/frontends/api/src/mitxonline/hooks/enrollment/index.ts @@ -72,6 +72,27 @@ const useDestroyEnrollment = () => { }) } +const useDestroyProgramEnrollment = () => { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (programId: number) => + programEnrollmentsApi.v3ProgramEnrollmentsDestroy({ + program_id: programId, + }), + onSuccess: (_data, vars) => { + queryClient.setQueryData( + enrollmentQueries.programEnrollmentsList().queryKey, + (data) => data?.filter((enrollment) => enrollment.program.id !== vars), + ) + }, + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: enrollmentKeys.programEnrollmentsList(), + }) + }, + }) +} + const useCreateProgramEnrollment = () => { const queryClient = useQueryClient() return useMutation({ @@ -110,6 +131,7 @@ export { useCreateEnrollment, useUpdateEnrollment, useDestroyEnrollment, + useDestroyProgramEnrollment, useCreateProgramEnrollment, useCreateVerifiedProgramEnrollment, } diff --git a/frontends/api/src/mitxonline/test-utils/urls.ts b/frontends/api/src/mitxonline/test-utils/urls.ts index 598d86ca9d..06c09475c8 100644 --- a/frontends/api/src/mitxonline/test-utils/urls.ts +++ b/frontends/api/src/mitxonline/test-utils/urls.ts @@ -27,6 +27,8 @@ const enrollment = { const programEnrollments = { enrollmentsListV3: () => `${API_BASE_URL}/api/v3/program_enrollments/`, + programEnrollment: (programId: number) => + `${API_BASE_URL}/api/v3/program_enrollments/${programId}/`, } const b2b = { diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx index 84ca2d3d0b..e0c33932e8 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx @@ -25,6 +25,7 @@ import { EmailSettingsDialog, JustInTimeDialog, UnenrollDialog, + UnenrollProgramDialog, } from "./DashboardDialogs" import NiceModal from "@ebay/nice-modal-react" import { @@ -47,6 +48,7 @@ import { CourseRunEnrollmentV3, V3UserProgramEnrollment, CourseRunV2, + DisplayModeEnum, } from "@mitodl/mitxonline-api-axios/v2" import CourseEnrollmentDialog from "@/page-components/EnrollmentDialogs/CourseEnrollmentDialog" @@ -196,6 +198,23 @@ const getContextMenuItems = ( href: detailsUrl, }) } + + if ( + program.display_mode !== DisplayModeEnum.Course && + !isVerifiedEnrollmentMode(resource.data.enrollment_mode) + ) { + menuItems.push({ + className: "dashboard-card-menu-item", + key: "unenroll-program", + label: "Unenroll", + onClick: () => { + NiceModal.show(UnenrollProgramDialog, { + title, + programId: program.id, + }) + }, + }) + } } if (resource.type === DashboardType.CourseRunEnrollment) { const detailsUrl = useProductPages diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx index de52fcf48e..93687b1aff 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.test.tsx @@ -147,6 +147,191 @@ describe("DashboardDialogs", () => { }) }) +describe("UnenrollProgramDialog", () => { + const setupProgramCard = ( + enrollmentMode: string | null = "audit", + displayMode: string | null = null, + ) => { + const mitxOnlineUser = mitxonline.factories.user.user() + setMockResponse.get(mitxonline.urls.userMe.get(), mitxOnlineUser) + + const programEnrollment = + mitxonline.factories.enrollment.programEnrollmentV3({ + enrollment_mode: enrollmentMode, + program: { display_mode: displayMode } as never, + }) + + return { programEnrollment } + } + + test("Shows unenroll option for free (audit) program enrollments", async () => { + const { programEnrollment } = setupProgramCard("audit", null) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + expect( + await screen.findByRole("menuitem", { name: "Unenroll" }), + ).toBeInTheDocument() + }) + + test("Does not show unenroll option for paid (verified) program enrollments", async () => { + const { programEnrollment } = setupProgramCard("verified", null) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + expect( + screen.queryByRole("menuitem", { name: "Unenroll" }), + ).not.toBeInTheDocument() + }) + + test("Does not show unenroll option for program-as-course display_mode programs", async () => { + const { programEnrollment } = setupProgramCard("audit", "course") + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + expect( + screen.queryByRole("menuitem", { name: "Unenroll" }), + ).not.toBeInTheDocument() + }) + + test("Confirming unenroll from a program fires the proper API call", async () => { + const { programEnrollment } = setupProgramCard("audit", null) + + setMockResponse.delete( + mitxonline.urls.programEnrollments.programEnrollment( + programEnrollment.program.id, + ), + null, + ) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + const unenrollMenuItem = await screen.findByRole("menuitem", { + name: "Unenroll", + }) + await user.click(unenrollMenuItem) + + const dialog = await screen.findByRole("dialog", { + name: `Unenroll from ${programEnrollment.program.title}`, + }) + expect(dialog).toBeInTheDocument() + expect( + within(dialog).getByText( + `Are you sure you want to unenroll from ${programEnrollment.program.title}?`, + ), + ).toBeInTheDocument() + + const confirmButton = within(dialog).getByRole("button", { + name: "Unenroll", + }) + await user.click(confirmButton) + + expect(mockAxiosInstance.request).toHaveBeenCalledWith( + expect.objectContaining({ + method: "DELETE", + url: mitxonline.urls.programEnrollments.programEnrollment( + programEnrollment.program.id, + ), + }), + ) + }) + + test("Cancelling the dialog does not fire the API call", async () => { + const { programEnrollment } = setupProgramCard("audit", null) + + renderWithProviders( + , + ) + + const desktopCard = await screen.findByTestId("enrollment-card-desktop") + const contextMenuButton = within(desktopCard).getByLabelText("More options") + await user.click(contextMenuButton) + + await user.click(await screen.findByRole("menuitem", { name: "Unenroll" })) + await screen.findByRole("dialog", { + name: `Unenroll from ${programEnrollment.program.title}`, + }) + + await user.click(screen.getByRole("button", { name: "Cancel" })) + + expect(mockAxiosInstance.request).not.toHaveBeenCalledWith( + expect.objectContaining({ method: "DELETE" }), + ) + }) + + test.each(["enrollment-card-desktop", "enrollment-card-mobile"] as const)( + "Unenroll option is accessible from the %s overflow menu", + async (cardTestId) => { + const { programEnrollment } = setupProgramCard("audit", null) + + renderWithProviders( + , + ) + + const card = await screen.findByTestId(cardTestId) + await user.click(within(card).getByLabelText("More options")) + + expect( + await screen.findByRole("menuitem", { name: "Unenroll" }), + ).toBeInTheDocument() + }, + ) +}) + describe("JustInTimeDialog", () => { const getFields = (root: HTMLElement) => { return { diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx index 5d62d27526..5568084752 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardDialogs.tsx @@ -17,6 +17,7 @@ import { useFormik } from "formik" import { useCreateB2bEnrollment, useDestroyEnrollment, + useDestroyProgramEnrollment, useUpdateEnrollment, } from "api/mitxonline-hooks/enrollment" import { @@ -318,4 +319,77 @@ const EmailSettingsDialog = NiceModal.create(EmailSettingsDialogInner) const UnenrollDialog = NiceModal.create(UnenrollDialogInner) const JustInTimeDialog = NiceModal.create(JustInTimeDialogInner) -export { EmailSettingsDialog, UnenrollDialog, JustInTimeDialog } +type UnenrollProgramDialogProps = { + title: string + programId: number +} + +const UnenrollProgramDialogInner: React.FC = ({ + title, + programId, +}) => { + const modal = NiceModal.useModal() + const destroyProgramEnrollment = useDestroyProgramEnrollment() + const formik = useFormik({ + enableReinitialize: true, + validateOnChange: false, + validateOnBlur: false, + initialValues: {}, + onSubmit: async () => { + await destroyProgramEnrollment.mutateAsync(programId) + modal.hide() + }, + }) + return ( + + + + + } + > + + Are you sure you want to unenroll from {title}? + + {destroyProgramEnrollment.isError && ( + + There was a problem unenrolling you from this program. Please try + again later. + + )} + + ) +} + +const UnenrollProgramDialog = NiceModal.create(UnenrollProgramDialogInner) + +export { + EmailSettingsDialog, + UnenrollDialog, + UnenrollProgramDialog, + JustInTimeDialog, +} diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx index 14661062a5..c357d3f719 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/EnrollmentDisplay.tsx @@ -14,7 +14,7 @@ import { theme, } from "ol-components" import { Alert } from "@mitodl/smoot-design" -import { useQuery } from "@tanstack/react-query" +import { keepPreviousData, useQuery } from "@tanstack/react-query" import { EnrollmentStatus, getEnrollmentStatus, @@ -790,6 +790,9 @@ const AllEnrollmentsDisplay: React.FC = () => { page_size: enrolledProgramIds.length, }), enabled: enrolledProgramIds.length > 0, + // If the query key changes, show the old data while loading + // example: Deleting a program enrollment + placeholderData: keepPreviousData, }) const filteredProgramEnrollments = enrolledPrograms @@ -820,6 +823,7 @@ const AllEnrollmentsDisplay: React.FC = () => { page_size: homeCourseProgramModuleIds.length || undefined, }), enabled: homeCourseProgramModuleIds.length > 0, + placeholderData: keepPreviousData, }) const homeCourseProgramsById = new Map( diff --git a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx index fe14c1c100..028bde95ed 100644 --- a/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx +++ b/frontends/main/src/app-pages/SearchPage/SearchPage.test.tsx @@ -152,50 +152,6 @@ describe("SearchPage", () => { }, ) - test("Vector Hybrid Search passes correct params and hides count", async () => { - setMockApiResponses({ - search: { - count: 700, - metadata: { - aggregations: { - resource_type_group: [{ key: "course", doc_count: 100 }], - }, - suggestions: [], - }, - results: factories.learningResources.resources({ count: 5 }).results, - }, - }) - - // Authenticate as path editor (admin) - setMockResponse.get(urls.userMe.get(), { - is_learning_path_editor: true, - is_authenticated: true, - }) - - renderWithProviders(, { url: "?vector_search=true&q=test" }) - - await waitFor(() => { - const call = makeRequest.mock.calls.find(([_method, url]) => { - return url.includes(urls.search.vectorResources()) - }) - expect(call).toBeDefined() - }) - - const call = makeRequest.mock.calls.find(([_method, url]) => - url.includes(urls.search.vectorResources()), - ) - invariant(call) - const fullUrl = new URL(call[1], "http://mit.edu") - const apiSearchParams = fullUrl.searchParams - - expect(apiSearchParams.get("hybrid_search")).toBe("true") - expect(apiSearchParams.get("q")).toBe("test") - - // Ensure count is hidden - const hideCountText = screen.queryByText("700 results") - expect(hideCountText).toBeNull() - }) - test("Toggling facets", async () => { setMockApiResponses({ search: { @@ -1060,4 +1016,61 @@ describe("UniversalAIBanner", () => { expect(screen.queryByText("Universal AI")).not.toBeInTheDocument() expect(screen.queryByText("New on MIT Learn")).not.toBeInTheDocument() }) + + test("Vector Hybrid Search passes correct params and renders expected count/facets", async () => { + setMockApiResponses({ + search: { + count: 700, + metadata: { + aggregations: { + resource_type_group: [{ key: "course", doc_count: 100 }], + }, + suggestions: [], + }, + results: factories.learningResources.resources({ count: 5 }).results, + }, + }) + + // Authenticate as path editor (admin) + setMockResponse.get(urls.userMe.get(), { + is_learning_path_editor: true, + is_authenticated: true, + }) + + renderWithProviders(, { url: "?vector_search=true&q=test" }) + + await waitFor(() => { + const call = makeRequest.mock.calls.find(([_method, url]) => { + return url.includes(urls.search.vectorResources()) + }) + expect(call).toBeDefined() + }) + + const call = makeRequest.mock.calls.find(([_method, url]) => + url.includes(urls.search.vectorResources()), + ) + invariant(call) + const fullUrl = new URL(call[1], "http://mit.edu") + const apiSearchParams = fullUrl.searchParams + + expect(apiSearchParams.get("hybrid_search")).toBe("true") + expect(apiSearchParams.get("q")).toBe("test") + + // Ensure count is visible + const countText = await screen.findByText("700 results") + expect(countText).toBeVisible() + + // Ensure facets are visible + await waitFor(() => { + const tabs = screen.getAllByRole("tab") + expect( + tabs.map((tab) => (tab.textContent || "").replace(/\s/g, "")), + ).toEqual([ + "All(100)", + "Courses(100)", + "Programs(0)", + "LearningMaterials(0)", + ]) + }) + }) }) diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx index 3ba3d82805..f329052083 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/FeaturedVideo.tsx @@ -19,7 +19,7 @@ const FeaturedGrid = styled.div(({ theme }) => ({ display: "grid", gridTemplateColumns: "3.95fr 4.6fr", columnGap: "40px", - alignItems: "flex-start", + alignItems: "center", [theme.breakpoints.down("sm")]: { gridTemplateColumns: "1fr", }, @@ -60,14 +60,10 @@ const FeaturedTitle = styled.h2(({ theme }) => ({ fontWeight: theme.typography.fontWeightBold, color: theme.custom.colors.darkGray2, letterSpacing: "-1.28px", - lineHeight: "110%", + lineHeight: "120%", margin: "0 0 16px", cursor: "pointer", - fontSize: "64px", - overflow: "hidden", - display: "-webkit-box", - WebkitLineClamp: 2, - WebkitBoxOrient: "vertical", + fontSize: "48px", transition: "color 0.2s", "& .mobile-title": { display: "none", diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx index 41429b7b76..386f04429b 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoCard.tsx @@ -22,11 +22,11 @@ const VideoCardItem = styled(Link)({ }, "&:hover .play-overlay": { - opacity: 1, + opacity: 0.5, }, "&:focus-visible .play-overlay": { - opacity: 1, + opacity: 0.5, }, [theme.breakpoints.down("sm")]: { @@ -108,8 +108,8 @@ const CardTitle = styled(Typography)(({ theme }) => ({ })) const PlayIcon = styled(RiPlayCircleFill)({ - width: 48, - height: 48, + width: 36, + height: 36, }) const CardMetaRow = styled.div({ diff --git a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx index daff1d30a4..2cbde3d7f3 100644 --- a/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx +++ b/frontends/main/src/app-pages/VideoPlaylistCollectionPage/VideoDetailPage.tsx @@ -7,7 +7,7 @@ import Image from "next/image" import { useFeatureFlagEnabled } from "posthog-js/react" import { Typography, styled, theme, Skeleton, Breadcrumbs } from "ol-components" import VideoContainer from "./VideoContainer" -import { RiShareForwardFill } from "@remixicon/react" +import { RiShareForwardFill, RiPlayCircleFill } from "@remixicon/react" import { useQuery } from "@tanstack/react-query" import { useLearningResourcesDetail, @@ -76,6 +76,11 @@ const StyledBreadcrumbs = styled(Breadcrumbs)(() => ({ "& > span > span": { paddingBottom: 0, paddingLeft: "4px" }, })) +const PlayIcon = styled(RiPlayCircleFill)({ + width: 36, + height: 36, +}) + const ContentArea = styled.div(({ theme }) => ({ padding: "56px 0 80px", [theme.breakpoints.down("sm")]: { @@ -237,6 +242,18 @@ const MoreFromItem = styled(Link)({ textDecoration: "none", "&:hover .mf-title": { color: theme.custom.colors.red }, + "&:hover .video-card-title, &:focus-visible .video-card-title": { + color: theme.custom.colors.red, + }, + + "&:hover .play-overlay": { + opacity: 0.5, + }, + + "&:focus-visible .play-overlay": { + opacity: 0.5, + }, + "&:first-child": { padding: "0 0 24px 0", }, @@ -346,6 +363,18 @@ const DurationText = styled.span(({ theme }) => ({ fontWeight: theme.typography.fontWeightBold, })) +const PlayOverlay = styled.div({ + position: "absolute", + inset: 0, + display: "flex", + alignItems: "center", + justifyContent: "center", + color: "#fff", + opacity: 0, + transition: "opacity 0.2s", + backgroundColor: "rgba(0, 0, 0, 0.18)", +}) + const ScreenReaderOnly = styled.span({ position: "absolute", width: 1, @@ -660,6 +689,9 @@ const VideoDetailPage: React.FC = ({ {itemDuration && ( {itemDuration} )} + + + diff --git a/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx index 2cf28c753e..e0b53a8ee8 100644 --- a/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/main/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -516,8 +516,8 @@ const searchModeDropdownOptions = Object.entries( /** * Extracts only the fields supported by the vector search API from a broader - * search params object, dropping admin-only params (e.g., aggregations, - * content_file_score_weight) that the vector endpoint does not accept. + * search params object, dropping admin-only params (e.g., content_file_score_weight) + * that the vector endpoint does not accept. * * The `as` casts for enum arrays are safe because the v0 and v1 generated * clients define separate (but structurally identical) enum types for the same @@ -526,6 +526,7 @@ const searchModeDropdownOptions = Object.entries( const toVectorSearchParams = ( params: ReturnType, ): VectorSearchRequest => ({ + aggregations: params.aggregations as VectorSearchRequest["aggregations"], certification: params.certification, certification_type: params.certification_type as VectorSearchRequest["certification_type"], @@ -625,10 +626,13 @@ const SearchDisplay: React.FC = ({ const wantsVectorSearch = searchParams.get("vector_search") === "true" const isVectorSearch = wantsVectorSearch && user?.is_learning_path_editor + const queryOptions = isVectorSearch + ? learningResourceQueries.vectorSearch(toVectorSearchParams(allParams)) + : learningResourceQueries.search(allParams as LRSearchRequest) + + // @ts-expect-error Typescript has trouble unifying the different query key types const { data, isLoading, isFetching } = useQuery({ - ...(isVectorSearch - ? learningResourceQueries.vectorSearch(toVectorSearchParams(allParams)) - : learningResourceQueries.search(allParams as LRSearchRequest)), + ...queryOptions, enabled: !wantsVectorSearch || !isUserLoading, placeholderData: keepPreviousData, select: (timedData: { @@ -985,9 +989,7 @@ const SearchDisplay: React.FC = ({ * the count when data is loaded even if count is same as previous * count. */} - {isFetching || isLoading || isVectorSearch - ? "" - : `${data?.count} results`} + {isFetching || isLoading ? "" : `${data?.count} results`} diff --git a/learning_resources/views.py b/learning_resources/views.py index 9d43d1aeb0..e4e96dc12b 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -109,6 +109,7 @@ is_admin_user, ) from main.utils import cache_page_for_all_users, cache_page_for_anonymous_users, chunks +from vector_search.serializers import LearningResourcesSearchFiltersSerializer def show_content_file_content(user): @@ -130,6 +131,10 @@ def show_content_file_content(user): log = logging.getLogger(__name__) +def _clean_filter_params(params: dict) -> dict: + return {k: v for k, v in params.items() if v not in (None, [], "")} + + @extend_schema_view( list=extend_schema( summary="List", @@ -204,11 +209,18 @@ class LearningResourceViewSet( resource_type_name_plural = "Learning Resources" serializer_class = LearningResourceSerializer + @property + def filter_backends(self): + if self.action in ("similar", "vector_similar"): + return [] + return super().filter_backends + @extend_schema( summary="Get similar resources", parameters=[ OpenApiParameter(name="id", type=int, location=OpenApiParameter.PATH), OpenApiParameter(name="limit", type=int, location=OpenApiParameter.QUERY), + LearningResourcesSearchFiltersSerializer, ], responses=LearningResourceSerializer(many=True), ) @@ -225,7 +237,7 @@ class LearningResourceViewSet( ) def similar(self, request, *_, **kwargs): """ - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource @@ -235,13 +247,21 @@ def similar(self, request, *_, **kwargs): """ limit = int(request.GET.get("limit", 12)) pk = int(kwargs.get("id")) - learning_resource = get_object_or_404(LearningResource, id=pk) - learning_resource = LearningResource.objects.for_search_serialization().get( - id=pk + filter_serializer = LearningResourcesSearchFiltersSerializer(data=request.GET) + filter_serializer.is_valid(raise_exception=True) + filter_params = _clean_filter_params(filter_serializer.validated_data) + learning_resource = get_object_or_404( + LearningResource.objects.for_search_serialization(), + id=pk, ) resource_data = serialize_learning_resource_for_update(learning_resource) similar = get_similar_resources( - resource_data, limit, 2, 3, use_embeddings=False + resource_data, + limit, + 2, + 3, + use_embeddings=False, + filter_params=filter_params, ) return Response(LearningResourceSerializer(list(similar), many=True).data) @@ -250,6 +270,7 @@ def similar(self, request, *_, **kwargs): parameters=[ OpenApiParameter(name="id", type=int, location=OpenApiParameter.PATH), OpenApiParameter(name="limit", type=int, location=OpenApiParameter.QUERY), + LearningResourcesSearchFiltersSerializer, ], responses=LearningResourceSerializer(many=True), ) @@ -268,7 +289,7 @@ def similar(self, request, *_, **kwargs): ) def vector_similar(self, request, *_, **kwargs): """ - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource @@ -279,6 +300,10 @@ def vector_similar(self, request, *_, **kwargs): limit = int(request.GET.get("limit", 12)) pk = int(kwargs.get("id")) + filter_serializer = LearningResourcesSearchFiltersSerializer(data=request.GET) + filter_serializer.is_valid(raise_exception=True) + filter_params = _clean_filter_params(filter_serializer.validated_data) + try: learning_resource = LearningResource.objects.for_search_serialization().get( id=pk @@ -289,7 +314,12 @@ def vector_similar(self, request, *_, **kwargs): resource_data = serialize_learning_resource_for_update(learning_resource) try: similar = get_similar_resources( - resource_data, limit, 2, 3, use_embeddings=True + resource_data, + limit, + 2, + 3, + use_embeddings=True, + filter_params=filter_params, ) return Response(LearningResourceSerializer(list(similar), many=True).data) except _InactiveRpcError as ircp: diff --git a/learning_resources/views_test.py b/learning_resources/views_test.py index e145c54b8a..dcb750fa61 100644 --- a/learning_resources/views_test.py +++ b/learning_resources/views_test.py @@ -1318,6 +1318,130 @@ def test_vector_similar_resources_endpoint_only_returns_published(mocker, client assert len(response_ids) == 1 +def test_vector_similar_passes_resource_type_filter_to_qdrant(mocker, client): + """resource_type query param is translated to a Qdrant filter""" + from qdrant_client import models as qdrant_models + + from learning_resources.models import LearningResource + + resources = LearningResourceFactory.create_batch(3) + similar_for = resources[0].id + mocker.patch( + "vector_search.utils.qdrant_client", + return_value=QdrantClient( + host="hidden_port_addr.com", + port=None, + prefix="custom", + check_compatibility=False, + ), + ) + mock_similar = mocker.patch( + "learning_resources_search.api._qdrant_similar_results", + return_value=[ + serialize_learning_resource_for_update(lr) + for lr in LearningResource.objects.for_search_serialization().filter( + id__in=[r.id for r in resources[1:]] + ) + ], + ) + + client.get( + reverse("lr:v1:learning_resources_api-vector-similar", args=[similar_for]), + {"resource_type": "video_playlist"}, + ) + + query_filter = mock_similar.call_args.kwargs["query_filter"] + assert query_filter is not None + assert any( + isinstance(c, qdrant_models.FieldCondition) + and c.key == "resource_type" + and c.match.any == ["video_playlist"] + for c in query_filter.must + ) + + +def test_vector_similar_rejects_invalid_resource_type(mocker, client): + """Unknown resource_type value returns 400""" + resource = LearningResourceFactory.create() + mocker.patch( + "vector_search.utils.qdrant_client", + return_value=QdrantClient( + host="hidden_port_addr.com", + port=None, + prefix="custom", + check_compatibility=False, + ), + ) + resp = client.get( + reverse("lr:v1:learning_resources_api-vector-similar", args=[resource.id]), + {"resource_type": "not_a_real_type"}, + ) + assert resp.status_code == 400 + + +def test_vector_similar_no_filter_passes_none(mocker, client): + """Absence of filter params yields query_filter=None""" + resource = LearningResourceFactory.create() + mocker.patch( + "vector_search.utils.qdrant_client", + return_value=QdrantClient( + host="hidden_port_addr.com", + port=None, + prefix="custom", + check_compatibility=False, + ), + ) + mock_similar = mocker.patch( + "learning_resources_search.api._qdrant_similar_results", + return_value=[], + ) + client.get( + reverse("lr:v1:learning_resources_api-vector-similar", args=[resource.id]) + ) + assert mock_similar.call_args.kwargs["query_filter"] is None + + +def test_similar_passes_filter_params_to_opensearch(mocker, client): + """resource_type query param is forwarded as filter_params to the OpenSearch path""" + resource = LearningResourceFactory.create() + mock_similar = mocker.patch( + "learning_resources_search.api.get_similar_resources_opensearch", + return_value=[], + ) + client.get( + reverse("lr:v1:learning_resources_api-similar", args=[resource.id]), + {"resource_type": "video_playlist"}, + ) + assert mock_similar.call_args.kwargs["filter_params"] == { + "resource_type": ["video_playlist"] + } + + +def test_similar_rejects_invalid_resource_type(client): + """Unknown resource_type value returns 400 on the OpenSearch similarity endpoint""" + resource = LearningResourceFactory.create() + resp = client.get( + reverse("lr:v1:learning_resources_api-similar", args=[resource.id]), + {"resource_type": "not_a_real_type"}, + ) + assert resp.status_code == 400 + + +@pytest.mark.parametrize(("free_value", "expected"), [("true", True), ("false", False)]) +def test_similar_boolean_filter_forwarded(mocker, client, free_value, expected): + """Boolean filter params (free=true/false) are forwarded correctly to the OpenSearch path""" + resource = LearningResourceFactory.create() + mock_similar = mocker.patch( + "learning_resources_search.api.get_similar_resources_opensearch", + return_value=[], + ) + client.get( + reverse("lr:v1:learning_resources_api-similar", args=[resource.id]), + {"free": free_value}, + ) + assert mock_similar.call_args.kwargs["filter_params"] == {"free": expected} + + @pytest.mark.skip_nplusone_check def test_learning_resources_display_info_list_view(mocker, client): """Test learning_resources_display_info_list_view returns expected results""" diff --git a/learning_resources_search/api.py b/learning_resources_search/api.py index ad03163a35..d5e10ce417 100644 --- a/learning_resources_search/api.py +++ b/learning_resources_search/api.py @@ -6,6 +6,7 @@ from datetime import UTC, datetime from django.conf import settings +from django.db.models import QuerySet from opensearch_dsl import Search from opensearch_dsl.query import MoreLikeThis, Percolate from opensearchpy.exceptions import NotFoundError @@ -1079,20 +1080,21 @@ def get_similar_topics( return list(dict(counter.most_common(num_topics)).keys()) -def get_similar_resources( +def get_similar_resources( # noqa: PLR0913 value_doc: dict, num_resources: int, min_term_freq: int, min_doc_freq: int, use_embeddings, -) -> list[str]: + filter_params=None, +) -> QuerySet[LearningResource]: """ Get a list of similar resources based on another resource Args: value_doc (dict): a document representing the data fields we want to search with - num_topics (int): + num_resources (int): number of resources to return min_term_freq (int): minimum times a term needs to show up in input @@ -1100,15 +1102,24 @@ def get_similar_resources( minimum times a term needs to show up in docs use_embeddings (bool): use vector embeddings to retrieve results + filter_params (dict): + optional filter parameters (validated serializer data); each + backend translates these to its own filter format Returns: - list of str: - list of topic values + QuerySet[LearningResource]: + queryset of learning resources """ if use_embeddings: - return get_similar_resources_qdrant(value_doc, num_resources) + return get_similar_resources_qdrant( + value_doc, num_resources, filter_params=filter_params + ) return get_similar_resources_opensearch( - value_doc, num_resources, min_term_freq, min_doc_freq + value_doc, + num_resources, + min_term_freq, + min_doc_freq, + filter_params=filter_params, ) @@ -1117,15 +1128,23 @@ def _qdrant_similar_results( num_resources=6, collection_name=RESOURCES_COLLECTION_NAME, score_threshold=0, + query_filter=None, ): """ Get similar resources from qdrant Args: - doc (dict): - a document representing the data fields we want to search with + input_query: + query input for Qdrant similarity search; may be either a point id + or a raw embedding vector, depending on the caller num_resources (int): number of resources to return + collection_name (str): + qdrant collection name + score_threshold (float): + minimum similarity score + query_filter: + optional Qdrant filter to apply alongside the similarity search Returns: list of dict: @@ -1145,11 +1164,14 @@ def _qdrant_similar_results( limit=num_resources, using=encoder.model_short_name(), score_threshold=score_threshold, + query_filter=query_filter, ).points ] -def get_similar_resources_qdrant(value_doc: dict, num_resources: int): +def get_similar_resources_qdrant( + value_doc: dict, num_resources: int, filter_params=None +) -> QuerySet[LearningResource]: """ Get a list of similar resources from qdrant @@ -1158,16 +1180,24 @@ def get_similar_resources_qdrant(value_doc: dict, num_resources: int): a document representing the data fields we want to search with num_resources (int): number of resources to return + filter_params (dict): + optional filter parameters (validated serializer data) to narrow + the similarity search Returns: - list of str: - list of learning resources + list of learning resources """ - from vector_search.utils import vector_point_id, vector_point_key + from vector_search.utils import ( + qdrant_query_conditions, + vector_point_id, + vector_point_key, + ) + query_filter = qdrant_query_conditions(filter_params) if filter_params else None hits = _qdrant_similar_results( input_query=vector_point_id(vector_point_key(value_doc)), num_resources=num_resources, + query_filter=query_filter, ) return ( LearningResource.objects.for_search_serialization() @@ -1181,24 +1211,30 @@ def get_similar_resources_qdrant(value_doc: dict, num_resources: int): def get_similar_resources_opensearch( - value_doc: dict, num_resources: int, min_term_freq: int, min_doc_freq: int -) -> list[str]: + value_doc: dict, + num_resources: int, + min_term_freq: int, + min_doc_freq: int, + filter_params=None, +) -> QuerySet[LearningResource]: """ Get a list of similar resources from opensearch Args: value_doc (dict): a document representing the data fields we want to search with - num_topics (int): + num_resources (int): number of resources to return min_term_freq (int): minimum times a term needs to show up in input min_doc_freq (int): minimum times a term needs to show up in docs + filter_params (dict): + optional filter parameters (validated serializer data) to narrow + the similarity search Returns: - list of str: - list of learning resources + list of learning resources """ indexes = relevant_indexes( LEARNING_RESOURCE_TYPES, [], endpoint=LEARNING_RESOURCE, use_hybrid_search=False @@ -1218,10 +1254,21 @@ def get_similar_resources_opensearch( min_term_freq=min_term_freq, min_doc_freq=min_doc_freq, ) + # generate_filter_clauses expects list values; scalar booleans (from + # ArrayWrappedBoolean validated_data) must be wrapped before iterating. + normalized_params = { + k: [v] if isinstance(v, bool) else v for k, v in (filter_params or {}).items() + } + filter_clauses = generate_filter_clauses(normalized_params) + # Wrap all filters in a bool/must so opensearch-dsl serializes them as a + # single AND'd filter dict rather than a bare list (which it may not handle). + combined_filter = { + "bool": { + "must": [{"exists": {"field": "resource_type"}}, *filter_clauses.values()] + } + } # return only learning_resources - search = search.query( - "bool", must=[mlt_query], filter={"exists": {"field": "resource_type"}} - ) + search = search.query("bool", must=[mlt_query], filter=combined_filter) response = search.execute() return LearningResource.objects.for_search_serialization().filter( id__in=[ diff --git a/learning_resources_search/api_test.py b/learning_resources_search/api_test.py index 50f15d4520..68b129ebe5 100644 --- a/learning_resources_search/api_test.py +++ b/learning_resources_search/api_test.py @@ -4460,3 +4460,106 @@ def test_get_similar_topics_qdrant_uses_cached_embedding(mocker): encoder_instance.embed.assert_not_called() # Assert that the result is as expected assert result == ["topic1", "topic2"] + + +def test_get_similar_resources_qdrant_passes_filter_params(mocker): + """filter_params are translated to a Qdrant filter and forwarded to _qdrant_similar_results""" + from learning_resources_search.api import get_similar_resources_qdrant + + mock_similar = mocker.patch( + "learning_resources_search.api._qdrant_similar_results", + return_value=[], + ) + sentinel_filter = object() + mocker.patch( + "vector_search.utils.qdrant_query_conditions", + return_value=sentinel_filter, + ) + get_similar_resources_qdrant( + value_doc={"id": 1, "readable_id": "abc", "platform": {"code": "ocw"}}, + num_resources=5, + filter_params={"resource_type": ["video_playlist"]}, + ) + assert mock_similar.call_args.kwargs["query_filter"] is sentinel_filter + + +def test_get_similar_resources_opensearch_passes_filter_params(mocker): + """filter_params are translated to OpenSearch clauses via generate_filter_clauses""" + from learning_resources_search.api import get_similar_resources_opensearch + + mock_search = mocker.MagicMock() + mock_search.extra.return_value = mock_search + mock_search.query.return_value = mock_search + mock_search.execute.return_value = mocker.MagicMock(hits=[]) + mocker.patch("learning_resources_search.api.Search", return_value=mock_search) + mocker.patch( + "learning_resources_search.api.relevant_indexes", return_value=["index"] + ) + + get_similar_resources_opensearch( + value_doc={"id": 1, "readable_id": "abc"}, + num_resources=5, + min_term_freq=2, + min_doc_freq=3, + filter_params={"resource_type": ["video_playlist"]}, + ) + + _, kwargs = mock_search.query.call_args + must_clauses = kwargs["filter"]["bool"]["must"] + assert any( + f + == { + "bool": { + "should": [ + { + "term": { + "resource_type": { + "value": "video_playlist", + "case_insensitive": True, + } + } + } + ] + } + } + for f in must_clauses + ) + + +@pytest.mark.parametrize("free_value", [True, False]) +def test_get_similar_resources_opensearch_boolean_filter(mocker, free_value): + """Scalar boolean filter_params are normalized to lists before generate_filter_clauses""" + from learning_resources_search.api import get_similar_resources_opensearch + + mock_search = mocker.MagicMock() + mock_search.extra.return_value = mock_search + mock_search.query.return_value = mock_search + mock_search.execute.return_value = mocker.MagicMock(hits=[]) + mocker.patch("learning_resources_search.api.Search", return_value=mock_search) + mocker.patch( + "learning_resources_search.api.relevant_indexes", return_value=["index"] + ) + + # Should not raise TypeError from iterating a scalar bool, + # and False must not be skipped by the truthiness check. + get_similar_resources_opensearch( + value_doc={"id": 1, "readable_id": "abc"}, + num_resources=5, + min_term_freq=2, + min_doc_freq=3, + filter_params={"free": free_value}, + ) + + _, kwargs = mock_search.query.call_args + must_clauses = kwargs["filter"]["bool"]["must"] + assert any( + f + == { + "bool": { + "should": [ + {"term": {"free": {"value": free_value, "case_insensitive": True}}} + ] + } + } + for f in must_clauses + ) diff --git a/main/settings.py b/main/settings.py index 9f01c3535f..766e05645c 100644 --- a/main/settings.py +++ b/main/settings.py @@ -34,7 +34,7 @@ from main.settings_pluggy import * # noqa: F403 from openapi.settings_spectacular import open_spectacular_settings -VERSION = "0.63.5" +VERSION = "0.63.6" log = logging.getLogger() @@ -822,10 +822,10 @@ def get_all_config_keys(): QDRANT_CLIENT_TIMEOUT = get_int(name="QDRANT_CLIENT_TIMEOUT", default=10) VECTOR_HYBRID_SEARCH_PREFETCH_MULTIPLIER = get_int( - name="VECTOR_HYBRID_SEARCH_PREFETCH_MULTIPLIER", default=20 + name="VECTOR_HYBRID_SEARCH_PREFETCH_MULTIPLIER", default=5 ) VECTOR_HYBRID_SEARCH_PREFETCH_MAX_LIMIT = get_int( - name="VECTOR_HYBRID_SEARCH_PREFETCH_MAX_LIMIT", default=10000 + name="VECTOR_HYBRID_SEARCH_PREFETCH_MAX_LIMIT", default=500 ) # toggle to use requests (default for local) or webdriver which renders js elements EMBEDDINGS_EXTERNAL_FETCH_USE_WEBDRIVER = get_bool( diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 4bb97c19c2..85bfabe127 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -827,6 +827,58 @@ paths: description: Vector Search for content summary: Content File Vector Search parameters: + - in: query + name: aggregations + schema: + type: array + items: + enum: + - key + - course_number + - platform + - offered_by + - file_extension + - content_feature_type + - run_readable_id + - resource_readable_id + - run_title + - edx_module_id + - content_type + - description + - title + - url + - file_type + - summary + - flashcards + - checksum + type: string + description: |- + * `key` - Key + * `course_number` - Course Number + * `platform` - Platform + * `offered_by` - Offered By + * `file_extension` - File Extension + * `content_feature_type` - Content Feature Type + * `run_readable_id` - Run Readable Id + * `resource_readable_id` - Resource Readable Id + * `run_title` - Run Title + * `edx_module_id` - Edx Module Id + * `content_type` - Content Type + * `description` - Description + * `title` - Title + * `url` - Url + * `file_type` - File Type + * `summary` - Summary + * `flashcards` - Flashcards + * `checksum` - Checksum + description: "aggregations for facet counts \n\n* `key` - Key\n\ + * `course_number` - Course Number\n* `platform` - Platform\n* `offered_by`\ + \ - Offered By\n* `file_extension` - File Extension\n* `content_feature_type`\ + \ - Content Feature Type\n* `run_readable_id` - Run Readable Id\n* `resource_readable_id`\ + \ - Resource Readable Id\n* `run_title` - Run Title\n* `edx_module_id` -\ + \ Edx Module Id\n* `content_type` - Content Type\n* `description` - Description\n\ + * `title` - Title\n* `url` - Url\n* `file_type` - File Type\n* `summary`\ + \ - Summary\n* `flashcards` - Flashcards\n* `checksum` - Checksum" - in: query name: collection_name schema: @@ -961,6 +1013,61 @@ paths: description: Vector Search for learning resources summary: Vector Search parameters: + - in: query + name: aggregations + schema: + type: array + items: + enum: + - readable_id + - resource_type + - certification + - certification_type + - professional + - free + - course_feature + - topic + - ocw_topic + - level + - department + - platform + - offered_by + - delivery + - title + - url + - resource_type_group + - resource_category + - published + type: string + description: |- + * `readable_id` - Readable Id + * `resource_type` - Resource Type + * `certification` - Certification + * `certification_type` - Certification Type + * `professional` - Professional + * `free` - Free + * `course_feature` - Course Feature + * `topic` - Topic + * `ocw_topic` - Ocw Topic + * `level` - Level + * `department` - Department + * `platform` - Platform + * `offered_by` - Offered By + * `delivery` - Delivery + * `title` - Title + * `url` - Url + * `resource_type_group` - Resource Type Group + * `resource_category` - Resource Category + * `published` - Published + description: "aggregations for facet counts \n\n* `readable_id`\ + \ - Readable Id\n* `resource_type` - Resource Type\n* `certification` -\ + \ Certification\n* `certification_type` - Certification Type\n* `professional`\ + \ - Professional\n* `free` - Free\n* `course_feature` - Course Feature\n\ + * `topic` - Topic\n* `ocw_topic` - Ocw Topic\n* `level` - Level\n* `department`\ + \ - Department\n* `platform` - Platform\n* `offered_by` - Offered By\n*\ + \ `delivery` - Delivery\n* `title` - Title\n* `url` - Url\n* `resource_type_group`\ + \ - Resource Type Group\n* `resource_category` - Resource Category\n* `published`\ + \ - Published" - in: query name: certification schema: @@ -1255,6 +1362,13 @@ paths: schema: type: boolean nullable: true + - in: query + name: published + schema: + type: boolean + default: true + description: If the resource is published. We default to True unless passed + in - in: query name: q schema: diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index be7ebf931d..c29644cba6 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -2692,7 +2692,7 @@ paths: get: operationId: learning_resources_similar_list description: |- - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by filters. Args: id (integer): The id of the learning resource @@ -2705,76 +2705,69 @@ paths: name: certification schema: type: boolean + nullable: true + description: True if the learning resource offers a certificate - in: query name: certification_type schema: type: array items: - type: string enum: - - completion - micromasters - - none - professional - description: |- - The type of certification offered - - * `micromasters` - MicroMasters Credential - * `professional` - Professional Certificate - * `completion` - Certificate of Completion - * `none` - No Certificate - explode: true - style: form + - completion + - none + type: string + description: |- + * `micromasters` - MicroMasters Credential + * `professional` - Professional Certificate + * `completion` - Certificate of Completion + * `none` - No Certificate + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ + \ Credential\n* `professional` - Professional Certificate\n* `completion`\ + \ - Certificate of Completion\n* `none` - No Certificate" - in: query name: course_feature schema: type: array items: type: string - description: Content feature for the resources. Load the 'api/v1/course_features' - endpoint for a list of course features - explode: true - style: form + minLength: 1 + description: The course feature. Possible options are at api/v1/course_features/ - in: query name: delivery schema: type: array items: - type: array - items: - enum: - - online - - hybrid - - in_person - - offline - type: string - description: |- - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline enum: + - online - hybrid - in_person - offline - - online - description: |- - The delivery of course/program resources - - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline - explode: true - style: form + type: string + description: |- + * `online` - Online + * `hybrid` - Hybrid + * `in_person` - In person + * `offline` - Offline + description: "The delivery options in which the learning resource is offered\ + \ \n\n* `online` - Online\n* `hybrid` - Hybrid\n* `in_person`\ + \ - In person\n* `offline` - Offline" - in: query name: department schema: type: array items: - type: string enum: - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + - '9' - '10' - '11' - '12' @@ -2783,7 +2776,6 @@ paths: - '16' - '17' - '18' - - '2' - '20' - 21A - 21G @@ -2792,13 +2784,6 @@ paths: - 21M - '22' - '24' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - - '9' - CC - CMS-W - EC @@ -2811,53 +2796,68 @@ paths: - SP - STS - WGS - description: |- - The department that offers learning resources - - * `1` - Civil and Environmental Engineering - * `2` - Mechanical Engineering - * `3` - Materials Science and Engineering - * `4` - Architecture - * `5` - Chemistry - * `6` - Electrical Engineering and Computer Science - * `7` - Biology - * `8` - Physics - * `9` - Brain and Cognitive Sciences - * `10` - Chemical Engineering - * `11` - Urban Studies and Planning - * `12` - Earth, Atmospheric, and Planetary Sciences - * `14` - Economics - * `15` - Management - * `16` - Aeronautics and Astronautics - * `17` - Political Science - * `18` - Mathematics - * `20` - Biological Engineering - * `21A` - Anthropology - * `21G` - Global Languages - * `21H` - History - * `21L` - Literature - * `21M` - Music and Theater Arts - * `22` - Nuclear Science and Engineering - * `24` - Linguistics and Philosophy - * `CC` - Concourse - * `CMS-W` - Comparative Media Studies/Writing - * `EC` - Edgerton Center - * `ES` - Experimental Study Group - * `ESD` - Engineering Systems Division - * `HST` - Medical Engineering and Science - * `IDS` - Data, Systems, and Society - * `MAS` - Media Arts and Sciences - * `PE` - Athletics, Physical Education and Recreation - * `SP` - Special Programs - * `STS` - Science, Technology, and Society - * `WGS` - Women's and Gender Studies - explode: true - style: form + type: string + description: |- + * `1` - Civil and Environmental Engineering + * `2` - Mechanical Engineering + * `3` - Materials Science and Engineering + * `4` - Architecture + * `5` - Chemistry + * `6` - Electrical Engineering and Computer Science + * `7` - Biology + * `8` - Physics + * `9` - Brain and Cognitive Sciences + * `10` - Chemical Engineering + * `11` - Urban Studies and Planning + * `12` - Earth, Atmospheric, and Planetary Sciences + * `14` - Economics + * `15` - Management + * `16` - Aeronautics and Astronautics + * `17` - Political Science + * `18` - Mathematics + * `20` - Biological Engineering + * `21A` - Anthropology + * `21G` - Global Languages + * `21H` - History + * `21L` - Literature + * `21M` - Music and Theater Arts + * `22` - Nuclear Science and Engineering + * `24` - Linguistics and Philosophy + * `CC` - Concourse + * `CMS-W` - Comparative Media Studies/Writing + * `EC` - Edgerton Center + * `ES` - Experimental Study Group + * `ESD` - Engineering Systems Division + * `HST` - Medical Engineering and Science + * `IDS` - Data, Systems, and Society + * `MAS` - Media Arts and Sciences + * `PE` - Athletics, Physical Education and Recreation + * `SP` - Special Programs + * `STS` - Science, Technology, and Society + * `WGS` - Women's and Gender Studies + description: "The department that offers the learning resource \ + \ \n\n* `1` - Civil and Environmental Engineering\n* `2` - Mechanical Engineering\n\ + * `3` - Materials Science and Engineering\n* `4` - Architecture\n* `5` -\ + \ Chemistry\n* `6` - Electrical Engineering and Computer Science\n* `7`\ + \ - Biology\n* `8` - Physics\n* `9` - Brain and Cognitive Sciences\n* `10`\ + \ - Chemical Engineering\n* `11` - Urban Studies and Planning\n* `12` -\ + \ Earth, Atmospheric, and Planetary Sciences\n* `14` - Economics\n* `15`\ + \ - Management\n* `16` - Aeronautics and Astronautics\n* `17` - Political\ + \ Science\n* `18` - Mathematics\n* `20` - Biological Engineering\n* `21A`\ + \ - Anthropology\n* `21G` - Global Languages\n* `21H` - History\n* `21L`\ + \ - Literature\n* `21M` - Music and Theater Arts\n* `22` - Nuclear Science\ + \ and Engineering\n* `24` - Linguistics and Philosophy\n* `CC` - Concourse\n\ + * `CMS-W` - Comparative Media Studies/Writing\n* `EC` - Edgerton Center\n\ + * `ES` - Experimental Study Group\n* `ESD` - Engineering Systems Division\n\ + * `HST` - Medical Engineering and Science\n* `IDS` - Data, Systems, and\ + \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ + \ Education and Recreation\n* `SP` - Special Programs\n* `STS` - Science,\ + \ Technology, and Society\n* `WGS` - Women's and Gender Studies" - in: query name: free schema: type: boolean - description: The course/program is offered for free + nullable: true - in: path name: id schema: @@ -2868,225 +2868,178 @@ paths: schema: type: array items: - type: string enum: - - advanced + - undergraduate - graduate - high_school + - noncredit + - advanced - intermediate - introductory - - noncredit - - undergraduate - description: |- - The academic level of the resources - - * `undergraduate` - Undergraduate - * `graduate` - Graduate - * `high_school` - High School - * `noncredit` - Non-Credit - * `advanced` - Advanced - * `intermediate` - Intermediate - * `introductory` - Introductory - explode: true - style: form + type: string + description: |- + * `undergraduate` - Undergraduate + * `graduate` - Graduate + * `high_school` - High School + * `noncredit` - Non-Credit + * `advanced` - Advanced + * `intermediate` - Intermediate + * `introductory` - Introductory - in: query name: limit schema: type: integer - in: query - name: offered_by + name: ocw_topic schema: type: array items: type: string + minLength: 1 + description: The ocw topic name. + - in: query + name: offered_by + schema: + type: array + items: enum: - - bootcamps - - climate - - mitpe - mitx - ocw - - see + - bootcamps - xpro - description: |- - The organization that offers a learning resource - - * `mitx` - MITx - * `ocw` - MIT OpenCourseWare - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `climate` - MIT Climate - explode: true - style: form + - mitpe + - see + - climate + type: string + description: |- + * `mitx` - MITx + * `ocw` - MIT OpenCourseWare + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `climate` - MIT Climate + description: "The organization that offers the learning resource \ + \ \n\n* `mitx` - MITx\n* `ocw` - MIT OpenCourseWare\n* `bootcamps` -\ + \ Bootcamps\n* `xpro` - MIT xPRO\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `climate` - MIT Climate" - in: query name: platform schema: type: array items: - type: string enum: - - bootcamps - - canvas - - climate - - csail - - ctl - edx - - emeritus - - globalalumni - - mitpe - - mitxonline - ocw - oll - - ovs - - podcast - - scc + - mitxonline + - bootcamps + - xpro + - csail + - mitpe - see - - simplilearn - - susskind + - scc + - ctl - whu - - xpro + - susskind + - globalalumni + - simplilearn + - emeritus + - podcast - youtube - description: |- - The platform on which learning resources are offered - - * `edx` - edX - * `ocw` - MIT OpenCourseWare - * `oll` - Open Learning Library - * `mitxonline` - MITx Online - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `csail` - CSAIL - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `scc` - Schwarzman College of Computing - * `ctl` - Center for Transportation & Logistics - * `whu` - WHU - * `susskind` - Susskind - * `globalalumni` - Global Alumni - * `simplilearn` - Simplilearn - * `emeritus` - Emeritus - * `podcast` - Podcast - * `youtube` - YouTube - * `canvas` - Canvas - * `climate` - MIT Climate - * `ovs` - ODL Video Service - explode: true - style: form + - canvas + - climate + - ovs + type: string + description: |- + * `edx` - edX + * `ocw` - MIT OpenCourseWare + * `oll` - Open Learning Library + * `mitxonline` - MITx Online + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `csail` - CSAIL + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `scc` - Schwarzman College of Computing + * `ctl` - Center for Transportation & Logistics + * `whu` - WHU + * `susskind` - Susskind + * `globalalumni` - Global Alumni + * `simplilearn` - Simplilearn + * `emeritus` - Emeritus + * `podcast` - Podcast + * `youtube` - YouTube + * `canvas` - Canvas + * `climate` - MIT Climate + * `ovs` - ODL Video Service + description: "The platform on which the learning resource is offered \ + \ \n\n* `edx` - edX\n* `ocw` - MIT OpenCourseWare\n* `oll` - Open\ + \ Learning Library\n* `mitxonline` - MITx Online\n* `bootcamps` - Bootcamps\n\ + * `xpro` - MIT xPRO\n* `csail` - CSAIL\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `scc` - Schwarzman College of\ + \ Computing\n* `ctl` - Center for Transportation & Logistics\n* `whu` -\ + \ WHU\n* `susskind` - Susskind\n* `globalalumni` - Global Alumni\n* `simplilearn`\ + \ - Simplilearn\n* `emeritus` - Emeritus\n* `podcast` - Podcast\n* `youtube`\ + \ - YouTube\n* `canvas` - Canvas\n* `climate` - MIT Climate\n* `ovs` - ODL\ + \ Video Service" - in: query name: professional schema: type: boolean - - in: query - name: readable_id - schema: - type: array - items: - type: string - description: A unique text identifier for the resources - explode: true - style: form - - in: query - name: resource_id - schema: - type: array - items: - type: integer - description: Comma-separated list of learning resource IDs - explode: true - style: form + nullable: true - in: query name: resource_type schema: type: array items: - type: string enum: - course - - document + - program - learning_path - podcast - podcast_episode - - program - video - video_playlist - description: |- - The type of learning resource - - * `course` - Course - * `program` - Program - * `learning_path` - Learning Path - * `podcast` - Podcast - * `podcast_episode` - Podcast Episode - * `video` - Video - * `video_playlist` - Video Playlist - * `document` - Document - explode: true - style: form + - document + type: string + description: |- + * `course` - course + * `program` - program + * `learning_path` - learning path + * `podcast` - podcast + * `podcast_episode` - podcast episode + * `video` - video + * `video_playlist` - video playlist + * `document` - document + description: "The type of learning resource \n\n* `course` - course\n\ + * `program` - program\n* `learning_path` - learning path\n* `podcast` -\ + \ podcast\n* `podcast_episode` - podcast episode\n* `video` - video\n* `video_playlist`\ + \ - video playlist\n* `document` - document" - in: query name: resource_type_group schema: type: array items: - type: string enum: - course - - learning_material - program - description: |- - The resource type group of the learning resources - - * `course` - Course - * `program` - Program - * `learning_material` - Learning Material - explode: true - style: form - - in: query - name: sortby - schema: - type: string - enum: - - -id - - -last_modified - - -mitcoursenumber - - -readable_id - - -start_date - - -views - - id - - last_modified - - mitcoursenumber - - new - - readable_id - - start_date - - upcoming - - views - description: |- - Sort By - - * `id` - Object ID ascending - * `-id` - Object ID descending - * `readable_id` - Readable ID ascending - * `-readable_id` - Readable ID descending - * `last_modified` - Last Modified Date ascending - * `-last_modified` - Last Modified Date descending - * `new` - Newest resources first - * `start_date` - Start Date ascending - * `-start_date` - Start Date descending - * `mitcoursenumber` - MIT course number ascending - * `-mitcoursenumber` - MIT course number descending - * `views` - Popularity ascending - * `-views` - Popularity descending - * `upcoming` - Next start date ascending + - learning_material + type: string + description: |- + * `course` - Course + * `program` - Program + * `learning_material` - Learning Material + description: "The category of learning resource \n\n* `course`\ + \ - Course\n* `program` - Program\n* `learning_material` - Learning Material" - in: query name: topic schema: type: array items: type: string - description: Topics covered by the resources. Load the '/api/v1/topics' endpoint - for a list of topics - explode: true - style: form + minLength: 1 + description: The topic name. To see a list of options go to api/v1/topics/ tags: - learning_resources responses: @@ -3102,7 +3055,7 @@ paths: get: operationId: learning_resources_vector_similar_list description: |- - Fetch similar learning resources + Fetch similar learning resources, optionally narrowed by Qdrant filters. Args: id (integer): The id of the learning resource @@ -3115,76 +3068,69 @@ paths: name: certification schema: type: boolean + nullable: true + description: True if the learning resource offers a certificate - in: query name: certification_type schema: type: array items: - type: string enum: - - completion - micromasters - - none - professional - description: |- - The type of certification offered - - * `micromasters` - MicroMasters Credential - * `professional` - Professional Certificate - * `completion` - Certificate of Completion - * `none` - No Certificate - explode: true - style: form + - completion + - none + type: string + description: |- + * `micromasters` - MicroMasters Credential + * `professional` - Professional Certificate + * `completion` - Certificate of Completion + * `none` - No Certificate + description: "The type of certificate \n\n* `micromasters` - MicroMasters\ + \ Credential\n* `professional` - Professional Certificate\n* `completion`\ + \ - Certificate of Completion\n* `none` - No Certificate" - in: query name: course_feature schema: type: array items: type: string - description: Content feature for the resources. Load the 'api/v1/course_features' - endpoint for a list of course features - explode: true - style: form + minLength: 1 + description: The course feature. Possible options are at api/v1/course_features/ - in: query name: delivery schema: type: array items: - type: array - items: - enum: - - online - - hybrid - - in_person - - offline - type: string - description: |- - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline enum: + - online - hybrid - in_person - offline - - online - description: |- - The delivery of course/program resources - - * `online` - Online - * `hybrid` - Hybrid - * `in_person` - In person - * `offline` - Offline - explode: true - style: form - - in: query - name: department - schema: - type: array - items: type: string - enum: + description: |- + * `online` - Online + * `hybrid` - Hybrid + * `in_person` - In person + * `offline` - Offline + description: "The delivery options in which the learning resource is offered\ + \ \n\n* `online` - Online\n* `hybrid` - Hybrid\n* `in_person`\ + \ - In person\n* `offline` - Offline" + - in: query + name: department + schema: + type: array + items: + enum: - '1' + - '2' + - '3' + - '4' + - '5' + - '6' + - '7' + - '8' + - '9' - '10' - '11' - '12' @@ -3193,7 +3139,6 @@ paths: - '16' - '17' - '18' - - '2' - '20' - 21A - 21G @@ -3202,13 +3147,6 @@ paths: - 21M - '22' - '24' - - '3' - - '4' - - '5' - - '6' - - '7' - - '8' - - '9' - CC - CMS-W - EC @@ -3221,53 +3159,68 @@ paths: - SP - STS - WGS - description: |- - The department that offers learning resources - - * `1` - Civil and Environmental Engineering - * `2` - Mechanical Engineering - * `3` - Materials Science and Engineering - * `4` - Architecture - * `5` - Chemistry - * `6` - Electrical Engineering and Computer Science - * `7` - Biology - * `8` - Physics - * `9` - Brain and Cognitive Sciences - * `10` - Chemical Engineering - * `11` - Urban Studies and Planning - * `12` - Earth, Atmospheric, and Planetary Sciences - * `14` - Economics - * `15` - Management - * `16` - Aeronautics and Astronautics - * `17` - Political Science - * `18` - Mathematics - * `20` - Biological Engineering - * `21A` - Anthropology - * `21G` - Global Languages - * `21H` - History - * `21L` - Literature - * `21M` - Music and Theater Arts - * `22` - Nuclear Science and Engineering - * `24` - Linguistics and Philosophy - * `CC` - Concourse - * `CMS-W` - Comparative Media Studies/Writing - * `EC` - Edgerton Center - * `ES` - Experimental Study Group - * `ESD` - Engineering Systems Division - * `HST` - Medical Engineering and Science - * `IDS` - Data, Systems, and Society - * `MAS` - Media Arts and Sciences - * `PE` - Athletics, Physical Education and Recreation - * `SP` - Special Programs - * `STS` - Science, Technology, and Society - * `WGS` - Women's and Gender Studies - explode: true - style: form + type: string + description: |- + * `1` - Civil and Environmental Engineering + * `2` - Mechanical Engineering + * `3` - Materials Science and Engineering + * `4` - Architecture + * `5` - Chemistry + * `6` - Electrical Engineering and Computer Science + * `7` - Biology + * `8` - Physics + * `9` - Brain and Cognitive Sciences + * `10` - Chemical Engineering + * `11` - Urban Studies and Planning + * `12` - Earth, Atmospheric, and Planetary Sciences + * `14` - Economics + * `15` - Management + * `16` - Aeronautics and Astronautics + * `17` - Political Science + * `18` - Mathematics + * `20` - Biological Engineering + * `21A` - Anthropology + * `21G` - Global Languages + * `21H` - History + * `21L` - Literature + * `21M` - Music and Theater Arts + * `22` - Nuclear Science and Engineering + * `24` - Linguistics and Philosophy + * `CC` - Concourse + * `CMS-W` - Comparative Media Studies/Writing + * `EC` - Edgerton Center + * `ES` - Experimental Study Group + * `ESD` - Engineering Systems Division + * `HST` - Medical Engineering and Science + * `IDS` - Data, Systems, and Society + * `MAS` - Media Arts and Sciences + * `PE` - Athletics, Physical Education and Recreation + * `SP` - Special Programs + * `STS` - Science, Technology, and Society + * `WGS` - Women's and Gender Studies + description: "The department that offers the learning resource \ + \ \n\n* `1` - Civil and Environmental Engineering\n* `2` - Mechanical Engineering\n\ + * `3` - Materials Science and Engineering\n* `4` - Architecture\n* `5` -\ + \ Chemistry\n* `6` - Electrical Engineering and Computer Science\n* `7`\ + \ - Biology\n* `8` - Physics\n* `9` - Brain and Cognitive Sciences\n* `10`\ + \ - Chemical Engineering\n* `11` - Urban Studies and Planning\n* `12` -\ + \ Earth, Atmospheric, and Planetary Sciences\n* `14` - Economics\n* `15`\ + \ - Management\n* `16` - Aeronautics and Astronautics\n* `17` - Political\ + \ Science\n* `18` - Mathematics\n* `20` - Biological Engineering\n* `21A`\ + \ - Anthropology\n* `21G` - Global Languages\n* `21H` - History\n* `21L`\ + \ - Literature\n* `21M` - Music and Theater Arts\n* `22` - Nuclear Science\ + \ and Engineering\n* `24` - Linguistics and Philosophy\n* `CC` - Concourse\n\ + * `CMS-W` - Comparative Media Studies/Writing\n* `EC` - Edgerton Center\n\ + * `ES` - Experimental Study Group\n* `ESD` - Engineering Systems Division\n\ + * `HST` - Medical Engineering and Science\n* `IDS` - Data, Systems, and\ + \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ + \ Education and Recreation\n* `SP` - Special Programs\n* `STS` - Science,\ + \ Technology, and Society\n* `WGS` - Women's and Gender Studies" - in: query name: free schema: type: boolean - description: The course/program is offered for free + nullable: true - in: path name: id schema: @@ -3278,225 +3231,178 @@ paths: schema: type: array items: - type: string enum: - - advanced + - undergraduate - graduate - high_school + - noncredit + - advanced - intermediate - introductory - - noncredit - - undergraduate - description: |- - The academic level of the resources - - * `undergraduate` - Undergraduate - * `graduate` - Graduate - * `high_school` - High School - * `noncredit` - Non-Credit - * `advanced` - Advanced - * `intermediate` - Intermediate - * `introductory` - Introductory - explode: true - style: form + type: string + description: |- + * `undergraduate` - Undergraduate + * `graduate` - Graduate + * `high_school` - High School + * `noncredit` - Non-Credit + * `advanced` - Advanced + * `intermediate` - Intermediate + * `introductory` - Introductory - in: query name: limit schema: type: integer - in: query - name: offered_by + name: ocw_topic schema: type: array items: type: string + minLength: 1 + description: The ocw topic name. + - in: query + name: offered_by + schema: + type: array + items: enum: - - bootcamps - - climate - - mitpe - mitx - ocw - - see + - bootcamps - xpro - description: |- - The organization that offers a learning resource - - * `mitx` - MITx - * `ocw` - MIT OpenCourseWare - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `climate` - MIT Climate - explode: true - style: form + - mitpe + - see + - climate + type: string + description: |- + * `mitx` - MITx + * `ocw` - MIT OpenCourseWare + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `climate` - MIT Climate + description: "The organization that offers the learning resource \ + \ \n\n* `mitx` - MITx\n* `ocw` - MIT OpenCourseWare\n* `bootcamps` -\ + \ Bootcamps\n* `xpro` - MIT xPRO\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `climate` - MIT Climate" - in: query name: platform schema: type: array items: - type: string enum: - - bootcamps - - canvas - - climate - - csail - - ctl - edx - - emeritus - - globalalumni - - mitpe - - mitxonline - ocw - oll - - ovs - - podcast - - scc + - mitxonline + - bootcamps + - xpro + - csail + - mitpe - see - - simplilearn - - susskind + - scc + - ctl - whu - - xpro + - susskind + - globalalumni + - simplilearn + - emeritus + - podcast - youtube - description: |- - The platform on which learning resources are offered - - * `edx` - edX - * `ocw` - MIT OpenCourseWare - * `oll` - Open Learning Library - * `mitxonline` - MITx Online - * `bootcamps` - Bootcamps - * `xpro` - MIT xPRO - * `csail` - CSAIL - * `mitpe` - MIT Professional Education - * `see` - MIT Sloan Executive Education - * `scc` - Schwarzman College of Computing - * `ctl` - Center for Transportation & Logistics - * `whu` - WHU - * `susskind` - Susskind - * `globalalumni` - Global Alumni - * `simplilearn` - Simplilearn - * `emeritus` - Emeritus - * `podcast` - Podcast - * `youtube` - YouTube - * `canvas` - Canvas - * `climate` - MIT Climate - * `ovs` - ODL Video Service - explode: true - style: form + - canvas + - climate + - ovs + type: string + description: |- + * `edx` - edX + * `ocw` - MIT OpenCourseWare + * `oll` - Open Learning Library + * `mitxonline` - MITx Online + * `bootcamps` - Bootcamps + * `xpro` - MIT xPRO + * `csail` - CSAIL + * `mitpe` - MIT Professional Education + * `see` - MIT Sloan Executive Education + * `scc` - Schwarzman College of Computing + * `ctl` - Center for Transportation & Logistics + * `whu` - WHU + * `susskind` - Susskind + * `globalalumni` - Global Alumni + * `simplilearn` - Simplilearn + * `emeritus` - Emeritus + * `podcast` - Podcast + * `youtube` - YouTube + * `canvas` - Canvas + * `climate` - MIT Climate + * `ovs` - ODL Video Service + description: "The platform on which the learning resource is offered \ + \ \n\n* `edx` - edX\n* `ocw` - MIT OpenCourseWare\n* `oll` - Open\ + \ Learning Library\n* `mitxonline` - MITx Online\n* `bootcamps` - Bootcamps\n\ + * `xpro` - MIT xPRO\n* `csail` - CSAIL\n* `mitpe` - MIT Professional Education\n\ + * `see` - MIT Sloan Executive Education\n* `scc` - Schwarzman College of\ + \ Computing\n* `ctl` - Center for Transportation & Logistics\n* `whu` -\ + \ WHU\n* `susskind` - Susskind\n* `globalalumni` - Global Alumni\n* `simplilearn`\ + \ - Simplilearn\n* `emeritus` - Emeritus\n* `podcast` - Podcast\n* `youtube`\ + \ - YouTube\n* `canvas` - Canvas\n* `climate` - MIT Climate\n* `ovs` - ODL\ + \ Video Service" - in: query name: professional schema: type: boolean - - in: query - name: readable_id - schema: - type: array - items: - type: string - description: A unique text identifier for the resources - explode: true - style: form - - in: query - name: resource_id - schema: - type: array - items: - type: integer - description: Comma-separated list of learning resource IDs - explode: true - style: form + nullable: true - in: query name: resource_type schema: type: array items: - type: string enum: - course - - document + - program - learning_path - podcast - podcast_episode - - program - video - video_playlist - description: |- - The type of learning resource - - * `course` - Course - * `program` - Program - * `learning_path` - Learning Path - * `podcast` - Podcast - * `podcast_episode` - Podcast Episode - * `video` - Video - * `video_playlist` - Video Playlist - * `document` - Document - explode: true - style: form + - document + type: string + description: |- + * `course` - course + * `program` - program + * `learning_path` - learning path + * `podcast` - podcast + * `podcast_episode` - podcast episode + * `video` - video + * `video_playlist` - video playlist + * `document` - document + description: "The type of learning resource \n\n* `course` - course\n\ + * `program` - program\n* `learning_path` - learning path\n* `podcast` -\ + \ podcast\n* `podcast_episode` - podcast episode\n* `video` - video\n* `video_playlist`\ + \ - video playlist\n* `document` - document" - in: query name: resource_type_group schema: type: array items: - type: string enum: - course - - learning_material - program - description: |- - The resource type group of the learning resources - - * `course` - Course - * `program` - Program - * `learning_material` - Learning Material - explode: true - style: form - - in: query - name: sortby - schema: - type: string - enum: - - -id - - -last_modified - - -mitcoursenumber - - -readable_id - - -start_date - - -views - - id - - last_modified - - mitcoursenumber - - new - - readable_id - - start_date - - upcoming - - views - description: |- - Sort By - - * `id` - Object ID ascending - * `-id` - Object ID descending - * `readable_id` - Readable ID ascending - * `-readable_id` - Readable ID descending - * `last_modified` - Last Modified Date ascending - * `-last_modified` - Last Modified Date descending - * `new` - Newest resources first - * `start_date` - Start Date ascending - * `-start_date` - Start Date descending - * `mitcoursenumber` - MIT course number ascending - * `-mitcoursenumber` - MIT course number descending - * `views` - Popularity ascending - * `-views` - Popularity descending - * `upcoming` - Next start date ascending + - learning_material + type: string + description: |- + * `course` - Course + * `program` - Program + * `learning_material` - Learning Material + description: "The category of learning resource \n\n* `course`\ + \ - Course\n* `program` - Program\n* `learning_material` - Learning Material" - in: query name: topic schema: type: array items: type: string - description: Topics covered by the resources. Load the '/api/v1/topics' endpoint - for a list of topics - explode: true - style: form + minLength: 1 + description: The topic name. To see a list of options go to api/v1/topics/ tags: - learning_resources responses: diff --git a/pyproject.toml b/pyproject.toml index f04451cc20..d0ce1d10cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ - "Django==4.2.29", + "Django==4.2.30", "attrs>=25.0.0,<26", "base36>=0.1.1,<0.2", "beautifulsoup4>=4.8.2,<5", @@ -57,7 +57,7 @@ dependencies = [ "langchain>=0.3.11,<0.4", "langchain-experimental>=0.3.4,<0.4", "langchain-openai>=0.3.2,<0.4", - "litellm==1.81.13", + "litellm==1.83.0", "llama-index>=0.14.0,<0.15", "llama-index-llms-openai>=0.6.0,<0.7", "lxml>=6.0.0,<7", diff --git a/uv.lock b/uv.lock index 25c925a54a..f3e976b943 100644 --- a/uv.lock +++ b/uv.lock @@ -546,41 +546,41 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] @@ -748,16 +748,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/2b/8f/77a4b8ec50c821193 [[package]] name = "django" -version = "4.2.29" +version = "4.2.30" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, { name = "sqlparse" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/7306757cf2ac016d718d8a5dbae66de630addaa73dca2c341fc388458e71/django-4.2.29.tar.gz", hash = "sha256:86d91bc8086569c8d08f9c55888b583a921ac1f95ed3bdc7d5659d4709542014", size = 10438980, upload-time = "2026-03-03T13:56:42.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/b5/f1a53dc68da6429d6e0345bb848161e2381a2e9f02700148911e8582c2b3/django-4.2.30.tar.gz", hash = "sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c", size = 10468707, upload-time = "2026-04-07T14:05:45.57Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/2b/bd0a1d1846d5580e9f209b9e0128b4859e381ec1b39d6d175c61294bb530/django-4.2.29-py3-none-any.whl", hash = "sha256:074d7c4d2808050e528388bda442bd491f06def4df4fe863f27066851bba010c", size = 7996481, upload-time = "2026-03-03T13:56:36.495Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/a7c96f239cf91313a6589233fed55111c7063b26683b226802732c455dbc/django-4.2.30-py3-none-any.whl", hash = "sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65", size = 7997231, upload-time = "2026-04-07T14:05:38.241Z" }, ] [[package]] @@ -2139,7 +2139,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.81.13" +version = "1.83.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2155,9 +2155,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/80/b6cb799e7100953d848e106d0575db34c75bc3b57f31f2eefdfb1e23655f/litellm-1.81.13.tar.gz", hash = "sha256:083788d9c94e3371ff1c42e40e0e8198c497772643292a65b1bc91a3b3b537ea", size = 16562861, upload-time = "2026-02-17T02:00:47.466Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/92/6ce9737554994ca8e536e5f4f6a87cc7c4774b656c9eb9add071caf7d54b/litellm-1.83.0.tar.gz", hash = "sha256:860bebc76c4bb27b4cf90b4a77acd66dba25aced37e3db98750de8a1766bfb7a", size = 17333062, upload-time = "2026-03-31T05:08:25.331Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/f3/fffb7932870163cea7addc392165647a9a8a5489967de486c854226f1141/litellm-1.81.13-py3-none-any.whl", hash = "sha256:ae4aea2a55e85993f5f6dd36d036519422d24812a1a3e8540d9e987f2d7a4304", size = 14587505, upload-time = "2026-02-17T02:00:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/19/2c/a670cc050fcd6f45c6199eb99e259c73aea92edba8d5c2fc1b3686d36217/litellm-1.83.0-py3-none-any.whl", hash = "sha256:88c536d339248f3987571493015784671ba3f193a328e1ea6780dbebaa2094a8", size = 15610306, upload-time = "2026-03-31T05:08:21.987Z" }, ] [[package]] @@ -2663,7 +2663,7 @@ requires-dist = [ { name = "deepmerge", specifier = ">=2.0,<3" }, { name = "dj-database-url", specifier = ">=3.0.0,<4" }, { name = "dj-static", specifier = ">=0.0.6,<0.0.7" }, - { name = "django", specifier = "==4.2.29" }, + { name = "django", specifier = "==4.2.30" }, { name = "django-anymail", extras = ["mailgun"], specifier = ">=13.0,<14" }, { name = "django-bitfield", specifier = ">=2.2.0,<3" }, { name = "django-cache-memoize", specifier = ">=0.2.0,<0.3" }, @@ -2699,7 +2699,7 @@ requires-dist = [ { name = "langchain-experimental", specifier = ">=0.3.4,<0.4" }, { name = "langchain-litellm", specifier = ">=0.5.1" }, { name = "langchain-openai", specifier = ">=0.3.2,<0.4" }, - { name = "litellm", specifier = "==1.81.13" }, + { name = "litellm", specifier = "==1.83.0" }, { name = "llama-index", specifier = ">=0.14.0,<0.15" }, { name = "llama-index-llms-openai", specifier = ">=0.6.0,<0.7" }, { name = "lxml", specifier = ">=6.0.0,<7" }, @@ -4340,7 +4340,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -4348,9 +4348,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] diff --git a/vector_search/constants.py b/vector_search/constants.py index 0adc4f31a4..074fa67991 100644 --- a/vector_search/constants.py +++ b/vector_search/constants.py @@ -45,6 +45,8 @@ "title": "title", "url": "url", "resource_type_group": "resource_type_group", + "resource_category": "resource_category", + "published": "published", } @@ -71,6 +73,7 @@ "url": models.PayloadSchemaType.KEYWORD, "title": models.PayloadSchemaType.KEYWORD, "resource_type_group": models.PayloadSchemaType.KEYWORD, + "resource_category": models.PayloadSchemaType.KEYWORD, } """ @@ -92,3 +95,14 @@ QDRANT_TOPIC_INDEXES = { "name": models.PayloadSchemaType.KEYWORD, } + + +CONTENT_FILES_RETRIEVE_PAYLOAD = True +RESOURCES_RETRIEVE_PAYLOAD = ["readable_id"] + + +COLLECTION_PARAM_MAP = { + RESOURCES_COLLECTION_NAME: QDRANT_RESOURCE_PARAM_MAP, + TOPICS_COLLECTION_NAME: QDRANT_TOPICS_PARAM_MAP, + CONTENT_FILES_COLLECTION_NAME: QDRANT_CONTENT_FILE_PARAM_MAP, +} diff --git a/vector_search/serializers.py b/vector_search/serializers.py index c4c2e7a56a..dbd5614467 100644 --- a/vector_search/serializers.py +++ b/vector_search/serializers.py @@ -20,24 +20,21 @@ SearchResponseMetadata, SearchResponseSerializer, ) +from vector_search.constants import ( + QDRANT_CONTENT_FILE_PARAM_MAP, + QDRANT_RESOURCE_PARAM_MAP, +) -class LearningResourcesVectorSearchRequestSerializer(serializers.Serializer): +class LearningResourcesSearchFiltersSerializer(serializers.Serializer): """ - Request serializer for vector based search - instead of id we use readable_id in case we upload qdrant snapshots + Shared filter fields for Qdrant-backed learning resource queries. + + Every field here must have a corresponding entry in + vector_search.constants.QDRANT_RESOURCE_PARAM_MAP so it can be translated + to a Qdrant filter by qdrant_query_conditions(). """ - q = serializers.CharField(required=False, help_text="The search text") - offset = serializers.IntegerField( - required=False, help_text="The initial index from which to return the results" - ) - limit = serializers.IntegerField( - required=False, help_text="Number of results to return per page" - ) - readable_id = serializers.CharField( - required=False, help_text="The readable id of the resource" - ) offered_by_choices = [(e.name.lower(), e.value) for e in OfferedBy] offered_by = serializers.ListField( required=False, @@ -148,6 +145,36 @@ class LearningResourcesVectorSearchRequestSerializer(serializers.Serializer): \n\n{build_choice_description_list(resource_type_group_choices)}" ), ) + + +class LearningResourcesVectorSearchRequestSerializer( + LearningResourcesSearchFiltersSerializer +): + """ + Request serializer for vector based search + instead of id we use readable_id in case we upload qdrant snapshots + """ + + published = serializers.BooleanField( + required=False, + default=True, + help_text="If the resource is published. We default to True unless passed in", + ) + aggregation_choices = [ + (key, key.replace("_", " ").title()) for key in QDRANT_RESOURCE_PARAM_MAP + ] + aggregations = serializers.ListField( + required=False, + child=serializers.ChoiceField(choices=aggregation_choices), + help_text=( + f"aggregations for facet counts \ + \n\n{build_choice_description_list(aggregation_choices)}" + ), + ) + + readable_id = serializers.CharField( + required=False, help_text="The readable id of the resource" + ) url__isnull = serializers.BooleanField( required=False, default=None, @@ -160,6 +187,13 @@ class LearningResourcesVectorSearchRequestSerializer(serializers.Serializer): allow_null=True, help_text="Filter to learning resources where title is null/not null", ) + q = serializers.CharField(required=False, help_text="The search text") + offset = serializers.IntegerField( + required=False, help_text="The initial index from which to return the results" + ) + limit = serializers.IntegerField( + required=False, help_text="Number of results to return per page" + ) hybrid_search = serializers.BooleanField( required=False, default=False, @@ -174,14 +208,14 @@ class LearningResourcesVectorSearchResponseSerializer(SearchResponseSerializer): @extend_schema_field(LearningResourceSerializer(many=True)) def get_results(self, instance): - return instance.get("hits", {}) + return instance.get("hits", []) def get_count(self, instance) -> int: - return instance.get("total", {}).get("value") + return instance.get("total", {}).get("value", 0) - def get_metadata(self, _) -> SearchResponseMetadata: + def get_metadata(self, instance) -> SearchResponseMetadata: return { - "aggregations": [], + "aggregations": instance.get("aggregations", {}), "suggest": [], } @@ -198,6 +232,17 @@ class ContentFileVectorSearchRequestSerializer(serializers.Serializer): limit = serializers.IntegerField( required=False, help_text="Number of results to return per page" ) + aggregation_choices = [ + (key, key.replace("_", " ").title()) for key in QDRANT_CONTENT_FILE_PARAM_MAP + ] + aggregations = serializers.ListField( + required=False, + child=serializers.ChoiceField(choices=aggregation_choices), + help_text=( + f"aggregations for facet counts \ + \n\n{build_choice_description_list(aggregation_choices)}" + ), + ) sortby = serializers.ChoiceField( required=False, choices=CONTENT_FILE_SORTBY_OPTIONS, @@ -275,14 +320,14 @@ class ContentFileVectorSearchResponseSerializer(SearchResponseSerializer): """ def get_count(self, instance) -> int: - return instance["total"]["value"] + return instance.get("total", {}).get("value", 0) @extend_schema_field(ContentFileSerializer(many=True)) def get_results(self, instance): - return instance["hits"] + return instance.get("hits", []) - def get_metadata(self, *_) -> SearchResponseMetadata: + def get_metadata(self, instance) -> SearchResponseMetadata: return { - "aggregations": [], + "aggregations": instance.get("aggregations", {}), "suggest": [], } diff --git a/vector_search/serializers_test.py b/vector_search/serializers_test.py index a7057c2cc3..c7e61bb9cd 100644 --- a/vector_search/serializers_test.py +++ b/vector_search/serializers_test.py @@ -1,3 +1,39 @@ -import pytest +from vector_search.serializers import ( + LearningResourcesSearchFiltersSerializer, + LearningResourcesVectorSearchRequestSerializer, +) -pytestmark = pytest.mark.django_db + +def test_filter_serializer_accepts_resource_type(): + s = LearningResourcesSearchFiltersSerializer( + data={"resource_type": ["video_playlist"]} + ) + assert s.is_valid(), s.errors + assert s.validated_data["resource_type"] == ["video_playlist"] + + +def test_filter_serializer_rejects_invalid_resource_type(): + s = LearningResourcesSearchFiltersSerializer(data={"resource_type": ["not_a_type"]}) + assert not s.is_valid() + assert "resource_type" in s.errors + + +def test_filter_serializer_has_no_search_fields(): + fields = LearningResourcesSearchFiltersSerializer().fields + assert "q" not in fields + assert "offset" not in fields + assert "limit" not in fields + assert "hybrid_search" not in fields + assert "readable_id" not in fields + # isnull filters are Qdrant-only and must not be in the shared base, + # since generate_filter_clauses (OpenSearch) doesn't support them + assert "url__isnull" not in fields + assert "title__isnull" not in fields + + +def test_vector_search_request_serializer_inherits_filter_fields(): + fields = LearningResourcesVectorSearchRequestSerializer().fields + assert "resource_type" in fields + assert "platform" in fields + assert "q" in fields + assert "hybrid_search" in fields diff --git a/vector_search/utils.py b/vector_search/utils.py index d5a50e05f0..89791ab06e 100644 --- a/vector_search/utils.py +++ b/vector_search/utils.py @@ -1,3 +1,4 @@ +import asyncio import gc import logging import uuid @@ -32,13 +33,13 @@ ) from main.utils import checksum_for_content from vector_search.constants import ( + COLLECTION_PARAM_MAP, CONTENT_FILES_COLLECTION_NAME, QDRANT_CONTENT_FILE_INDEXES, QDRANT_CONTENT_FILE_PARAM_MAP, QDRANT_LEARNING_RESOURCE_INDEXES, QDRANT_RESOURCE_PARAM_MAP, QDRANT_TOPIC_INDEXES, - QDRANT_TOPICS_PARAM_MAP, RESOURCES_COLLECTION_NAME, TOPICS_COLLECTION_NAME, ) @@ -871,7 +872,11 @@ def process_batch(docs_batch, summaries_list): def _resource_vector_hits(search_result): - hits = [hit.payload["readable_id"] for hit in search_result] + hits = [ + readable_id + for readable_id in (hit.payload.get("readable_id") for hit in search_result) + if readable_id + ] """ Always lookup learning resources by readable_id for portability in case we load points from external systems @@ -981,17 +986,74 @@ def document_exists(document, collection_name=RESOURCES_COLLECTION_NAME): return count_result.count > 0 +async def async_qdrant_aggregations( + aggregation_keys: list, + params: dict, + collection_name: str = RESOURCES_COLLECTION_NAME, +) -> dict: + """ + Compute facet aggregations from Qdrant for each requested field. + Issues one concurrent facet query per aggregation key and returns results + in the same shape used by the OpenSearch aggregation API: + ``{"delivery": [{"key": "online", "doc_count": 24}, ...], ...}`` + Args: + aggregation_keys: list of aggregation parameter names. + Must be valid keys in the collection's param map + (e.g. ``QDRANT_RESOURCE_PARAM_MAP``). + params: dict of all search parameters, which are used to construct + a Qdrant ``models.Filter`` for each facet query. + collection_name: name of the Qdrant collection to query. + Returns: + dict mapping each requested aggregation name to a list of + ``{"key": str, "doc_count": int}`` dicts sorted by + ``doc_count`` descending. + """ + if not aggregation_keys: + return {} + + param_map = COLLECTION_PARAM_MAP.get(collection_name, QDRANT_RESOURCE_PARAM_MAP) + client = async_qdrant_client() + + async def _get_facet(agg_key: str): + qdrant_field = param_map.get(agg_key) + if not qdrant_field: + return agg_key, [] + + filtered_params = { + k: v for k, v in params.items() if k.partition("__")[0] != agg_key + } + facet_filter = qdrant_query_conditions( + filtered_params, collection_name=collection_name + ) + + result = await client.facet( + collection_name=collection_name, + key=qdrant_field, + facet_filter=facet_filter, + limit=100, + ) + hits = [ + { + "key": str(hit.value).lower() + if isinstance(hit.value, bool) + else str(hit.value), + "doc_count": hit.count, + } + for hit in result.hits + ] + hits.sort(key=lambda x: x["doc_count"], reverse=True) + return agg_key, hits + + results = await asyncio.gather(*[_get_facet(key) for key in aggregation_keys]) + return dict(results) + + def qdrant_query_conditions(params, collection_name=RESOURCES_COLLECTION_NAME): """ Return a list of Qdrant FieldCondition objects based on params """ - collection_param_map = { - RESOURCES_COLLECTION_NAME: QDRANT_RESOURCE_PARAM_MAP, - TOPICS_COLLECTION_NAME: QDRANT_TOPICS_PARAM_MAP, - CONTENT_FILES_COLLECTION_NAME: QDRANT_CONTENT_FILE_PARAM_MAP, - } - qdrant_param_map = collection_param_map.get(collection_name) + qdrant_param_map = COLLECTION_PARAM_MAP.get(collection_name) if not params or not qdrant_param_map: return None must = [] diff --git a/vector_search/utils_test.py b/vector_search/utils_test.py index 8917834c5b..c9d2458a44 100644 --- a/vector_search/utils_test.py +++ b/vector_search/utils_test.py @@ -1,3 +1,4 @@ +import asyncio import random from decimal import Decimal from unittest.mock import MagicMock @@ -44,6 +45,7 @@ _get_text_splitter, _is_markdown_content, _resource_vector_hits, + async_qdrant_aggregations, create_qdrant_collections, embed_learning_resources, embed_topics, @@ -1519,3 +1521,247 @@ def test_resource_vector_hits_preserves_qdrant_score_order(): expected_readable_ids = [r.readable_id for r in shuffled] actual_readable_ids = [r["readable_id"] for r in result] assert actual_readable_ids == expected_readable_ids + + +def _make_facet_hit(count=0, value="test"): + """Build a minimal mock that looks like a Qdrant FacetHit.""" + hit = MagicMock() + hit.value = value + hit.count = count + return hit + + +def _make_facet_response(hits): + """Build a minimal mock that looks like a Qdrant FacetResponse.""" + resp = MagicMock() + resp.hits = hits + return resp + + +def test_async_qdrant_aggregations_empty_keys(mocker): + """Should return {} immediately without calling Qdrant when aggregation_keys is empty.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + result = asyncio.run(async_qdrant_aggregations([], {})) + assert result == {} + mock_client.facet.assert_not_called() + + +def test_async_qdrant_aggregations_unknown_key(mocker): + """An aggregation key not present in the param map should return an empty list.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + result = asyncio.run( + async_qdrant_aggregations( + ["nonexistent_field"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + assert result == {"nonexistent_field": []} + mock_client.facet.assert_not_called() + + +def test_async_qdrant_aggregations_single_key(mocker): + """A valid single aggregation key should query Qdrant and return correctly shaped data.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + mock_client.facet.return_value = _make_facet_response( + [ + _make_facet_hit(42, value="course"), + _make_facet_hit(7, value="podcast"), + ] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["resource_type"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + assert "resource_type" in result + hits = result["resource_type"] + # Should be sorted descending by doc_count + assert hits[0] == {"key": "course", "doc_count": 42} + assert hits[1] == {"key": "podcast", "doc_count": 7} + + mock_client.facet.assert_awaited_once() + call_kwargs = mock_client.facet.call_args.kwargs + assert call_kwargs["collection_name"] == RESOURCES_COLLECTION_NAME + assert call_kwargs["key"] == QDRANT_RESOURCE_PARAM_MAP["resource_type"] + assert call_kwargs["limit"] == 100 + + +def test_async_qdrant_aggregations_multiple_keys(mocker): + """Multiple valid keys should each issue a concurrent Qdrant facet call.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + # Return different data depending on the 'key' kwarg + def _facet_side_effect(**kwargs): + if kwargs["key"] == QDRANT_RESOURCE_PARAM_MAP["resource_type"]: + return _make_facet_response([_make_facet_hit(10, value="course")]) + if kwargs["key"] == QDRANT_RESOURCE_PARAM_MAP["platform"]: + return _make_facet_response( + [_make_facet_hit(30, value="ocw"), _make_facet_hit(20, value="edx")] + ) + return _make_facet_response([]) + + mock_client.facet.side_effect = _facet_side_effect + + result = asyncio.run( + async_qdrant_aggregations( + ["resource_type", "platform"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + assert set(result.keys()) == {"resource_type", "platform"} + assert result["resource_type"] == [{"key": "course", "doc_count": 10}] + # Descending sort + assert result["platform"][0] == {"key": "ocw", "doc_count": 30} + assert result["platform"][1] == {"key": "edx", "doc_count": 20} + assert mock_client.facet.await_count == 2 + + +def test_async_qdrant_aggregations_excludes_own_param_from_filter(mocker): + """ + When building the per-facet filter, the aggregation key's own param + must be excluded so that all values for that facet are counted. + """ + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + mock_client.facet.return_value = _make_facet_response([]) + + params = { + "resource_type": ["course"], + "platform": ["ocw"], + } + + asyncio.run( + async_qdrant_aggregations( + ["resource_type"], + params, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + mock_client.facet.assert_awaited_once() + call_kwargs = mock_client.facet.call_args.kwargs + + # The facet_filter should NOT contain a condition for resource_type + # (it was stripped out so we get all resource_type facet values), + # but it SHOULD still filter by platform. + facet_filter = call_kwargs.get("facet_filter") + # facet_filter is a qdrant models.Filter with must conditions + assert facet_filter is not None + condition_keys = [c.key for c in facet_filter.must if hasattr(c, "key")] + assert QDRANT_RESOURCE_PARAM_MAP["platform"] in condition_keys + assert QDRANT_RESOURCE_PARAM_MAP["resource_type"] not in condition_keys + + +def test_async_qdrant_aggregations_bool_values_lowercased(mocker): + """Boolean hit values must be returned as lowercase strings ('true'/'false').""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + mock_client.facet.return_value = _make_facet_response( + [ + _make_facet_hit(5, value=True), + _make_facet_hit(3, value=False), + ] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["free"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + keys = {hit["key"] for hit in result["free"]} + assert "true" in keys + assert "false" in keys + # Verify no raw booleans slipped through + assert True not in keys + assert False not in keys + + +def test_async_qdrant_aggregations_sorted_by_doc_count_desc(mocker): + """Results must be sorted by doc_count in descending order.""" + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + + mock_client.facet.return_value = _make_facet_response( + [ + _make_facet_hit(5, value="edx"), + _make_facet_hit(100, value="ocw"), + _make_facet_hit(20, value="xpro"), + ] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["platform"], + {}, + collection_name=RESOURCES_COLLECTION_NAME, + ) + ) + + counts = [hit["doc_count"] for hit in result["platform"]] + assert counts == sorted(counts, reverse=True) + + +def test_async_qdrant_aggregations_uses_content_file_param_map(mocker): + """ + When collection_name is CONTENT_FILES_COLLECTION_NAME the function must + use QDRANT_CONTENT_FILE_PARAM_MAP to resolve the Qdrant field name. + """ + mock_client = mocker.AsyncMock() + mocker.patch( + "vector_search.utils.async_qdrant_client", + return_value=mock_client, + ) + mock_client.facet.return_value = _make_facet_response( + [_make_facet_hit(8, value=".pdf")] + ) + + result = asyncio.run( + async_qdrant_aggregations( + ["file_extension"], + {}, + collection_name=CONTENT_FILES_COLLECTION_NAME, + ) + ) + + assert "file_extension" in result + call_kwargs = mock_client.facet.call_args.kwargs + assert call_kwargs["collection_name"] == CONTENT_FILES_COLLECTION_NAME + # The Qdrant field for 'file_extension' should come from the content-file map + assert call_kwargs["key"] == QDRANT_CONTENT_FILE_PARAM_MAP["file_extension"] diff --git a/vector_search/views.py b/vector_search/views.py index 4466edd129..5335351c46 100644 --- a/vector_search/views.py +++ b/vector_search/views.py @@ -17,6 +17,12 @@ from authentication.decorators import blocked_ip_exempt from learning_resources.constants import GROUP_CONTENT_FILE_CONTENT_VIEWERS from main.utils import cache_page_for_anonymous_users +from vector_search.constants import ( + CONTENT_FILES_COLLECTION_NAME, + CONTENT_FILES_RETRIEVE_PAYLOAD, + RESOURCES_COLLECTION_NAME, + RESOURCES_RETRIEVE_PAYLOAD, +) from vector_search.serializers import ( ContentFileVectorSearchRequestSerializer, ContentFileVectorSearchResponseSerializer, @@ -24,11 +30,10 @@ LearningResourcesVectorSearchResponseSerializer, ) from vector_search.utils import ( - CONTENT_FILES_COLLECTION_NAME, - RESOURCES_COLLECTION_NAME, _content_file_vector_hits, _merge_dicts, _resource_vector_hits, + async_qdrant_aggregations, async_qdrant_client, dense_encoder, qdrant_query_conditions, @@ -82,12 +87,12 @@ async def dispatch(self, request, *args, **kwargs): self.response = self.finalize_response(request, response, *args, **kwargs) return self.response - async def async_vector_search( # noqa: PLR0913 + async def async_vector_search( # noqa: PLR0913, PLR0915 self, query_string: str, params: dict, limit: int = 10, - offset: int = 10, + offset: int = 0, search_collection=RESOURCES_COLLECTION_NAME, *, hybrid_search: bool = False, @@ -113,8 +118,19 @@ async def async_vector_search( # noqa: PLR0913 "collection_name": search_collection, "query_filter": search_filter, "with_vectors": False, - "with_payload": True, - "search_params": models.SearchParams(indexed_only=True, exact=False), + "with_payload": RESOURCES_RETRIEVE_PAYLOAD + if search_collection == RESOURCES_COLLECTION_NAME + else CONTENT_FILES_RETRIEVE_PAYLOAD, + "search_params": models.SearchParams( + quantization=models.QuantizationSearchParams( + ignore=False, + rescore=True, + oversampling=1, + ), + hnsw_ef=64, + indexed_only=True, + exact=False, + ), "limit": limit, } @@ -151,6 +167,7 @@ async def async_vector_search( # noqa: PLR0913 search_params.pop("search_params", None) search_params["group_by"] = params.get("group_by") search_params["group_size"] = params.get("group_size", 1) + search_params["with_payload"] = True group_result = await client.query_points_groups(**search_params) search_result = [] for group in group_result.groups: @@ -171,29 +188,56 @@ async def async_vector_search( # noqa: PLR0913 result_obj = await client.query_points(**search_params) search_result = result_obj.points else: - scroll_res = await client.scroll( - collection_name=search_collection, - scroll_filter=search_filter, - limit=limit, - offset=offset, - with_vectors=False, - ) - search_result = scroll_res[0] - - if search_collection == RESOURCES_COLLECTION_NAME: - hits = await sync_to_async(_resource_vector_hits)(search_result) - else: - hits = await sync_to_async(_content_file_vector_hits)(search_result) + # Qdrant's scroll API uses a point-ID cursor for `offset`, not a + # numeric skip count. We implement integer offset by consuming + # scroll pages until the desired number of records are skipped. + remaining_to_skip = offset + next_page_offset = None + search_result = [] + while True: + fetch_size = min(max(remaining_to_skip, limit), 1000) + scroll_res = await client.scroll( + collection_name=search_collection, + scroll_filter=search_filter, + limit=fetch_size, + offset=next_page_offset, + with_vectors=False, + ) + page_points, next_page_offset = scroll_res + if remaining_to_skip > 0: + skipped = min(remaining_to_skip, len(page_points)) + page_points = page_points[skipped:] + remaining_to_skip -= skipped + search_result.extend(page_points) + if len(search_result) >= limit or not next_page_offset: + break + search_result = search_result[:limit] + + hits_coroutine = ( + sync_to_async(_resource_vector_hits)(search_result) + if search_collection == RESOURCES_COLLECTION_NAME + else sync_to_async(_content_file_vector_hits)(search_result) + ) - count_result = await client.count( - collection_name=search_collection, - count_filter=search_filter, - exact=False, + aggregation_keys = params.get("aggregations") or [] + hits, count_result, aggregations = await asyncio.gather( + hits_coroutine, + client.count( + collection_name=search_collection, + count_filter=search_filter, + exact=False, + ), + async_qdrant_aggregations( + aggregation_keys, + params, + collection_name=search_collection, + ), ) return { "hits": hits, "total": {"value": count_result.count}, + "aggregations": aggregations or {}, } def handle_exception(self, exc): diff --git a/vector_search/views_test.py b/vector_search/views_test.py index 774ca25436..981fc4371b 100644 --- a/vector_search/views_test.py +++ b/vector_search/views_test.py @@ -14,7 +14,7 @@ def test_vector_search_filters(mocker, client): mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -63,7 +63,7 @@ def test_vector_search_filters_empty_query(mocker, client): mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mock_qdrant.count = mocker.AsyncMock(return_value=CountResult(count=10)) @@ -124,7 +124,7 @@ def test_content_file_vector_search_filters( mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -201,7 +201,7 @@ def test_content_file_vector_search_filters_empty_query( mock_qdrant = mocker.patch( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -255,7 +255,7 @@ def test_content_file_vector_search_filters_custom_collection( "qdrant_client.AsyncQdrantClient", return_value=mocker.AsyncMock() )() custom_collection_name = "foo_bar_collection" - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch( @@ -300,7 +300,7 @@ def test_content_file_vector_search_group_parameters(mocker, client, django_user )() custom_collection_name = "foo_bar_collection" - mock_qdrant.scroll = mocker.AsyncMock(return_value=[[]]) + mock_qdrant.scroll = mocker.AsyncMock(return_value=([], None)) mock_qdrant.query_points = mocker.AsyncMock() mock_qdrant.query_points_groups = mocker.AsyncMock() mocker.patch(