"""FileUploadConfig marshmallow schemas.""" from typing import Any, Dict from abacus_common_logic.marshalling.custom_fields import ma from marshmallow import ValidationError, validates_schema from abacus_file_upload.constants import ( S3_MAX_CHUNK_SIZE_BYTES, S3_MAX_FILE_SIZE_BYTES, S3_MAX_MULTIPART_THRESHOLD_BYTES, S3_MIN_CHUNK_SIZE_BYTES, S3_MIN_MULTIPART_THRESHOLD_BYTES, UPLOAD_TYPES, ) from abacus_file_upload.utils import format_bytes # Validation helper functions def validate_file_types(file_types: list) -> None: """Validate allowed_file_types list. Args: file_types: List of file type strings Raises: ValidationError: If file types are invalid or contain leading dots """ if not file_types: # Empty list raise ValidationError( 'allowed_file_types cannot be empty. Use null to allow all types.', field_name='allowed_file_types', ) for file_type in file_types: if not file_type or not isinstance(file_type, str): raise ValidationError( 'All file types must be non-empty strings', field_name='allowed_file_types', ) if file_type.startswith('.'): raise ValidationError( 'File types should not include leading dot (use "csv" not ".csv")', field_name='allowed_file_types', ) def validate_max_file_size(max_size: int) -> None: """Validate that max_file_size_bytes is within S3 limits. Args: max_size: Maximum file size in bytes Raises: ValidationError: If max file size is outside S3 limits (0 bytes - 5 TB) """ if max_size < 0: raise ValidationError( 'max_file_size_bytes must be >= 0 bytes', field_name='max_file_size_bytes', ) if max_size > S3_MAX_FILE_SIZE_BYTES: max_file_size = format_bytes(S3_MAX_FILE_SIZE_BYTES) raise ValidationError( f'max_file_size_bytes must be <= {S3_MAX_FILE_SIZE_BYTES} bytes ({max_file_size})', field_name='max_file_size_bytes', ) def validate_min_chunk_size(min_chunk: int) -> None: """Validate min_multipart_chunk_size_bytes is within S3 limits. Args: min_chunk: Minimum chunk size in bytes Raises: ValidationError: If chunk size is outside S3 limits (5MB - 5GB) """ if min_chunk < S3_MIN_CHUNK_SIZE_BYTES: min_chunk_size = format_bytes(S3_MIN_CHUNK_SIZE_BYTES) raise ValidationError( f'min_multipart_chunk_size_bytes must be >= {S3_MIN_CHUNK_SIZE_BYTES} bytes ({min_chunk_size})', field_name='min_multipart_chunk_size_bytes', ) if min_chunk > S3_MAX_CHUNK_SIZE_BYTES: max_chunk_size = format_bytes(S3_MAX_CHUNK_SIZE_BYTES) raise ValidationError( f'min_multipart_chunk_size_bytes must be <= {S3_MAX_CHUNK_SIZE_BYTES} bytes ({max_chunk_size})', field_name='min_multipart_chunk_size_bytes', ) def validate_multipart_threshold(threshold: int) -> None: """Validate that threshold is within S3 limits. Args: threshold: Multipart threshold size in bytes Raises: ValidationError: If threshold is outside S3 limits (5MB - 5 GB) """ if threshold < S3_MIN_MULTIPART_THRESHOLD_BYTES: min_threshold = format_bytes(S3_MIN_MULTIPART_THRESHOLD_BYTES) raise ValidationError( f'multipart_threshold_bytes must be >= {S3_MIN_MULTIPART_THRESHOLD_BYTES} bytes ({min_threshold})', field_name='multipart_threshold_bytes', ) if threshold > S3_MAX_MULTIPART_THRESHOLD_BYTES: max_threshold = format_bytes(S3_MAX_MULTIPART_THRESHOLD_BYTES) raise ValidationError( f'multipart_threshold_bytes must be <= {S3_MAX_MULTIPART_THRESHOLD_BYTES} bytes ({max_threshold})', field_name='multipart_threshold_bytes', ) def validate_file_upload_config_data(data: Dict[str, Any]) -> None: """Validate file upload config data constraints. Args: data: Dictionary of config data to validate Raises: ValidationError: If any validation fails """ # Validate min_multipart_chunk_size_bytes min_chunk = data.get('min_multipart_chunk_size_bytes') if min_chunk is not None: validate_min_chunk_size(min_chunk) # Validate max_file_size_bytes max_size = data.get('max_file_size_bytes') if max_size is not None: validate_max_file_size(max_size) # Validate multipart_threshold_bytes threshold = data.get('multipart_threshold_bytes') if threshold is not None: validate_multipart_threshold(threshold) # Validate allowed_file_types allowed_types = data.get('allowed_file_types') if allowed_types is not None: validate_file_types(allowed_types) class FileUploadConfigDetailSchema(ma.Schema): """FileUploadConfig detail response schema.""" file_upload_config_id = ma.IntegerId( metadata={ 'description': 'Unique identifier for the upload configuration', 'example': 1, } ) upload_type = ma.Enum( options=UPLOAD_TYPES, metadata={ 'description': 'Type of upload this configuration applies to', 'example': 'adjustments', }, ) s3_key_template = ma.NonemptyString( metadata={ 'description': 'S3 key template for organizing uploaded files. Supports variables like {date}, {uuid}', 'example': 'uploads/{date}/{uuid}.{ext}', } ) allowed_file_types = ma.List( ma.String(), allow_none=True, metadata={ 'description': 'List of allowed file extensions (without leading dot)', 'example': ['xlsx', 'csv', 'txt'], }, ) max_file_size_bytes = ma.Integer( metadata={ 'description': 'Maximum allowed file size in bytes (up to 5TB)', 'example': 104857600, # 100MB } ) multipart_threshold_bytes = ma.Integer( allow_none=True, metadata={ 'description': 'File size threshold for using multipart upload (5MB - 5GB)', 'example': 104857600, # 100MB }, ) min_multipart_chunk_size_bytes = ma.Integer( allow_none=True, metadata={ 'description': 'Minimum chunk size for multipart uploads (5MB - 5GB)', 'example': 5242880, # 5MB }, ) description = ma.String( allow_none=True, metadata={ 'description': 'Description of this upload configuration', 'example': 'Configuration for adjustment file uploads', }, ) event_name = ma.String( allow_none=True, metadata={ 'description': 'event can trigger a lambda or an Airflow DAG', 'example': 'event for adjustment file approve', }, ) class FileUploadConfigDetailVerboseSchema(FileUploadConfigDetailSchema): """FileUploadConfig detail response schema with audit fields.""" created_at = ma.FormattedDateTime( metadata={ 'description': 'Timestamp when the configuration was created', 'example': '2025-11-24T04:35:43.396Z', } ) created_by = ma.String( metadata={ 'description': 'User identity ID of the user who created the configuration', 'example': '12345', } ) last_modified = ma.FormattedDateTime( metadata={ 'description': 'Timestamp when the configuration was last modified', 'example': '2025-11-24T04:36:15.123Z', } ) last_modified_by = ma.String( metadata={ 'description': 'User identity ID of the user who last modified the configuration', 'example': '12345', } ) deleted_at = ma.FormattedDateTime( allow_none=True, metadata={ 'description': 'Timestamp when the configuration was deleted (soft delete)', 'example': None, }, ) deleted_by = ma.String( allow_none=True, metadata={ 'description': 'User identity ID of the user who deleted the configuration', 'example': '67890', }, ) class FileUploadConfigPostSchema(ma.Schema): """FileUploadConfig POST request schema.""" upload_type = ma.Enum( options=UPLOAD_TYPES, required=True, metadata={ 'description': 'Type of upload this configuration applies to', 'example': 'adjustments', }, ) s3_key_template = ma.NonemptyString( required=True, metadata={ 'description': 'S3 key template for organizing uploaded files. Supports variables like {date}, {uuid}, {ext}', 'example': 'uploads/{date}/{uuid}.{ext}', }, ) allowed_file_types = ma.List( ma.String(), allow_none=True, metadata={ 'description': 'List of allowed file extensions (without leading dot). Null allows all types', 'example': ['xlsx', 'csv', 'txt'], }, ) max_file_size_bytes = ma.Integer( metadata={ 'description': 'Maximum allowed file size in bytes (0 to 5TB)', 'example': 104857600, # 100MB } ) multipart_threshold_bytes = ma.Integer( allow_none=True, metadata={ 'description': 'File size threshold for using multipart upload. Must be between 5MB and 5GB', 'example': 104857600, # 100MB }, ) min_multipart_chunk_size_bytes = ma.Integer( allow_none=True, metadata={ 'description': 'Minimum chunk size for multipart uploads. Must be between 5MB and 5GB', 'example': 5242880, # 5MB }, ) description = ma.String( allow_none=True, metadata={ 'description': 'Description of this upload configuration', 'example': 'Configuration for adjustment file uploads', }, ) event_name = ma.String( allow_none=True, metadata={ 'description': 'An event that can be configured to trigger a lambda or an Airflow DAG', 'example': 'event for adjustment file approve', }, ) @validates_schema def validate_data(self, data, **kwargs): """Validate file size constraints and relationships.""" validate_file_upload_config_data(data) class FileUploadConfigPutSchema(ma.Schema): """FileUploadConfig PUT request schema.""" s3_key_template = ma.NonemptyString( metadata={ 'description': 'S3 key template for organizing uploaded files. Supports variables like {date}, {uuid}, {ext}', 'example': 'uploads/{date}/{uuid}.{ext}', } ) allowed_file_types = ma.List( ma.String(), allow_none=True, metadata={ 'description': 'List of allowed file extensions (without leading dot). Null allows all types', 'example': ['xlsx', 'csv', 'txt'], }, ) max_file_size_bytes = ma.Integer( metadata={ 'description': 'Maximum allowed file size in bytes (0 to 5TB)', 'example': 104857600, # 100MB } ) multipart_threshold_bytes = ma.Integer( allow_none=True, metadata={ 'description': 'File size threshold for using multipart upload. Must be between 5MB and 5GB', 'example': 104857600, # 100MB }, ) min_multipart_chunk_size_bytes = ma.Integer( allow_none=True, metadata={ 'description': 'Minimum chunk size for multipart uploads. Must be between 5MB and 5GB', 'example': 5242880, # 5MB }, ) description = ma.String( allow_none=True, metadata={ 'description': 'Description of this upload configuration', 'example': 'Updated configuration for adjustment file uploads', }, ) event_name = ma.String( allow_none=True, metadata={ 'description': 'An event that can be configured to trigger a lambda or an Airflow DAG', 'example': 'event for adjustment file approve', }, ) @validates_schema def validate_data(self, data, **kwargs): """Validate file size constraints and relationships.""" validate_file_upload_config_data(data)