mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
🎨 enhance range validations
This commit is contained in:
parent
265b1816e8
commit
5e0509ca48
2 changed files with 205 additions and 41 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
"""Content traits for the pipeline."""
|
"""Content traits for the pipeline."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
|
||||||
# TC003 is there because Path in TYPECHECKING will fail in tests
|
# TC003 is there because Path in TYPECHECKING will fail in tests
|
||||||
from pathlib import Path # noqa: TC003
|
from pathlib import Path # noqa: TC003
|
||||||
from typing import ClassVar, Optional
|
from typing import ClassVar, Optional
|
||||||
|
|
@ -8,7 +10,7 @@ from typing import ClassVar, Optional
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from .representation import Representation
|
from .representation import Representation
|
||||||
from .time import FrameRanged, Sequence
|
from .time import FrameRanged, Handles, Sequence
|
||||||
from .trait import (
|
from .trait import (
|
||||||
MissingTraitError,
|
MissingTraitError,
|
||||||
TraitBase,
|
TraitBase,
|
||||||
|
|
@ -125,20 +127,55 @@ class FileLocations(TraitBase):
|
||||||
# If there are no file paths, we can't validate
|
# If there are no file paths, we can't validate
|
||||||
msg = "No file locations defined (empty list)"
|
msg = "No file locations defined (empty list)"
|
||||||
raise TraitValidationError(self.name, msg)
|
raise TraitValidationError(self.name, msg)
|
||||||
|
if representation.contains_trait(FrameRanged):
|
||||||
|
self._validate_frame_range(representation)
|
||||||
|
|
||||||
|
def _validate_frame_range(self, representation: Representation) -> None:
|
||||||
|
"""Validate the frame range against the file paths.
|
||||||
|
|
||||||
|
If the representation contains a FrameRanged trait, this method will
|
||||||
|
validate the frame range against the file paths. If the frame range
|
||||||
|
does not match the file paths, the trait is invalid. It takes into
|
||||||
|
account the Handles and Sequence traits.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
representation (Representation): Representation to validate.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TraitValidationError: If the trait is invalid within the
|
||||||
|
representation.
|
||||||
|
|
||||||
|
"""
|
||||||
tmp_frame_ranged: FrameRanged = get_sequence_from_files(
|
tmp_frame_ranged: FrameRanged = get_sequence_from_files(
|
||||||
[f.file_path for f in self.file_paths])
|
[f.file_path for f in self.file_paths])
|
||||||
|
|
||||||
frames_from_spec = None
|
frames_from_spec = None
|
||||||
try:
|
with contextlib.suppress(MissingTraitError):
|
||||||
sequence: Sequence = representation.get_trait(Sequence)
|
sequence: Sequence = representation.get_trait(Sequence)
|
||||||
if sequence.frame_spec:
|
if sequence.frame_spec:
|
||||||
frames_from_spec: list[int] = sequence.get_frame_list(
|
frames_from_spec: list[int] = sequence.get_frame_list(
|
||||||
self, sequence.frame_regex)
|
self, sequence.frame_regex)
|
||||||
|
|
||||||
except MissingTraitError:
|
frame_start_with_handles, frame_end_with_handles = \
|
||||||
# If there is no sequence trait, we can't validate it
|
self._get_frame_info_with_handles(representation, frames_from_spec)
|
||||||
pass
|
|
||||||
|
if frame_start_with_handles \
|
||||||
|
and tmp_frame_ranged.frame_start != frame_start_with_handles:
|
||||||
|
# If the detected frame range does not match the combined
|
||||||
|
# FrameRanged and Handles trait, the
|
||||||
|
# trait is invalid.
|
||||||
|
msg = (
|
||||||
|
f"Frame range defined by {self.name} "
|
||||||
|
f"({tmp_frame_ranged.frame_start}-"
|
||||||
|
f"{tmp_frame_ranged.frame_end}) "
|
||||||
|
"in files does not match "
|
||||||
|
"frame range "
|
||||||
|
f"({frame_start_with_handles}-"
|
||||||
|
f"{frame_end_with_handles}) defined in FrameRanged trait."
|
||||||
|
)
|
||||||
|
|
||||||
|
raise TraitValidationError(self.name, msg)
|
||||||
|
|
||||||
if frames_from_spec:
|
if frames_from_spec:
|
||||||
if len(frames_from_spec) != len(self.file_paths) :
|
if len(frames_from_spec) != len(self.file_paths) :
|
||||||
# If the number of file paths does not match the frame range,
|
# If the number of file paths does not match the frame range,
|
||||||
|
|
@ -155,40 +192,94 @@ class FileLocations(TraitBase):
|
||||||
# the rest is validated by Sequence validators.
|
# the rest is validated by Sequence validators.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
length_with_handles: int = (
|
||||||
|
frame_end_with_handles - frame_start_with_handles + 1
|
||||||
|
)
|
||||||
|
|
||||||
if len(self.file_paths) - 1 != \
|
if len(self.file_paths) != length_with_handles:
|
||||||
tmp_frame_ranged.frame_end - tmp_frame_ranged.frame_start:
|
|
||||||
# If the number of file paths does not match the frame range,
|
# If the number of file paths does not match the frame range,
|
||||||
# the trait is invalid
|
# the trait is invalid
|
||||||
msg = (
|
msg = (
|
||||||
f"Number of file locations ({len(self.file_paths) - 1}) "
|
f"Number of file locations ({len(self.file_paths)}) "
|
||||||
"does not match frame range "
|
"does not match frame range "
|
||||||
f"({tmp_frame_ranged.frame_end - tmp_frame_ranged.frame_start})" # noqa: E501
|
f"({length_with_handles})"
|
||||||
)
|
)
|
||||||
raise TraitValidationError(self.name, msg)
|
raise TraitValidationError(self.name, msg)
|
||||||
|
|
||||||
try:
|
frame_ranged: FrameRanged = representation.get_trait(FrameRanged)
|
||||||
frame_ranged: FrameRanged = representation.get_trait(FrameRanged)
|
|
||||||
|
|
||||||
if frame_ranged.frame_start != tmp_frame_ranged.frame_start or \
|
if frame_start_with_handles != tmp_frame_ranged.frame_start or \
|
||||||
frame_ranged.frame_end != tmp_frame_ranged.frame_end:
|
frame_end_with_handles != tmp_frame_ranged.frame_end:
|
||||||
# If the frame range does not match the sequence trait, the
|
# If the frame range does not match the FrameRanged trait, the
|
||||||
# trait is invalid. Note that we don't check the frame rate
|
# trait is invalid. Note that we don't check the frame rate
|
||||||
# because it is not stored in the file paths and is not
|
# because it is not stored in the file paths and is not
|
||||||
# determined by `get_sequence_from_files`.
|
# determined by `get_sequence_from_files`.
|
||||||
msg = (
|
msg = (
|
||||||
"Frame range "
|
"Frame range "
|
||||||
f"({frame_ranged.frame_start}-{frame_ranged.frame_end}) "
|
f"({frame_ranged.frame_start}-{frame_ranged.frame_end}) "
|
||||||
"in sequence trait does not match "
|
"in sequence trait does not match "
|
||||||
"frame range "
|
"frame range "
|
||||||
f"({tmp_frame_ranged.frame_start}-{tmp_frame_ranged.frame_end}) " # noqa: E501
|
f"({tmp_frame_ranged.frame_start}-"
|
||||||
"defined in files."
|
f"{tmp_frame_ranged.frame_end}) "
|
||||||
)
|
)
|
||||||
raise TraitValidationError(self.name, msg)
|
raise TraitValidationError(self.name, msg)
|
||||||
|
|
||||||
except MissingTraitError:
|
def _get_frame_info_with_handles(
|
||||||
# If there is no frame_ranged trait, we can't validate it
|
self,
|
||||||
pass
|
representation: Representation,
|
||||||
|
frames_from_spec: list[int]) -> tuple[int, int]:
|
||||||
|
"""Get the frame range with handles from the representation.
|
||||||
|
|
||||||
|
This will return frame start and frame end with handles calculated
|
||||||
|
in if there actually is the Handles trait in the representation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
representation (Representation): Representation to get the frame
|
||||||
|
range from.
|
||||||
|
frames_from_spec (list[int]): List of frames from the frame spec.
|
||||||
|
This list is modified in place to take into
|
||||||
|
account the handles.
|
||||||
|
|
||||||
|
Mutates:
|
||||||
|
frames_from_spec: List of frames from the frame spec.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int]: Start and end frame with handles.
|
||||||
|
|
||||||
|
"""
|
||||||
|
frame_start = frame_end = 0
|
||||||
|
frame_start_handle = frame_end_handle = 0
|
||||||
|
# If there is no sequence trait, we can't validate it
|
||||||
|
if frames_from_spec and representation.contains_trait(FrameRanged):
|
||||||
|
# if there is no FrameRanged trait (but really there should be)
|
||||||
|
# we can use the frame range from the frame spec
|
||||||
|
frame_start = min(frames_from_spec)
|
||||||
|
frame_end = max(frames_from_spec)
|
||||||
|
|
||||||
|
# Handle the frame range
|
||||||
|
with contextlib.suppress(MissingTraitError):
|
||||||
|
frame_start = representation.get_trait(FrameRanged).frame_start
|
||||||
|
frame_end = representation.get_trait(FrameRanged).frame_end
|
||||||
|
|
||||||
|
# Handle the handles :P
|
||||||
|
with contextlib.suppress(MissingTraitError):
|
||||||
|
handles: Handles = representation.get_trait(Handles)
|
||||||
|
if not handles.inclusive:
|
||||||
|
# if handless are exclusive, we need to adjust the frame range
|
||||||
|
frame_start_handle = handles.frame_start_handle
|
||||||
|
frame_end_handle = handles.frame_end_handle
|
||||||
|
if frames_from_spec:
|
||||||
|
frames_from_spec.extend(
|
||||||
|
range(frame_start - frame_start_handle, frame_start)
|
||||||
|
)
|
||||||
|
frames_from_spec.extend(
|
||||||
|
range(frame_end + 1, frame_end_handle + frame_end + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
frame_start_with_handles = frame_start - frame_start_handle
|
||||||
|
frame_end_with_handles = frame_end + frame_end_handle
|
||||||
|
|
||||||
|
return frame_start_with_handles, frame_end_with_handles
|
||||||
|
|
||||||
|
|
||||||
class RootlessLocation(TraitBase):
|
class RootlessLocation(TraitBase):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Temporal (time related) traits."""
|
"""Temporal (time related) traits."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||||
|
|
||||||
|
|
@ -137,20 +138,62 @@ class Sequence(TraitBase):
|
||||||
|
|
||||||
# if there is FileLocations trait, run validation
|
# if there is FileLocations trait, run validation
|
||||||
# on it as well
|
# on it as well
|
||||||
try:
|
|
||||||
from .content import FileLocations
|
with contextlib.suppress(MissingTraitError):
|
||||||
file_locs: FileLocations = representation.get_trait(
|
self._validate_file_locations(representation)
|
||||||
FileLocations)
|
|
||||||
file_locs.validate(representation)
|
def _validate_file_locations(self, representation: Representation) -> None:
|
||||||
# validate if file locations on representation
|
"""Validate file locations trait.
|
||||||
# matches the frame list (if any)
|
|
||||||
self.validate_frame_list(file_locs)
|
If along with the Sequence trait, there is a FileLocations trait,
|
||||||
self.validate_frame_padding(file_locs)
|
then we need to validate if the file locations match the frame
|
||||||
except MissingTraitError:
|
list specification.
|
||||||
pass
|
|
||||||
|
Args:
|
||||||
|
representation (Representation): Representation instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TraitValidationError: If file locations do not match the
|
||||||
|
frame list specification
|
||||||
|
|
||||||
|
"""
|
||||||
|
from .content import FileLocations
|
||||||
|
file_locs: FileLocations = representation.get_trait(
|
||||||
|
FileLocations)
|
||||||
|
# validate if file locations on representation
|
||||||
|
# matches the frame list (if any)
|
||||||
|
# we need to extend the expected frames with Handles
|
||||||
|
frame_start = None
|
||||||
|
frame_end = None
|
||||||
|
handles_frame_start = None
|
||||||
|
handles_frame_end = None
|
||||||
|
with contextlib.suppress(MissingTraitError):
|
||||||
|
handles: Handles = representation.get_trait(Handles)
|
||||||
|
# if handles are inclusive, they should be already
|
||||||
|
# accounted in the FrameRaged frame spec
|
||||||
|
if not handles.inclusive:
|
||||||
|
handles_frame_start = handles.frame_start_handle
|
||||||
|
handles_frame_end = handles.frame_end_handle
|
||||||
|
with contextlib.suppress(MissingTraitError):
|
||||||
|
frame_ranged: FrameRanged = representation.get_trait(
|
||||||
|
FrameRanged)
|
||||||
|
frame_start = frame_ranged.frame_start
|
||||||
|
frame_end = frame_ranged.frame_end
|
||||||
|
self.validate_frame_list(
|
||||||
|
file_locs,
|
||||||
|
frame_start,
|
||||||
|
frame_end,
|
||||||
|
handles_frame_start,
|
||||||
|
handles_frame_end)
|
||||||
|
self.validate_frame_padding(file_locs)
|
||||||
|
|
||||||
def validate_frame_list(
|
def validate_frame_list(
|
||||||
self, file_locations: FileLocations) -> None:
|
self,
|
||||||
|
file_locations: FileLocations,
|
||||||
|
frame_start: Optional[int] = None,
|
||||||
|
frame_end: Optional[int] = None,
|
||||||
|
handles_frame_start: Optional[int] = None,
|
||||||
|
handles_frame_end: Optional[int] = None) -> None:
|
||||||
"""Validate frame list.
|
"""Validate frame list.
|
||||||
|
|
||||||
This will take FileLocations trait and validate if the
|
This will take FileLocations trait and validate if the
|
||||||
|
|
@ -164,6 +207,10 @@ class Sequence(TraitBase):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_locations (FileLocations): File locations trait.
|
file_locations (FileLocations): File locations trait.
|
||||||
|
frame_start (Optional[int]): Frame start.
|
||||||
|
frame_end (Optional[int]): Frame end.
|
||||||
|
handles_frame_start (Optional[int]): Frame start handle.
|
||||||
|
handles_frame_end (Optional[int]): Frame end handle.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TraitValidationError: If frame list does not match
|
TraitValidationError: If frame list does not match
|
||||||
|
|
@ -177,6 +224,32 @@ class Sequence(TraitBase):
|
||||||
file_locations, self.frame_regex)
|
file_locations, self.frame_regex)
|
||||||
|
|
||||||
expected_frames = self.list_spec_to_frames(self.frame_spec)
|
expected_frames = self.list_spec_to_frames(self.frame_spec)
|
||||||
|
if frame_start is None or frame_end is None:
|
||||||
|
if min(expected_frames) != frame_start:
|
||||||
|
msg = (
|
||||||
|
"Frame start does not match the expected frame start. "
|
||||||
|
f"Expected: {frame_start}, Found: {min(expected_frames)}"
|
||||||
|
)
|
||||||
|
raise TraitValidationError(self.name, msg)
|
||||||
|
|
||||||
|
if max(expected_frames) != frame_end:
|
||||||
|
msg = (
|
||||||
|
"Frame end does not match the expected frame end. "
|
||||||
|
f"Expected: {frame_end}, Found: {max(expected_frames)}"
|
||||||
|
)
|
||||||
|
raise TraitValidationError(self.name, msg)
|
||||||
|
|
||||||
|
# we need to extend the expected frames with Handles
|
||||||
|
if handles_frame_start is not None:
|
||||||
|
expected_frames.extend(
|
||||||
|
range(
|
||||||
|
min(frames) - handles_frame_start, min(frames) + 1))
|
||||||
|
|
||||||
|
if handles_frame_end is not None:
|
||||||
|
expected_frames.extend(
|
||||||
|
range(
|
||||||
|
max(frames), max(frames) + handles_frame_end + 1))
|
||||||
|
|
||||||
if set(frames) != set(expected_frames):
|
if set(frames) != set(expected_frames):
|
||||||
msg = (
|
msg = (
|
||||||
"Frame list does not match the expected frames. "
|
"Frame list does not match the expected frames. "
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue