Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## 2025-05-08 "Metadata Field Management & Type Safety" - version 1.11.0

### Added
- **Metadata Field Management (in `pythonik.specs.metadata.MetadataSpec`):**
- `create_metadata_field()`: Added method to create new metadata fields.
- `update_metadata_field()`: Added method to update existing metadata fields by name.
- `delete_metadata_field()`: Added method to delete metadata fields by name.
- **Type Safety & Modeling (in `pythonik.models.metadata.fields`):**
- Defined `IconikFieldType` Enum to represent all known metadata field types from the Iconik API.
- Integrated `IconikFieldType` into `FieldCreate`, `FieldUpdate`, and `Field` Pydantic models for robust type validation and clarity.
- **Testing:**
- Added comprehensive test coverage for metadata field type handling and new CRUD operations:
- Parameterized test (`test_create_metadata_field_for_all_types`) for every defined `IconikFieldType`.
- Test (`test_create_metadata_field_with_unknown_type_raises_validation_error`) for API returning unrecognized `field_type`.

### Technical Details
This update introduces full programmatic management of metadata fields, including creation, update, and deletion capabilities. A dedicated Enum (`IconikFieldType`) enhances type safety and developer experience when working with these fields. This Enum is integrated throughout the relevant Pydantic models and the new `MetadataSpec` methods. Comprehensive tests ensure correct handling of all defined field types, robust behavior against unknown types from the API, and functionality of the new CRUD operations.

## 2025-05-02 "Segment Deletion" - version 1.10.0

### Added
Expand Down
73 changes: 73 additions & 0 deletions pythonik/models/metadata/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# pythonik/models/metadata/fields.py
from typing import List, Optional
from datetime import datetime
from enum import Enum
from pydantic import BaseModel, HttpUrl

class IconikFieldType(str, Enum):
"""Known Iconik metadata field types based on documentation.
Actual values sent to/received from API.
"""
STRING = "string" # General short text
TEXT = "text" # Longer text (potentially multi-line in UI but distinct type)
TEXT_AREA = "text_area" # Explicitly for larger amounts of text data
INTEGER = "integer"
FLOAT = "float"
BOOLEAN = "boolean" # For Yes/No fields
DATE = "date"
DATETIME = "datetime" # For Date Time fields
DROPDOWN = "drop_down" # For fields with predefined options
EMAIL = "email"
TAG_CLOUD = "tag_cloud" # For free-form tag collections
URL = "url"

class FieldOption(BaseModel):
"""Represents an option for a metadata field (e.g., for dropdowns)."""
label: Optional[str] = None
value: Optional[str] = None

class _FieldConfigurable(BaseModel):
"""Base model for common configurable attributes of metadata fields."""
label: Optional[str] = None
field_type: Optional[IconikFieldType] = None
description: Optional[str] = None
options: Optional[List[FieldOption]] = None
required: Optional[bool] = None
auto_set: Optional[bool] = None
hide_if_not_set: Optional[bool] = None
is_block_field: Optional[bool] = None
is_warning_field: Optional[bool] = None
multi: Optional[bool] = None
read_only: Optional[bool] = None
representative: Optional[bool] = None
sortable: Optional[bool] = None
use_as_facet: Optional[bool] = None
min_value: Optional[float] = None
max_value: Optional[float] = None
external_id: Optional[str] = None
source_url: Optional[HttpUrl] = None

class FieldCreate(_FieldConfigurable):
"""Data Transfer Object for creating a new metadata field."""
name: str
label: str
field_type: IconikFieldType

class FieldUpdate(_FieldConfigurable):
"""
Data Transfer Object for updating an existing metadata field.
All fields are optional to support partial updates.
'name' is specified in the URL path for updates, not in the body.
"""
pass

class Field(_FieldConfigurable):
"""Represents a metadata field as returned by the API."""
id: str
name: str
label: str
field_type: IconikFieldType

date_created: Optional[datetime] = None
date_modified: Optional[datetime] = None
mapped_field_name: Optional[str] = None
75 changes: 74 additions & 1 deletion pythonik/specs/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
UpdateMetadata,
UpdateMetadataResponse,
)
from pythonik.models.metadata.fields import Field, FieldCreate, FieldUpdate
from pythonik.specs.base import Spec
from typing import Literal, Union, Dict, Any, List

Expand All @@ -27,6 +28,10 @@
UPDATE_VIEW_PATH = GET_VIEW_PATH
DELETE_VIEW_PATH = GET_VIEW_PATH

# Field paths
FIELDS_BASE_PATH = "fields/"
FIELD_BY_NAME_PATH = "fields/{field_name}/"


ObjectType = Literal["segments"]

Expand Down Expand Up @@ -383,7 +388,7 @@ def delete_view(self, view_id: str, **kwargs) -> Response:
- can_delete_metadata_views

Returns:
Response: Empty response with 204 status code
Response: An empty response, expecting HTTP 204 No Content on success.

Raises:
- 400 Bad request
Expand All @@ -392,3 +397,71 @@ def delete_view(self, view_id: str, **kwargs) -> Response:
"""
resp = self._delete(DELETE_VIEW_PATH.format(view_id=view_id), **kwargs)
return self.parse_response(resp, None)

# Metadata Field Management
# -------------------------

def create_metadata_field(
self,
field_data: FieldCreate,
exclude_defaults: bool = True,
**kwargs,
) -> Response:
"""Create a new metadata field.

Args:
field_data: The data for the new field.
exclude_defaults: Whether to exclude default values when dumping Pydantic models.
**kwargs: Additional kwargs to pass to the request.

Returns:
Response: The created metadata field.
"""
json_data = self._prepare_model_data(
field_data, exclude_defaults=exclude_defaults
)
resp = self._post(FIELDS_BASE_PATH, json=json_data, **kwargs)
return self.parse_response(resp, Field)

def update_metadata_field(
self,
field_name: str,
field_data: FieldUpdate,
exclude_defaults: bool = True,
**kwargs,
) -> Response:
"""Update an existing metadata field by its name.

Args:
field_name: The name of the field to update.
field_data: The data to update the field with.
exclude_defaults: Whether to exclude default values when dumping Pydantic models.
**kwargs: Additional kwargs to pass to the request.

Returns:
Response: The updated metadata field.
"""
json_data = self._prepare_model_data(
field_data, exclude_defaults=exclude_defaults
)
endpoint = FIELD_BY_NAME_PATH.format(field_name=field_name)
resp = self._put(endpoint, json=json_data, **kwargs)
return self.parse_response(resp, Field)

def delete_metadata_field(
self,
field_name: str,
**kwargs,
) -> Response:
"""Delete a metadata field by its name.

Args:
field_name: The name of the field to delete.
**kwargs: Additional kwargs to pass to the request.

Returns:
Response: An empty response, expecting HTTP 204 No Content on success.
"""
endpoint = FIELD_BY_NAME_PATH.format(field_name=field_name)
resp = self._delete(endpoint, **kwargs)
return self.parse_response(resp)
Loading