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/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 63c93ce..dd79b80 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): @@ -129,5 +130,4 @@ class FieldListResponse(BaseModel): prev_url: Optional[str] = None total: Optional[int] = None - class Config: - use_enum_values = True + model_config = ConfigDict(use_enum_values=True) 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/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_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_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