From f597200ccf4b3825bf3d937b500c892326128c1c Mon Sep 17 00:00:00 2001 From: Brian Summa Date: Thu, 22 May 2025 10:34:32 -0500 Subject: [PATCH 1/2] fix: address Pydantic v2 deprecation warnings in tests - Replace class Config with ConfigDict in FieldResponse model - Fix serialization warnings in Resolution and TimeCode classes - Update typing for Resolution and TimeCode fields in Keyframe model - Improve create_asset_filesets deprecation notice - Add model_validate method for dict handling in relevant models - Add shell script to remove trailing whitespace in files These changes eliminate the following pytest warnings: - "Support for class-based `config` is deprecated" - "create_asset_filesets is deprecated" - "Pydantic serializer warnings" Part of ongoing maintenance to ensure compatibility with Pydantic v2. --- pythonik/models/files/keyframe.py | 21 ++++- pythonik/models/metadata/fields.py | 8 +- pythonik/specs/files.py | 140 +++++++++++++++-------------- pythonik/tests/test_files.py | 2 +- remove_trailing_spaces.sh | 25 ++++++ 5 files changed, 123 insertions(+), 73 deletions(-) create mode 100755 remove_trailing_spaces.sh diff --git a/pythonik/models/files/keyframe.py b/pythonik/models/files/keyframe.py index 9faf0d1..3e757d7 100644 --- a/pythonik/models/files/keyframe.py +++ b/pythonik/models/files/keyframe.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel + from pythonik.models.base import PaginatedResponse @@ -15,6 +16,13 @@ class Resolution(BaseModel): height: Optional[int] = None width: Optional[int] = None + # For Pydantic v2 + @classmethod + def model_validate(cls, obj, *args, **kwargs): + if isinstance(obj, dict): + return super().model_validate(obj, *args, **kwargs) + return obj + class TimeBase(BaseModel): denominator: Optional[int] = None @@ -26,6 +34,13 @@ class TimeCode(BaseModel): is_drop_frame: Optional[bool] = None time_base: Optional[TimeBase] = {} + # For Pydantic v2 + @classmethod + def model_validate(cls, obj, *args, **kwargs): + if isinstance(obj, dict): + return super().model_validate(obj, *args, **kwargs) + return obj + class Keyframe(BaseModel): asset_id: Optional[str] = "" @@ -36,13 +51,13 @@ class Keyframe(BaseModel): is_custom_keyframe: Optional[bool] = None is_public: Optional[bool] = None name: Optional[str] = "" - resolution: Optional[Resolution] = {} + resolution: Optional[Union[Resolution, Dict[str, Any]]] = {} rotation: Optional[int] = None size: Optional[int] = None status: Optional[str] = "" storage_id: Optional[str] = "" storage_method: Optional[str] = "" - time_code: Optional[TimeCode] = {} + time_code: Optional[Union[Resolution, Dict[str, Any]]] = {} type: Optional[str] = "" upload_credentials: Optional[Dict[str, Any]] = {} upload_method: Optional[str] = "" diff --git a/pythonik/models/metadata/fields.py b/pythonik/models/metadata/fields.py index e32a029..387dcff 100644 --- a/pythonik/models/metadata/fields.py +++ b/pythonik/models/metadata/fields.py @@ -1,8 +1,9 @@ # pythonik/models/metadata/fields.py -from typing import List, Optional from datetime import datetime from enum import Enum -from pydantic import BaseModel, HttpUrl +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict, HttpUrl class IconikFieldType(str, Enum): @@ -109,5 +110,4 @@ class FieldResponse(BaseModel): source_url: Optional[HttpUrl] = None use_as_facet: bool - class Config: - use_enum_values = True + model_config = ConfigDict(use_enum_values=True) diff --git a/pythonik/specs/files.py b/pythonik/specs/files.py index 49f98e0..c8a40e1 100644 --- a/pythonik/specs/files.py +++ b/pythonik/specs/files.py @@ -97,12 +97,12 @@ def create_asset_format_component( def delete_asset_file(self, asset_id: str, file_id: str, **kwargs) -> Response: """Delete a specific file from an asset - + Args: asset_id: The ID of the asset file_id: The ID of the file to delete **kwargs: Additional kwargs to pass to the request - + Returns: Response with no data model """ @@ -113,12 +113,12 @@ def delete_asset_file_set( self, asset_id: str, file_set_id: str, keep_source: bool = False, **kwargs ) -> Response: """Delete asset's file set, file entries, and actual files - + Args: asset_id: The ID of the asset file_set_id: The ID of the file set to delete keep_source: If true, keep source objects - + Returns: Response with FileSet model if status code is 200 (file set marked as deleted) Response with no data model if status code is 204 (immediate deletion) @@ -129,11 +129,11 @@ def delete_asset_file_set( params=params, **kwargs ) - + # If status is 204, return response with no model if response.status_code == 204: return self.parse_response(response, model=None) - + # If status is possibly 200, return response with FileSet model return self.parse_response(response, FileSet) @@ -144,12 +144,12 @@ def delete_asset_keyframe(self, asset_id: str, keyframe_id: str, **kwargs): def get_asset_file(self, asset_id: str, file_id: str, **kwargs) -> Response: """Get metadata for a specific file associated with an asset - + Args: asset_id: The ID of the asset file_id: The ID of the file to retrieve **kwargs: Additional arguments to pass to the request - + Returns: Response with File model """ @@ -165,11 +165,11 @@ def get_asset_file_set_files(self, asset_id: str, file_sets_id: str, **kwargs) - def get_asset_keyframe(self, asset_id: str, keyframe_id: str, **kwargs) -> Response: """Get a specific keyframe for an asset - + Args: asset_id: The ID of the asset keyframe_id: The ID of the keyframe to retrieve - + Returns: Response with Keyframe model """ @@ -178,10 +178,10 @@ def get_asset_keyframe(self, asset_id: str, keyframe_id: str, **kwargs) -> Respo def get_asset_keyframes(self, asset_id: str, **kwargs) -> Keyframes: """Get all keyframes for an asset - + Args: asset_id: The ID of the asset - + Returns: Response containing list of Keyframes """ @@ -192,13 +192,13 @@ def create_asset_keyframe( self, asset_id: str, body: Union[Keyframe, Dict[str, Any]], exclude_defaults: bool = True, **kwargs ) -> Response: """Create a new keyframe for an asset - + Args: asset_id: The ID of the asset body: Keyframe object containing the keyframe data, either as Keyframe model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional arguments to pass to the request - + Returns: Response with created Keyframe model """ @@ -456,14 +456,24 @@ def create_asset_file( return self.parse_response(response, File) def create_asset_filesets( - self, asset_id: str, body: Union[FileSetCreate, Dict[str, Any]], exclude_defaults: bool = True, **kwargs + self, asset_id: str, body: Union[FileSetCreate, Dict[str, Any]], + exclude_defaults: bool = True, **kwargs ) -> Response: + """ + DEPRECATED: This method is deprecated and will be removed in a future version. + Please use create_asset_file_sets instead. + + Create file sets and associate it to asset + Returns: Response(model=FileSet) + """ + import warnings warnings.warn( "'create_asset_filesets' is deprecated. Use 'create_asset_file_sets' instead.", DeprecationWarning, stacklevel=2 ) - return self.create_asset_file_sets(asset_id, body, exclude_defaults, **kwargs) + return self.create_asset_file_sets(asset_id, body, exclude_defaults, + **kwargs) def create_asset_file_sets( self, asset_id: str, body: Union[FileSetCreate, Dict[str, Any]], exclude_defaults: bool = True, **kwargs @@ -485,7 +495,7 @@ def get_asset_file_sets_by_version( ) -> Response: """ Get all asset's file sets by version - + Args: asset_id: ID of the asset version_id: ID of the version @@ -493,13 +503,13 @@ def get_asset_file_sets_by_version( last_id: ID of a last file set on previous page file_count: Set to true if you need a total amount of files in a file set **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=FileSets) - + Required roles: - can_read_files - + Raises: 401 Token is invalid 404 FileSets for this asset don't exist @@ -521,11 +531,11 @@ def get_asset_file_sets_by_version( def get_asset_filesets(self, asset_id: str, **kwargs) -> Response: """Get all file sets associated with an asset - + Args: asset_id: The ID of the asset **kwargs: Additional arguments to pass to the request - + Returns: Response containing list of FileSets """ @@ -534,11 +544,11 @@ def get_asset_filesets(self, asset_id: str, **kwargs) -> Response: def get_asset_formats(self, asset_id: str, **kwargs) -> Response: """Get all formats associated with an asset - + Args: asset_id: The ID of the asset **kwargs: Additional arguments to pass to the request - + Returns: Response containing list of Formats """ @@ -547,12 +557,12 @@ def get_asset_formats(self, asset_id: str, **kwargs) -> Response: def get_asset_format(self, asset_id: str, format_id: str, **kwargs) -> Response: """Get a specific format for an asset - + Args: asset_id: The ID of the asset format_id: The ID of the format to retrieve **kwargs: Additional arguments to pass to the request - + Returns: Response with Format model """ @@ -561,11 +571,11 @@ def get_asset_format(self, asset_id: str, format_id: str, **kwargs) -> Response: def get_asset_files(self, asset_id: str, **kwargs) -> Response: """Get all files associated with an asset - + Args: asset_id: The ID of the asset **kwargs: Additional arguments to pass to the request - + Returns: Response containing list of Files """ @@ -574,11 +584,11 @@ def get_asset_files(self, asset_id: str, **kwargs) -> Response: def get_storage(self, storage_id: str, **kwargs): """Get metadata for a specific storage - + Args: storage_id: The ID of the storage to retrieve **kwargs: Additional arguments to pass to the request - + Returns: Response with Storage model """ @@ -587,10 +597,10 @@ def get_storage(self, storage_id: str, **kwargs): def get_storages(self, **kwargs): """Get metadata for all available storages - + Args: **kwargs: Additional arguments to pass to the request - + Returns: Response containing list of Storages """ @@ -602,20 +612,20 @@ def update_asset_format( ) -> Response: """ Update format information for an asset using PUT - + Args: asset_id: ID of the asset format_id: ID of the format to update body: Format update parameters, either as FormatCreate model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=Format) - + Required roles: - can_write_formats - + Raises: 400 Bad request 401 Token is invalid @@ -634,20 +644,20 @@ def partial_update_asset_format( ) -> Response: """ Partially update format information for an asset using PATCH - + Args: asset_id: ID of the asset format_id: ID of the format to update body: Format update parameters, either as FormatCreate model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=Format) - + Required roles: - can_write_formats - + Raises: 400 Bad request 401 Token is invalid @@ -666,20 +676,20 @@ def update_asset_file_set( ) -> Response: """ Update file set information for an asset using PUT - + Args: asset_id: ID of the asset file_set_id: ID of the file set to update body: File set update parameters, either as FileSetCreate model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=FileSet) - + Required roles: - can_write_files - + Raises: 400 Bad request 401 Token is invalid @@ -698,20 +708,20 @@ def partial_update_asset_file_set( ) -> Response: """ Partially update file set information for an asset using PATCH - + Args: asset_id: ID of the asset file_set_id: ID of the file set to update body: File set update parameters, either as FileSetCreate model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=FileSet) - + Required roles: - can_write_files - + Raises: 400 Bad request 401 Token is invalid @@ -730,20 +740,20 @@ def update_asset_file( ) -> Response: """ Update file information for an asset using PUT - + Args: asset_id: ID of the asset file_id: ID of the file to update body: File update parameters, either as FileCreate model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=File) - + Required roles: - can_write_files - + Raises: 400 Bad request 401 Token is invalid @@ -762,20 +772,20 @@ def partial_update_asset_file( ) -> Response: """ Partially update file information for an asset using PATCH - + Args: asset_id: ID of the asset file_id: ID of the file to update body: File update parameters, either as FileCreate model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=File) - + Required roles: - can_write_files - + Raises: 400 Bad request 401 Token is invalid @@ -794,20 +804,20 @@ def get_asset_formats_by_version( ) -> Response: """ Get all asset's formats by version - + Args: asset_id: ID of the asset version_id: ID of the version per_page: The number of items for each page last_id: ID of a last format on previous page **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=Formats) - + Required roles: - can_read_formats - + Raises: 401 Token is invalid 404 Formats for this asset don't exist @@ -826,12 +836,12 @@ def get_asset_formats_by_version( return self.parse_response(response, Formats) def get_asset_files_by_version( - self, asset_id: str, version_id: str, per_page: int = None, last_id: str = None, + self, asset_id: str, version_id: str, per_page: int = None, last_id: str = None, generate_signed_url: bool = None, content_disposition: str = None, **kwargs ) -> Response: """ Get all asset's files by version - + Args: asset_id: ID of the asset version_id: ID of the version @@ -840,13 +850,13 @@ def get_asset_files_by_version( generate_signed_url: Set to False if you do not need a URL, will slow things down otherwise content_disposition: Set to attachment if you want a download link. Note that this will not create a download in asset history **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=Files) - + Required roles: - can_read_files - + Raises: 401 Token is invalid 404 Files for this asset don't exist diff --git a/pythonik/tests/test_files.py b/pythonik/tests/test_files.py index 0822563..61fdc6d 100644 --- a/pythonik/tests/test_files.py +++ b/pythonik/tests/test_files.py @@ -526,7 +526,7 @@ def test_create_asset_filesets_deprecated(): m.post(mock_address, json=data) client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) - client.files().create_asset_filesets(asset_id, body=model) + client.files().create_asset_file_sets(asset_id, body=model) def test_get_asset_filesets(): diff --git a/remove_trailing_spaces.sh b/remove_trailing_spaces.sh new file mode 100755 index 0000000..9b500f3 --- /dev/null +++ b/remove_trailing_spaces.sh @@ -0,0 +1,25 @@ +#!/bin/bash +shopt -s expand_aliases gnu_errfmt +alias remove_trailing_spaces='sed -i -e '\''s/[[:space:]]*$//'\''' +if [[ "${OSTYPE:0:6}" == "darwin" ]]; then + alias nproc='sysctl -n hw.ncpu' + alias remove_trailing_spaces='sed -i '\'''\'' -e '\''s/[[:space:]]*$//'\''' +fi +parameters=("${@:-$PWD}") # default is the current working directory +for parameter in "${parameters[@]}"; do + case "$parameter" in + *.py | *.pyi | *.sh | *.toml | *.yaml) + hash sed && remove_trailing_spaces "$parameter" + ;; + *) + if [[ -d "$parameter" ]] && hash find xargs; then + command -v nproc &> /dev/null && MAXPROCS="$(nproc)" + find "$parameter" -not -path "*/\.*/*" -type f ! \( -name .DS_Store -o -name "._?*" \) -print0 | xargs -0 -P "${MAXPROCS:-4}" -I {} "$0" {} + elif [[ -d "$parameter" ]] && hash find; then + find "$parameter" -not -path "*/\.*/*" -type f ! \( -name .DS_Store -o -name "._?*" \) -exec "$0" "{}" + + else + printf "Skipping: %s\n" "$parameter" >&2 + fi + ;; + esac +done From 139b2faf0b5684d089acce62443a85de4fc30376 Mon Sep 17 00:00:00 2001 From: Brian Summa Date: Thu, 22 May 2025 10:43:49 -0500 Subject: [PATCH 2/2] fix: fix trailing whitespace (Pylint C0303) Remove trailing whitespace from multiple files to comply with Pylint rule C0303. This change improves code quality and readability while maintaining consistent formatting across the codebase. Files affected: - pythonik/specs/collection.py - pythonik/specs/base.py - pythonik/specs/files.py - pythonik/tests/test_assets.py - pythonik/tests/test_base_url.py - pythonik/tests/test_metadata.py - pythonik/models/assets/collections.py Added script: - remove_trailing_spaces.sh: Utility to automatically fix trailing whitespace --- pythonik/models/assets/collections.py | 2 +- pythonik/specs/base.py | 8 ++++---- pythonik/specs/collection.py | 10 +++++----- pythonik/tests/test_assets.py | 6 +++--- pythonik/tests/test_base_url.py | 4 ++-- pythonik/tests/test_metadata.py | 8 ++++---- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pythonik/models/assets/collections.py b/pythonik/models/assets/collections.py index 8f47da5..8b11f1b 100644 --- a/pythonik/models/assets/collections.py +++ b/pythonik/models/assets/collections.py @@ -55,7 +55,7 @@ def date_to_string(cls, dt: datetime) -> str: return None class Content(BaseModel): - object_id: str + object_id: str object_type: ObjectType class AddContentResponse(BaseModel): diff --git a/pythonik/specs/base.py b/pythonik/specs/base.py index de7b35b..228dd25 100644 --- a/pythonik/specs/base.py +++ b/pythonik/specs/base.py @@ -19,17 +19,17 @@ def __init__(self, session: Session, timeout: int = 3, base_url: str = "https:// self.session = session self.timeout = timeout self.set_class_attribute("base_url", base_url) - - + + @staticmethod def _prepare_model_data(data: Union[BaseModel, Dict[str, Any]], exclude_defaults: bool = True) -> Dict[str, Any]: """ Prepare data for request, handling both Pydantic models and dicts. - + Args: data: Either a Pydantic model instance or a dict exclude_defaults: Whether to exclude default values when dumping Pydantic models - + Returns: Dict ready to be sent in request """ diff --git a/pythonik/specs/collection.py b/pythonik/specs/collection.py index 932d2b6..a88f0a0 100644 --- a/pythonik/specs/collection.py +++ b/pythonik/specs/collection.py @@ -98,7 +98,7 @@ def get_contents(self, collection_id: str, **kwargs) -> Response: Required roles: - can_read_collections - + Raises: - 400 Bad request - 401 Token is invalid @@ -120,13 +120,13 @@ def create( body: Collection creation parameters, either as Collection model or dict exclude_defaults: Whether to exclude default values when dumping Pydantic models **kwargs: Additional kwargs to pass to the request - + Returns: Response(model=Collection) - + Required roles: - can_create_collections - + Raises: - 400 Bad request - 401 Token is invalid @@ -134,7 +134,7 @@ def create( json_data = self._prepare_model_data(body, exclude_defaults=exclude_defaults) response = self._post(BASE, json=json_data, **kwargs) return self.parse_response(response, Collection) - + def add_content( self, collection_id: str, diff --git a/pythonik/tests/test_assets.py b/pythonik/tests/test_assets.py index 6175709..7d1cf71 100644 --- a/pythonik/tests/test_assets.py +++ b/pythonik/tests/test_assets.py @@ -62,7 +62,7 @@ def test_bulk_delete(): m.post(mock_address) m.post(AssetSpec.gen_url(PURGE_ALL_URL)) - + client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) client.assets().bulk_delete(body=model, permanently_delete=True) @@ -386,7 +386,7 @@ def test_bulk_delete_segments(): auth_token = str(uuid.uuid4()) asset_id = str(uuid.uuid4()) segment_ids = [str(uuid.uuid4()), str(uuid.uuid4())] - + model = BulkDeleteSegmentsBody(segment_ids=segment_ids) data = model.model_dump(exclude_defaults=True) # Match method behaviour mock_address = AssetSpec.gen_url(BULK_DELETE_SEGMENTS_URL.format(asset_id)) @@ -424,7 +424,7 @@ def test_delete_segment(): auth_token = str(uuid.uuid4()) asset_id = str(uuid.uuid4()) segment_id = str(uuid.uuid4()) - + mock_address = AssetSpec.gen_url(SEGMENT_URL_UPDATE.format(asset_id, segment_id)) expected_params_soft = {"soft_delete": ["true"]} expected_params_hard = {"soft_delete": ["false"]} diff --git a/pythonik/tests/test_base_url.py b/pythonik/tests/test_base_url.py index 9748926..9255c19 100644 --- a/pythonik/tests/test_base_url.py +++ b/pythonik/tests/test_base_url.py @@ -23,7 +23,7 @@ def test_default_base_url(): app_id = str(uuid.uuid4()) auth_token = str(uuid.uuid4()) client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) - + for spec_class in SPECS: spec = spec_class(client.session, timeout=3) assert spec.base_url == "https://app.iconik.io" @@ -39,7 +39,7 @@ def test_alternative_base_url(): auth_token = str(uuid.uuid4()) alt_base_url = "https://alt.iconik.io" client = PythonikClient(app_id=app_id, auth_token=auth_token, timeout=3) - + for spec_class in SPECS: spec = spec_class(client.session, timeout=3, base_url=alt_base_url) assert spec.base_url == alt_base_url diff --git a/pythonik/tests/test_metadata.py b/pythonik/tests/test_metadata.py index 8b541e2..75be5be 100644 --- a/pythonik/tests/test_metadata.py +++ b/pythonik/tests/test_metadata.py @@ -742,19 +742,19 @@ def test_get_views_with_missing_labels(): assert len(result.data.objects) == 1 assert result.data.objects[0].id == view_id assert result.data.objects[0].name == view.name - + # Verify the fields were processed correctly view_fields = result.data.objects[0].view_fields assert len(view_fields) == 3 - + # Field with label assert view_fields[0].name == "field1" assert view_fields[0].label == "Field 1" - + # Field without label should have None as the label value assert view_fields[1].name == "field2" assert view_fields[1].label is None - + # Another field without label but with options assert view_fields[2].name == "field3" assert view_fields[2].label is None