|
3 | 3 | import math |
4 | 4 | import re |
5 | 5 | from abc import ABC, abstractmethod |
6 | | -from typing import Any, get_args, get_origin |
| 6 | +from typing import Any, ClassVar, get_args, get_origin |
7 | 7 |
|
8 | 8 | from pydantic import BaseModel, Field, computed_field |
9 | 9 |
|
10 | 10 | from celeste.artifacts import AudioArtifact, ImageArtifact, VideoArtifact |
11 | 11 | from celeste.exceptions import ConstraintViolationError |
12 | | -from celeste.mime_types import AudioMimeType, ImageMimeType, VideoMimeType |
13 | | -from celeste.types import AudioContent, ImageContent, VideoContent |
| 12 | +from celeste.mime_types import AudioMimeType, ImageMimeType, MimeType, VideoMimeType |
14 | 13 |
|
15 | 14 |
|
16 | 15 | class Constraint(BaseModel, ABC): |
@@ -267,193 +266,105 @@ def __call__(self, value: type[BaseModel]) -> type[BaseModel]: |
267 | 266 | return value |
268 | 267 |
|
269 | 268 |
|
270 | | -class ImageConstraint(Constraint): |
271 | | - """Constraint for validating a single image artifact - validates mime_type.""" |
| 269 | +class _MediaConstraint[M: MimeType](Constraint): |
| 270 | + """Base for single-media-artifact constraints.""" |
| 271 | + |
| 272 | + _artifact_type: ClassVar[type] |
| 273 | + _media_label: ClassVar[str] |
272 | 274 |
|
273 | | - supported_mime_types: list[ImageMimeType] | None = None |
274 | | - """Supported MIME types for the image.""" |
| 275 | + supported_mime_types: list[M] | None = None |
275 | 276 |
|
276 | | - def __call__(self, value: ImageArtifact) -> ImageArtifact: |
277 | | - """Validate single image artifact against constraint.""" |
| 277 | + def __call__(self, value: Any) -> Any: # noqa: ANN401 |
| 278 | + """Validate single media artifact against constraint.""" |
278 | 279 | if isinstance(value, list): |
279 | | - msg = "ImageConstraint requires a single ImageArtifact, not a list" |
| 280 | + msg = f"{self.__class__.__name__} requires a single {self._artifact_type.__name__}, not a list" |
280 | 281 | raise ConstraintViolationError(msg) |
281 | | - |
282 | | - if not isinstance(value, ImageArtifact): |
283 | | - msg = f"Must be ImageArtifact, got {type(value).__name__}" |
| 282 | + if not isinstance(value, self._artifact_type): |
| 283 | + msg = f"Must be {self._artifact_type.__name__}, got {type(value).__name__}" |
284 | 284 | raise ConstraintViolationError(msg) |
285 | | - |
286 | 285 | if ( |
287 | 286 | self.supported_mime_types is not None |
288 | | - and value.mime_type not in self.supported_mime_types |
| 287 | + and value.mime_type not in self.supported_mime_types # type: ignore[attr-defined] |
289 | 288 | ): |
290 | 289 | supported_values = [mt.value for mt in self.supported_mime_types] |
291 | | - got_value = value.mime_type.value if value.mime_type else None |
| 290 | + got_value = value.mime_type.value if value.mime_type else None # type: ignore[attr-defined] |
292 | 291 | msg = f"mime_type must be one of {supported_values}, got {got_value!r}" |
293 | 292 | raise ConstraintViolationError(msg) |
294 | | - |
295 | 293 | return value |
296 | 294 |
|
297 | 295 |
|
298 | | -class ImagesConstraint(Constraint): |
299 | | - """Constraint for validating image artifacts list - validates mime_type and count limits.""" |
| 296 | +class _MediaListConstraint[M: MimeType](Constraint): |
| 297 | + """Base for plural-media-artifact constraints.""" |
300 | 298 |
|
301 | | - supported_mime_types: list[ImageMimeType] | None = None |
302 | | - """Supported MIME types.""" |
| 299 | + _artifact_type: ClassVar[type] |
| 300 | + _media_label: ClassVar[str] |
303 | 301 |
|
| 302 | + supported_mime_types: list[M] | None = None |
304 | 303 | max_count: int | None = None |
305 | | - """Maximum number of images.""" |
306 | 304 |
|
307 | | - def __call__(self, value: ImageContent) -> list[ImageArtifact]: |
308 | | - """Validate image artifact(s) against constraint and normalize to list.""" |
309 | | - # Normalize: if single ImageArtifact is passed, wrap it in a list |
310 | | - images = value if isinstance(value, list) else [value] |
311 | | - |
312 | | - if self.max_count is not None and len(images) > self.max_count: |
313 | | - msg = f"Must have at most {self.max_count} image(s), got {len(images)}" |
| 305 | + def __call__(self, value: Any) -> Any: # noqa: ANN401 |
| 306 | + """Validate media artifact(s) against constraint and normalize to list.""" |
| 307 | + items = value if isinstance(value, list) else [value] |
| 308 | + if self.max_count is not None and len(items) > self.max_count: |
| 309 | + msg = f"Must have at most {self.max_count} {self._media_label}(s), got {len(items)}" |
314 | 310 | raise ConstraintViolationError(msg) |
315 | | - |
316 | 311 | if self.supported_mime_types is not None: |
317 | | - for i, img in enumerate(images): |
318 | | - if not isinstance(img, ImageArtifact): |
319 | | - msg = f"Image {i + 1}: Must be ImageArtifact, got {type(img).__name__}" |
| 312 | + label = self._media_label.capitalize() |
| 313 | + for i, item in enumerate(items): |
| 314 | + if not isinstance(item, self._artifact_type): |
| 315 | + msg = f"{label} {i + 1}: Must be {self._artifact_type.__name__}, got {type(item).__name__}" |
320 | 316 | raise ConstraintViolationError(msg) |
321 | | - if img.mime_type not in self.supported_mime_types: |
| 317 | + if item.mime_type not in self.supported_mime_types: # type: ignore[attr-defined] |
322 | 318 | supported_values = [mt.value for mt in self.supported_mime_types] |
323 | | - got_value = img.mime_type.value if img.mime_type else None |
| 319 | + got_value = item.mime_type.value if item.mime_type else None # type: ignore[attr-defined] |
324 | 320 | msg = ( |
325 | | - f"Image {i + 1}: mime_type must be one of {supported_values}, " |
| 321 | + f"{label} {i + 1}: mime_type must be one of {supported_values}, " |
326 | 322 | f"got {got_value!r}" |
327 | 323 | ) |
328 | 324 | raise ConstraintViolationError(msg) |
| 325 | + return items |
329 | 326 |
|
330 | | - return images |
331 | | - |
332 | | - |
333 | | -class VideoConstraint(Constraint): |
334 | | - """Constraint for validating a single video artifact - validates mime_type.""" |
335 | | - |
336 | | - supported_mime_types: list[VideoMimeType] | None = None |
337 | | - """Supported MIME types for the video.""" |
338 | | - |
339 | | - def __call__(self, value: VideoArtifact) -> VideoArtifact: |
340 | | - """Validate single video artifact against constraint.""" |
341 | | - if isinstance(value, list): |
342 | | - msg = "VideoConstraint requires a single VideoArtifact, not a list" |
343 | | - raise ConstraintViolationError(msg) |
344 | 327 |
|
345 | | - if not isinstance(value, VideoArtifact): |
346 | | - msg = f"Must be VideoArtifact, got {type(value).__name__}" |
347 | | - raise ConstraintViolationError(msg) |
| 328 | +class ImageConstraint(_MediaConstraint[ImageMimeType]): |
| 329 | + """Constraint for validating a single image artifact - validates mime_type.""" |
348 | 330 |
|
349 | | - if ( |
350 | | - self.supported_mime_types is not None |
351 | | - and value.mime_type not in self.supported_mime_types |
352 | | - ): |
353 | | - supported_values = [mt.value for mt in self.supported_mime_types] |
354 | | - got_value = value.mime_type.value if value.mime_type else None |
355 | | - msg = f"mime_type must be one of {supported_values}, got {got_value!r}" |
356 | | - raise ConstraintViolationError(msg) |
| 331 | + _artifact_type = ImageArtifact |
| 332 | + _media_label = "image" |
357 | 333 |
|
358 | | - return value |
359 | 334 |
|
| 335 | +class ImagesConstraint(_MediaListConstraint[ImageMimeType]): |
| 336 | + """Constraint for validating image artifacts list - validates mime_type and count limits.""" |
360 | 337 |
|
361 | | -class VideosConstraint(Constraint): |
362 | | - """Constraint for validating video artifacts list - validates mime_type and count limits.""" |
| 338 | + _artifact_type = ImageArtifact |
| 339 | + _media_label = "image" |
363 | 340 |
|
364 | | - supported_mime_types: list[VideoMimeType] | None = None |
365 | | - """Supported MIME types.""" |
366 | 341 |
|
367 | | - max_count: int | None = None |
368 | | - """Maximum number of videos.""" |
| 342 | +class VideoConstraint(_MediaConstraint[VideoMimeType]): |
| 343 | + """Constraint for validating a single video artifact - validates mime_type.""" |
369 | 344 |
|
370 | | - def __call__(self, value: VideoContent) -> list[VideoArtifact]: |
371 | | - """Validate video artifact(s) against constraint and normalize to list.""" |
372 | | - # Normalize: if single VideoArtifact is passed, wrap it in a list |
373 | | - videos = value if isinstance(value, list) else [value] |
| 345 | + _artifact_type = VideoArtifact |
| 346 | + _media_label = "video" |
374 | 347 |
|
375 | | - if self.max_count is not None and len(videos) > self.max_count: |
376 | | - msg = f"Must have at most {self.max_count} video(s), got {len(videos)}" |
377 | | - raise ConstraintViolationError(msg) |
378 | 348 |
|
379 | | - if self.supported_mime_types is not None: |
380 | | - for i, vid in enumerate(videos): |
381 | | - if not isinstance(vid, VideoArtifact): |
382 | | - msg = f"Video {i + 1}: Must be VideoArtifact, got {type(vid).__name__}" |
383 | | - raise ConstraintViolationError(msg) |
384 | | - if vid.mime_type not in self.supported_mime_types: |
385 | | - supported_values = [mt.value for mt in self.supported_mime_types] |
386 | | - got_value = vid.mime_type.value if vid.mime_type else None |
387 | | - msg = ( |
388 | | - f"Video {i + 1}: mime_type must be one of {supported_values}, " |
389 | | - f"got {got_value!r}" |
390 | | - ) |
391 | | - raise ConstraintViolationError(msg) |
| 349 | +class VideosConstraint(_MediaListConstraint[VideoMimeType]): |
| 350 | + """Constraint for validating video artifacts list - validates mime_type and count limits.""" |
392 | 351 |
|
393 | | - return videos |
| 352 | + _artifact_type = VideoArtifact |
| 353 | + _media_label = "video" |
394 | 354 |
|
395 | 355 |
|
396 | | -class AudioConstraint(Constraint): |
| 356 | +class AudioConstraint(_MediaConstraint[AudioMimeType]): |
397 | 357 | """Constraint for validating a single audio artifact - validates mime_type.""" |
398 | 358 |
|
399 | | - supported_mime_types: list[AudioMimeType] | None = None |
400 | | - """Supported MIME types for the audio.""" |
| 359 | + _artifact_type = AudioArtifact |
| 360 | + _media_label = "audio" |
401 | 361 |
|
402 | | - def __call__(self, value: AudioArtifact) -> AudioArtifact: |
403 | | - """Validate single audio artifact against constraint.""" |
404 | | - if isinstance(value, list): |
405 | | - msg = "AudioConstraint requires a single AudioArtifact, not a list" |
406 | | - raise ConstraintViolationError(msg) |
407 | | - |
408 | | - if not isinstance(value, AudioArtifact): |
409 | | - msg = f"Must be AudioArtifact, got {type(value).__name__}" |
410 | | - raise ConstraintViolationError(msg) |
411 | 362 |
|
412 | | - if ( |
413 | | - self.supported_mime_types is not None |
414 | | - and value.mime_type not in self.supported_mime_types |
415 | | - ): |
416 | | - supported_values = [mt.value for mt in self.supported_mime_types] |
417 | | - got_value = value.mime_type.value if value.mime_type else None |
418 | | - msg = f"mime_type must be one of {supported_values}, got {got_value!r}" |
419 | | - raise ConstraintViolationError(msg) |
420 | | - |
421 | | - return value |
422 | | - |
423 | | - |
424 | | -class AudiosConstraint(Constraint): |
| 363 | +class AudiosConstraint(_MediaListConstraint[AudioMimeType]): |
425 | 364 | """Constraint for validating audio artifacts list - validates mime_type and count limits.""" |
426 | 365 |
|
427 | | - supported_mime_types: list[AudioMimeType] | None = None |
428 | | - """Supported MIME types.""" |
429 | | - |
430 | | - max_count: int | None = None |
431 | | - """Maximum number of audios.""" |
432 | | - |
433 | | - def __call__(self, value: AudioContent) -> list[AudioArtifact]: |
434 | | - """Validate audio artifact(s) against constraint and normalize to list.""" |
435 | | - # Normalize: if single AudioArtifact is passed, wrap it in a list |
436 | | - audios = value if isinstance(value, list) else [value] |
437 | | - |
438 | | - if self.max_count is not None and len(audios) > self.max_count: |
439 | | - msg = f"Must have at most {self.max_count} audio(s), got {len(audios)}" |
440 | | - raise ConstraintViolationError(msg) |
441 | | - |
442 | | - if self.supported_mime_types is not None: |
443 | | - for i, aud in enumerate(audios): |
444 | | - if not isinstance(aud, AudioArtifact): |
445 | | - msg = f"Audio {i + 1}: Must be AudioArtifact, got {type(aud).__name__}" |
446 | | - raise ConstraintViolationError(msg) |
447 | | - if aud.mime_type not in self.supported_mime_types: |
448 | | - supported_values = [mt.value for mt in self.supported_mime_types] |
449 | | - got_value = aud.mime_type.value if aud.mime_type else None |
450 | | - msg = ( |
451 | | - f"Audio {i + 1}: mime_type must be one of {supported_values}, " |
452 | | - f"got {got_value!r}" |
453 | | - ) |
454 | | - raise ConstraintViolationError(msg) |
455 | | - |
456 | | - return audios |
| 366 | + _artifact_type = AudioArtifact |
| 367 | + _media_label = "audio" |
457 | 368 |
|
458 | 369 |
|
459 | 370 | __all__ = [ |
|
0 commit comments