From f61929fbae59eac5d5d0356d0ec05f82301b9347 Mon Sep 17 00:00:00 2001 From: Sidney Glinton Date: Wed, 15 Oct 2025 15:04:51 -0400 Subject: [PATCH] feat(mc): Add dynamic filter options endpoint for model catalog (#1727) * feat: generate server and client files only Signed-off-by: Sidney Glinton * feat: add support for catalog dynamic catalog filtering Signed-off-by: Sidney Glinton * feat: catalog test fix Signed-off-by: Sidney Glinton * feat: add tests for filter options Signed-off-by: Sidney Glinton * fix: remove duplicate filterQuery parameter and regenerate OpenAPI files Signed-off-by: Sidney Glinton * fix: check range bug Signed-off-by: Sidney Glinton * feat: add filter options and fix openapi spec conflicts Signed-off-by: Sidney Glinton * feat: add filteroptionrange Signed-off-by: Sidney Glinton * fix: gitattributes file Signed-off-by: Sidney Glinton * fix: regenerate OpenAPI code and remove stale converter files Signed-off-by: Sidney Glinton * fix(catalog): regenerate files And fix badly generated openapi code. Signed-off-by: Paul Boyd * Update api/openapi/src/catalog.yaml Co-authored-by: Paul Boyd Signed-off-by: Sidney Glinton * Update api/openapi/src/catalog.yaml Co-authored-by: Paul Boyd Signed-off-by: Sidney Glinton * Update api/openapi/src/catalog.yaml Co-authored-by: Paul Boyd Signed-off-by: Sidney Glinton * fix: pr feedback changes Signed-off-by: Sidney Glinton * feat: regen files Signed-off-by: Sidney Glinton * fix: build and remove sourceLabel fields Signed-off-by: Sidney Glinton * fix: regen on openapi gen 7.13.0 Signed-off-by: Sidney Glinton * feat: regen openapi files after rebase Signed-off-by: Sidney Glinton * fix: build and tests and rebase Signed-off-by: Sidney Glinton * Update catalog/internal/server/openapi/api_model_catalog_service_service_test.go Co-authored-by: Paul Boyd Signed-off-by: Sidney Glinton * fix: revert logic on older refactor Signed-off-by: Sidney Glinton * feat: regen files Signed-off-by: Sidney Glinton * feat: regen catalog Signed-off-by: Sidney Glinton --------- Signed-off-by: Sidney Glinton Signed-off-by: Paul Boyd Co-authored-by: Paul Boyd --- .gitattributes | 3 + api/openapi/catalog.yaml | 56 ++++++ api/openapi/src/catalog.yaml | 56 ++++++ catalog/internal/catalog/catalog.go | 4 + catalog/internal/catalog/catalog_test.go | 4 + catalog/internal/catalog/db_catalog.go | 63 ++++++ catalog/internal/catalog/db_catalog_test.go | 162 +++++++++++++++ catalog/internal/catalog/hf_catalog.go | 9 + catalog/internal/db/models/catalog_model.go | 3 + catalog/internal/db/service/catalog_model.go | 53 +++++ .../internal/db/service/catalog_model_test.go | 79 ++++++++ .../server/openapi/.openapi-generator/FILES | 3 + catalog/internal/server/openapi/api.go | 2 + .../openapi/api_model_catalog_service.go | 17 ++ .../api_model_catalog_service_service.go | 13 +- .../api_model_catalog_service_service_test.go | 46 +++++ .../internal/server/openapi/type_asserts.go | 20 ++ .../server/openapi/type_asserts_overrides.go | 19 ++ catalog/pkg/openapi/.openapi-generator/FILES | 3 + .../pkg/openapi/api_model_catalog_service.go | 130 ++++++++++++ catalog/pkg/openapi/model_filter_option.go | 189 ++++++++++++++++++ .../pkg/openapi/model_filter_option_range.go | 160 +++++++++++++++ .../pkg/openapi/model_filter_options_list.go | 125 ++++++++++++ catalog/scripts/gen_type_asserts.sh | 4 +- 24 files changed, 1220 insertions(+), 3 deletions(-) create mode 100644 catalog/pkg/openapi/model_filter_option.go create mode 100644 catalog/pkg/openapi/model_filter_option_range.go create mode 100644 catalog/pkg/openapi/model_filter_options_list.go diff --git a/.gitattributes b/.gitattributes index 12da470e..2b360d24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,6 +20,9 @@ catalog/pkg/openapi/model_catalog_model_list.go linguist-generated=true catalog/pkg/openapi/model_catalog_source.go linguist-generated=true catalog/pkg/openapi/model_catalog_source_list.go linguist-generated=true catalog/pkg/openapi/model_error.go linguist-generated=true +catalog/pkg/openapi/model_filter_option.go linguist-generated=true +catalog/pkg/openapi/model_filter_option_range.go linguist-generated=true +catalog/pkg/openapi/model_filter_options_list.go linguist-generated=true catalog/pkg/openapi/model_metadata_bool_value.go linguist-generated=true catalog/pkg/openapi/model_metadata_double_value.go linguist-generated=true catalog/pkg/openapi/model_metadata_int_value.go linguist-generated=true diff --git a/api/openapi/catalog.yaml b/api/openapi/catalog.yaml index accdb76c..38649129 100644 --- a/api/openapi/catalog.yaml +++ b/api/openapi/catalog.yaml @@ -67,6 +67,22 @@ paths: "500": $ref: "#/components/responses/InternalServerError" operationId: findModels + /api/model_catalog/v1alpha1/models/filter_options: + description: Lists options for `filterQuery` when listing models. + get: + summary: Lists fields and available options that can be used in `filterQuery` on the list models endpoint. + tags: + - ModelCatalogService + responses: + "200": + $ref: "#/components/responses/FilterOptionsResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: findModelsFilterOptions /api/model_catalog/v1alpha1/sources: summary: Path used to get the list of catalog sources. description: >- @@ -434,6 +450,40 @@ components: message: description: Error message type: string + FilterOption: + type: object + required: + - type + properties: + type: + type: string + description: The data type of the filter option + enum: + - string + - number + values: + type: array + description: Known values of the property for string types with a small number of possible options. + items: {} + range: + $ref: "#/components/schemas/FilterOptionRange" + FilterOptionRange: + type: object + description: Min and max values for number types. + properties: + min: + type: number + max: + type: number + FilterOptionsList: + description: List of FilterOptions + type: object + properties: + filters: + type: object + description: A single filter option. + additionalProperties: + $ref: "#/components/schemas/FilterOption" MetadataBoolValue: description: A bool property value. type: object @@ -598,6 +648,12 @@ components: schema: $ref: "#/components/schemas/Error" description: Conflict with current state of target resource + FilterOptionsResponse: + content: + application/json: + schema: + $ref: "#/components/schemas/FilterOptionsList" + description: A response containing options for a `filterQuery` parameter. InternalServerError: content: application/json: diff --git a/api/openapi/src/catalog.yaml b/api/openapi/src/catalog.yaml index 1ffa57bd..cf85d111 100644 --- a/api/openapi/src/catalog.yaml +++ b/api/openapi/src/catalog.yaml @@ -67,6 +67,22 @@ paths: "500": $ref: "#/components/responses/InternalServerError" operationId: findModels + /api/model_catalog/v1alpha1/models/filter_options: + description: Lists options for `filterQuery` when listing models. + get: + summary: Lists fields and available options that can be used in `filterQuery` on the list models endpoint. + tags: + - ModelCatalogService + responses: + "200": + $ref: "#/components/responses/FilterOptionsResponse" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + operationId: findModelsFilterOptions /api/model_catalog/v1alpha1/sources: summary: Path used to get the list of catalog sources. description: >- @@ -295,6 +311,40 @@ components: items: $ref: "#/components/schemas/CatalogSource" - $ref: "#/components/schemas/BaseResourceList" + FilterOption: + type: object + required: + - type + properties: + type: + type: string + description: The data type of the filter option + enum: + - string + - number + values: + type: array + description: Known values of the property for string types with a small number of possible options. + items: {} + range: + $ref: "#/components/schemas/FilterOptionRange" + FilterOptionRange: + type: object + description: Min and max values for number types. + properties: + min: + type: number + max: + type: number + FilterOptionsList: + description: List of FilterOptions + type: object + properties: + filters: + type: object + description: A single filter option. + additionalProperties: + $ref: "#/components/schemas/FilterOption" OrderByField: description: Supported fields for ordering result entities. enum: @@ -335,6 +385,12 @@ components: schema: $ref: "#/components/schemas/CatalogSource" description: A response containing a `CatalogSource` entity. + FilterOptionsResponse: + content: + application/json: + schema: + $ref: "#/components/schemas/FilterOptionsList" + description: A response containing options for a `filterQuery` parameter. parameters: orderBy: diff --git a/catalog/internal/catalog/catalog.go b/catalog/internal/catalog/catalog.go index ee2760d4..20989244 100644 --- a/catalog/internal/catalog/catalog.go +++ b/catalog/internal/catalog/catalog.go @@ -41,4 +41,8 @@ type APIProvider interface { // model is found with that name, it returns nil. If the model is // found, but has no artifacts, an empty list is returned. GetArtifacts(ctx context.Context, modelName string, sourceID string, params ListArtifactsParams) (model.CatalogArtifactList, error) + + // GetFilterOptions returns all available filter options for models. + // This includes field names, data types, and available values or ranges. + GetFilterOptions(ctx context.Context) (*model.FilterOptionsList, error) } diff --git a/catalog/internal/catalog/catalog_test.go b/catalog/internal/catalog/catalog_test.go index 1231bf2a..d032199e 100644 --- a/catalog/internal/catalog/catalog_test.go +++ b/catalog/internal/catalog/catalog_test.go @@ -416,6 +416,10 @@ func (m *MockCatalogModelRepository) Save(model dbmodels.CatalogModel) (dbmodels return savedModel, nil } +func (m *MockCatalogModelRepository) GetFilterableProperties(maxLength int) (map[string][]string, error) { + return make(map[string][]string), nil +} + // MockCatalogModelArtifactRepository mocks the CatalogModelArtifactRepository interface. type MockCatalogModelArtifactRepository struct { SavedArtifacts []dbmodels.CatalogModelArtifact diff --git a/catalog/internal/catalog/db_catalog.go b/catalog/internal/catalog/db_catalog.go index e61ee6fe..c655e6bd 100644 --- a/catalog/internal/catalog/db_catalog.go +++ b/catalog/internal/catalog/db_catalog.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strconv" dbmodels "github.com/kubeflow/model-registry/catalog/internal/db/models" @@ -184,6 +185,68 @@ func (d *dbCatalogImpl) GetArtifacts(ctx context.Context, modelName string, sour return *artifactList, nil } +func (d *dbCatalogImpl) GetFilterOptions(ctx context.Context) (*apimodels.FilterOptionsList, error) { + // Max length threshold for filter values (excludes verbose fields like readme, description) + const maxFilterValueLength = 100 + + // Query database for filterable properties + filterableProps, err := d.catalogModelRepository.GetFilterableProperties(maxFilterValueLength) + if err != nil { + return nil, err + } + + // Build FilterOptionsList + options := make(map[string]apimodels.FilterOption, maxFilterValueLength) + + // Process each property and its values + for fieldName, values := range filterableProps { + // Skip internal/technical fields that shouldn't be exposed as filters + if fieldName == "source_id" || fieldName == "logo" || fieldName == "license_link" { + continue + } + + // Deduplicate values + uniqueValues := make(map[string]bool) + + // Parse JSON arrays for fields like language and tasks + for _, value := range values { + var arrayValues []string + if err := json.Unmarshal([]byte(value), &arrayValues); err == nil { + // Successfully parsed as array, add individual values + for _, v := range arrayValues { + uniqueValues[v] = true + } + } else { + // Not a JSON array + uniqueValues[value] = true + } + } + + if len(uniqueValues) > 0 { + sortedValues := make([]string, 0, len(uniqueValues)) + for v := range uniqueValues { + sortedValues = append(sortedValues, v) + } + sort.Strings(sortedValues) + + // Convert to []interface{} (supports future non-string filter types) + expandedValues := make([]interface{}, len(sortedValues)) + for i, v := range sortedValues { + expandedValues[i] = v + } + + options[fieldName] = apimodels.FilterOption{ + Type: "string", + Values: expandedValues, + } + } + } + + return &apimodels.FilterOptionsList{ + Filters: &options, + }, nil +} + func mapDBModelToAPIModel(m dbmodels.CatalogModel) apimodels.CatalogModel { res := apimodels.CatalogModel{} diff --git a/catalog/internal/catalog/db_catalog_test.go b/catalog/internal/catalog/db_catalog_test.go index 944d67c0..7e6557b5 100644 --- a/catalog/internal/catalog/db_catalog_test.go +++ b/catalog/internal/catalog/db_catalog_test.go @@ -801,6 +801,168 @@ func TestDBCatalog(t *testing.T) { assert.Contains(t, err.Error(), "invalid model name") }) }) + + t.Run("TestGetFilterOptions", func(t *testing.T) { + // Create models with various properties for filter options testing + model1 := &models.CatalogModelImpl{ + TypeID: apiutils.Of(int32(catalogModelTypeID)), + Attributes: &models.CatalogModelAttributes{ + Name: apiutils.Of("filter-options-model-1"), + ExternalID: apiutils.Of("filter-opt-1"), + }, + Properties: &[]mr_models.Properties{ + {Name: "source_id", StringValue: apiutils.Of("filter-test-source")}, + {Name: "license", StringValue: apiutils.Of("MIT")}, + {Name: "provider", StringValue: apiutils.Of("HuggingFace")}, + {Name: "maturity", StringValue: apiutils.Of("stable")}, + {Name: "library_name", StringValue: apiutils.Of("transformers")}, + {Name: "language", StringValue: apiutils.Of(`["python", "rust"]`)}, + {Name: "tasks", StringValue: apiutils.Of(`["text-classification", "token-classification"]`)}, + }, + } + + model2 := &models.CatalogModelImpl{ + TypeID: apiutils.Of(int32(catalogModelTypeID)), + Attributes: &models.CatalogModelAttributes{ + Name: apiutils.Of("filter-options-model-2"), + ExternalID: apiutils.Of("filter-opt-2"), + }, + Properties: &[]mr_models.Properties{ + {Name: "source_id", StringValue: apiutils.Of("filter-test-source")}, + {Name: "license", StringValue: apiutils.Of("Apache-2.0")}, + {Name: "provider", StringValue: apiutils.Of("OpenAI")}, + {Name: "maturity", StringValue: apiutils.Of("experimental")}, + {Name: "library_name", StringValue: apiutils.Of("openai")}, + {Name: "language", StringValue: apiutils.Of(`["python", "javascript"]`)}, + {Name: "tasks", StringValue: apiutils.Of(`["text-generation", "conversational"]`)}, + {Name: "readme", StringValue: apiutils.Of("This is a very long readme that exceeds 100 characters and should be excluded from filter options because it's too verbose for filtering purposes.")}, + }, + } + + model3 := &models.CatalogModelImpl{ + TypeID: apiutils.Of(int32(catalogModelTypeID)), + Attributes: &models.CatalogModelAttributes{ + Name: apiutils.Of("filter-options-model-3"), + ExternalID: apiutils.Of("filter-opt-3"), + }, + Properties: &[]mr_models.Properties{ + {Name: "source_id", StringValue: apiutils.Of("filter-test-source")}, + {Name: "license", StringValue: apiutils.Of("MIT")}, + {Name: "provider", StringValue: apiutils.Of("PyTorch")}, + {Name: "maturity", StringValue: apiutils.Of("stable")}, + {Name: "language", StringValue: apiutils.Of(`["python"]`)}, + {Name: "tasks", StringValue: apiutils.Of(`["image-classification"]`)}, + {Name: "logo", StringValue: apiutils.Of("https://example.com/logo.png")}, + {Name: "license_link", StringValue: apiutils.Of("https://example.com/license")}, + }, + } + + _, err := catalogModelRepo.Save(model1) + require.NoError(t, err) + _, err = catalogModelRepo.Save(model2) + require.NoError(t, err) + _, err = catalogModelRepo.Save(model3) + require.NoError(t, err) + + // Test GetFilterOptions + filterOptions, err := dbCatalog.GetFilterOptions(ctx) + require.NoError(t, err) + require.NotNil(t, filterOptions) + require.NotNil(t, filterOptions.Filters) + + filters := *filterOptions.Filters + + // Should include short properties + assert.Contains(t, filters, "license") + assert.Contains(t, filters, "provider") + assert.Contains(t, filters, "maturity") + assert.Contains(t, filters, "library_name") + assert.Contains(t, filters, "language") + assert.Contains(t, filters, "tasks") + + // Should exclude internal/verbose fields + assert.NotContains(t, filters, "source_id", "source_id should be excluded") + assert.NotContains(t, filters, "logo", "logo should be excluded") + assert.NotContains(t, filters, "license_link", "license_link should be excluded") + assert.NotContains(t, filters, "readme", "readme should be excluded (too long)") + + licenseFilter := filters["license"] + assert.Equal(t, "string", licenseFilter.Type) + assert.NotNil(t, licenseFilter.Values) + assert.GreaterOrEqual(t, len(licenseFilter.Values), 2, "Should have at least MIT and Apache-2.0") + + // Convert to string slice for easier checking + licenseValues := make([]string, 0) + for _, v := range licenseFilter.Values { + if strVal, ok := v.(string); ok { + licenseValues = append(licenseValues, strVal) + } + } + assert.Contains(t, licenseValues, "MIT") + assert.Contains(t, licenseValues, "Apache-2.0") + + // Verify provider filter options + providerFilter := filters["provider"] + assert.Equal(t, "string", providerFilter.Type) + providerValues := make([]string, 0) + for _, v := range providerFilter.Values { + if strVal, ok := v.(string); ok { + providerValues = append(providerValues, strVal) + } + } + assert.Contains(t, providerValues, "HuggingFace") + assert.Contains(t, providerValues, "OpenAI") + assert.Contains(t, providerValues, "PyTorch") + + // Verify JSON array fields are properly parsed and expanded + languageFilter := filters["language"] + assert.Equal(t, "string", languageFilter.Type) + languageValues := make([]string, 0) + for _, v := range languageFilter.Values { + if strVal, ok := v.(string); ok { + languageValues = append(languageValues, strVal) + } + } + // Should contain individual values from JSON arrays + assert.Contains(t, languageValues, "python") + assert.Contains(t, languageValues, "rust") + assert.Contains(t, languageValues, "javascript") + + // Verify tasks are properly expanded + tasksFilter := filters["tasks"] + assert.Equal(t, "string", tasksFilter.Type) + tasksValues := make([]string, 0) + for _, v := range tasksFilter.Values { + if strVal, ok := v.(string); ok { + tasksValues = append(tasksValues, strVal) + } + } + assert.Contains(t, tasksValues, "text-classification") + assert.Contains(t, tasksValues, "token-classification") + assert.Contains(t, tasksValues, "text-generation") + assert.Contains(t, tasksValues, "conversational") + assert.Contains(t, tasksValues, "image-classification") + + // Verify no duplicates + pythonCount := 0 + for _, v := range languageValues { + if v == "python" { + pythonCount++ + } + } + assert.Equal(t, 1, pythonCount, "python should appear only once (deduplicated)") + + // Verify maturity options + maturityFilter := filters["maturity"] + maturityValues := make([]string, 0) + for _, v := range maturityFilter.Values { + if strVal, ok := v.(string); ok { + maturityValues = append(maturityValues, strVal) + } + } + assert.Contains(t, maturityValues, "stable") + assert.Contains(t, maturityValues, "experimental") + }) } // Helper functions to get type IDs from database diff --git a/catalog/internal/catalog/hf_catalog.go b/catalog/internal/catalog/hf_catalog.go index 4832eca6..0ae837b7 100644 --- a/catalog/internal/catalog/hf_catalog.go +++ b/catalog/internal/catalog/hf_catalog.go @@ -49,6 +49,15 @@ func (h *hfCatalogImpl) GetArtifacts(ctx context.Context, modelName string, sour }, nil } +func (h *hfCatalogImpl) GetFilterOptions(ctx context.Context) (*openapi.FilterOptionsList, error) { + // TODO: Implement HuggingFace filter options retrieval + // For now, return empty options to satisfy interface + emptyFilters := make(map[string]openapi.FilterOption) + return &openapi.FilterOptionsList{ + Filters: &emptyFilters, + }, nil +} + // validateCredentials checks if the HuggingFace API credentials are valid func (h *hfCatalogImpl) validateCredentials(ctx context.Context) error { glog.Infof("Validating HuggingFace API credentials") diff --git a/catalog/internal/db/models/catalog_model.go b/catalog/internal/db/models/catalog_model.go index 1769e7d2..9106f481 100644 --- a/catalog/internal/db/models/catalog_model.go +++ b/catalog/internal/db/models/catalog_model.go @@ -37,4 +37,7 @@ type CatalogModelRepository interface { GetByName(name string) (CatalogModel, error) List(listOptions CatalogModelListOptions) (*models.ListWrapper[CatalogModel], error) Save(model CatalogModel) (CatalogModel, error) + // GetFilterableProperties returns a map of property names to their unique values + // Only includes string properties where all values are shorter than maxLength + GetFilterableProperties(maxLength int) (map[string][]string, error) } diff --git a/catalog/internal/db/service/catalog_model.go b/catalog/internal/db/service/catalog_model.go index 37ed30b5..f66cba37 100644 --- a/catalog/internal/db/service/catalog_model.go +++ b/catalog/internal/db/service/catalog_model.go @@ -231,3 +231,56 @@ func mapDataLayerToCatalogModel(modelCtx schema.Context, propertiesCtx []schema. return catalogModel } + +// GetFilterableProperties returns property names and their unique values +// Only includes properties where ALL values are shorter than maxLength +func (r *CatalogModelRepositoryImpl) GetFilterableProperties(maxLength int) (map[string][]string, error) { + config := r.GetConfig() + + // Get table names using GORM utilities for database compatibility + contextTable := utils.GetTableName(config.DB, &schema.Context{}) + propertyTable := utils.GetTableName(config.DB, &schema.ContextProperty{}) + + // Simplified query: get distinct property name/value pairs + query := fmt.Sprintf(` + SELECT DISTINCT cp.name, cp.string_value + FROM %s cp + WHERE cp.context_id IN ( + SELECT id FROM %s WHERE type_id = ? + ) + AND cp.name IN ( + SELECT name FROM ( + SELECT name, MAX(CHAR_LENGTH(string_value)) as max_len + FROM %s + WHERE context_id IN ( + SELECT id FROM %s WHERE type_id = ? + ) + AND string_value IS NOT NULL + AND string_value != '' + GROUP BY name + ) AS field_lengths + WHERE max_len <= ? + ) + AND cp.string_value IS NOT NULL + AND cp.string_value != '' + ORDER BY cp.name, cp.string_value + `, propertyTable, contextTable, propertyTable, contextTable) + + type propertyRow struct { + Name string + StringValue string + } + + var rows []propertyRow + if err := config.DB.Raw(query, config.TypeID, config.TypeID, maxLength).Scan(&rows).Error; err != nil { + return nil, fmt.Errorf("error querying filterable properties: %w", err) + } + + // Aggregate values by property name in Go + result := make(map[string][]string) + for _, row := range rows { + result[row.Name] = append(result[row.Name], row.StringValue) + } + + return result, nil +} diff --git a/catalog/internal/db/service/catalog_model_test.go b/catalog/internal/db/service/catalog_model_test.go index 1c45693d..e0dc76ab 100644 --- a/catalog/internal/db/service/catalog_model_test.go +++ b/catalog/internal/db/service/catalog_model_test.go @@ -337,6 +337,85 @@ func TestCatalogModelRepository(t *testing.T) { require.NotNil(t, retrieved.GetCustomProperties()) assert.Len(t, *retrieved.GetCustomProperties(), 2) }) + + t.Run("TestGetFilterableProperties", func(t *testing.T) { + // Create models with various property lengths + shortValueModel := &models.CatalogModelImpl{ + Attributes: &models.CatalogModelAttributes{ + Name: apiutils.Of("short-value-model"), + ExternalID: apiutils.Of("short-ext"), + }, + Properties: &[]dbmodels.Properties{ + {Name: "license", StringValue: apiutils.Of("MIT")}, + {Name: "provider", StringValue: apiutils.Of("HuggingFace")}, + {Name: "maturity", StringValue: apiutils.Of("stable")}, + }, + } + + longValueModel := &models.CatalogModelImpl{ + Attributes: &models.CatalogModelAttributes{ + Name: apiutils.Of("long-value-model"), + ExternalID: apiutils.Of("long-ext"), + }, + Properties: &[]dbmodels.Properties{ + {Name: "license", StringValue: apiutils.Of("Apache-2.0")}, + {Name: "readme", StringValue: apiutils.Of("This is a very long readme that should be excluded from filterable properties because it exceeds the maximum length threshold of 100 characters. It contains detailed information about the model.")}, + {Name: "description", StringValue: apiutils.Of("This is also a very long description that should be excluded from filterable properties because it exceeds 100 chars")}, + }, + } + + jsonArrayModel := &models.CatalogModelImpl{ + Attributes: &models.CatalogModelAttributes{ + Name: apiutils.Of("json-array-model"), + ExternalID: apiutils.Of("json-ext"), + }, + Properties: &[]dbmodels.Properties{ + {Name: "language", StringValue: apiutils.Of(`["python", "go"]`)}, + {Name: "tasks", StringValue: apiutils.Of(`["text-classification", "question-answering"]`)}, + }, + } + + _, err := repo.Save(shortValueModel) + require.NoError(t, err) + _, err = repo.Save(longValueModel) + require.NoError(t, err) + _, err = repo.Save(jsonArrayModel) + require.NoError(t, err) + + // Test with max length of 100 + result, err := repo.GetFilterableProperties(100) + require.NoError(t, err) + require.NotNil(t, result) + + // Should include short properties + assert.Contains(t, result, "license") + assert.Contains(t, result, "provider") + assert.Contains(t, result, "maturity") + assert.Contains(t, result, "language") + assert.Contains(t, result, "tasks") + + // Should exclude long properties + assert.NotContains(t, result, "readme") + assert.NotContains(t, result, "description") + + // Verify license has both values + licenseValues := result["license"] + assert.GreaterOrEqual(t, len(licenseValues), 2) + assert.Contains(t, licenseValues, "MIT") + assert.Contains(t, licenseValues, "Apache-2.0") + + // Test with smaller max length + result, err = repo.GetFilterableProperties(10) + require.NoError(t, err) + require.NotNil(t, result) + + // Should include only very short properties + assert.Contains(t, result, "license") + // Should exclude longer properties + assert.NotContains(t, result, "provider") // "HuggingFace" is > 10 chars + assert.NotContains(t, result, "language") + assert.NotContains(t, result, "tasks") + }) } // Helper function to get or create CatalogModel type ID diff --git a/catalog/internal/server/openapi/.openapi-generator/FILES b/catalog/internal/server/openapi/.openapi-generator/FILES index 7130db6b..45b01737 100644 --- a/catalog/internal/server/openapi/.openapi-generator/FILES +++ b/catalog/internal/server/openapi/.openapi-generator/FILES @@ -18,6 +18,9 @@ model_catalog_model_list.go model_catalog_source.go model_catalog_source_list.go model_error.go +model_filter_option.go +model_filter_option_range.go +model_filter_options_list.go model_metadata_bool_value.go model_metadata_double_value.go model_metadata_int_value.go diff --git a/catalog/internal/server/openapi/api.go b/catalog/internal/server/openapi/api.go index da122e51..7d0c48eb 100644 --- a/catalog/internal/server/openapi/api.go +++ b/catalog/internal/server/openapi/api.go @@ -22,6 +22,7 @@ import ( // pass the data to a ModelCatalogServiceAPIServicer to perform the required actions, then write the service results to the http response. type ModelCatalogServiceAPIRouter interface { FindModels(http.ResponseWriter, *http.Request) + FindModelsFilterOptions(http.ResponseWriter, *http.Request) FindSources(http.ResponseWriter, *http.Request) GetModel(http.ResponseWriter, *http.Request) GetAllModelArtifacts(http.ResponseWriter, *http.Request) @@ -33,6 +34,7 @@ type ModelCatalogServiceAPIRouter interface { // and updated with the logic required for the API. type ModelCatalogServiceAPIServicer interface { FindModels(context.Context, []string, string, []string, string, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) + FindModelsFilterOptions(context.Context) (ImplResponse, error) FindSources(context.Context, string, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) GetModel(context.Context, string, string) (ImplResponse, error) GetAllModelArtifacts(context.Context, string, string, string, string, model.OrderByField, model.SortOrder, string) (ImplResponse, error) diff --git a/catalog/internal/server/openapi/api_model_catalog_service.go b/catalog/internal/server/openapi/api_model_catalog_service.go index 30922b6f..dc9ef130 100644 --- a/catalog/internal/server/openapi/api_model_catalog_service.go +++ b/catalog/internal/server/openapi/api_model_catalog_service.go @@ -57,6 +57,11 @@ func (c *ModelCatalogServiceAPIController) Routes() Routes { "/api/model_catalog/v1alpha1/models", c.FindModels, }, + "FindModelsFilterOptions": Route{ + strings.ToUpper("Get"), + "/api/model_catalog/v1alpha1/models/filter_options", + c.FindModelsFilterOptions, + }, "FindSources": Route{ strings.ToUpper("Get"), "/api/model_catalog/v1alpha1/sources", @@ -96,6 +101,18 @@ func (c *ModelCatalogServiceAPIController) FindModels(w http.ResponseWriter, r * EncodeJSONResponse(result.Body, &result.Code, w) } +// FindModelsFilterOptions - Lists fields and available options that can be used in `filterQuery` on the list models endpoint. +func (c *ModelCatalogServiceAPIController) FindModelsFilterOptions(w http.ResponseWriter, r *http.Request) { + result, err := c.service.FindModelsFilterOptions(r.Context()) + // If an error occurred, encode the error with the status code + if err != nil { + c.errorHandler(w, r, err, &result) + return + } + // If no error, encode the body and the result code + EncodeJSONResponse(result.Body, &result.Code, w) +} + // FindSources - List All CatalogSources func (c *ModelCatalogServiceAPIController) FindSources(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() diff --git a/catalog/internal/server/openapi/api_model_catalog_service_service.go b/catalog/internal/server/openapi/api_model_catalog_service_service.go index c7d1659c..69ec2e1d 100644 --- a/catalog/internal/server/openapi/api_model_catalog_service_service.go +++ b/catalog/internal/server/openapi/api_model_catalog_service_service.go @@ -42,11 +42,11 @@ func (m *ModelCatalogServiceAPIService) GetAllModelArtifacts(ctx context.Context } artifacts, err := m.provider.GetArtifacts(ctx, modelName, sourceID, catalog.ListArtifactsParams{ + ArtifactType: &artifactType, PageSize: pageSizeInt, OrderBy: orderBy, SortOrder: sortOrder, NextPageToken: &nextPageToken, - ArtifactType: &artifactType, }) if err != nil { statusCode := api.ErrToStatus(err) @@ -64,7 +64,7 @@ func (m *ModelCatalogServiceAPIService) GetAllModelArtifacts(ctx context.Context return Response(http.StatusOK, artifacts), nil } -func (m *ModelCatalogServiceAPIService) FindModels(ctx context.Context, sourceIDs []string, q string, sourceLabels []string, filterQuery, pageSize string, orderBy model.OrderByField, sortOrder model.SortOrder, nextPageToken string) (ImplResponse, error) { +func (m *ModelCatalogServiceAPIService) FindModels(ctx context.Context, sourceIDs []string, q string, sourceLabels []string, filterQuery string, pageSize string, orderBy model.OrderByField, sortOrder model.SortOrder, nextPageToken string) (ImplResponse, error) { var err error pageSizeInt := int32(10) @@ -107,6 +107,15 @@ func (m *ModelCatalogServiceAPIService) FindModels(ctx context.Context, sourceID return Response(http.StatusOK, models), nil } +func (m *ModelCatalogServiceAPIService) FindModelsFilterOptions(ctx context.Context) (ImplResponse, error) { + filterOptions, err := m.provider.GetFilterOptions(ctx) + if err != nil { + return ErrorResponse(http.StatusInternalServerError, err), err + } + + return Response(http.StatusOK, filterOptions), nil +} + func (m *ModelCatalogServiceAPIService) GetModel(ctx context.Context, sourceID, modelName string) (ImplResponse, error) { if newName, err := url.PathUnescape(modelName); err == nil { modelName = newName diff --git a/catalog/internal/server/openapi/api_model_catalog_service_service_test.go b/catalog/internal/server/openapi/api_model_catalog_service_service_test.go index 5075f93c..190470ee 100644 --- a/catalog/internal/server/openapi/api_model_catalog_service_service_test.go +++ b/catalog/internal/server/openapi/api_model_catalog_service_service_test.go @@ -776,6 +776,11 @@ func (m *mockModelProvider) GetArtifacts(ctx context.Context, name string, sourc }, nil } +func (m *mockModelProvider) GetFilterOptions(ctx context.Context) (*model.FilterOptionsList, error) { + emptyFilters := make(map[string]model.FilterOption) + return &model.FilterOptionsList{Filters: &emptyFilters}, nil +} + func TestGetModel(t *testing.T) { testCases := []struct { name string @@ -1002,3 +1007,44 @@ func TestGetAllModelArtifacts(t *testing.T) { }) } } + +func TestFindModelsFilterOptions(t *testing.T) { + testCases := []struct { + name string + provider catalog.APIProvider + expectedStatus int + expectedError bool + }{ + { + name: "Successfully retrieve filter options", + provider: &mockModelProvider{ + models: map[string]*model.CatalogModel{}, + }, + expectedStatus: http.StatusOK, + expectedError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sources := catalog.NewSourceCollection() + service := NewModelCatalogServiceAPIService(tc.provider, sources) + + resp, err := service.FindModelsFilterOptions(context.Background()) + + assert.Equal(t, tc.expectedStatus, resp.Code) + + if tc.expectedError { + assert.Error(t, err) + return + } + require.NotNil(t, resp.Body) + + // Type assertion to access the FilterOptionsList + filterOptions, ok := resp.Body.(*model.FilterOptionsList) + require.True(t, ok, "Response body should be a FilterOptionsList") + + require.NotNil(t, filterOptions.Filters) + }) + } +} diff --git a/catalog/internal/server/openapi/type_asserts.go b/catalog/internal/server/openapi/type_asserts.go index b54eae4e..5fe02de6 100644 --- a/catalog/internal/server/openapi/type_asserts.go +++ b/catalog/internal/server/openapi/type_asserts.go @@ -257,6 +257,26 @@ func AssertErrorRequired(obj model.Error) error { return nil } +// AssertFilterOptionRangeConstraints checks if the values respects the defined constraints +func AssertFilterOptionRangeConstraints(obj model.FilterOptionRange) error { + return nil +} + +// AssertFilterOptionRangeRequired checks if the required fields are not zero-ed +func AssertFilterOptionRangeRequired(obj model.FilterOptionRange) error { + return nil +} + +// AssertFilterOptionsListConstraints checks if the values respects the defined constraints +func AssertFilterOptionsListConstraints(obj model.FilterOptionsList) error { + return nil +} + +// AssertFilterOptionsListRequired checks if the required fields are not zero-ed +func AssertFilterOptionsListRequired(obj model.FilterOptionsList) error { + return nil +} + // AssertMetadataBoolValueConstraints checks if the values respects the defined constraints func AssertMetadataBoolValueConstraints(obj model.MetadataBoolValue) error { return nil diff --git a/catalog/internal/server/openapi/type_asserts_overrides.go b/catalog/internal/server/openapi/type_asserts_overrides.go index 4e950e74..40b4a6d4 100644 --- a/catalog/internal/server/openapi/type_asserts_overrides.go +++ b/catalog/internal/server/openapi/type_asserts_overrides.go @@ -10,3 +10,22 @@ func AssertCatalogArtifactRequired(obj model.CatalogArtifact) error { // checks the fields from CatalogModelArtifact, which doesn't compile. return nil } + +// AssertFilterOptionRequired checks if the required fields are not zero-ed +func AssertFilterOptionRequired(obj model.FilterOption) error { + elements := map[string]interface{}{ + "type": obj.Type, + } + for name, el := range elements { + if isZero := IsZeroValue(el); isZero { + return &RequiredError{Field: name} + } + } + + if obj.Range != nil { + if err := AssertFilterOptionRangeRequired(*obj.Range); err != nil { + return err + } + } + return nil +} diff --git a/catalog/pkg/openapi/.openapi-generator/FILES b/catalog/pkg/openapi/.openapi-generator/FILES index 7291aeab..d7ca8add 100644 --- a/catalog/pkg/openapi/.openapi-generator/FILES +++ b/catalog/pkg/openapi/.openapi-generator/FILES @@ -15,6 +15,9 @@ model_catalog_model_list.go model_catalog_source.go model_catalog_source_list.go model_error.go +model_filter_option.go +model_filter_option_range.go +model_filter_options_list.go model_metadata_bool_value.go model_metadata_double_value.go model_metadata_int_value.go diff --git a/catalog/pkg/openapi/api_model_catalog_service.go b/catalog/pkg/openapi/api_model_catalog_service.go index f764523a..51d7f9bb 100644 --- a/catalog/pkg/openapi/api_model_catalog_service.go +++ b/catalog/pkg/openapi/api_model_catalog_service.go @@ -260,6 +260,136 @@ func (a *ModelCatalogServiceAPIService) FindModelsExecute(r ApiFindModelsRequest return localVarReturnValue, localVarHTTPResponse, nil } +type ApiFindModelsFilterOptionsRequest struct { + ctx context.Context + ApiService *ModelCatalogServiceAPIService +} + +func (r ApiFindModelsFilterOptionsRequest) Execute() (*FilterOptionsList, *http.Response, error) { + return r.ApiService.FindModelsFilterOptionsExecute(r) +} + +/* +FindModelsFilterOptions Lists fields and available options that can be used in `filterQuery` on the list models endpoint. + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiFindModelsFilterOptionsRequest +*/ +func (a *ModelCatalogServiceAPIService) FindModelsFilterOptions(ctx context.Context) ApiFindModelsFilterOptionsRequest { + return ApiFindModelsFilterOptionsRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return FilterOptionsList +func (a *ModelCatalogServiceAPIService) FindModelsFilterOptionsExecute(r ApiFindModelsFilterOptionsRequest) (*FilterOptionsList, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *FilterOptionsList + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "ModelCatalogServiceAPIService.FindModelsFilterOptions") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/model_catalog/v1alpha1/models/filter_options" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiFindSourcesRequest struct { ctx context.Context ApiService *ModelCatalogServiceAPIService diff --git a/catalog/pkg/openapi/model_filter_option.go b/catalog/pkg/openapi/model_filter_option.go new file mode 100644 index 00000000..5dd7b37d --- /dev/null +++ b/catalog/pkg/openapi/model_filter_option.go @@ -0,0 +1,189 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the FilterOption type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &FilterOption{} + +// FilterOption struct for FilterOption +type FilterOption struct { + // The data type of the filter option + Type string `json:"type"` + // Known values of the property for string types with a small number of possible options. + Values []interface{} `json:"values,omitempty"` + Range *FilterOptionRange `json:"range,omitempty"` +} + +// NewFilterOption instantiates a new FilterOption object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewFilterOption(type_ string) *FilterOption { + this := FilterOption{} + this.Type = type_ + return &this +} + +// NewFilterOptionWithDefaults instantiates a new FilterOption object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewFilterOptionWithDefaults() *FilterOption { + this := FilterOption{} + return &this +} + +// GetType returns the Type field value +func (o *FilterOption) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *FilterOption) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *FilterOption) SetType(v string) { + o.Type = v +} + +// GetValues returns the Values field value if set, zero value otherwise. +func (o *FilterOption) GetValues() []interface{} { + if o == nil || IsNil(o.Values) { + var ret []interface{} + return ret + } + return o.Values +} + +// GetValuesOk returns a tuple with the Values field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FilterOption) GetValuesOk() ([]interface{}, bool) { + if o == nil || IsNil(o.Values) { + return nil, false + } + return o.Values, true +} + +// HasValues returns a boolean if a field has been set. +func (o *FilterOption) HasValues() bool { + if o != nil && !IsNil(o.Values) { + return true + } + + return false +} + +// SetValues gets a reference to the given []interface{} and assigns it to the Values field. +func (o *FilterOption) SetValues(v []interface{}) { + o.Values = v +} + +// GetRange returns the Range field value if set, zero value otherwise. +func (o *FilterOption) GetRange() FilterOptionRange { + if o == nil || IsNil(o.Range) { + var ret FilterOptionRange + return ret + } + return *o.Range +} + +// GetRangeOk returns a tuple with the Range field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FilterOption) GetRangeOk() (*FilterOptionRange, bool) { + if o == nil || IsNil(o.Range) { + return nil, false + } + return o.Range, true +} + +// HasRange returns a boolean if a field has been set. +func (o *FilterOption) HasRange() bool { + if o != nil && !IsNil(o.Range) { + return true + } + + return false +} + +// SetRange gets a reference to the given FilterOptionRange and assigns it to the Range field. +func (o *FilterOption) SetRange(v FilterOptionRange) { + o.Range = &v +} + +func (o FilterOption) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o FilterOption) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["type"] = o.Type + if !IsNil(o.Values) { + toSerialize["values"] = o.Values + } + if !IsNil(o.Range) { + toSerialize["range"] = o.Range + } + return toSerialize, nil +} + +type NullableFilterOption struct { + value *FilterOption + isSet bool +} + +func (v NullableFilterOption) Get() *FilterOption { + return v.value +} + +func (v *NullableFilterOption) Set(val *FilterOption) { + v.value = val + v.isSet = true +} + +func (v NullableFilterOption) IsSet() bool { + return v.isSet +} + +func (v *NullableFilterOption) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFilterOption(val *FilterOption) *NullableFilterOption { + return &NullableFilterOption{value: val, isSet: true} +} + +func (v NullableFilterOption) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFilterOption) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_filter_option_range.go b/catalog/pkg/openapi/model_filter_option_range.go new file mode 100644 index 00000000..12a6ede0 --- /dev/null +++ b/catalog/pkg/openapi/model_filter_option_range.go @@ -0,0 +1,160 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the FilterOptionRange type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &FilterOptionRange{} + +// FilterOptionRange Min and max values for number types. +type FilterOptionRange struct { + Min *float32 `json:"min,omitempty"` + Max *float32 `json:"max,omitempty"` +} + +// NewFilterOptionRange instantiates a new FilterOptionRange object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewFilterOptionRange() *FilterOptionRange { + this := FilterOptionRange{} + return &this +} + +// NewFilterOptionRangeWithDefaults instantiates a new FilterOptionRange object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewFilterOptionRangeWithDefaults() *FilterOptionRange { + this := FilterOptionRange{} + return &this +} + +// GetMin returns the Min field value if set, zero value otherwise. +func (o *FilterOptionRange) GetMin() float32 { + if o == nil || IsNil(o.Min) { + var ret float32 + return ret + } + return *o.Min +} + +// GetMinOk returns a tuple with the Min field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FilterOptionRange) GetMinOk() (*float32, bool) { + if o == nil || IsNil(o.Min) { + return nil, false + } + return o.Min, true +} + +// HasMin returns a boolean if a field has been set. +func (o *FilterOptionRange) HasMin() bool { + if o != nil && !IsNil(o.Min) { + return true + } + + return false +} + +// SetMin gets a reference to the given float32 and assigns it to the Min field. +func (o *FilterOptionRange) SetMin(v float32) { + o.Min = &v +} + +// GetMax returns the Max field value if set, zero value otherwise. +func (o *FilterOptionRange) GetMax() float32 { + if o == nil || IsNil(o.Max) { + var ret float32 + return ret + } + return *o.Max +} + +// GetMaxOk returns a tuple with the Max field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FilterOptionRange) GetMaxOk() (*float32, bool) { + if o == nil || IsNil(o.Max) { + return nil, false + } + return o.Max, true +} + +// HasMax returns a boolean if a field has been set. +func (o *FilterOptionRange) HasMax() bool { + if o != nil && !IsNil(o.Max) { + return true + } + + return false +} + +// SetMax gets a reference to the given float32 and assigns it to the Max field. +func (o *FilterOptionRange) SetMax(v float32) { + o.Max = &v +} + +func (o FilterOptionRange) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o FilterOptionRange) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Min) { + toSerialize["min"] = o.Min + } + if !IsNil(o.Max) { + toSerialize["max"] = o.Max + } + return toSerialize, nil +} + +type NullableFilterOptionRange struct { + value *FilterOptionRange + isSet bool +} + +func (v NullableFilterOptionRange) Get() *FilterOptionRange { + return v.value +} + +func (v *NullableFilterOptionRange) Set(val *FilterOptionRange) { + v.value = val + v.isSet = true +} + +func (v NullableFilterOptionRange) IsSet() bool { + return v.isSet +} + +func (v *NullableFilterOptionRange) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFilterOptionRange(val *FilterOptionRange) *NullableFilterOptionRange { + return &NullableFilterOptionRange{value: val, isSet: true} +} + +func (v NullableFilterOptionRange) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFilterOptionRange) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/pkg/openapi/model_filter_options_list.go b/catalog/pkg/openapi/model_filter_options_list.go new file mode 100644 index 00000000..c5862fbe --- /dev/null +++ b/catalog/pkg/openapi/model_filter_options_list.go @@ -0,0 +1,125 @@ +/* +Model Catalog REST API + +REST API for Model Registry to create and manage ML model metadata + +API version: v1alpha1 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the FilterOptionsList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &FilterOptionsList{} + +// FilterOptionsList List of FilterOptions +type FilterOptionsList struct { + // A single filter option. + Filters *map[string]FilterOption `json:"filters,omitempty"` +} + +// NewFilterOptionsList instantiates a new FilterOptionsList object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewFilterOptionsList() *FilterOptionsList { + this := FilterOptionsList{} + return &this +} + +// NewFilterOptionsListWithDefaults instantiates a new FilterOptionsList object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewFilterOptionsListWithDefaults() *FilterOptionsList { + this := FilterOptionsList{} + return &this +} + +// GetFilters returns the Filters field value if set, zero value otherwise. +func (o *FilterOptionsList) GetFilters() map[string]FilterOption { + if o == nil || IsNil(o.Filters) { + var ret map[string]FilterOption + return ret + } + return *o.Filters +} + +// GetFiltersOk returns a tuple with the Filters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *FilterOptionsList) GetFiltersOk() (*map[string]FilterOption, bool) { + if o == nil || IsNil(o.Filters) { + return nil, false + } + return o.Filters, true +} + +// HasFilters returns a boolean if a field has been set. +func (o *FilterOptionsList) HasFilters() bool { + if o != nil && !IsNil(o.Filters) { + return true + } + + return false +} + +// SetFilters gets a reference to the given map[string]FilterOption and assigns it to the Filters field. +func (o *FilterOptionsList) SetFilters(v map[string]FilterOption) { + o.Filters = &v +} + +func (o FilterOptionsList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o FilterOptionsList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Filters) { + toSerialize["filters"] = o.Filters + } + return toSerialize, nil +} + +type NullableFilterOptionsList struct { + value *FilterOptionsList + isSet bool +} + +func (v NullableFilterOptionsList) Get() *FilterOptionsList { + return v.value +} + +func (v *NullableFilterOptionsList) Set(val *FilterOptionsList) { + v.value = val + v.isSet = true +} + +func (v NullableFilterOptionsList) IsSet() bool { + return v.isSet +} + +func (v *NullableFilterOptionsList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFilterOptionsList(val *FilterOptionsList) *NullableFilterOptionsList { + return &NullableFilterOptionsList{value: val, isSet: true} +} + +func (v NullableFilterOptionsList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFilterOptionsList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/catalog/scripts/gen_type_asserts.sh b/catalog/scripts/gen_type_asserts.sh index 900240e5..e5ed01f7 100755 --- a/catalog/scripts/gen_type_asserts.sh +++ b/catalog/scripts/gen_type_asserts.sh @@ -7,7 +7,9 @@ ASSERT_FILE_PATH="$1/type_asserts.go" PROJECT_ROOT=$(realpath "$(dirname "$0")"/..) # These files generate with incorrect logic: -rm -f "$1/model_metadata_value.go" "$1/model_catalog_artifact.go" +rm -f "$1/model_metadata_value.go" \ + "$1/model_catalog_artifact.go" \ + "$1/model_filter_option.go" python3 "${PROJECT_ROOT}/scripts/gen_type_asserts.py" $1 >"$ASSERT_FILE_PATH"