From 129e3a2ed3c17a47059ef50675f8eaa75943b4e9 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 13 Jan 2025 07:33:45 +0000 Subject: [PATCH 01/11] [#147] Renamed the `Link` class to `File`. --- src/stalker/__init__.py | 8 +- src/stalker/db/setup.py | 2 +- src/stalker/models/entity.py | 33 +- src/stalker/models/link.py | 73 ++-- src/stalker/models/mixins.py | 30 +- src/stalker/models/project.py | 2 +- src/stalker/models/review.py | 91 +++-- src/stalker/models/template.py | 4 +- src/stalker/models/type.py | 8 +- src/stalker/models/version.py | 58 +-- tests/db/test_db.py | 198 +++++----- tests/mixins/test_create_secondary_table.py | 76 ++-- .../test_declarative_reference_mixin.py | 14 +- tests/mixins/test_reference_mixin.py | 76 ++-- tests/models/test_asset.py | 14 +- tests/models/test_daily.py | 114 +++--- tests/models/test_generic.py | 2 +- tests/models/test_link.py | 362 +++++++++--------- tests/models/test_project.py | 14 +- tests/models/test_sequence.py | 14 +- tests/models/test_shot.py | 14 +- tests/models/test_simple_entity.py | 34 +- tests/models/test_structure.py | 2 +- tests/models/test_version.py | 102 ++--- 24 files changed, 673 insertions(+), 672 deletions(-) diff --git a/src/stalker/__init__.py b/src/stalker/__init__.py index 59ececb0..f8429a97 100644 --- a/src/stalker/__init__.py +++ b/src/stalker/__init__.py @@ -22,7 +22,7 @@ from stalker.models.department import Department, DepartmentUser from stalker.models.entity import Entity, EntityGroup, SimpleEntity from stalker.models.format import ImageFormat -from stalker.models.link import Link +from stalker.models.link import File from stalker.models.message import Message from stalker.models.mixins import ( ACLMixin, @@ -46,7 +46,7 @@ ProjectUser, ) from stalker.models.repository import Repository -from stalker.models.review import Daily, DailyLink, Review +from stalker.models.review import Daily, DailyFile, Review from stalker.models.scene import Scene from stalker.models.schedulers import SchedulerBase, TaskJugglerScheduler from stalker.models.sequence import Sequence @@ -75,19 +75,19 @@ "CodeMixin", "DAGMixin", "Daily", - "DailyLink", + "DailyFile", "DateRangeMixin", "Department", "DepartmentUser", "Entity", "EntityGroup", "EntityType", + "File", "FilenameTemplate", "Good", "Group", "ImageFormat", "Invoice", - "Link", "LocalSession", "Message", "Note", diff --git a/src/stalker/db/setup.py b/src/stalker/db/setup.py index 17f6a6bf..52665324 100644 --- a/src/stalker/db/setup.py +++ b/src/stalker/db/setup.py @@ -113,12 +113,12 @@ def init() -> None: "Department", "Entity", "EntityGroup", + "File", "FilenameTemplate", "Good", "Group", "ImageFormat", "Invoice", - "Link", "Message", "Note", "Page", diff --git a/src/stalker/models/entity.py b/src/stalker/models/entity.py index e3255dd5..b3c0107a 100644 --- a/src/stalker/models/entity.py +++ b/src/stalker/models/entity.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: # pragma: no cover from stalker.models.auth import User - from stalker.models.link import Link + from stalker.models.link import File from stalker.models.note import Note from stalker.models.tag import Tag from stalker.models.type import Type @@ -198,11 +198,11 @@ class attribute to control auto naming behavior. ) thumbnail_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("Links.id", use_alter=True, name="z") + ForeignKey("Files.id", use_alter=True, name="z") ) - thumbnail: Mapped[Optional["Link"]] = relationship( - primaryjoin="SimpleEntities.c.thumbnail_id==Links.c.id", + thumbnail: Mapped[Optional["File"]] = relationship( + primaryjoin="SimpleEntities.c.thumbnail_id==Files.c.id", post_update=True, ) @@ -225,7 +225,7 @@ def __init__( updated_by: Optional["User"] = None, date_created: Optional[datetime] = None, date_updated: Optional[datetime] = None, - thumbnail: Optional["Link"] = None, + thumbnail: Optional["File"] = None, html_style: Optional[str] = "", html_class: Optional[str] = "", **kwargs: Optional[Dict[str, Any]], @@ -617,26 +617,27 @@ def _validate_type(self, key: str, type_: "Type") -> "Type": return type_ @validates("thumbnail") - def _validate_thumbnail(self, key: str, thumb: "Link") -> "Link": + def _validate_thumbnail(self, key: str, thumb: "File") -> "File": """Validate the given thumb value. Args: key (str): The name of the validated column. - thumb (Link): The thumb value to be validated. + thumb (File): The thumb value to be validated. Raises: - TypeError: If the given thumb value is not None and not a Link instance. + TypeError: If the given thumb value is not None and not a File + instance. Returns: - Union[None, Link]: The validated thumb value. + Union[None, File]: The validated thumb value. """ if thumb is not None: - from stalker import Link + from stalker import File - if not isinstance(thumb, Link): + if not isinstance(thumb, File): raise TypeError( f"{self.__class__.__name__}.thumbnail should be a " - "stalker.models.link.Link instance, " + "stalker.models.link.File instance, " f"not {thumb.__class__.__name__}: '{thumb}'" ) return thumb @@ -682,8 +683,8 @@ def _validate_html_style(self, key: str, html_style: str) -> str: if not isinstance(html_style, str): raise TypeError( - f"{self.__class__.__name__}.html_style should be a basestring " - f"instance, not {html_style.__class__.__name__}: '{html_style}'" + f"{self.__class__.__name__}.html_style should be a str, " + f"not {html_style.__class__.__name__}: '{html_style}'" ) return html_style @@ -706,8 +707,8 @@ def _validate_html_class(self, key: str, html_class: str) -> str: if not isinstance(html_class, str): raise TypeError( - f"{self.__class__.__name__}.html_class should be a basestring " - f"instance, not {html_class.__class__.__name__}: '{html_class}'" + f"{self.__class__.__name__}.html_class should be a str, " + f"not {html_class.__class__.__name__}: '{html_class}'" ) return html_class diff --git a/src/stalker/models/link.py b/src/stalker/models/link.py index caf68182..40851baf 100644 --- a/src/stalker/models/link.py +++ b/src/stalker/models/link.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Link related classes and utility functions are situated here.""" +"""File related classes and utility functions are situated here.""" import os from typing import Any, Dict, Optional, Union @@ -13,51 +13,52 @@ logger = get_logger(__name__) -class Link(Entity): - """Holds data about external links. +class File(Entity): + """Holds data about external files or file sequences. - Links are all about giving some external information to the current entity (external - to the database, so it can be something on the :class:`.Repository` or in the Web or - anywhere that the server can reach). The type of the link (general, file, folder, - web page, image, image sequence, video, movie, sound, text etc.) can be defined by a - :class:`.Type` instance (you can also use multiple :class:`.Tag` instances to add - more information, and to filter them back). Again it is defined by the needs of the - studio. + Files are all about giving some external information to the current entity + (external to the database, so it can be something on the + :class:`.Repository` or in the Web or anywhere that the server can reach). + The type of the file (general, file, folder, web page, image, image + sequence, video, movie, sound, text etc.) can be defined by a + :class:`.Type` instance (you can also use multiple :class:`.Tag` instances + to add more information, and to filter them back). Again it is defined by + the needs of the studio. - For sequences of files the file name should be in "%h%p%t %R" format in PySeq_ - formatting rules. + For sequences of files the file name should be in "%h%p%t %R" format in + PySeq_ formatting rules. - There are three secondary attributes (properties to be more precise) ``path``, - ``filename`` and ``extension``. These attributes are derived from the - :attr:`.full_path` attribute and they modify it. + There are three secondary attributes (properties to be more precise) + ``path``, ``filename`` and ``extension``. These attributes are derived from + the :attr:`.full_path` attribute and they modify it. Path - It is the path part of the full_path + It is the path part of the full_path. Filename - It is the filename part of the full_path, also includes the extension, so - changing the filename also changes the extension part. + It is the filename part of the full_path, also includes the extension, + so changing the filename also changes the extension part. Extension - It is the extension part of the full_path. It also includes the extension - separator ('.' for most of the file systems). + It is the extension part of the full_path. It also includes the + extension separator ('.' for most of the file systems). Args: - full_path (str): The full path to the link, it can be a path to a folder or a - file in the file system, or a web page. For file sequences use "%h%p%t %R" - format, for more information see `PySeq Documentation`_. It can be set to - empty string (or None which will be converted to an empty string - automatically). + full_path (str): The full path to the File, it can be a path to a + folder or a file in the file system, or a web page. For file + sequences use "%h%p%t %R" format, for more information see + `PySeq Documentation`_. It can be set to empty string (or None + which will be converted to an empty string automatically). .. _PySeq: http://packages.python.org/pyseq/ .. _PySeq Documentation: http://packages.python.org/pyseq/ """ __auto_name__ = True - __tablename__ = "Links" - __mapper_args__ = {"polymorphic_identity": "Link"} + __tablename__ = "Files" + __mapper_args__ = {"polymorphic_identity": "File"} - link_id: Mapped[int] = mapped_column( + file_id: Mapped[int] = mapped_column( "id", ForeignKey("Entities.id"), primary_key=True, @@ -67,7 +68,7 @@ class Link(Entity): original_filename: Mapped[Optional[str]] = mapped_column(String(256)) # file systems full_path: Mapped[Optional[str]] = mapped_column( - Text, doc="The full path of the url to the link." + Text, doc="The full path of the url to the file." ) def __init__( @@ -76,7 +77,7 @@ def __init__( original_filename: Optional[str] = "", **kwargs: Optional[Dict[str, Any]], ) -> None: - super(Link, self).__init__(**kwargs) + super(File, self).__init__(**kwargs) self.full_path = full_path self.original_filename = original_filename @@ -255,18 +256,18 @@ def extension(self, extension: Union[None, str]) -> None: self.filename = os.path.splitext(self.filename)[0] + extension def __eq__(self, other: Any) -> bool: - """Check if the other is equal to this Link. + """Check if the other is equal to this File. Args: other (Any): The other object to be checked for equality. Returns: - bool: If the other object is a Link instance and has the same full_path and - type value. + bool: If the other object is a File instance and has the same + full_path and type value. """ return ( - super(Link, self).__eq__(other) - and isinstance(other, Link) + super(File, self).__eq__(other) + and isinstance(other, File) and self.full_path == other.full_path and self.type == other.type ) @@ -279,4 +280,4 @@ def __hash__(self) -> int: Returns: int: The hash value. """ - return super(Link, self).__hash__() + return super(File, self).__hash__() diff --git a/src/stalker/models/mixins.py b/src/stalker/models/mixins.py index 667d423e..18580beb 100644 --- a/src/stalker/models/mixins.py +++ b/src/stalker/models/mixins.py @@ -58,7 +58,7 @@ from stalker.models.auth import Permission from stalker.models.project import Project from stalker.models.status import Status, StatusList - from stalker.models.link import Link + from stalker.models.link import File from stalker.models.studio import WorkingHours @@ -1042,20 +1042,20 @@ def _validate_project(self, key: str, project: "Project") -> "Project": class ReferenceMixin(object): """Adds reference capabilities to the mixed in class. - References are :class:`stalker.models.link.Link` instances or anything + References are :class:`stalker.models.link.File` instances or anything derived from it, which adds information to the attached objects. The aim of the References are generally to give more info to direct the evolution of the object. Args: - references (Link): A list of :class:`.Link` instances. + references (File): A list of :class:`.File` instances. """ # add this lines for Sphinx # __tablename__ = "ReferenceMixins" def __init__( - self, references: Optional[List["Link"]] = None, **kwargs: Dict[str, Any] + self, references: Optional[List["File"]] = None, **kwargs: Dict[str, Any] ) -> None: if references is None: references = [] @@ -1063,7 +1063,7 @@ def __init__( self.references = references @declared_attr - def references(cls) -> Mapped[Optional[List["Link"]]]: + def references(cls) -> Mapped[Optional[List["File"]]]: """Create the references attribute as a declared attribute. Returns: @@ -1072,40 +1072,40 @@ def references(cls) -> Mapped[Optional[List["Link"]]]: # get secondary table secondary_table = create_secondary_table( cls.__name__, - "Link", + "File", cls.__tablename__, - "Links", + "Files", f"{cls.__name__}_References", ) # return the relationship return relationship( secondary=secondary_table, - doc="""A list of :class:`.Link` instances given as a reference for + doc="""A list of :class:`.File` instances given as a reference for this entity. """, ) @validates("references") - def _validate_references(self, key: str, reference: "Link") -> "Link": + def _validate_references(self, key: str, reference: "File") -> "File": """Validate the given reference. Args: key (str): The name of the validated column. - reference (Link): The reference value to be validated. + reference (File): The reference value to be validated. Raises: - TypeError: If the reference is not a Link instance. + TypeError: If the reference is not a File instance. Returns: - Link: The validated reference value. + File: The validated reference value. """ - from stalker.models.link import Link + from stalker.models.link import File # all the items should be instance of stalker.models.entity.Entity - if not isinstance(reference, Link): + if not isinstance(reference, File): raise TypeError( f"All the items in the {self.__class__.__name__}.references should " - "be stalker.models.link.Link instances, " + "be stalker.models.link.File instances, " f"not {reference.__class__.__name__}: '{reference}'" ) return reference diff --git a/src/stalker/models/project.py b/src/stalker/models/project.py index eb1dbf18..490bd6bd 100644 --- a/src/stalker/models/project.py +++ b/src/stalker/models/project.py @@ -173,7 +173,7 @@ class Project(Entity, ReferenceMixin, StatusMixin, DateRangeMixin, CodeMixin): Deleting a :class:`.Project` instance will cascade the delete operation to all the :class:`.Task` s related to that particular Project and it will cascade the delete operation to :class:`.TimeLog` s, :class:`.Version` s, - :class:`.Link` s and :class:`.Review` s etc.. So one can delete a + :class:`.File` s and :class:`.Review` s etc.. So one can delete a :class:`.Project` instance without worrying about the non-project related data like :class:`.User` s or :class:`.Department` s to be deleted. diff --git a/src/stalker/models/review.py b/src/stalker/models/review.py index 09bc2f0e..10f198ad 100644 --- a/src/stalker/models/review.py +++ b/src/stalker/models/review.py @@ -12,7 +12,7 @@ from stalker.log import get_logger from stalker.models.entity import Entity, SimpleEntity from stalker.models.enum import DependencyTarget, TimeUnit, TraversalDirection -from stalker.models.link import Link +from stalker.models.link import File from stalker.models.mixins import ( ProjectMixin, ScheduleMixin, @@ -409,11 +409,11 @@ class Daily(Entity, StatusMixin, ProjectMixin): Dailies are sessions where outputs of a group of tasks are reviewed all together by the resources and responsible of those tasks. - The main purpose of a ``Daily`` is to gather a group of :class:`.Link` + The main purpose of a ``Daily`` is to gather a group of :class:`.File` instances and introduce a simple way of presenting them as a group. :class:`.Note` s created during a Daily session can be directly stored - both in the :class:`.Link` and the :class:`.Daily` instances and a *join* + both in the :class:`.File` and the :class:`.Daily` instances and a *join* will reveal which :class:`.Note` is created in which :class:`.Daily`. """ @@ -427,29 +427,29 @@ class Daily(Entity, StatusMixin, ProjectMixin): primary_key=True, ) - links: Mapped[Optional[List[Link]]] = association_proxy( - "link_relations", "link", creator=lambda n: DailyLink(link=n) + files: Mapped[Optional[List[File]]] = association_proxy( + "file_relations", "file", creator=lambda n: DailyFile(file=n) ) - link_relations: Mapped[Optional[List["DailyLink"]]] = relationship( + file_relations: Mapped[Optional[List["DailyFile"]]] = relationship( back_populates="daily", cascade="all, delete-orphan", - primaryjoin="Dailies.c.id==Daily_Links.c.daily_id", + primaryjoin="Dailies.c.id==Daily_Files.c.daily_id", ) def __init__( self, - links: Optional[List[Link]] = None, + files: Optional[List[File]] = None, **kwargs: Dict[str, Any], ) -> None: super(Daily, self).__init__(**kwargs) StatusMixin.__init__(self, **kwargs) ProjectMixin.__init__(self, **kwargs) - if links is None: - links = [] + if files is None: + files = [] - self.links = links + self.files = files @property def versions(self) -> List["Version"]: @@ -463,7 +463,7 @@ def versions(self) -> List["Version"]: return ( Version.query.join(Version.outputs) - .join(DailyLink) + .join(DailyFile) .join(Daily) .filter(Daily.id == self.id) .all() @@ -483,47 +483,46 @@ def tasks(self) -> List["Task"]: return ( Task.query.join(Task.versions) .join(Version.outputs) - .join(DailyLink) + .join(DailyFile) .join(Daily) .filter(Daily.id == self.id) .all() ) -class DailyLink(Base): - """The association object used in Daily-to-Link relation.""" +class DailyFile(Base): + """The association object used in Daily-to-File relation.""" - __tablename__ = "Daily_Links" + __tablename__ = "Daily_Files" daily_id: Mapped[int] = mapped_column( ForeignKey("Dailies.id"), primary_key=True, ) daily: Mapped[Daily] = relationship( - back_populates="link_relations", - primaryjoin="DailyLink.daily_id==Daily.daily_id", + back_populates="file_relations", + primaryjoin="DailyFile.daily_id==Daily.daily_id", ) - link_id: Mapped[int] = mapped_column( - ForeignKey("Links.id"), + file_id: Mapped[int] = mapped_column( + ForeignKey("Files.id"), primary_key=True, ) - link: Mapped[Link] = relationship( - primaryjoin="DailyLink.link_id==Link.link_id", - doc="""stalker.models.link.Link instances related to the Daily - instance. + file: Mapped[File] = relationship( + primaryjoin="DailyFile.file_id==File.file_id", + doc="""stalker.models.link.File instances related to the Daily instance. - Attach the same :class:`.Link` s that are linked as an output to a - certain :class:`.Version` s instance to this attribute. + Attach the same :class:`.File` instances that are linked as an output + to a certain :class:`.Version` s instance to this attribute. This attribute is an **association_proxy** so and the real attribute - that the data is related to is the :attr:`.link_relations` attribute. + that the data is related to is the :attr:`.file_relations` attribute. - You can use the :attr:`.link_relations` attribute to change the - ``rank`` attribute of the :class:`.DailyLink` instance (which is the - returned data), thus change the order of the ``Links``. + You can use the :attr:`.file_relations` attribute to change the + ``rank`` attribute of the :class:`.DailyFile` instance (which is the + returned data), thus change the order of the ``Files``. - This is done in that way to be able to store the order of the links in + This is done in that way to be able to store the order of the files in this Daily instance. """, ) @@ -532,38 +531,38 @@ class DailyLink(Base): rank: Mapped[Optional[int]] = mapped_column(default=0) def __init__( - self, daily: Optional[Daily] = None, link: Optional[Link] = None, rank: int = 0 + self, daily: Optional[Daily] = None, file: Optional[File] = None, rank: int = 0 ) -> None: - super(DailyLink, self).__init__() + super(DailyFile, self).__init__() self.daily = daily - self.link = link + self.file = file self.rank = rank - @validates("link") - def _validate_link(self, key: str, link: Union[None, Link]) -> Union[None, Link]: - """Validate the given link instance. + @validates("file") + def _validate_file(self, key: str, file: Union[None, File]) -> Union[None, File]: + """Validate the given file instance. Args: key (str): The name of the validated column. - link (Union[None, Link]): The like value to be validated. + file (Union[None, File]): The like value to be validated. Raises: - TypeError: When the given like value is not a Link instance. + TypeError: When the given like value is not a File instance. Returns: - Union[None, Link]: The validated Link instance. + Union[None, File]: The validated File instance. """ - from stalker import Link + from stalker import File - if link is not None and not isinstance(link, Link): + if file is not None and not isinstance(file, File): raise TypeError( - f"{self.__class__.__name__}.link should be an instance of " - "stalker.models.link.Link instance, " - f"not {link.__class__.__name__}: '{link}'" + f"{self.__class__.__name__}.file should be an instance of " + "stalker.models.link.File instance, " + f"not {file.__class__.__name__}: '{file}'" ) - return link + return file @validates("daily") def _validate_daily( diff --git a/src/stalker/models/template.py b/src/stalker/models/template.py index 63b730aa..3b31e044 100644 --- a/src/stalker/models/template.py +++ b/src/stalker/models/template.py @@ -30,8 +30,8 @@ class FilenameTemplate(Entity, TargetEntityTypeMixin): # shortened for this example s1 = Structure(name="Commercial Project Structure") - # this is going to be used by Stalker to decide the :stalker:`.Link` - # :stalker:`.Link.filename` and :stalker:`.Link.path` (which is the way + # this is going to be used by Stalker to decide the :stalker:`.File` + # :stalker:`.File.filename` and :stalker:`.File.path` (which is the way # Stalker links external files to Version instances) f1 = FilenameTemplate( name="Asset Version Template", diff --git a/src/stalker/models/type.py b/src/stalker/models/type.py index a9f7246b..1abc40a2 100644 --- a/src/stalker/models/type.py +++ b/src/stalker/models/type.py @@ -26,14 +26,14 @@ class Type(Entity, TargetEntityTypeMixin, CodeMixin): The purpose of the :class:`.Type` class is just to define a new type for a specific :class:`.Entity`. For example, you can have a ``Character`` :class:`.Asset` or you can have a ``Commercial`` :class:`.Project` or you - can define a :class:`.Link` as an ``Image`` etc., to create a new + can define a :class:`.File` as an ``Image`` etc., to create a new :class:`.Type` for various classes: ..code-block: Python Type(name="Character", target_entity_type="Asset") Type(name="Commercial", target_entity_type="Project") - Type(name="Image", target_entity_type="Link") + Type(name="Image", target_entity_type="File") or: @@ -41,7 +41,7 @@ class Type(Entity, TargetEntityTypeMixin, CodeMixin): Type(name="Character", target_entity_type=Asset.entity_type) Type(name="Commercial", target_entity_type=Project.entity_type) - Type(name="Image", target_entity_type=Link.entity_type) + Type(name="Image", target_entity_type=File.entity_type) or even better: @@ -49,7 +49,7 @@ class Type(Entity, TargetEntityTypeMixin, CodeMixin): Type(name="Character", target_entity_type=Asset) Type(name="Commercial", target_entity_type=Project) - Type(name="Image", target_entity_type=Link) + Type(name="Image", target_entity_type=File) By using :class:`.Type` s, one can able to sort and group same type of entities. diff --git a/src/stalker/models/version.py b/src/stalker/models/version.py index 337dc5fe..28c65dcb 100644 --- a/src/stalker/models/version.py +++ b/src/stalker/models/version.py @@ -15,7 +15,7 @@ from stalker.db.session import DBSession from stalker.log import get_logger from stalker.models.enum import TraversalDirection -from stalker.models.link import Link +from stalker.models.link import File from stalker.models.mixins import DAGMixin from stalker.models.review import Review from stalker.models.task import Task @@ -25,7 +25,7 @@ logger = get_logger(__name__) -class Version(Link, DAGMixin): +class Version(File, DAGMixin): """Holds information about the versions created for a class:`.Task`. A :class:`.Version` instance holds information about the versions created @@ -80,11 +80,11 @@ class Version(Link, DAGMixin): :attr:`.revision_number` under the same :class:`.Task` will be considered in the same version stream and version number attribute will be set accordingly. The default is 1. - inputs (List[Link]): A list o :class:`.Link` instances, holding the + inputs (List[File]): A list o :class:`.File` instances, holding the inputs of the current version. It could be a texture for a Maya file or an image sequence for Nuke, or anything those you can think as the input for the current Version. - outputs (List[Link]): A list of :class:`.Link` instances, holding the + outputs (List[File]): A list of :class:`.File` instances, holding the outputs of the current version. It could be the rendered image sequences out of Maya or Nuke, or it can be a Targa file which is the output of a Photoshop file, or anything that you can think as @@ -105,7 +105,7 @@ class Version(Link, DAGMixin): __dag_cascade__ = "save-update, merge" version_id: Mapped[int] = mapped_column( - "id", ForeignKey("Links.id"), primary_key=True + "id", ForeignKey("Files.id"), primary_key=True ) __id_column__ = "version_id" @@ -132,23 +132,23 @@ class Version(Link, DAGMixin): """, ) - inputs: Mapped[Optional[List[Link]]] = relationship( + inputs: Mapped[Optional[List[File]]] = relationship( secondary="Version_Inputs", primaryjoin="Versions.c.id==Version_Inputs.c.version_id", - secondaryjoin="Version_Inputs.c.link_id==Links.c.id", + secondaryjoin="Version_Inputs.c.file_id==Files.c.id", doc="""The inputs of the current version. - It is a list of :class:`.Link` instances. + It is a list of :class:`.File` instances. """, ) - outputs: Mapped[Optional[List[Link]]] = relationship( + outputs: Mapped[Optional[List[File]]] = relationship( secondary="Version_Outputs", primaryjoin="Versions.c.id==Version_Outputs.c.version_id", - secondaryjoin="Version_Outputs.c.link_id==Links.c.id", + secondaryjoin="Version_Outputs.c.file_id==Files.c.id", doc="""The outputs of the current version. - It is a list of :class:`.Link` instances. + It is a list of :class:`.File` instances. """, ) @@ -162,8 +162,8 @@ class Version(Link, DAGMixin): def __init__( self, task: Optional[Task] = None, - inputs: Optional[List["Version"]] = None, - outputs: Optional[List["Version"]] = None, + inputs: Optional[List["File"]] = None, + outputs: Optional[List["File"]] = None, parent: Optional["Version"] = None, full_path: Optional[str] = None, created_with: Optional[str] = None, @@ -406,18 +406,18 @@ def _validate_inputs(self, key, input_): Args: key (str): The name of the validated column. - input_ (Link): The input value to be validated. + input_ (File): The input value to be validated. Raises: - TypeError: If the input is not a :class:`.Link` instance. + TypeError: If the input is not a :class:`.File` instance. Returns: - Link: The validated input value. + File: The validated input value. """ - if not isinstance(input_, Link): + if not isinstance(input_, File): raise TypeError( "All elements in {}.inputs should be all " - "stalker.models.link.Link instances, not {}: '{}'".format( + "stalker.models.link.File instances, not {}: '{}'".format( self.__class__.__name__, input_.__class__.__name__, input_ ) ) @@ -425,23 +425,23 @@ def _validate_inputs(self, key, input_): return input_ @validates("outputs") - def _validate_outputs(self, key, output) -> Link: + def _validate_outputs(self, key, output) -> File: """Validate the given output value. Args: key (str): The name of the validated column. - output (Link): The output value to be validated. + output (File): The output value to be validated. Raises: - TypeError: If the output is not a :class:`.Link` instance. + TypeError: If the output is not a :class:`.File` instance. Returns: - Link: The validated output value. + File: The validated output value. """ - if not isinstance(output, Link): + if not isinstance(output, File): raise TypeError( "All elements in {}.outputs should be all " - "stalker.models.link.Link instances, not {}: '{}'".format( + "stalker.models.link.File instances, not {}: '{}'".format( self.__class__.__name__, output.__class__.__name__, output ) ) @@ -465,8 +465,8 @@ def update_paths(self) -> None: """Update the path variables. Raises: - RuntimeError: If no Version related FilenameTemplate is found in the related - Project.structure. + RuntimeError: If no Version related FilenameTemplate is found in + the related `Project.structure`. """ kwargs = self._template_variables() @@ -672,9 +672,9 @@ def request_review(self): Base.metadata, Column("version_id", Integer, ForeignKey("Versions.id"), primary_key=True), Column( - "link_id", + "file_id", Integer, - ForeignKey("Links.id", onupdate="CASCADE", ondelete="CASCADE"), + ForeignKey("Files.id", onupdate="CASCADE", ondelete="CASCADE"), primary_key=True, ), ) @@ -684,5 +684,5 @@ def request_review(self): "Version_Outputs", Base.metadata, Column("version_id", Integer, ForeignKey("Versions.id"), primary_key=True), - Column("link_id", Integer, ForeignKey("Links.id"), primary_key=True), + Column("file_id", Integer, ForeignKey("Files.id"), primary_key=True), ) diff --git a/tests/db/test_db.py b/tests/db/test_db.py index 5d2099fd..4d15ca0e 100644 --- a/tests/db/test_db.py +++ b/tests/db/test_db.py @@ -19,7 +19,7 @@ BudgetEntry, Client, Daily, - DailyLink, + DailyFile, Department, Entity, EntityGroup, @@ -28,7 +28,7 @@ Group, ImageFormat, Invoice, - Link, + File, Note, Page, Payment, @@ -95,7 +95,7 @@ "Entity", "EntityGroup", "ImageFormat", - "Link", + "File", "Message", "Note", "Page", @@ -1922,13 +1922,13 @@ def test_persistence_of_daily(setup_postgresql_db): DBSession.add(test_version4) DBSession.commit() - test_link1 = Link(original_filename="test_render1.jpg") - test_link2 = Link(original_filename="test_render2.jpg") - test_link3 = Link(original_filename="test_render3.jpg") - test_link4 = Link(original_filename="test_render4.jpg") + test_file1 = File(original_filename="test_render1.jpg") + test_file2 = File(original_filename="test_render2.jpg") + test_file3 = File(original_filename="test_render3.jpg") + test_file4 = File(original_filename="test_render4.jpg") - test_version1.outputs = [test_link1, test_link2, test_link3] - test_version4.outputs = [test_link4] + test_version1.outputs = [test_file1, test_file2, test_file3] + test_version4.outputs = [test_file4] DBSession.add_all( [ @@ -1939,20 +1939,20 @@ def test_persistence_of_daily(setup_postgresql_db): test_version2, test_version3, test_version4, - test_link1, - test_link2, - test_link3, - test_link4, + test_file1, + test_file2, + test_file3, + test_file4, ] ) DBSession.commit() # arguments name = "Test Daily" - links = [test_link1, test_link2, test_link3] + files = [test_file1, test_file2, test_file3] daily = Daily(name=name, project=test_project) - daily.links = links + daily.files = files DBSession.add(daily) DBSession.commit() @@ -1964,38 +1964,38 @@ def test_persistence_of_daily(setup_postgresql_db): daily_db = DBSession.get(Daily, daily_id) assert daily_db.name == name - assert daily_db.links == links + assert daily_db.files == files assert daily_db.project == test_project - link1_id = test_link1.id - link2_id = test_link2.id - link3_id = test_link3.id - link4_id = test_link4.id + file1_id = test_file1.id + file2_id = test_file2.id + file3_id = test_file3.id + file4_id = test_file4.id # delete tests DBSession.delete(daily_db) DBSession.commit() - # test if links are still there - test_link1_db = DBSession.get(Link, link1_id) - test_link2_db = DBSession.get(Link, link2_id) - test_link3_db = DBSession.get(Link, link3_id) - test_link4_db = DBSession.get(Link, link4_id) + # test if files are still there + test_file1_db = DBSession.get(File, file1_id) + test_file2_db = DBSession.get(File, file2_id) + test_file3_db = DBSession.get(File, file3_id) + test_file4_db = DBSession.get(File, file4_id) - assert test_link1_db is not None - assert isinstance(test_link1_db, Link) + assert test_file1_db is not None + assert isinstance(test_file1_db, File) - assert test_link2_db is not None - assert isinstance(test_link2_db, Link) + assert test_file2_db is not None + assert isinstance(test_file2_db, File) - assert test_link3_db is not None - assert isinstance(test_link3_db, Link) + assert test_file3_db is not None + assert isinstance(test_file3_db, File) - assert test_link4_db is not None - assert isinstance(test_link4_db, Link) + assert test_file4_db is not None + assert isinstance(test_file4_db, File) - assert DailyLink.query.all() == [] - assert Link.query.count() == 8 # including versions + assert DailyFile.query.all() == [] + assert File.query.count() == 8 # including versions def test_persistence_of_department(setup_postgresql_db): @@ -2352,16 +2352,16 @@ def test_persistence_of_filename_template(setup_postgresql_db): """Persistence of FilenameTemplate.""" ref_type = Type.query.filter_by(name="Reference").first() - # create a FilenameTemplate object for movie links + # create a FilenameTemplate object for movie files kwargs = { - "name": "Movie Links Template", - "target_entity_type": "Link", + "name": "Movie Files Template", + "target_entity_type": "File", "type": ref_type, - "description": "this is a template to be used for links to movie" "files", - "path": "REFS/{{link_type.name}}", - "filename": "{{link.file_name}}", + "description": "This is a template to be used for movie files.", + "path": "REFS/{{file.type.name}}", + "filename": "{{file.file_name}}", "output_path": "OUTPUT", - "output_file_code": "{{link.file_name}}", + "output_file_code": "{{file.file_name}}", } new_type_template = FilenameTemplate(**kwargs) @@ -2466,28 +2466,28 @@ def test_persistence_of_image_format(setup_postgresql_db): assert im_format_db.width == width -def test_persistence_of_link(setup_postgresql_db): - """Persistence of Link.""" +def test_persistence_of_file(setup_postgresql_db): + """Persistence of File.""" # user user1 = User( name="Test User 1", login="tu1", email="test@users.com", password="secret" ) DBSession.add(user1) DBSession.commit() - # create a link Type - sound_link_type = Type(name="Sound", code="sound", target_entity_type="Link") + # create a file Type + sound_file_type = Type(name="Sound", code="sound", target_entity_type="File") - # create a Link + # create a File kwargs = { "name": "My Sound", "full_path": "M:/PROJECTS/my_movie_sound.wav", - "type": sound_link_type, + "type": sound_file_type, "created_by": user1, } - link1 = Link(**kwargs) + file1 = File(**kwargs) # persist it - DBSession.add_all([sound_link_type, link1]) + DBSession.add_all([sound_file_type, file1]) DBSession.commit() # use it as a task reference @@ -2496,50 +2496,50 @@ def test_persistence_of_link(setup_postgresql_db): project1 = Project(name="Test Project 1", code="TP1", repository=repo1) task1 = Task(name="Test Task", project=project1, responsible=[user1]) - task1.references.append(link1) + task1.references.append(file1) DBSession.add(task1) DBSession.commit() # store attributes - created_by = link1.created_by - date_created = link1.date_created - date_updated = link1.date_updated - description = link1.description - name = link1.name - nice_name = link1.nice_name - notes = link1.notes - full_path = link1.full_path - tags = link1.tags - type_ = link1.type - updated_by = link1.updated_by - - # delete the link - del link1 + created_by = file1.created_by + date_created = file1.date_created + date_updated = file1.date_updated + description = file1.description + name = file1.name + nice_name = file1.nice_name + notes = file1.notes + full_path = file1.full_path + tags = file1.tags + type_ = file1.type + updated_by = file1.updated_by + + # delete the File + del file1 # retrieve it back - link1_db = Link.query.filter_by(name=kwargs["name"]).first() - - assert isinstance(link1_db, Link) - - assert link1_db.created_by == created_by - assert link1_db.date_created == date_created - assert link1_db.date_updated == date_updated - assert link1_db.description == description - assert link1_db.name == name - assert link1_db.nice_name == nice_name - assert link1_db.notes == notes - assert link1_db.full_path == full_path - assert link1_db.tags == tags - assert link1_db.type == type_ - assert link1_db.updated_by == updated_by - assert link1_db == task1.references[0] + file1_db = File.query.filter_by(name=kwargs["name"]).first() + + assert isinstance(file1_db, File) + + assert file1_db.created_by == created_by + assert file1_db.date_created == date_created + assert file1_db.date_updated == date_updated + assert file1_db.description == description + assert file1_db.name == name + assert file1_db.nice_name == nice_name + assert file1_db.notes == notes + assert file1_db.full_path == full_path + assert file1_db.tags == tags + assert file1_db.type == type_ + assert file1_db.updated_by == updated_by + assert file1_db == task1.references[0] # delete tests - task1.references.remove(link1_db) + task1.references.remove(file1_db) - # Deleting a Link should not delete anything else - DBSession.delete(link1_db) + # Deleting a File should not delete anything else + DBSession.delete(file1_db) DBSession.commit() # We still should have the user and the type intact @@ -2754,18 +2754,18 @@ def test_persistence_of_project(setup_postgresql_db): # create data for mixins # Reference Mixin - link_type = Type(name="Image", code="image", target_entity_type="Link") - ref1 = Link( + file_type = Type(name="Image", code="image", target_entity_type="File") + ref1 = File( name="Ref1", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="1.jpg", - type=link_type, + type=file_type, ) - ref2 = Link( + ref2 = File( name="Ref2", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="1.jpg", - type=link_type, + type=file_type, ) DBSession.save([lead, ref1, ref2]) working_hours = WorkingHours( @@ -3277,7 +3277,7 @@ def test_persistence_of_shot(setup_postgresql_db): def test_persistence_of_simple_entity(setup_postgresql_db): """Persistence of SimpleEntity.""" - thumbnail = Link() + thumbnail = File() DBSession.add(thumbnail) kwargs = { "name": "Simple Entity 1", @@ -3495,12 +3495,12 @@ def test_persistence_of_structure(setup_postgresql_db): type=v_type, ) - # create a new link type - image_link_type = Type( + # create a new file type + image_file_type = Type( name="Image", code="image", - description="It is used for links showing an image", - target_entity_type="Link", + description="It is used for image files.", + target_entity_type="File", ) # get reference Type of FilenameTemplates @@ -3517,7 +3517,7 @@ def test_persistence_of_structure(setup_postgresql_db): "shows where to place the image files", path="REFS/{{reference.type.name}}", filename="{{reference.file_name}}", - target_entity_type="Link", + target_entity_type="File", type=r_type, ) @@ -3544,7 +3544,7 @@ def test_persistence_of_structure(setup_postgresql_db): modeling_task_type, animation_task_type, char_asset_type, - image_link_type, + image_file_type, ] ) DBSession.commit() @@ -3774,8 +3774,8 @@ def test_persistence_of_task(setup_postgresql_db): DBSession.commit() # references - ref1 = Link(full_path="some_path", original_filename="original_filename") - ref2 = Link(full_path="some_path", original_filename="original_filename") + ref1 = File(full_path="some_path", original_filename="original_filename") + ref2 = File(full_path="some_path", original_filename="original_filename") task1.references.append(ref1) task1.references.append(ref2) @@ -4444,7 +4444,7 @@ def test_persistence_of_version(setup_postgresql_db): full_path="M:/Shows/Proj1/Seq1/Shots/SH001/Lighting" "/Proj1_Seq1_Sh001_MAIN_Lighting_v001.ma", outputs=[ - Link( + File( name="Renders", full_path="M:/Shows/Proj1/Seq1/Shots/SH001/Lighting/" "Output/test1.###.jpg", diff --git a/tests/mixins/test_create_secondary_table.py b/tests/mixins/test_create_secondary_table.py index 5caea1ea..daad2ddd 100644 --- a/tests/mixins/test_create_secondary_table.py +++ b/tests/mixins/test_create_secondary_table.py @@ -32,9 +32,9 @@ def test_primary_cls_name_is_none(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( None, # "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -50,9 +50,9 @@ def test_primary_cls_name_is_not_a_string(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( 1234, # "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -68,9 +68,9 @@ def test_primary_cls_name_is_empty_string(setup_test_class): with pytest.raises(ValueError) as cm: create_secondary_table( "", # "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -85,9 +85,9 @@ def test_secondary_cls_name_is_none(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( "TestEntity", - None, # "Link", + None, # "File", "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -103,9 +103,9 @@ def test_secondary_cls_name_is_not_a_string(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( "TestEntity", - 1234, # "Link", + 1234, # "File", "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -121,9 +121,9 @@ def test_secondary_cls_name_is_empty_string(setup_test_class): with pytest.raises(ValueError) as cm: create_secondary_table( "TestEntity", - "", # "Link", + "", # "File", "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -137,12 +137,12 @@ def test_secondary_cls_name_is_converted_to_plural(setup_test_class): """secondary_cls_name is converted to plural.""" return_value = create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", None, # "TestEntity_References" ) - assert return_value.name == "TestEntity_Links" + assert return_value.name == "TestEntity_Files" def test_primary_cls_table_name_is_none(setup_test_class): @@ -151,9 +151,9 @@ def test_primary_cls_table_name_is_none(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( "TestEntity", - "Link", + "File", None, # "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -169,9 +169,9 @@ def test_primary_cls_table_name_is_not_a_string(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( "TestEntity", - "Link", + "File", 1234, # "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -187,9 +187,9 @@ def test_primary_cls_table_name_is_empty_string(setup_test_class): with pytest.raises(ValueError) as cm: create_secondary_table( "TestEntity", - "Link", + "File", "", # "TestEntities", - "Links", + "Files", "TestEntity_References", ) @@ -205,9 +205,9 @@ def test_secondary_cls_table_name_is_none(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - None, # "Links", + None, # "Files", "TestEntity_References", ) @@ -223,9 +223,9 @@ def test_secondary_cls_table_name_is_not_a_string(setup_test_class): with pytest.raises(TypeError) as cm: create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - 1234, # "Links", + 1234, # "Files", "TestEntity_References", ) @@ -241,9 +241,9 @@ def test_secondary_cls_table_name_is_empty_string(setup_test_class): with pytest.raises(ValueError) as cm: create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - "", # "Links", + "", # "Files", "TestEntity_References", ) @@ -257,12 +257,12 @@ def test_secondary_table_name_can_be_none(setup_test_class): """secondary_table_name can be None.""" return_value = create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", None, # "TestEntity_References" ) - assert return_value.name == "TestEntity_Links" + assert return_value.name == "TestEntity_Files" def test_secondary_table_name_is_not_a_str(setup_test_class): @@ -270,9 +270,9 @@ def test_secondary_table_name_is_not_a_str(setup_test_class): with pytest.raises(TypeError) as cm: _ = create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", 1234, # "TestEntity_References" ) assert str(cm.value) == ( @@ -286,24 +286,24 @@ def test_secondary_table_name_is_an_empty_str(setup_test_class): """secondary_table_name is an empty string generates new name from class names.""" return_value = create_secondary_table( "TestEntity", - "Link", + "File", "TestEntities", - "Links", + "Files", "", # "TestEntity_References" ) - assert return_value.name == "TestEntity_Links" + assert return_value.name == "TestEntity_Files" def test_secondary_table_name_already_exists_in_base_metadata(setup_test_class): """secondary_table_name already exists will use that table.""" assert "TestEntity_References" not in Base.metadata return_value_1 = create_secondary_table( - "TestEntity", "Link", "TestEntities", "Links", "TestEntity_References" + "TestEntity", "File", "TestEntities", "Files", "TestEntity_References" ) assert "TestEntity_References" in Base.metadata # should not generate any errors return_value_2 = create_secondary_table( - "TestEntity", "Link", "TestEntities", "Links", "TestEntity_References" + "TestEntity", "File", "TestEntities", "Files", "TestEntity_References" ) # and return the same table assert return_value_2.name == "TestEntity_References" @@ -313,6 +313,6 @@ def test_secondary_table_name_already_exists_in_base_metadata(setup_test_class): def test_returns_a_table(setup_test_class): """create_secondary_table returns a table.""" return_value = create_secondary_table( - "TestEntity", "Link", "TestEntities", "Links", "TestEntity_References" + "TestEntity", "File", "TestEntities", "Files", "TestEntity_References" ) assert isinstance(return_value, Table) diff --git a/tests/mixins/test_declarative_reference_mixin.py b/tests/mixins/test_declarative_reference_mixin.py index e16d1a08..787b88d9 100644 --- a/tests/mixins/test_declarative_reference_mixin.py +++ b/tests/mixins/test_declarative_reference_mixin.py @@ -3,7 +3,7 @@ from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column -from stalker import Link, SimpleEntity +from stalker import File, SimpleEntity from stalker.models.mixins import ReferenceMixin @@ -40,11 +40,11 @@ def test_reference_mixin_setup(): a_ins = DeclRefMixA(name="ozgur") b_ins = DeclRefMixB(name="bozgur") - new_link1 = Link(name="test link 1", full_path="none") - new_link2 = Link(name="test link 2", full_path="no path") + new_file1 = File(name="test file 1", full_path="none") + new_file2 = File(name="test file 2", full_path="no path") - a_ins.references.append(new_link1) - b_ins.references.append(new_link2) + a_ins.references.append(new_file1) + b_ins.references.append(new_file2) - assert new_link1 in a_ins.references - assert new_link2 in b_ins.references + assert new_file1 in a_ins.references + assert new_file2 in b_ins.references diff --git a/tests/mixins/test_reference_mixin.py b/tests/mixins/test_reference_mixin.py index 5081514d..a12102b5 100644 --- a/tests/mixins/test_reference_mixin.py +++ b/tests/mixins/test_reference_mixin.py @@ -6,7 +6,7 @@ from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import Mapped, mapped_column -from stalker import Entity, Link, ReferenceMixin, SimpleEntity, Type +from stalker import Entity, File, ReferenceMixin, SimpleEntity, Type class RefMixFooClass(SimpleEntity, ReferenceMixin): @@ -29,39 +29,39 @@ def setup_reference_mixin_tester(): Returns: dict: test data. """ - # link type + # file type data = dict() - data["test_link_type"] = Type( - name="Test Link Type", - code="testlink", - target_entity_type=Link, + data["test_file_type"] = Type( + name="Test File Type", + code="testfile", + target_entity_type=File, ) - # create a couple of Link objects - data["test_link1"] = Link( - name="Test Link 1", - type=data["test_link_type"], + # create a couple of File objects + data["test_file1"] = File( + name="Test File 1", + type=data["test_file_type"], full_path="test_path", filename="test_filename", ) - data["test_link2"] = Link( - name="Test Link 2", - type=data["test_link_type"], + data["test_file2"] = File( + name="Test File 2", + type=data["test_file_type"], full_path="test_path", filename="test_filename", ) - data["test_link3"] = Link( - name="Test Link 3", - type=data["test_link_type"], + data["test_file3"] = File( + name="Test File 3", + type=data["test_file_type"], full_path="test_path", filename="test_filename", ) - data["test_link4"] = Link( - name="Test Link 4", - type=data["test_link_type"], + data["test_file4"] = File( + name="Test File 4", + type=data["test_file_type"], full_path="test_path", filename="test_filename", ) @@ -74,11 +74,11 @@ def setup_reference_mixin_tester(): name="Test Entity 2", ) - data["test_links"] = [ - data["test_link1"], - data["test_link2"], - data["test_link3"], - data["test_link4"], + data["test_files"] = [ + data["test_file1"], + data["test_file2"], + data["test_file3"], + data["test_file4"], ] data["test_foo_obj"] = RefMixFooClass(name="Ref Mixin Test") @@ -102,10 +102,10 @@ def test_references_attribute_only_accepts_list_like_objects( assert str(cm.value) == "Incompatible collection type: str is not list-like" -def test_references_attribute_accepting_only_lists_of_link_instances( +def test_references_attribute_accepting_only_lists_of_file_instances( setup_reference_mixin_tester, ): - """references attribute accepting only lists of Links.""" + """references attribute accepting only lists of Files.""" data = setup_reference_mixin_tester test_value = [1, 2.2, "some references"] @@ -114,29 +114,29 @@ def test_references_attribute_accepting_only_lists_of_link_instances( assert str(cm.value) == ( "All the items in the RefMixFooClass.references should be " - "stalker.models.link.Link instances, not int: '1'" + "stalker.models.link.File instances, not int: '1'" ) -def test_references_attribute_elements_accepts_links_only(setup_reference_mixin_tester): - """TypeError is raised if non Link assigned to references attribute.""" +def test_references_attribute_elements_accepts_files_only(setup_reference_mixin_tester): + """TypeError is raised if non File assigned to references attribute.""" data = setup_reference_mixin_tester with pytest.raises(TypeError) as cm: data["test_foo_obj"].references = [data["test_entity1"], data["test_entity2"]] assert str(cm.value) == ( "All the items in the RefMixFooClass.references should be " - "stalker.models.link.Link instances, not Entity: ''" + "stalker.models.link.File instances, not Entity: ''" ) def test_references_attribute_is_working_as_expected(setup_reference_mixin_tester): """references attribute working as expected.""" data = setup_reference_mixin_tester - data["test_foo_obj"].references = data["test_links"] - assert data["test_foo_obj"].references == data["test_links"] + data["test_foo_obj"].references = data["test_files"] + assert data["test_foo_obj"].references == data["test_files"] - test_value = [data["test_link1"], data["test_link2"]] + test_value = [data["test_file1"], data["test_file2"]] data["test_foo_obj"].references = test_value assert sorted(data["test_foo_obj"].references, key=lambda x: x.name) == sorted( test_value, key=lambda x: x.name @@ -156,13 +156,13 @@ class GreatEntity(SimpleEntity, ReferenceMixin): my_ge = GreatEntity(name="Test") # we should have a references attribute right now _ = my_ge.references - image_link_type = Type(name="Image", code="image", target_entity_type="Link") - new_link = Link( - name="NewTestLink", + image_file_type = Type(name="Image", code="image", target_entity_type="File") + new_file = File( + name="NewTestFile", full_path="nopath", filename="nofilename", - type=image_link_type, + type=image_file_type, ) - test_value = [new_link] + test_value = [new_file] my_ge.references = test_value assert my_ge.references == test_value diff --git a/tests/models/test_asset.py b/tests/models/test_asset.py index 11dd8a7f..f4f1b015 100644 --- a/tests/models/test_asset.py +++ b/tests/models/test_asset.py @@ -6,7 +6,7 @@ from stalker import ( Asset, Entity, - Link, + File, Project, Repository, Sequence, @@ -210,20 +210,20 @@ def test_inequality(setup_asset_tests): def test_reference_mixin_initialization(setup_asset_tests): """ReferenceMixin part is initialized correctly.""" data = setup_asset_tests - link_type_1 = Type(name="Image", code="image", target_entity_type="Link") - link1 = Link( + file_type_1 = Type(name="Image", code="image", target_entity_type="File") + file1 = File( name="Artwork 1", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="a.jpg", - type=link_type_1, + type=file_type_1, ) - link2 = Link( + file2 = File( name="Artwork 2", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="b.jbg", - type=link_type_1, + type=file_type_1, ) - references = [link1, link2] + references = [file1, file2] data["kwargs"]["code"] = "SH12314" data["kwargs"]["references"] = references new_asset = Asset(**data["kwargs"]) diff --git a/tests/models/test_daily.py b/tests/models/test_daily.py index 11491725..64870b08 100644 --- a/tests/models/test_daily.py +++ b/tests/models/test_daily.py @@ -5,8 +5,8 @@ from stalker import ( Daily, - DailyLink, - Link, + DailyFile, + File, Project, Repository, Status, @@ -88,17 +88,17 @@ def setup_daily_tests(): data["test_version3"] = Version(task=data["test_task1"]) data["test_version4"] = Version(task=data["test_task2"]) - data["test_link1"] = Link(original_filename="test_render1.jpg") - data["test_link2"] = Link(original_filename="test_render2.jpg") - data["test_link3"] = Link(original_filename="test_render3.jpg") - data["test_link4"] = Link(original_filename="test_render4.jpg") + data["test_file1"] = File(original_filename="test_render1.jpg") + data["test_file2"] = File(original_filename="test_render2.jpg") + data["test_file3"] = File(original_filename="test_render3.jpg") + data["test_file4"] = File(original_filename="test_render4.jpg") data["test_version1"].outputs = [ - data["test_link1"], - data["test_link2"], - data["test_link3"], + data["test_file1"], + data["test_file2"], + data["test_file3"], ] - data["test_version4"].outputs = [data["test_link4"]] + data["test_version4"].outputs = [data["test_file4"]] return data @@ -113,31 +113,31 @@ def test_daily_instance_creation(setup_daily_tests): assert isinstance(daily, Daily) -def test_links_argument_is_skipped(setup_daily_tests): - """links attribute is an empty list if the links argument is skipped.""" +def test_files_argument_is_skipped(setup_daily_tests): + """files attribute is an empty list if the files argument is skipped.""" data = setup_daily_tests daily = Daily( name="Test Daily", project=data["test_project"], status_list=data["daily_status_list"], ) - assert daily.links == [] + assert daily.files == [] -def test_links_argument_is_none(setup_daily_tests): - """links attribute is an empty list if the links argument is None.""" +def test_files_argument_is_none(setup_daily_tests): + """files attribute is an empty list if the files argument is None.""" data = setup_daily_tests daily = Daily( name="Test Daily", - links=None, + files=None, project=data["test_project"], status_list=data["daily_status_list"], ) - assert daily.links == [] + assert daily.files == [] -def test_links_attribute_is_set_to_none(setup_daily_tests): - """TypeError is raised if the links attribute is set to None.""" +def test_files_attribute_is_set_to_none(setup_daily_tests): + """TypeError is raised if the files attribute is set to None.""" data = setup_daily_tests daily = Daily( name="Test Daily", @@ -145,67 +145,67 @@ def test_links_attribute_is_set_to_none(setup_daily_tests): status_list=data["daily_status_list"], ) with pytest.raises(TypeError): - daily.links = None + daily.files = None -def test_links_argument_is_not_a_list_instance(setup_daily_tests): - """TypeError is raised if the links argument is not a list.""" +def test_files_argument_is_not_a_list_instance(setup_daily_tests): + """TypeError is raised if the files argument is not a list.""" data = setup_daily_tests with pytest.raises(TypeError) as cm: Daily( name="Test Daily", - links="not a list of Daily instances", + files="not a list of Daily instances", project=data["test_project"], status_list=data["daily_status_list"], ) assert ( - str(cm.value) == "DailyLink.link should be an instance of " - "stalker.models.link.Link instance, not str: 'n'" + str(cm.value) == "DailyFile.file should be an instance of " + "stalker.models.link.File instance, not str: 'n'" ) -def test_links_argument_is_not_a_list_of_link_instances(setup_daily_tests): - """TypeError is raised if the links argument is not a list of Link instances.""" +def test_files_argument_is_not_a_list_of_file_instances(setup_daily_tests): + """TypeError is raised if the files argument is not a list of File instances.""" data = setup_daily_tests with pytest.raises(TypeError) as cm: Daily( name="Test Daily", - links=["not", 1, "list", "of", Daily, "instances"], + files=["not", 1, "list", "of", File, "instances"], project=data["test_project"], status_list=data["daily_status_list"], ) assert str(cm.value) == ( - "DailyLink.link should be an instance of stalker.models.link.Link instance, " + "DailyFile.file should be an instance of stalker.models.link.File instance, " "not str: 'not'" ) -def test_links_argument_is_working_as_expected(setup_daily_tests): - """links argument value is correctly passed to the links attribute.""" +def test_files_argument_is_working_as_expected(setup_daily_tests): + """files argument value is correctly passed to the files attribute.""" data = setup_daily_tests - test_value = [data["test_link1"], data["test_link2"]] + test_value = [data["test_file1"], data["test_file2"]] daily = Daily( name="Test Daily", - links=test_value, + files=test_value, project=data["test_project"], status_list=data["daily_status_list"], ) - assert daily.links == test_value + assert daily.files == test_value -def test_links_attribute_is_working_as_expected(setup_daily_tests): - """links attribute is working as expected.""" +def test_files_attribute_is_working_as_expected(setup_daily_tests): + """files attribute is working as expected.""" data = setup_daily_tests daily = Daily( name="Test Daily", project=data["test_project"], status_list=data["daily_status_list"], ) - daily.links.append(data["test_link1"]) + daily.files.append(data["test_file1"]) - assert daily.links == [data["test_link1"]] + assert daily.files == [data["test_file1"]] def test_versions_attribute_is_read_only(setup_daily_tests): @@ -288,52 +288,52 @@ def setup_daily_db_tests(setup_postgresql_db): DBSession.add(data["test_version4"]) DBSession.commit() - data["test_link1"] = Link(original_filename="test_render1.jpg") - data["test_link2"] = Link(original_filename="test_render2.jpg") - data["test_link3"] = Link(original_filename="test_render3.jpg") - data["test_link4"] = Link(original_filename="test_render4.jpg") + data["test_file1"] = File(original_filename="test_render1.jpg") + data["test_file2"] = File(original_filename="test_render2.jpg") + data["test_file3"] = File(original_filename="test_render3.jpg") + data["test_file4"] = File(original_filename="test_render4.jpg") DBSession.add_all( [ - data["test_link1"], - data["test_link2"], - data["test_link3"], - data["test_link4"], + data["test_file1"], + data["test_file2"], + data["test_file3"], + data["test_file4"], ] ) data["test_version1"].outputs = [ - data["test_link1"], - data["test_link2"], - data["test_link3"], + data["test_file1"], + data["test_file2"], + data["test_file3"], ] - data["test_version4"].outputs = [data["test_link4"]] + data["test_version4"].outputs = [data["test_file4"]] DBSession.commit() yield data def test_tasks_attribute_will_return_a_list_of_tasks(setup_daily_db_tests): - """tasks attribute is a list of Task instances related to the given links.""" + """tasks attribute is a list of Task instances related to the given files.""" data = setup_daily_db_tests daily = Daily( name="Test Daily", project=data["test_project"], status_list=data["daily_status_list"], ) - daily.links = [data["test_link1"], data["test_link2"]] + daily.files = [data["test_file1"], data["test_file2"]] DBSession.add(daily) DBSession.commit() assert daily.tasks == [data["test_task1"]] def test_versions_attribute_will_return_a_list_of_versions(setup_daily_db_tests): - """versions attribute is a list of Version instances related to the given links.""" + """versions attribute is a list of Version instances related to the given files.""" data = setup_daily_db_tests daily = Daily( name="Test Daily", project=data["test_project"], status_list=data["daily_status_list"], ) - daily.links = [data["test_link1"], data["test_link2"]] + daily.files = [data["test_file1"], data["test_file2"]] DBSession.add(daily) DBSession.commit() assert daily.versions == [data["test_version1"]] @@ -341,16 +341,16 @@ def test_versions_attribute_will_return_a_list_of_versions(setup_daily_db_tests) def test_rank_argument_is_skipped(): """rank attribute will use the default value is if skipped.""" - dl = DailyLink() + dl = DailyFile() assert dl.rank == 0 def test_daily_argument_is_not_a_daily_instance(setup_daily_tests): """TypeError is raised if the daily argument is not a Daily and not None.""" with pytest.raises(TypeError) as cm: - DailyLink(daily="not a daily") + DailyFile(daily="not a daily") assert str(cm.value) == ( - "DailyLink.daily should be an instance of stalker.models.review.Daily " + "DailyFile.daily should be an instance of stalker.models.review.Daily " "instance, not str: 'not a daily'" ) diff --git a/tests/models/test_generic.py b/tests/models/test_generic.py index 25c116d9..ff4a5682 100644 --- a/tests/models/test_generic.py +++ b/tests/models/test_generic.py @@ -20,7 +20,7 @@ ("template", "templates"), ("group", "groups"), ("format", "formats"), - ("link", "links"), + ("file", "files"), ("session", "sessions"), ("note", "notes"), ("permission", "permissions"), diff --git a/tests/models/test_link.py b/tests/models/test_link.py index b599d629..b99a1136 100644 --- a/tests/models/test_link.py +++ b/tests/models/test_link.py @@ -4,382 +4,382 @@ import pytest -from stalker import Link, Type +from stalker import File, Type @pytest.fixture(scope="function") -def setup_link_tests(): - """Set up the test for the Link class.""" +def setup_file_tests(): + """Set up the test for the File class.""" data = dict() - # create a mock LinkType object - data["test_link_type1"] = Type( + # create a mock FileType object + data["test_file_type1"] = Type( name="Test Type 1", code="test type1", - target_entity_type="Link", + target_entity_type="File", ) - data["test_link_type2"] = Type( + data["test_file_type2"] = Type( name="Test Type 2", code="test type2", - target_entity_type="Link", + target_entity_type="File", ) data["kwargs"] = { - "name": "An Image Link", + "name": "An Image File", "full_path": "C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.exr", "original_filename": "this_is_an_image.jpg", - "type": data["test_link_type1"], + "type": data["test_file_type1"], } - data["test_link"] = Link(**data["kwargs"]) + data["test_file"] = File(**data["kwargs"]) return data def test___auto_name__class_attribute_is_set_to_true(): - """__auto_name__ class attribute is set to False for Link class.""" - assert Link.__auto_name__ is True + """__auto_name__ class attribute is set to False for File class.""" + assert File.__auto_name__ is True -def test_full_path_argument_is_none(setup_link_tests): +def test_full_path_argument_is_none(setup_file_tests): """full_path argument is None is set the full_path attribute to an empty string.""" - data = setup_link_tests + data = setup_file_tests data["kwargs"]["full_path"] = None - new_link = Link(**data["kwargs"]) - assert new_link.full_path == "" + new_file = File(**data["kwargs"]) + assert new_file.full_path == "" -def test_full_path_attribute_is_set_to_none(setup_link_tests): +def test_full_path_attribute_is_set_to_none(setup_file_tests): """full_path attr to None is set its value to an empty string.""" - data = setup_link_tests - data["test_link"].full_path = "" + data = setup_file_tests + data["test_file"].full_path = "" -def test_full_path_argument_is_empty_an_empty_string(setup_link_tests): +def test_full_path_argument_is_empty_an_empty_string(setup_file_tests): """full_path attr is set to an empty str if full_path arg is an empty string.""" - data = setup_link_tests + data = setup_file_tests data["kwargs"]["full_path"] = "" - new_link = Link(**data["kwargs"]) - assert new_link.full_path == "" + new_file = File(**data["kwargs"]) + assert new_file.full_path == "" -def test_full_path_attribute_is_set_to_empty_string(setup_link_tests): +def test_full_path_attribute_is_set_to_empty_string(setup_file_tests): """full_path attr value is set to an empty string.""" - data = setup_link_tests - data["test_link"].full_path = "" - assert data["test_link"].full_path == "" + data = setup_file_tests + data["test_file"].full_path = "" + assert data["test_file"].full_path == "" -def test_full_path_argument_is_not_a_string(setup_link_tests): +def test_full_path_argument_is_not_a_string(setup_file_tests): """TypeError is raised if the full_path argument is not a string.""" - data = setup_link_tests + data = setup_file_tests test_value = 1 data["kwargs"]["full_path"] = test_value with pytest.raises(TypeError) as cm: - Link(**data["kwargs"]) + File(**data["kwargs"]) assert str(cm.value) == ( - "Link.full_path should be an instance of string, not int: '1'" + "File.full_path should be an instance of string, not int: '1'" ) -def test_full_path_attribute_is_not_a_string(setup_link_tests): +def test_full_path_attribute_is_not_a_string(setup_file_tests): """TypeError is raised if the full_path attribute is not a string instance.""" - data = setup_link_tests + data = setup_file_tests test_value = 1 with pytest.raises(TypeError) as cm: - data["test_link"].full_path = test_value + data["test_file"].full_path = test_value assert str(cm.value) == ( - "Link.full_path should be an instance of string, not int: '1'" + "File.full_path should be an instance of string, not int: '1'" ) -def test_full_path_windows_to_other_conversion(setup_link_tests): +def test_full_path_windows_to_other_conversion(setup_file_tests): """full_path is stored in internal format.""" - data = setup_link_tests + data = setup_file_tests windows_path = "M:\\path\\to\\object" expected_result = "M:/path/to/object" - data["test_link"].full_path = windows_path - assert data["test_link"].full_path == expected_result + data["test_file"].full_path = windows_path + assert data["test_file"].full_path == expected_result -def test_original_filename_argument_is_none(setup_link_tests): +def test_original_filename_argument_is_none(setup_file_tests): """original_filename arg is None will set the attr to filename of the full_path.""" - data = setup_link_tests + data = setup_file_tests data["kwargs"]["original_filename"] = None - new_link = Link(**data["kwargs"]) - filename = os.path.basename(new_link.full_path) - assert new_link.original_filename == filename + new_file = File(**data["kwargs"]) + filename = os.path.basename(new_file.full_path) + assert new_file.original_filename == filename -def test_original_filename_attribute_is_set_to_none(setup_link_tests): +def test_original_filename_attribute_is_set_to_none(setup_file_tests): """original_filename is equal to the filename of the full_path if it is None.""" - data = setup_link_tests - data["test_link"].original_filename = None - filename = os.path.basename(data["test_link"].full_path) - assert data["test_link"].original_filename == filename + data = setup_file_tests + data["test_file"].original_filename = None + filename = os.path.basename(data["test_file"].full_path) + assert data["test_file"].original_filename == filename -def test_original_filename_argument_is_empty_string(setup_link_tests): +def test_original_filename_argument_is_empty_string(setup_file_tests): """original_filename arg is empty str, attr is set to filename of the full_path.""" - data = setup_link_tests + data = setup_file_tests data["kwargs"]["original_filename"] = "" - new_link = Link(**data["kwargs"]) - filename = os.path.basename(new_link.full_path) - assert new_link.original_filename == filename + new_file = File(**data["kwargs"]) + filename = os.path.basename(new_file.full_path) + assert new_file.original_filename == filename -def test_original_filename_attribute_set_to_empty_string(setup_link_tests): +def test_original_filename_attribute_set_to_empty_string(setup_file_tests): """original_filename attr is empty str, attr is set to filename of the full_path.""" - data = setup_link_tests - data["test_link"].original_filename = "" - filename = os.path.basename(data["test_link"].full_path) - assert data["test_link"].original_filename == filename + data = setup_file_tests + data["test_file"].original_filename = "" + filename = os.path.basename(data["test_file"].full_path) + assert data["test_file"].original_filename == filename -def test_original_filename_argument_accepts_string_only(setup_link_tests): +def test_original_filename_argument_accepts_string_only(setup_file_tests): """original_filename arg accepts str only and raises TypeError for other types.""" - data = setup_link_tests + data = setup_file_tests test_value = 1 data["kwargs"]["original_filename"] = test_value with pytest.raises(TypeError) as cm: - Link(**data["kwargs"]) + File(**data["kwargs"]) assert str(cm.value) == ( - "Link.original_filename should be an instance of string, not int: '1'" + "File.original_filename should be an instance of string, not int: '1'" ) -def test_original_filename_attribute_accepts_string_only(setup_link_tests): +def test_original_filename_attribute_accepts_string_only(setup_file_tests): """original_filename attr accepts str only and raises TypeError for other types.""" - data = setup_link_tests + data = setup_file_tests test_value = 1.1 with pytest.raises(TypeError) as cm: - data["test_link"].original_filename = test_value + data["test_file"].original_filename = test_value assert str(cm.value) == ( - "Link.original_filename should be an instance of string, not float: '1.1'" + "File.original_filename should be an instance of string, not float: '1.1'" ) -def test_original_filename_argument_is_working_as_expected(setup_link_tests): +def test_original_filename_argument_is_working_as_expected(setup_file_tests): """original_filename argument is working as expected.""" - data = setup_link_tests - assert data["kwargs"]["original_filename"] == data["test_link"].original_filename + data = setup_file_tests + assert data["kwargs"]["original_filename"] == data["test_file"].original_filename -def test_original_filename_attribute_is_working_as_expected(setup_link_tests): +def test_original_filename_attribute_is_working_as_expected(setup_file_tests): """original_filename attribute is working as expected.""" - data = setup_link_tests + data = setup_file_tests new_value = "this_is_the_original_filename.jpg" - assert data["test_link"].original_filename != new_value - data["test_link"].original_filename = new_value - assert data["test_link"].original_filename == new_value + assert data["test_file"].original_filename != new_value + data["test_file"].original_filename = new_value + assert data["test_file"].original_filename == new_value -def test_equality_of_two_links(setup_link_tests): +def test_equality_of_two_files(setup_file_tests): """Equality operator.""" - data = setup_link_tests + data = setup_file_tests # with same parameters - mock_link1 = Link(**data["kwargs"]) - assert data["test_link"] == mock_link1 + mock_file1 = File(**data["kwargs"]) + assert data["test_file"] == mock_file1 # with different parameters - data["kwargs"]["type"] = data["test_link_type2"] - mock_link2 = Link(**data["kwargs"]) + data["kwargs"]["type"] = data["test_file_type2"] + mock_file2 = File(**data["kwargs"]) - assert not data["test_link"] == mock_link2 + assert not data["test_file"] == mock_file2 -def test_inequality_of_two_links(setup_link_tests): +def test_inequality_of_two_files(setup_file_tests): """Inequality operator.""" - data = setup_link_tests + data = setup_file_tests # with same parameters - mock_link1 = Link(**data["kwargs"]) - assert data["test_link"] == mock_link1 + mock_file1 = File(**data["kwargs"]) + assert data["test_file"] == mock_file1 # with different parameters - data["kwargs"]["type"] = data["test_link_type2"] - mock_link2 = Link(**data["kwargs"]) + data["kwargs"]["type"] = data["test_file_type2"] + mock_file2 = File(**data["kwargs"]) - assert not data["test_link"] != mock_link1 - assert data["test_link"] != mock_link2 + assert not data["test_file"] != mock_file1 + assert data["test_file"] != mock_file2 -def test_plural_class_name(setup_link_tests): - """Plural name of Link class.""" - data = setup_link_tests - assert data["test_link"].plural_class_name == "Links" +def test_plural_class_name(setup_file_tests): + """Plural name of File class.""" + data = setup_file_tests + assert data["test_file"].plural_class_name == "Files" -def test_path_attribute_is_set_to_none(setup_link_tests): +def test_path_attribute_is_set_to_none(setup_file_tests): """TypeError is raised if the path attribute is set to None.""" - data = setup_link_tests + data = setup_file_tests with pytest.raises(TypeError) as cm: - data["test_link"].path = None - assert str(cm.value) == "Link.path cannot be set to None" + data["test_file"].path = None + assert str(cm.value) == "File.path cannot be set to None" -def test_path_attribute_is_set_to_empty_string(setup_link_tests): +def test_path_attribute_is_set_to_empty_string(setup_file_tests): """ValueError is raised if the path attribute is set to an empty string.""" - data = setup_link_tests + data = setup_file_tests with pytest.raises(ValueError) as cm: - data["test_link"].path = "" - assert str(cm.value) == "Link.path cannot be an empty string" + data["test_file"].path = "" + assert str(cm.value) == "File.path cannot be an empty string" -def test_path_attribute_is_set_to_a_value_other_then_string(setup_link_tests): +def test_path_attribute_is_set_to_a_value_other_then_string(setup_file_tests): """TypeError is raised if the path attribute is set to a value other than string.""" - data = setup_link_tests + data = setup_file_tests with pytest.raises(TypeError) as cm: - data["test_link"].path = 1 - assert str(cm.value) == "Link.path should be an instance of str, not int: '1'" + data["test_file"].path = 1 + assert str(cm.value) == "File.path should be an instance of str, not int: '1'" -def test_path_attribute_value_comes_from_full_path(setup_link_tests): +def test_path_attribute_value_comes_from_full_path(setup_file_tests): """path attribute value is calculated from the full_path attribute.""" - data = setup_link_tests - assert data["test_link"].path == "C:/A_NEW_PROJECT/td/dsdf" + data = setup_file_tests + assert data["test_file"].path == "C:/A_NEW_PROJECT/td/dsdf" -def test_path_attribute_updates_the_full_path_attribute(setup_link_tests): +def test_path_attribute_updates_the_full_path_attribute(setup_file_tests): """path attribute is updating the full_path attribute.""" - data = setup_link_tests + data = setup_file_tests test_value = "/mnt/some/new/path" expected_full_path = "/mnt/some/new/path/" "22-fdfffsd-32342-dsf2332-dsfd-3.exr" - assert data["test_link"].path != test_value - data["test_link"].path = test_value - assert data["test_link"].path == test_value - assert data["test_link"].full_path == expected_full_path + assert data["test_file"].path != test_value + data["test_file"].path = test_value + assert data["test_file"].path == test_value + assert data["test_file"].full_path == expected_full_path -def test_filename_attribute_is_set_to_none(setup_link_tests): +def test_filename_attribute_is_set_to_none(setup_file_tests): """filename attribute is an empty string if it is set a None.""" - data = setup_link_tests - data["test_link"].filename = None - assert data["test_link"].filename == "" + data = setup_file_tests + data["test_file"].filename = None + assert data["test_file"].filename == "" -def test_filename_attribute_is_set_to_a_value_other_then_string(setup_link_tests): +def test_filename_attribute_is_set_to_a_value_other_then_string(setup_file_tests): """TypeError is raised if the filename attr is set to a value other than string.""" - data = setup_link_tests + data = setup_file_tests with pytest.raises(TypeError) as cm: - data["test_link"].filename = 3 - assert str(cm.value) == "Link.filename should be an instance of str, not int: '3'" + data["test_file"].filename = 3 + assert str(cm.value) == "File.filename should be an instance of str, not int: '3'" -def test_filename_attribute_is_set_to_empty_string(setup_link_tests): +def test_filename_attribute_is_set_to_empty_string(setup_file_tests): """filename value can be set to an empty string.""" - data = setup_link_tests - data["test_link"].filename = "" - assert data["test_link"].filename == "" + data = setup_file_tests + data["test_file"].filename = "" + assert data["test_file"].filename == "" -def test_filename_attribute_value_comes_from_full_path(setup_link_tests): +def test_filename_attribute_value_comes_from_full_path(setup_file_tests): """filename attribute value is calculated from the full_path attribute.""" - data = setup_link_tests - assert data["test_link"].filename == "22-fdfffsd-32342-dsf2332-dsfd-3.exr" + data = setup_file_tests + assert data["test_file"].filename == "22-fdfffsd-32342-dsf2332-dsfd-3.exr" -def test_filename_attribute_updates_the_full_path_attribute(setup_link_tests): +def test_filename_attribute_updates_the_full_path_attribute(setup_file_tests): """filename attribute is updating the full_path attribute.""" - data = setup_link_tests + data = setup_file_tests test_value = "new_filename.tif" - assert data["test_link"].filename != test_value - data["test_link"].filename = test_value - assert data["test_link"].filename == test_value - assert data["test_link"].full_path == "C:/A_NEW_PROJECT/td/dsdf/new_filename.tif" + assert data["test_file"].filename != test_value + data["test_file"].filename = test_value + assert data["test_file"].filename == test_value + assert data["test_file"].full_path == "C:/A_NEW_PROJECT/td/dsdf/new_filename.tif" -def test_filename_attribute_changes_also_the_extension(setup_link_tests): +def test_filename_attribute_changes_also_the_extension(setup_file_tests): """filename attribute also changes the extension attribute.""" - data = setup_link_tests - assert data["test_link"].extension != ".an_extension" - data["test_link"].filename = "some_filename_and.an_extension" - assert data["test_link"].extension == ".an_extension" + data = setup_file_tests + assert data["test_file"].extension != ".an_extension" + data["test_file"].filename = "some_filename_and.an_extension" + assert data["test_file"].extension == ".an_extension" -def test_extension_attribute_is_set_to_none(setup_link_tests): +def test_extension_attribute_is_set_to_none(setup_file_tests): """extension is an empty string if it is set to None.""" - data = setup_link_tests - data["test_link"].extension = None - assert data["test_link"].extension == "" + data = setup_file_tests + data["test_file"].extension = None + assert data["test_file"].extension == "" -def test_extension_attribute_is_set_to_empty_string(setup_link_tests): +def test_extension_attribute_is_set_to_empty_string(setup_file_tests): """extension attr can be set to an empty string.""" - data = setup_link_tests - data["test_link"].extension = "" - assert data["test_link"].extension == "" + data = setup_file_tests + data["test_file"].extension = "" + assert data["test_file"].extension == "" -def test_extension_attribute_is_set_to_a_value_other_then_string(setup_link_tests): +def test_extension_attribute_is_set_to_a_value_other_then_string(setup_file_tests): """TypeError is raised if the extension attr is not str.""" - data = setup_link_tests + data = setup_file_tests with pytest.raises(TypeError) as cm: - data["test_link"].extension = 123 + data["test_file"].extension = 123 assert str(cm.value) == ( - "Link.extension should be an instance of str, not int: '123'" + "File.extension should be an instance of str, not int: '123'" ) -def test_extension_attribute_value_comes_from_full_path(setup_link_tests): +def test_extension_attribute_value_comes_from_full_path(setup_file_tests): """extension attribute value is calculated from the full_path attribute.""" - data = setup_link_tests - assert data["test_link"].extension == ".exr" + data = setup_file_tests + assert data["test_file"].extension == ".exr" -def test_extension_attribute_updates_the_full_path_attribute(setup_link_tests): +def test_extension_attribute_updates_the_full_path_attribute(setup_file_tests): """extension attribute is updating the full_path attribute.""" - data = setup_link_tests + data = setup_file_tests test_value = ".iff" - assert data["test_link"].extension != test_value - data["test_link"].extension = test_value - assert data["test_link"].extension == test_value + assert data["test_file"].extension != test_value + data["test_file"].extension = test_value + assert data["test_file"].extension == test_value assert ( - data["test_link"].full_path + data["test_file"].full_path == "C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.iff" ) def test_extension_attribute_updates_the_full_path_attribute_with_the_dot( - setup_link_tests, + setup_file_tests, ): """full_path attr updated with the extension that doesn't have a dot.""" - data = setup_link_tests + data = setup_file_tests test_value = "iff" expected_value = ".iff" - assert data["test_link"].extension != test_value - data["test_link"].extension = test_value - assert data["test_link"].extension == expected_value + assert data["test_file"].extension != test_value + data["test_file"].extension = test_value + assert data["test_file"].extension == expected_value assert ( - data["test_link"].full_path + data["test_file"].full_path == "C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.iff" ) -def test_extension_attribute_is_also_change_the_filename_attribute(setup_link_tests): +def test_extension_attribute_is_also_change_the_filename_attribute(setup_file_tests): """extension attribute is updating the filename attribute.""" - data = setup_link_tests + data = setup_file_tests test_value = ".targa" expected_value = "22-fdfffsd-32342-dsf2332-dsfd-3.targa" - assert data["test_link"].filename != expected_value - data["test_link"].extension = test_value - assert data["test_link"].filename == expected_value + assert data["test_file"].filename != expected_value + data["test_file"].extension = test_value + assert data["test_file"].filename == expected_value -def test_format_path_converts_bytes_to_strings(setup_link_tests): +def test_format_path_converts_bytes_to_strings(setup_file_tests): """_format_path() converts bytes to strings.""" - data = setup_link_tests + data = setup_file_tests test_value = b"C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.iff" - result = data["test_link"]._format_path(test_value) + result = data["test_file"]._format_path(test_value) assert isinstance(result, str) assert result == test_value.decode("utf-8") -def test__hash__is_working_as_expected(setup_link_tests): +def test__hash__is_working_as_expected(setup_file_tests): """__hash__ is working as expected.""" - data = setup_link_tests - result = hash(data["test_link"]) + data = setup_file_tests + result = hash(data["test_file"]) assert isinstance(result, int) - assert result == data["test_link"].__hash__() + assert result == data["test_file"].__hash__() diff --git a/tests/models/test_project.py b/tests/models/test_project.py index 1760b622..c001e716 100644 --- a/tests/models/test_project.py +++ b/tests/models/test_project.py @@ -16,7 +16,7 @@ Entity, Client, ImageFormat, - Link, + File, Project, Repository, Sequence, @@ -1113,23 +1113,23 @@ def test_inequality(setup_project_db_test): def test_reference_mixin_initialization(setup_project_db_test): """ReferenceMixin part is initialized correctly.""" data = setup_project_db_test - link_type_1 = Type(name="Image", code="image", target_entity_type="Link") + file_type_1 = Type(name="Image", code="image", target_entity_type="File") - link1 = Link( + file1 = File( name="Artwork 1", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="a.jpg", - type=link_type_1, + type=file_type_1, ) - link2 = Link( + file2 = File( name="Artwork 2", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="b.jbg", - type=link_type_1, + type=file_type_1, ) - references = [link1, link2] + references = [file1, file2] data["kwargs"]["references"] = references new_project = Project(**data["kwargs"]) diff --git a/tests/models/test_sequence.py b/tests/models/test_sequence.py index 59efdcec..5470a872 100644 --- a/tests/models/test_sequence.py +++ b/tests/models/test_sequence.py @@ -5,7 +5,7 @@ from stalker import ( Entity, - Link, + File, Project, Repository, Sequence, @@ -175,21 +175,21 @@ def test_inequality(setup_sequence_db_tests): def test_reference_mixin_initialization(setup_sequence_db_tests): """ReferenceMixin part is initialized correctly.""" data = setup_sequence_db_tests - link_type_1 = Type(name="Image", code="image", target_entity_type="Link") + file_type_1 = Type(name="Image", code="image", target_entity_type="File") - link1 = Link( + file1 = File( name="Artwork 1", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="a.jpg", - type=link_type_1, + type=file_type_1, ) - link2 = Link( + file2 = File( name="Artwork 2", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="b.jbg", - type=link_type_1, + type=file_type_1, ) - references = [link1, link2] + references = [file1, file2] data["kwargs"]["references"] = references new_sequence = Sequence(**data["kwargs"]) assert new_sequence.references == references diff --git a/tests/models/test_shot.py b/tests/models/test_shot.py index d1e22090..875d1856 100644 --- a/tests/models/test_shot.py +++ b/tests/models/test_shot.py @@ -8,7 +8,7 @@ Asset, Entity, ImageFormat, - Link, + File, Project, Repository, Scene, @@ -1144,23 +1144,23 @@ def test_inequality(setup_shot_db_tests): def test_ReferenceMixin_initialization(setup_shot_db_tests): """ReferenceMixin part is initialized correctly.""" data = setup_shot_db_tests - link_type_1 = Type(name="Image", code="image", target_entity_type="Link") + file_type_1 = Type(name="Image", code="image", target_entity_type="File") - link1 = Link( + file1 = File( name="Artwork 1", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="a.jpg", - type=link_type_1, + type=file_type_1, ) - link2 = Link( + file2 = File( name="Artwork 2", full_path="/mnt/M/JOBs/TEST_PROJECT", filename="b.jbg", - type=link_type_1, + type=file_type_1, ) - references = [link1, link2] + references = [file1, file2] data["kwargs"]["code"] = "SH12314" data["kwargs"]["references"] = references diff --git a/tests/models/test_simple_entity.py b/tests/models/test_simple_entity.py index 19ef40db..ac58b18d 100644 --- a/tests/models/test_simple_entity.py +++ b/tests/models/test_simple_entity.py @@ -12,7 +12,7 @@ from stalker import ( Department, - Link, + File, Project, Repository, SimpleEntity, @@ -657,35 +657,35 @@ def test_thumbnail_attr_is_none(setup_simple_entity_tests): assert data["test_simple_entity"].thumbnail is None -def test_thumbnail_arg_is_not_a_link_instance(setup_simple_entity_tests): - """TypeError raised if the thumbnail arg is not a Link instance.""" +def test_thumbnail_arg_is_not_a_file_instance(setup_simple_entity_tests): + """TypeError raised if the thumbnail arg is not a File instance.""" data = setup_simple_entity_tests - data["kwargs"]["thumbnail"] = "not a Link" + data["kwargs"]["thumbnail"] = "not a File" with pytest.raises(TypeError) as cm: SimpleEntity(**data["kwargs"]) assert str(cm.value) == ( - "SimpleEntity.thumbnail should be a stalker.models.link.Link instance, " - "not str: 'not a Link'" + "SimpleEntity.thumbnail should be a stalker.models.link.File instance, " + "not str: 'not a File'" ) -def test_thumbnail_attr_is_not_a_link_instance(setup_simple_entity_tests): - """TypeError raised if the thumbnail is not a Link instance.""" +def test_thumbnail_attr_is_not_a_file_instance(setup_simple_entity_tests): + """TypeError raised if the thumbnail is not a File instance.""" data = setup_simple_entity_tests with pytest.raises(TypeError) as cm: - data["test_simple_entity"].thumbnail = "not a Link" + data["test_simple_entity"].thumbnail = "not a File" assert str(cm.value) == ( - "SimpleEntity.thumbnail should be a stalker.models.link.Link instance, " - "not str: 'not a Link'" + "SimpleEntity.thumbnail should be a stalker.models.link.File instance, " + "not str: 'not a File'" ) def test_thumbnail_arg_is_working_as_expected(setup_simple_entity_tests): """thumbnail arg value is passed to the thumbnail attr correctly.""" data = setup_simple_entity_tests - thumb = Link(full_path="some path") + thumb = File(full_path="some path") data["kwargs"]["thumbnail"] = thumb new_simple_entity = SimpleEntity(**data["kwargs"]) assert new_simple_entity.thumbnail == thumb @@ -694,7 +694,7 @@ def test_thumbnail_arg_is_working_as_expected(setup_simple_entity_tests): def test_thumbnail_attr_is_working_as_expected(setup_simple_entity_tests): """thumbnail attr is working as expected.""" data = setup_simple_entity_tests - thumb = Link(full_path="some path") + thumb = File(full_path="some path") assert not data["test_simple_entity"].thumbnail == thumb data["test_simple_entity"].thumbnail = thumb assert data["test_simple_entity"].thumbnail == thumb @@ -731,7 +731,7 @@ def test_html_style_arg_is_not_a_string(setup_simple_entity_tests): with pytest.raises(TypeError) as cm: SimpleEntity(**data["kwargs"]) assert str(cm.value) == ( - "SimpleEntity.html_style should be a basestring instance, not int: '123'" + "SimpleEntity.html_style should be a str, not int: '123'" ) @@ -741,7 +741,7 @@ def test_html_style_attr_is_not_set_to_a_string(setup_simple_entity_tests): with pytest.raises(TypeError) as cm: data["test_simple_entity"].html_style = 34324 assert str(cm.value) == ( - "SimpleEntity.html_style should be a basestring instance, not int: '34324'" + "SimpleEntity.html_style should be a str, not int: '34324'" ) @@ -793,7 +793,7 @@ def test_html_class_arg_is_not_a_string(setup_simple_entity_tests): with pytest.raises(TypeError) as cm: SimpleEntity(**data["kwargs"]) assert str(cm.value) == ( - "SimpleEntity.html_class should be a basestring instance, not int: '123'" + "SimpleEntity.html_class should be a str, not int: '123'" ) @@ -803,7 +803,7 @@ def test_html_class_attr_is_not_set_to_a_string(setup_simple_entity_tests): with pytest.raises(TypeError) as cm: data["test_simple_entity"].html_class = 34324 assert str(cm.value) == ( - "SimpleEntity.html_class should be a basestring instance, not int: '34324'" + "SimpleEntity.html_class should be a str, not int: '34324'" ) diff --git a/tests/models/test_structure.py b/tests/models/test_structure.py index 161eb1f9..17cae7f9 100644 --- a/tests/models/test_structure.py +++ b/tests/models/test_structure.py @@ -20,7 +20,7 @@ def setup_structure_tests(): name="Test Shot Template", target_entity_type="Shot", type=vers_type ) data["reference_template"] = FilenameTemplate( - name="Test Reference Template", target_entity_type="Link", type=ref_type + name="Test Reference Template", target_entity_type="File", type=ref_type ) data["test_templates"] = [ data["asset_template"], diff --git a/tests/models/test_version.py b/tests/models/test_version.py index 9524ad67..9e05e5e1 100644 --- a/tests/models/test_version.py +++ b/tests/models/test_version.py @@ -8,7 +8,7 @@ from stalker import ( Asset, FilenameTemplate, - Link, + File, Project, Repository, Scene, @@ -122,41 +122,41 @@ def setup_version_db_tests(setup_postgresql_db): DBSession.add(data["test_task1"]) DBSession.commit() - # a Link for the input file - data["test_input_link1"] = Link( - name="Input Link 1", + # a File for the input file + data["test_input_file1"] = File( + name="Input File 1", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_beauty_v001.###.exr", ) - DBSession.add(data["test_input_link1"]) + DBSession.add(data["test_input_file1"]) - data["test_input_link2"] = Link( - name="Input Link 2", + data["test_input_file2"] = File( + name="Input File 2", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_occ_v001.###.exr", ) - DBSession.add(data["test_input_link2"]) + DBSession.add(data["test_input_file2"]) - # a Link for the output file - data["test_output_link1"] = Link( - name="Output Link 1", + # a File for the output file + data["test_output_file1"] = File( + name="Output File 1", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_beauty_v001.###.exr", ) - DBSession.add(data["test_output_link1"]) + DBSession.add(data["test_output_file1"]) - data["test_output_link2"] = Link( - name="Output Link 2", + data["test_output_file2"] = File( + name="Output File 2", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_occ_v001.###.exr", ) - DBSession.add(data["test_output_link2"]) + DBSession.add(data["test_output_file2"]) DBSession.commit() # now create a version for the Task data["kwargs"] = { - "inputs": [data["test_input_link1"], data["test_input_link2"]], - "outputs": [data["test_output_link1"], data["test_output_link2"]], + "inputs": [data["test_input_file1"], data["test_input_file2"]], + "outputs": [data["test_output_file1"], data["test_output_file2"]], "task": data["test_task1"], "created_with": "Houdini", } @@ -582,8 +582,8 @@ def test_inputs_attribute_is_none(setup_version_db_tests): assert str(cm.value) == "Incompatible collection type: None is not list-like" -def test_inputs_argument_is_not_a_list_of_link_instances(setup_version_db_tests): - """TypeError raised if the inputs attr is not a Link instance.""" +def test_inputs_argument_is_not_a_list_of_file_instances(setup_version_db_tests): + """TypeError raised if the inputs attr is not a File instance.""" data = setup_version_db_tests test_value = [132, "231123"] data["kwargs"]["inputs"] = test_value @@ -592,12 +592,12 @@ def test_inputs_argument_is_not_a_list_of_link_instances(setup_version_db_tests) assert ( str(cm.value) == "All elements in Version.inputs should be all " - "stalker.models.link.Link instances, not int: '132'" + "stalker.models.link.File instances, not int: '132'" ) -def test_inputs_attribute_is_not_a_list_of_link_instances(setup_version_db_tests): - """TypeError raised if the inputs attr is set to something other than a Link.""" +def test_inputs_attribute_is_not_a_list_of_file_instances(setup_version_db_tests): + """TypeError raised if the inputs attr is set to something other than a File.""" data = setup_version_db_tests test_value = [132, "231123"] with pytest.raises(TypeError) as cm: @@ -605,7 +605,7 @@ def test_inputs_attribute_is_not_a_list_of_link_instances(setup_version_db_tests assert ( str(cm.value) == "All elements in Version.inputs should be all " - "stalker.models.link.Link instances, not int: '132'" + "stalker.models.link.File instances, not int: '132'" ) @@ -614,12 +614,12 @@ def test_inputs_attribute_is_working_as_expected(setup_version_db_tests): data = setup_version_db_tests data["kwargs"].pop("inputs") new_version = Version(**data["kwargs"]) - assert data["test_input_link1"] not in new_version.inputs - assert data["test_input_link2"] not in new_version.inputs + assert data["test_input_file1"] not in new_version.inputs + assert data["test_input_file2"] not in new_version.inputs - new_version.inputs = [data["test_input_link1"], data["test_input_link2"]] - assert data["test_input_link1"] in new_version.inputs - assert data["test_input_link2"] in new_version.inputs + new_version.inputs = [data["test_input_file1"], data["test_input_file2"]] + assert data["test_input_file1"] in new_version.inputs + assert data["test_input_file2"] in new_version.inputs def test_outputs_argument_is_skipped(setup_version_db_tests): @@ -646,8 +646,8 @@ def test_outputs_attribute_is_none(setup_version_db_tests): assert str(cm.value) == "Incompatible collection type: None is not list-like" -def test_outputs_argument_is_not_a_list_of_link_instances(setup_version_db_tests): - """TypeError raised if the outputs attr is not a Link instance.""" +def test_outputs_argument_is_not_a_list_of_file_instances(setup_version_db_tests): + """TypeError raised if the outputs attr is not a File instance.""" data = setup_version_db_tests test_value = [132, "231123"] data["kwargs"]["outputs"] = test_value @@ -656,12 +656,12 @@ def test_outputs_argument_is_not_a_list_of_link_instances(setup_version_db_tests assert ( str(cm.value) == "All elements in Version.outputs should be all " - "stalker.models.link.Link instances, not int: '132'" + "stalker.models.link.File instances, not int: '132'" ) -def test_outputs_attribute_is_not_a_list_of_link_instances(setup_version_db_tests): - """TypeError raised if the outputs attr is not a Link instance.""" +def test_outputs_attribute_is_not_a_list_of_file_instances(setup_version_db_tests): + """TypeError raised if the outputs attr is not a File instance.""" data = setup_version_db_tests test_value = [132, "231123"] with pytest.raises(TypeError) as cm: @@ -669,7 +669,7 @@ def test_outputs_attribute_is_not_a_list_of_link_instances(setup_version_db_test assert ( str(cm.value) == "All elements in Version.outputs should be all " - "stalker.models.link.Link instances, not int: '132'" + "stalker.models.link.File instances, not int: '132'" ) @@ -678,12 +678,12 @@ def test_outputs_attribute_is_working_as_expected(setup_version_db_tests): data = setup_version_db_tests data["kwargs"].pop("outputs") new_version = Version(**data["kwargs"]) - assert data["test_output_link1"] not in new_version.outputs - assert data["test_output_link2"] not in new_version.outputs + assert data["test_output_file1"] not in new_version.outputs + assert data["test_output_file2"] not in new_version.outputs - new_version.outputs = [data["test_output_link1"], data["test_output_link2"]] - assert data["test_output_link1"] in new_version.outputs - assert data["test_output_link2"] in new_version.outputs + new_version.outputs = [data["test_output_file1"], data["test_output_file2"]] + assert data["test_output_file1"] in new_version.outputs + assert data["test_output_file2"] in new_version.outputs def test_is_published_attribute_is_false_by_default(setup_version_db_tests): @@ -2060,36 +2060,36 @@ def setup_version_tests(): status_list=data["test_task_status_list"], ) - # a Link for the input file - data["test_input_link1"] = Link( - name="Input Link 1", + # a File for the input file + data["test_input_file1"] = File( + name="Input File 1", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_beauty_v001.###.exr", ) - data["test_input_link2"] = Link( - name="Input Link 2", + data["test_input_file2"] = File( + name="Input File 2", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_occ_v001.###.exr", ) - # a Link for the output file - data["test_output_link1"] = Link( - name="Output Link 1", + # a File for the output file + data["test_output_file1"] = File( + name="Output File 1", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_beauty_v001.###.exr", ) - data["test_output_link2"] = Link( - name="Output Link 2", + data["test_output_file2"] = File( + name="Output File 2", full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" "Outputs/SH001_occ_v001.###.exr", ) # now create a version for the Task data["kwargs"] = { - "inputs": [data["test_input_link1"], data["test_input_link2"]], - "outputs": [data["test_output_link1"], data["test_output_link2"]], + "inputs": [data["test_input_file1"], data["test_input_file2"]], + "outputs": [data["test_output_file1"], data["test_output_file2"]], "task": data["test_task1"], "created_with": "Houdini", } From cdc50587eabac6a02de12cf7675feac787ab64ea Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 13 Jan 2025 07:46:00 +0000 Subject: [PATCH 02/11] [#147] Renamed the `stalker.models.link` module to `stalker.models.file`. --- src/stalker/models/{link.py => file.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/stalker/models/{link.py => file.py} (100%) diff --git a/src/stalker/models/link.py b/src/stalker/models/file.py similarity index 100% rename from src/stalker/models/link.py rename to src/stalker/models/file.py From f9bd304e828a6c8a7ebd33d986ccb58d68d52b82 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 13 Jan 2025 07:46:59 +0000 Subject: [PATCH 03/11] [#147] Renamed the references to `stalker.models.link` module to `stalker.models.file`. --- docs/source/inheritance_diagram.rst | 5 +++-- docs/source/summary.rst | 5 +++-- src/stalker/__init__.py | 2 +- src/stalker/models/entity.py | 4 ++-- src/stalker/models/mixins.py | 8 ++++---- src/stalker/models/review.py | 6 +++--- src/stalker/models/version.py | 6 +++--- tests/mixins/test_reference_mixin.py | 4 ++-- tests/models/test_daily.py | 4 ++-- tests/models/test_simple_entity.py | 4 ++-- tests/models/test_version.py | 8 ++++---- 11 files changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/source/inheritance_diagram.rst b/docs/source/inheritance_diagram.rst index 5c4a9a99..0d47c9da 100644 --- a/docs/source/inheritance_diagram.rst +++ b/docs/source/inheritance_diagram.rst @@ -28,8 +28,8 @@ Inheritance Diagram stalker.models.entity.Entity stalker.models.entity.EntityGroup stalker.models.entity.SimpleEntity + stalker.models.file.File stalker.models.format.ImageFormat - stalker.models.link.Link stalker.models.message.Message stalker.models.mixins.ACLMixin stalker.models.mixins.CodeMixin @@ -48,7 +48,7 @@ Inheritance Diagram stalker.models.repository.Repository stalker.models.review.Review stalker.models.review.Daily - stalker.models.review.DailyLink + stalker.models.review.DailyFile stalker.models.scene.Scene stalker.models.schedulers.SchedulerBase stalker.models.schedulers.TaskJugglerScheduler @@ -69,6 +69,7 @@ Inheritance Diagram stalker.models.ticket.TicketLog stalker.models.type.EntityType stalker.models.type.Type + stalker.models.variant.Variant stalker.models.version.Version stalker.models.wiki.Page :parts: 1 diff --git a/docs/source/summary.rst b/docs/source/summary.rst index 5bd1e06a..6893b48f 100644 --- a/docs/source/summary.rst +++ b/docs/source/summary.rst @@ -35,8 +35,8 @@ Summary stalker.models.entity.Entity stalker.models.entity.EntityGroup stalker.models.entity.SimpleEntity + stalker.models.file.File stalker.models.format.ImageFormat - stalker.models.link.Link stalker.models.message.Message stalker.models.mixins.ACLMixin stalker.models.mixins.CodeMixin @@ -55,7 +55,7 @@ Summary stalker.models.repository.Repository stalker.models.review.Review stalker.models.review.Daily - stalker.models.review.DailyLink + stalker.models.review.DailyFile stalker.models.scene.Scene stalker.models.schedulers.SchedulerBase stalker.models.schedulers.TaskJugglerScheduler @@ -75,5 +75,6 @@ Summary stalker.models.ticket.TicketLog stalker.models.type.EntityType stalker.models.type.Type + stalker.models.variant.Variant stalker.models.version.Version stalker.models.wiki.Page diff --git a/src/stalker/__init__.py b/src/stalker/__init__.py index f8429a97..147ba980 100644 --- a/src/stalker/__init__.py +++ b/src/stalker/__init__.py @@ -22,7 +22,7 @@ from stalker.models.department import Department, DepartmentUser from stalker.models.entity import Entity, EntityGroup, SimpleEntity from stalker.models.format import ImageFormat -from stalker.models.link import File +from stalker.models.file import File from stalker.models.message import Message from stalker.models.mixins import ( ACLMixin, diff --git a/src/stalker/models/entity.py b/src/stalker/models/entity.py index b3c0107a..ab9207b6 100644 --- a/src/stalker/models/entity.py +++ b/src/stalker/models/entity.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: # pragma: no cover from stalker.models.auth import User - from stalker.models.link import File + from stalker.models.file import File from stalker.models.note import Note from stalker.models.tag import Tag from stalker.models.type import Type @@ -637,7 +637,7 @@ def _validate_thumbnail(self, key: str, thumb: "File") -> "File": if not isinstance(thumb, File): raise TypeError( f"{self.__class__.__name__}.thumbnail should be a " - "stalker.models.link.File instance, " + "stalker.models.file.File instance, " f"not {thumb.__class__.__name__}: '{thumb}'" ) return thumb diff --git a/src/stalker/models/mixins.py b/src/stalker/models/mixins.py index 18580beb..93791335 100644 --- a/src/stalker/models/mixins.py +++ b/src/stalker/models/mixins.py @@ -58,7 +58,7 @@ from stalker.models.auth import Permission from stalker.models.project import Project from stalker.models.status import Status, StatusList - from stalker.models.link import File + from stalker.models.file import File from stalker.models.studio import WorkingHours @@ -1042,7 +1042,7 @@ def _validate_project(self, key: str, project: "Project") -> "Project": class ReferenceMixin(object): """Adds reference capabilities to the mixed in class. - References are :class:`stalker.models.link.File` instances or anything + References are :class:`stalker.models.file.File` instances or anything derived from it, which adds information to the attached objects. The aim of the References are generally to give more info to direct the evolution of the object. @@ -1099,13 +1099,13 @@ def _validate_references(self, key: str, reference: "File") -> "File": Returns: File: The validated reference value. """ - from stalker.models.link import File + from stalker.models.file import File # all the items should be instance of stalker.models.entity.Entity if not isinstance(reference, File): raise TypeError( f"All the items in the {self.__class__.__name__}.references should " - "be stalker.models.link.File instances, " + "be stalker.models.file.File instances, " f"not {reference.__class__.__name__}: '{reference}'" ) return reference diff --git a/src/stalker/models/review.py b/src/stalker/models/review.py index 10f198ad..be89f74a 100644 --- a/src/stalker/models/review.py +++ b/src/stalker/models/review.py @@ -12,7 +12,7 @@ from stalker.log import get_logger from stalker.models.entity import Entity, SimpleEntity from stalker.models.enum import DependencyTarget, TimeUnit, TraversalDirection -from stalker.models.link import File +from stalker.models.file import File from stalker.models.mixins import ( ProjectMixin, ScheduleMixin, @@ -510,7 +510,7 @@ class DailyFile(Base): ) file: Mapped[File] = relationship( primaryjoin="DailyFile.file_id==File.file_id", - doc="""stalker.models.link.File instances related to the Daily instance. + doc="""stalker.models.file.File instances related to the Daily instance. Attach the same :class:`.File` instances that are linked as an output to a certain :class:`.Version` s instance to this attribute. @@ -558,7 +558,7 @@ def _validate_file(self, key: str, file: Union[None, File]) -> Union[None, File] if file is not None and not isinstance(file, File): raise TypeError( f"{self.__class__.__name__}.file should be an instance of " - "stalker.models.link.File instance, " + "stalker.models.file.File instance, " f"not {file.__class__.__name__}: '{file}'" ) diff --git a/src/stalker/models/version.py b/src/stalker/models/version.py index 28c65dcb..1a5e4b08 100644 --- a/src/stalker/models/version.py +++ b/src/stalker/models/version.py @@ -15,7 +15,7 @@ from stalker.db.session import DBSession from stalker.log import get_logger from stalker.models.enum import TraversalDirection -from stalker.models.link import File +from stalker.models.file import File from stalker.models.mixins import DAGMixin from stalker.models.review import Review from stalker.models.task import Task @@ -417,7 +417,7 @@ def _validate_inputs(self, key, input_): if not isinstance(input_, File): raise TypeError( "All elements in {}.inputs should be all " - "stalker.models.link.File instances, not {}: '{}'".format( + "stalker.models.file.File instances, not {}: '{}'".format( self.__class__.__name__, input_.__class__.__name__, input_ ) ) @@ -441,7 +441,7 @@ def _validate_outputs(self, key, output) -> File: if not isinstance(output, File): raise TypeError( "All elements in {}.outputs should be all " - "stalker.models.link.File instances, not {}: '{}'".format( + "stalker.models.file.File instances, not {}: '{}'".format( self.__class__.__name__, output.__class__.__name__, output ) ) diff --git a/tests/mixins/test_reference_mixin.py b/tests/mixins/test_reference_mixin.py index a12102b5..4f7eeacb 100644 --- a/tests/mixins/test_reference_mixin.py +++ b/tests/mixins/test_reference_mixin.py @@ -114,7 +114,7 @@ def test_references_attribute_accepting_only_lists_of_file_instances( assert str(cm.value) == ( "All the items in the RefMixFooClass.references should be " - "stalker.models.link.File instances, not int: '1'" + "stalker.models.file.File instances, not int: '1'" ) @@ -126,7 +126,7 @@ def test_references_attribute_elements_accepts_files_only(setup_reference_mixin_ assert str(cm.value) == ( "All the items in the RefMixFooClass.references should be " - "stalker.models.link.File instances, not Entity: ''" + "stalker.models.file.File instances, not Entity: ''" ) diff --git a/tests/models/test_daily.py b/tests/models/test_daily.py index 64870b08..1a4d712a 100644 --- a/tests/models/test_daily.py +++ b/tests/models/test_daily.py @@ -161,7 +161,7 @@ def test_files_argument_is_not_a_list_instance(setup_daily_tests): assert ( str(cm.value) == "DailyFile.file should be an instance of " - "stalker.models.link.File instance, not str: 'n'" + "stalker.models.file.File instance, not str: 'n'" ) @@ -177,7 +177,7 @@ def test_files_argument_is_not_a_list_of_file_instances(setup_daily_tests): ) assert str(cm.value) == ( - "DailyFile.file should be an instance of stalker.models.link.File instance, " + "DailyFile.file should be an instance of stalker.models.file.File instance, " "not str: 'not'" ) diff --git a/tests/models/test_simple_entity.py b/tests/models/test_simple_entity.py index ac58b18d..4d71ed9a 100644 --- a/tests/models/test_simple_entity.py +++ b/tests/models/test_simple_entity.py @@ -665,7 +665,7 @@ def test_thumbnail_arg_is_not_a_file_instance(setup_simple_entity_tests): SimpleEntity(**data["kwargs"]) assert str(cm.value) == ( - "SimpleEntity.thumbnail should be a stalker.models.link.File instance, " + "SimpleEntity.thumbnail should be a stalker.models.file.File instance, " "not str: 'not a File'" ) @@ -677,7 +677,7 @@ def test_thumbnail_attr_is_not_a_file_instance(setup_simple_entity_tests): data["test_simple_entity"].thumbnail = "not a File" assert str(cm.value) == ( - "SimpleEntity.thumbnail should be a stalker.models.link.File instance, " + "SimpleEntity.thumbnail should be a stalker.models.file.File instance, " "not str: 'not a File'" ) diff --git a/tests/models/test_version.py b/tests/models/test_version.py index 9e05e5e1..fb340fa8 100644 --- a/tests/models/test_version.py +++ b/tests/models/test_version.py @@ -592,7 +592,7 @@ def test_inputs_argument_is_not_a_list_of_file_instances(setup_version_db_tests) assert ( str(cm.value) == "All elements in Version.inputs should be all " - "stalker.models.link.File instances, not int: '132'" + "stalker.models.file.File instances, not int: '132'" ) @@ -605,7 +605,7 @@ def test_inputs_attribute_is_not_a_list_of_file_instances(setup_version_db_tests assert ( str(cm.value) == "All elements in Version.inputs should be all " - "stalker.models.link.File instances, not int: '132'" + "stalker.models.file.File instances, not int: '132'" ) @@ -656,7 +656,7 @@ def test_outputs_argument_is_not_a_list_of_file_instances(setup_version_db_tests assert ( str(cm.value) == "All elements in Version.outputs should be all " - "stalker.models.link.File instances, not int: '132'" + "stalker.models.file.File instances, not int: '132'" ) @@ -669,7 +669,7 @@ def test_outputs_attribute_is_not_a_list_of_file_instances(setup_version_db_test assert ( str(cm.value) == "All elements in Version.outputs should be all " - "stalker.models.link.File instances, not int: '132'" + "stalker.models.file.File instances, not int: '132'" ) From c11a20d21561729f6f637c5c48d51892f8005f76 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 13 Jan 2025 16:28:48 +0000 Subject: [PATCH 04/11] [#147] Renamed `tests/models/test_link.py` to `tests/models/test_file.py` to reflect the change in class name. --- tests/models/{test_link.py => test_file.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/models/{test_link.py => test_file.py} (100%) diff --git a/tests/models/test_link.py b/tests/models/test_file.py similarity index 100% rename from tests/models/test_link.py rename to tests/models/test_file.py From d7478068fedf9c50dcf4f33d36046f581161175c Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Tue, 14 Jan 2025 14:15:27 +0000 Subject: [PATCH 05/11] [#147] Updated tutorial to reflect changes in the API. --- docs/source/tutorial/asset_management.rst | 88 +++++++++++-------- docs/source/tutorial/conclusion.rst | 12 +-- docs/source/tutorial/creating_simple_data.rst | 8 +- .../tutorial/tutorial_files/tutorial.py | 17 ++-- 4 files changed, 70 insertions(+), 55 deletions(-) diff --git a/docs/source/tutorial/asset_management.rst b/docs/source/tutorial/asset_management.rst index a9a3cf2f..ef3f6c10 100644 --- a/docs/source/tutorial/asset_management.rst +++ b/docs/source/tutorial/asset_management.rst @@ -11,10 +11,10 @@ File Storage and Repository setup Contrary to a Source Code Management (SCM) System where revisions to a file is handled incrementally, Stalker handles file versions all together. Meaning -that, all the versions that are created for individual tasks -(:class:`.Version`) are individual files stored in a shared location accessible -to everyone in your studio. This location is called a :class:`.Repository` in -Stalker. +that, all the files (:class:`.File`) that are created for individual versions +(:class:`.Version`) of a task are individual files stored in a shared location +accessible to everyone in your studio. This location is called a +:class:`.Repository` in Stalker. Defining Repository Paths ------------------------- @@ -83,7 +83,7 @@ Structure" and assign it to our project: Starting with Stalker version 0.2.13, :class:`.Project` instances can be associated with multiple :class:`.Repository` instances. This allows for - more flexible file management, such as storing published versions on a + more flexible file management, such as storing published version files on a separate server or directing rendered outputs to a different location. While the following examples are simplified, future versions will showcase @@ -93,13 +93,13 @@ Creating Filename Templates --------------------------- Next we create :class:`.FilenameTemplate` instances. These templates define how -filenames and paths will be generated for :class:`.Version` instances +filenames and paths will be generated for by :class:`.Version` instances associated with tasks. Here, we create a :class:`.FilenameTemplate` named "Task Template for Commercials" that uses `Jinja2`_ syntax for the ``path`` and ``filename`` -arguments. The :class:`.Version` class knows how to render these templates -while calculating its ``path`` and ``filename`` attributes: +arguments. The :class:`.Version.generate_path()` method knows how to render +these templates to generate a ``pathlib.Path`` object: .. code-block:: python @@ -109,7 +109,7 @@ while calculating its ``path`` and ``filename`` attributes: name='Task Template for Commercials', target_entity_type='Task', path='$REPO{{project.repository.code}}/{{project.code}}/{%- for p in parent_tasks -%}{{p.nice_name}}/{%- endfor -%}', - filename='{{version.nice_name}}_v{{"%03d"|format(version.version_number)}}' + filename='{{version.nice_name}}_r{{"%02d"|format(version.revision_number)}}_v{{"%03d"|format(version.version_number)}}' ) # Append the template to the project structure @@ -132,8 +132,8 @@ Explanation of the Template: * ``{%- for p in parent_tasks -%}{{p.nice_name}}/{%- endfor -%}``: This loop iterates over parent tasks, creating subdirectories for each. -* ``{{version.nice_name}}_v{{"%03d"|format(version.version_number)}}``: This - defines the filename format with version number padding. +* ``{{version.nice_name}}_r{{"%02d"|format(version.revision_number)}}_v{{"%03d"|format(version.version_number)}}``: This + defines the filename format with revision and version numbers padded. Creating and Managing Versions ------------------------------ @@ -146,16 +146,20 @@ Now, let's create a :class:`.Version`` instance for the "comp" task: vers1 = Version(task=comp) - # Update paths using the template - vers1.update_paths() + # Generate a path using the template + path = vers1.generate_path() - print(vers1.path) # '$REPO33/FC/SH001/comp' - print(vers1.filename) # 'SH001_comp_Main_v001' - print(vers1.full_path) # '$REPO33/FC/SH001/comp/SH001_comp_Main_v001' + print(path.parent) # '$REPO33/FC/SH001/comp' + print(path.name) # 'SH001_comp_r01_v001' + print(path) # '$REPO33/FC/SH001/comp/SH001_comp_r01_v001' # Absolute paths with repository root based on your OS - print(vers1.absolute_path) # '/mnt/M/commercials/FC/SH001/comp' - print(vers1.absolute_full_path) # '/mnt/M/commercials/FC/SH001/comp/SH001_comp_Main_v001' + # unfortunately the Path object doesn't directly render environment variables + print(os.path.expandvars(path.parent)) # '/mnt/M/commercials/FC/SH001/comp' + print(os.path.expandvars(path)) # '/mnt/M/commercials/FC/SH001/comp/SH001_comp_r01_v001' + + # Get the revision number (manually incremented) + print(vers1.revision_number) # 1 # Get version number (automatically incremented) print(vers1.version_number) # 1 @@ -164,7 +168,7 @@ Now, let's create a :class:`.Version`` instance for the "comp" task: DBsession.commit() Stalker automatically generates a consistent path and filename for the version -based on the template. +based on the template. Stalker eliminates the need for those cumbersome and confusing file naming conventions like ``Shot1_comp_Final``, ``Shot1_comp_Final_revised``, @@ -192,16 +196,21 @@ identified. .. code-block:: python vers2 = Version(task=comp) - vers2.update_paths() - - print(vers2.version_number) # Output: 2 - print(vers2.filename) # Output: 'SH001_comp_Main_v002' + print(vers2.version_number) # Output: 2 + print(vers2.generate_path().name) # Output: 'SH001_comp_r01_v002' vers3 = Version(task=comp) - vers3.update_paths() + print(vers3.version_number) # Output: 3 + print(vers3.generate_path().name) # Output: 'SH001_comp_r01_v003' + +:attr:`.Version.revision_number` is not updated automatically and left for the +user to update: + +.. code-block:: python - print(vers3.version_number) # Output: 3 - print(vers3.filename) # Output: 'SH001_comp_Main_v003' + vers4 = Version(task=comp, revision_number=2) + print(vers4.version_number) # Output: 4 + print(vers4.generate_path().name) # Output: 'SH001_comp_r02_v001' Querying Versions ----------------- @@ -213,17 +222,17 @@ using the :attr:`.Task.versions` attribute or by doing a database query. # using pure Python vers_from_python = comp.versions - # [, - # , - # ] + # [, + # , + # ] # # Using SQLAlchemy query vers_from_query = Version.query.filter_by(task=comp).all() # again returns - # [, - # , - # ] + # [, + # , + # ] # Both methods return a list of Version objects assert vers_from_python == vers_from_query @@ -232,13 +241,16 @@ using the :attr:`.Task.versions` attribute or by doing a database query. .. note:: - Stalker stores :attr:`.Version.path` and :att:`.Version.filename` attribute - values within the database, independent of your operating system. The - :attr:`.Version.absolute_path` and :attr:`.Version.absolute_full_path` - attributes provide OS-specific paths by combining the - :attr:`.Repository.path` with the database values momentarily. + Files related to :class:`.Version` instances can be created by instantiating + the :class:`.File` class. The :attr:`.File.full_path` can be set with the + value coming from the :meth:`.Version.generate_path()` method, which by + default will contain environment variables for the repository path and + Stalker will save the :att:`.File.full_path` attribute value in the database + without converting the environment variables so the paths will stay + operating system independent. -You can define default directories within your project structure using custom templates. +You can define default directories within your project structure using custom +templates. .. code-block:: python diff --git a/docs/source/tutorial/conclusion.rst b/docs/source/tutorial/conclusion.rst index 0d41dc9d..eccf17c2 100644 --- a/docs/source/tutorial/conclusion.rst +++ b/docs/source/tutorial/conclusion.rst @@ -7,33 +7,33 @@ In this tutorial, you have nearly learned a quarter of what Stalker supplies as a Python library. Stalker provides a robust framework for production asset management, serving -the needs of both large and small studios. Its 15-year development history (as -of 2024) and use in major feature films and countless commercials is a +the needs of both large and small studios. Its 16-years development history (as +of 2025) and use in major feature films and countless commercials is a testament to its effectiveness. While Stalker itself lacks a graphical user interface (GUI), its power extends beyond raw code. Here are some additional tools that leverage Stalker's core functionality: - `Stalker Pyramid`_: A web application built using the `Pyramid`_ framework, utilizing Stalker as its database model. This allows for user-friendly web-based interaction with project data. -`Anima`_: +`Anima Pipeline`_: A pipeline library that incorporates Stalker, showcasing how its functionalities can be integrated into a pipeline management system. Notably, Anima demonstrates the creation of Qt UIs using Stalker. For a deeper dive into how Stalker interacts with UIs and web applications, -consider exploring the repositories of `Stalker Pyramid`_ and `Anima`_. +consider exploring the repositories of `Stalker Pyramid`_ and +`Anima Pipeline`_. By understanding how Stalker integrates with these tools, you can unlock its full potential for streamlining your production workflows. .. _Stalker Pyramid: https://www.github.com/eoyilmaz/stalker_pyramid -.. _Anima: https://github.com/eoyilmaz/anima +.. _Anima Pipeline: https://github.com/eoyilmaz/anima .. _Pyramid: https://trypyramid.com/ diff --git a/docs/source/tutorial/creating_simple_data.rst b/docs/source/tutorial/creating_simple_data.rst index fecfe531..0b0a571d 100644 --- a/docs/source/tutorial/creating_simple_data.rst +++ b/docs/source/tutorial/creating_simple_data.rst @@ -20,9 +20,9 @@ Project setup .. note:: When the Stalker database is first initialized (with ``db.setup.init()``), a set of default :class:`.Status` instances for :class:`.Task`, - :class:`.Asset`, :class:`.Shot`, :class:`.Sequence` and :class:`.Ticket` - classes are created, along with their respective :class:`.StatusList` - instances. + :class:`.Asset`, :class:`.Shot`, :class:`.Sequence`, :class:`.Ticket` and + :class:`.Variant` classes are created, along with their respective + :class:`.StatusList` instances. Creating a Repository --------------------- @@ -92,7 +92,7 @@ Let's add more details to the project: Saving the Project ------------------ -To save the proejct and its associated data to the database: +To save the project and its associated data to the database: .. code-block:: python diff --git a/docs/source/tutorial/tutorial_files/tutorial.py b/docs/source/tutorial/tutorial_files/tutorial.py index a5222e80..bea59b62 100644 --- a/docs/source/tutorial/tutorial_files/tutorial.py +++ b/docs/source/tutorial/tutorial_files/tutorial.py @@ -1,5 +1,8 @@ +# -*- coding: utf-8 -*- + +import os + import stalker.db.setup -from stalker import db stalker.db.setup.setup({"sqlalchemy.url": "sqlite:///"}) stalker.db.setup.init() @@ -254,12 +257,12 @@ vers1 = Version(task=comp) # we need to update the paths -vers1.update_paths() +path = vers1.generate_path() # check the path and filename -print(vers1.path) # '$REPO33/FC/SH001/comp' -print(vers1.filename) # 'SH001_comp_Main_v001' -print(vers1.full_path) # '$REPO33/FC/SH001/comp/SH001_comp_Main_v001' +print(path.parent) # '$REPO33/FC/SH001/comp' +print(path.name) # 'SH001_comp_Main_v001' +print(path) # '$REPO33/FC/SH001/comp/SH001_comp_Main_v001' # now the absolute values, values with repository root # because I'm running this code in a Linux laptop, my results are using the # linux path of the repository @@ -280,7 +283,7 @@ # be sure that you've committed the previous version to the database # to let Stalker now what number to give for the next version vers2 = Version(task=comp) -vers2.update_paths() # this call probably will disappear in next version of +vers2.generate_path() # this call probably will disappear in next version of # Stalker, so Stalker will automatically update the # paths on Version.__init__() @@ -292,7 +295,7 @@ # now create a new version vers3 = Version(task=comp) -vers3.update_paths() +vers3.generate_path() print(vers3.version_number) # 3 print(vers3.filename) # 'SH001_comp_Main_v002' From 3a49f49b5fce70527e92b7cb28352354da79fda7 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Tue, 14 Jan 2025 14:17:50 +0000 Subject: [PATCH 06/11] [#147] Updated example file to reflect changes in the API. --- examples/flat_project_example.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/flat_project_example.py b/examples/flat_project_example.py index a685790b..de4af260 100644 --- a/examples/flat_project_example.py +++ b/examples/flat_project_example.py @@ -9,6 +9,8 @@ Task/Asset/Shot/Sequence has its own folder and the Task hierarchy is directly reflected to folder hierarchy. """ +import os + import stalker.db.setup from stalker import ( db, @@ -86,18 +88,16 @@ # lets create a Maya file for the Model task t2_v1 = Version(task=t1) -t2_v1.update_paths() # for now this is needed to render the template, but will -# remove it later on -t2_v1.extension = ".ma" # set the extension for maya +# set the extension for maya +path1 = t2_v1.generate_path(extension=".ma") # for now this is needed to render the template, but will # lets create a new version for Lighting t3_v1 = Version(task=t3) -t3_v1.update_paths() -t3_v1.extension = ".ma" +path2 = t3_v1.generate_path(extension=".ma") # you should see that all are in the same folder -print(t2_v1.absolute_full_path) -print(t3_v1.absolute_full_path) +print(os.path.expandvars(path1)) +print(os.path.expandvars(path2)) # # Lets create a second Project that use some other folder structure @@ -164,7 +164,7 @@ # now create new maya files for them comp_v1 = Version(task=comp, variant_name="Test") -comp_v1.update_paths() -comp_v1.extension = ".ma" +path = comp_v1.generate_path(extension=".ma") -print(comp_v1.absolute_full_path) # as you see it is in a proper shot folder +# as you see it is in a proper shot folder +print(os.path.expandvars(path)) From 7200c581c91e3c1dde97a662d99470e490a9822f Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Tue, 14 Jan 2025 15:24:17 +0000 Subject: [PATCH 07/11] * [#147] `File` class is now also deriving from `ReferenceMixin` adding the `File.references` attribute. * [#147] Moved the `created_with` attribute from `Version` to `File`. * [#147] Removed the `Version.walk_inputs()` method. * [#147] Added the `File.walk_references()` method. * [#147] Updated `ReferenceMixin`: * [#147] Added the `primaryjoin` and `secondaryjoin` arguments on the relation, so that the `File` class can reference itself as it is also now deriving from `ReferenceMixin`. * [#147] Renamed the secondary column from `file_id` to `reference_id` which makes more sense and allows the `File` class to derive from `ReferenceMixin` too. * [#147] Updated `Daily.versions` and `Daily.tasks` properties to query the `Version` instances over the new `Version.files` attribute instead of the removed `Version.output` attribute. * [#147] Updated `Version` class: * [#147] It is now deriving from `Entity` instead of `File`, so it doesn't have any file related attributes anymore. * [#147] Removed the `inputs` and `outputs` attributes and introduced the `files` attribute to store `File` instances. * [#147] Renamed `Version.updated_paths()` to `Version.generate_path()` which now returns a `pathlib.Path` instance that is to be used with `File` instances, as the `Version` instance cannot store path values anymore. * [#147] The `absolute_full_path`, `absolute_path`, `full_path`, `path` and `filename` are now just properties returning data generated by the `Version.generate_path()` method. Which will be less useful as these properties are returning generated data and not stored ones. --- README.md | 15 +- src/stalker/db/types.py | 4 +- src/stalker/models/auth.py | 8 +- src/stalker/models/budget.py | 9 +- src/stalker/models/client.py | 4 +- src/stalker/models/enum.py | 17 +- src/stalker/models/file.py | 77 ++- src/stalker/models/mixins.py | 33 +- src/stalker/models/review.py | 20 +- src/stalker/models/scene.py | 6 +- src/stalker/models/schedulers.py | 4 +- src/stalker/models/sequence.py | 4 +- src/stalker/models/shot.py | 5 +- src/stalker/models/structure.py | 6 +- src/stalker/models/studio.py | 2 +- src/stalker/models/task.py | 54 +- src/stalker/models/ticket.py | 4 +- src/stalker/models/version.py | 307 +++++----- tests/db/test_db.py | 122 ++-- tests/mixins/test_dag_mixin.py | 4 +- tests/mixins/test_reference_mixin.py | 8 +- tests/models/test_budget.py | 2 +- tests/models/test_client.py | 4 +- tests/models/test_daily.py | 8 +- tests/models/test_file.py | 243 +++++++- tests/models/test_filename_template.py | 6 +- tests/models/test_group.py | 8 +- tests/models/test_price_list.py | 8 +- tests/models/test_scene.py | 10 +- tests/models/test_sequence.py | 7 +- tests/models/test_simple_entity.py | 8 +- tests/models/test_structure.py | 8 +- tests/models/test_task.py | 52 +- tests/models/test_task_dependency.py | 4 +- tests/models/test_task_juggler_scheduler.py | 18 +- tests/models/test_ticket.py | 4 +- tests/models/test_version.py | 586 +++++++++----------- tests/test_readme_tutorial.py | 12 +- whitelist.txt | 2 + 39 files changed, 990 insertions(+), 713 deletions(-) diff --git a/README.md b/README.md index 01e55f87..86d2e6e5 100644 --- a/README.md +++ b/README.md @@ -66,10 +66,21 @@ Let's say that you want to query all the Shot Lighting tasks where a specific asset is referenced: ```python -from stalker import Asset, Shot, Version +from stalker import Asset, File, Shot, Version my_asset = Asset.query.filter_by(name="My Asset").first() -refs = Version.query.filter_by(name="Lighting").filter(Version.inputs.contains(my_asset)).all() +# Let's assume we have multiple Versions created for this Asset already +my_asset_version = my_asset.versions[0] +# get a file from that version +my_asset_version_file = my_asset_version.files[0] +# now get any other Lighting Versions that is referencing this file +refs = ( + Version.query + .join(File, Version.files) + .filter(Version.name=="Lighting") + .filter(File.references.contains(my_asset_version_file)) + .all() +) ``` Let's say you want to get all the tasks assigned to you in a specific Project: diff --git a/src/stalker/db/types.py b/src/stalker/db/types.py index 76dd4d71..969ede26 100644 --- a/src/stalker/db/types.py +++ b/src/stalker/db/types.py @@ -70,7 +70,7 @@ def process_bind_param(self, value: Any, dialect: str) -> datetime.datetime: """Process bind param. Args: - value (any): The value. + value (Any): The value. dialect (str): The dialect. Returns: @@ -86,7 +86,7 @@ def process_result_value(self, value: Any, dialect: str) -> datetime.datetime: """Process result value. Args: - value (any): The value. + value (Any): The value. dialect (str): The dialect. Returns: diff --git a/src/stalker/models/auth.py b/src/stalker/models/auth.py index e7ccfd3e..1c5e40f0 100644 --- a/src/stalker/models/auth.py +++ b/src/stalker/models/auth.py @@ -284,7 +284,7 @@ class Group(Entity, ACLMixin): Args: name (str): The name of this group. - users list: A list of :class:`.User` instances, holding the desired + users list: A list of :class:`.User` instances holding the desired users in this group. """ @@ -333,9 +333,9 @@ def _validate_users(self, key: str, user: "User") -> "User": """ if not isinstance(user, User): raise TypeError( - f"{self.__class__.__name__}.users attribute must all be " - "stalker.models.auth.User " - f"instances, not {user.__class__.__name__}: '{user}'" + f"{self.__class__.__name__}.users should only contain " + "instances of stalker.models.auth.User, " + f"not {user.__class__.__name__}: '{user}'" ) return user diff --git a/src/stalker/models/budget.py b/src/stalker/models/budget.py index 963e48b2..91e3e131 100644 --- a/src/stalker/models/budget.py +++ b/src/stalker/models/budget.py @@ -235,8 +235,8 @@ def _validate_goods(self, key: str, good: "Good") -> "Good": """ if not isinstance(good, Good): raise TypeError( - f"{self.__class__.__name__}.goods should be a list of " - "stalker.model.budget.Good instances, " + f"{self.__class__.__name__}.goods should only contain " + "instances of stalker.model.budget.Good, " f"not {good.__class__.__name__}: '{good}'" ) return good @@ -299,8 +299,9 @@ def _validate_entry(self, key: str, entry: "BudgetEntry") -> "BudgetEntry": """ if not isinstance(entry, BudgetEntry): raise TypeError( - f"{self.__class__.__name__}.entries should be a list of BudgetEntry " - f"instances, not {entry.__class__.__name__}: '{entry}'" + f"{self.__class__.__name__}.entries should only contain " + "instances of BudgetEntry, " + f"not {entry.__class__.__name__}: '{entry}'" ) return entry diff --git a/src/stalker/models/client.py b/src/stalker/models/client.py index 84312b99..caa2ae28 100644 --- a/src/stalker/models/client.py +++ b/src/stalker/models/client.py @@ -159,8 +159,8 @@ def _validate_good(self, key: str, good: "Good") -> "Good": if not isinstance(good, Good): raise TypeError( - f"{self.__class__.__name__}.goods attribute should be all " - "stalker.models.budget.Good instances, " + f"{self.__class__.__name__}.goods should only " + "contain instances of stalker.models.budget.Good, " f"not {good.__class__.__name__}: '{good}'" ) diff --git a/src/stalker/models/enum.py b/src/stalker/models/enum.py index e2326f24..2729add4 100644 --- a/src/stalker/models/enum.py +++ b/src/stalker/models/enum.py @@ -34,7 +34,6 @@ def to_constraint( Args: constraint (Union[str, ScheduleConstraint]): Input `constraint` value. - quiet (bool): To raise any exception for invalid value. Raises: TypeError: Input value type is invalid. @@ -95,12 +94,15 @@ def process_bind_param(self, value, dialect) -> int: # just return the value return value.value - def process_result_value(self, value, dialect): + def process_result_value(self, value: int, dialect: str) -> ScheduleConstraint: """Return a ScheduleConstraint. Args: value (int): The integer value. dialect (str): The name of the dialect. + + Returns: + ScheduleConstraint: ScheduleConstraint created from the DB data. """ return ScheduleConstraint.to_constraint(value) @@ -188,6 +190,9 @@ def process_result_value(self, value: str, dialect: str) -> TimeUnit: Args: value (str): The string value to convert to TimeUnit. dialect (str): The name of the dialect. + + Returns: + TimeUnit: The TimeUnit which is created from the DB data. """ return TimeUnit.to_unit(value) @@ -251,7 +256,7 @@ def to_model(cls, model: Union[str, "ScheduleModel"]) -> "ScheduleModel": class ScheduleModelDecorator(TypeDecorator): """Store ScheduleModel as a str and restore as ScheduleModel.""" - impl = saEnum(*[m.value for m in ScheduleModel], name=f"ScheduleModel") + impl = saEnum(*[m.value for m in ScheduleModel], name="ScheduleModel") def process_bind_param(self, value, dialect) -> str: """Return the str value of the ScheduleModel. @@ -272,6 +277,9 @@ def process_result_value(self, value: str, dialect: str) -> ScheduleModel: Args: value (str): The string value to convert to ScheduleModel. dialect (str): The name of the dialect. + + Returns: + ScheduleModel: The ScheduleModel created from the DB data. """ return ScheduleModel.to_model(value) @@ -355,6 +363,9 @@ def process_result_value(self, value: str, dialect: str) -> DependencyTarget: Args: value (str): The string value to convert to DependencyTarget. dialect (str): The name of the dialect. + + Returns: + DependencyTarget: The DependencyTarget created from str. """ return DependencyTarget.to_target(value) diff --git a/src/stalker/models/file.py b/src/stalker/models/file.py index 40851baf..85da624d 100644 --- a/src/stalker/models/file.py +++ b/src/stalker/models/file.py @@ -2,19 +2,23 @@ """File related classes and utility functions are situated here.""" import os -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Generator, List, Optional, Union from sqlalchemy import ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column, validates from stalker.log import get_logger from stalker.models.entity import Entity +from stalker.models.enum import TraversalDirection +from stalker.models.mixins import ReferenceMixin +from stalker.utils import walk_hierarchy + logger = get_logger(__name__) -class File(Entity): - """Holds data about external files or file sequences. +class File(Entity, ReferenceMixin): + """Holds data about files or file sequences. Files are all about giving some external information to the current entity (external to the database, so it can be something on the @@ -43,6 +47,12 @@ class File(Entity): It is the extension part of the full_path. It also includes the extension separator ('.' for most of the file systems). + .. versionadded:: 1.1.0 + + Inputs or references can now be tracked per File instance through the + :attr:`.File.references` attribute. So, that all the references can be + tracked per individual file instance. + Args: full_path (str): The full path to the File, it can be a path to a folder or a file in the file system, or a web page. For file @@ -71,15 +81,21 @@ class File(Entity): Text, doc="The full path of the url to the file." ) + created_with: Mapped[Optional[str]] = mapped_column(String(256)) + def __init__( self, full_path: Optional[str] = "", original_filename: Optional[str] = "", + references: Optional[List["File"]] = None, + created_with: Optional[str] = None, **kwargs: Optional[Dict[str, Any]], ) -> None: super(File, self).__init__(**kwargs) + ReferenceMixin.__init__(self, references=references) self.full_path = full_path self.original_filename = original_filename + self.created_with = created_with @validates("full_path") def _validate_full_path(self, key: str, full_path: Union[None, str]) -> str: @@ -100,12 +116,40 @@ def _validate_full_path(self, key: str, full_path: Union[None, str]) -> str: if not isinstance(full_path, str): raise TypeError( - f"{self.__class__.__name__}.full_path should be an instance of string, " + f"{self.__class__.__name__}.full_path should be a str, " f"not {full_path.__class__.__name__}: '{full_path}'" ) return self._format_path(full_path) + @validates("created_with") + def _validate_created_with( + self, key: str, created_with: Union[None, str] + ) -> Union[None, str]: + """Validate the given created_with value. + + Args: + key (str): The name of the validated column. + created_with (str): The name of the application used to create this + File. + + Raises: + TypeError: If the given created_with attribute is not None and not + a string. + + Returns: + Union[None, str]: The validated created with value. + """ + if created_with is not None and not isinstance(created_with, str): + raise TypeError( + "{}.created_with should be an instance of str, not {}: '{}'".format( + self.__class__.__name__, + created_with.__class__.__name__, + created_with, + ) + ) + return created_with + @validates("original_filename") def _validate_original_filename( self, key: str, original_filename: Union[None, str] @@ -131,8 +175,7 @@ def _validate_original_filename( if not isinstance(original_filename, str): raise TypeError( - f"{self.__class__.__name__}.original_filename should be an instance of " - "string, " + f"{self.__class__.__name__}.original_filename should be a str, " f"not {original_filename.__class__.__name__}: '{original_filename}'" ) @@ -180,7 +223,7 @@ def path(self, path: str) -> None: if not isinstance(path, str): raise TypeError( - f"{self.__class__.__name__}.path should be an instance of str, " + f"{self.__class__.__name__}.path should be a str, " f"not {path.__class__.__name__}: '{path}'" ) @@ -215,7 +258,7 @@ def filename(self, filename: Union[None, str]) -> None: if not isinstance(filename, str): raise TypeError( - f"{self.__class__.__name__}.filename should be an instance of str, " + f"{self.__class__.__name__}.filename should be a str, " f"not {filename.__class__.__name__}: '{filename}'" ) @@ -245,7 +288,7 @@ def extension(self, extension: Union[None, str]) -> None: if not isinstance(extension, str): raise TypeError( - f"{self.__class__.__name__}.extension should be an instance of str, " + f"{self.__class__.__name__}.extension should be a str, " f"not {extension.__class__.__name__}: '{extension}'" ) @@ -255,6 +298,22 @@ def extension(self, extension: Union[None, str]) -> None: self.filename = os.path.splitext(self.filename)[0] + extension + def walk_references( + self, + method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst, + ) -> Generator[None, "File", None]: + """Walk the references of this file. + + Args: + method (Union[int, str, TraversalDirection]): The walk method + defined by the :class:`.TraversalDirection` enum. + + Yields: + File: Yield the File instances. + """ + for v in walk_hierarchy(self, "references", method=method): + yield v + def __eq__(self, other: Any) -> bool: """Check if the other is equal to this File. diff --git a/src/stalker/models/mixins.py b/src/stalker/models/mixins.py index 93791335..d0826888 100644 --- a/src/stalker/models/mixins.py +++ b/src/stalker/models/mixins.py @@ -531,9 +531,6 @@ def _validate_status(self, key: str, status: "Status") -> "Status": status = self.status_list[status] # check if the given status is in the status_list - # logger.debug(f'self.status_list: {self.status_list}') - # logger.debug(f'given status: {status}') - if status not in self.status_list: raise ValueError( f"The given Status instance for {self.__class__.__name__}.status is " @@ -1069,17 +1066,25 @@ def references(cls) -> Mapped[Optional[List["File"]]]: Returns: relationship: The relationship object related to the references attribute. """ + primary_cls_name = f"{cls.__name__}" + secondary_cls_name = "Reference" + primary_cls_table_name = f"{cls.__tablename__}" + secondary_cls_table_name = "Files" + secondary_table_name = f"{cls.__name__}_References" + # get secondary table secondary_table = create_secondary_table( - cls.__name__, - "File", - cls.__tablename__, - "Files", - f"{cls.__name__}_References", + primary_cls_name=primary_cls_name, + secondary_cls_name=secondary_cls_name, + primary_cls_table_name=primary_cls_table_name, + secondary_cls_table_name=secondary_cls_table_name, + secondary_table_name=secondary_table_name, ) # return the relationship return relationship( secondary=secondary_table, + primaryjoin=f"{primary_cls_table_name}.c.id=={secondary_table_name}.c.{primary_cls_name.lower()}_id", + secondaryjoin=f"{secondary_table_name}.c.{secondary_cls_name.lower()}_id=={secondary_cls_table_name}.c.id", doc="""A list of :class:`.File` instances given as a reference for this entity. """, @@ -1101,11 +1106,11 @@ def _validate_references(self, key: str, reference: "File") -> "File": """ from stalker.models.file import File - # all the items should be instance of stalker.models.entity.Entity + # all items should be instance of stalker.models.entity.Entity if not isinstance(reference, File): raise TypeError( - f"All the items in the {self.__class__.__name__}.references should " - "be stalker.models.file.File instances, " + f"{self.__class__.__name__}.references should only contain " + "instances of stalker.models.file.File, " f"not {reference.__class__.__name__}: '{reference}'" ) return reference @@ -1499,7 +1504,7 @@ def _validate_schedule_constraint( self, key: str, schedule_constraint: Union[None, int, str], - ) -> int: + ) -> ScheduleConstraint: """Validate the given schedule_constraint value. Args: @@ -1930,8 +1935,8 @@ def _validate_children(self, key: str, child: Self) -> Self: """ if not isinstance(child, self.__class__): raise TypeError( - "{cls}.children should be a list of {cls} (or derivative) " - "instances, not {child_cls}: '{child}'".format( + "{cls}.children should only contain instances of {cls} " + "(or derivative), not {child_cls}: '{child}'".format( cls=self.__class__.__name__, child_cls=child.__class__.__name__, child=child, diff --git a/src/stalker/models/review.py b/src/stalker/models/review.py index be89f74a..03c7ec24 100644 --- a/src/stalker/models/review.py +++ b/src/stalker/models/review.py @@ -164,6 +164,7 @@ def _validate_task( """Validate the given task value. Args: + key (str): The name of the validated column. task (Union[None, Task]): The task value to be validated. Raises: @@ -205,6 +206,13 @@ def _validate_version( Args: key (str): The name of the validated column. version (Union[None, Version]): The version value to be validated. + + Raises: + TypeError: If version is not a Version instance. + ValueError: If the version.task and the self.task is not matching. + + Returns: + Union[None, Version]: The validated version value. """ if version is None: return version @@ -456,13 +464,13 @@ def versions(self) -> List["Version"]: """Return the Version instances related to this Daily. Returns: - List[Task]: A list of :class:`.Version` instances that this Daily is - related to (through the outputs of the versions). + List[Task]: A list of :class:`.Version` instances that this Daily + is related to (through the files attribute of the versions). """ from stalker.models.version import Version return ( - Version.query.join(Version.outputs) + Version.query.join(Version.files) .join(DailyFile) .join(Daily) .filter(Daily.id == self.id) @@ -474,15 +482,15 @@ def tasks(self) -> List["Task"]: """Return the Task's related this Daily instance. Returns: - List[Task]: A list of :class:`.Task` instances that this Daily is - related to (through the outputs of the versions) + List[Task]: A list of :class:`.Task` instances that this Daily is + related to (through the files attribute of the versions). """ from stalker.models.version import Version from stalker.models.task import Task return ( Task.query.join(Task.versions) - .join(Version.outputs) + .join(Version.files) .join(DailyFile) .join(Daily) .filter(Daily.id == self.id) diff --git a/src/stalker/models/scene.py b/src/stalker/models/scene.py index acf6e610..4100a0c1 100644 --- a/src/stalker/models/scene.py +++ b/src/stalker/models/scene.py @@ -7,8 +7,8 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship, validates from stalker.log import get_logger -from stalker.models.task import Task from stalker.models.mixins import CodeMixin +from stalker.models.task import Task if TYPE_CHECKING: # pragma: no cover from stalker.models.shot import Shot @@ -77,8 +77,8 @@ def _validate_shots(self, key: str, shot: "Shot") -> "Shot": if not isinstance(shot, Shot): raise TypeError( - f"{self.__class__.__name__}.shots needs to be all " - "stalker.models.shot.Shot instances, " + f"{self.__class__.__name__}.shots should only contain " + "instances of stalker.models.shot.Shot, " f"not {shot.__class__.__name__}: '{shot}'" ) return shot diff --git a/src/stalker/models/schedulers.py b/src/stalker/models/schedulers.py index 58d1e578..dae10731 100644 --- a/src/stalker/models/schedulers.py +++ b/src/stalker/models/schedulers.py @@ -745,8 +745,8 @@ def _validate_projects(self, projects: List[Project]) -> List[Project]: projects = [] msg = ( - "{cls}.projects should be a list of " - "stalker.models.project.Project instances, not " + "{cls}.projects should only contain instances of " + "stalker.models.project.Project, not " "{projects_class}: '{projects}'" ) diff --git a/src/stalker/models/sequence.py b/src/stalker/models/sequence.py index 506f83ab..8edfdd6d 100644 --- a/src/stalker/models/sequence.py +++ b/src/stalker/models/sequence.py @@ -76,8 +76,8 @@ def _validate_shots(self, key: str, shot: "Shot") -> "Shot": if not isinstance(shot, Shot): raise TypeError( - f"{self.__class__.__name__}.shots should be all " - "stalker.models.shot.Shot instances, " + f"{self.__class__.__name__}.shots should only contain " + "instances of stalker.models.shot.Shot, " f"not {shot.__class__.__name__}: '{shot}'" ) return shot diff --git a/src/stalker/models/shot.py b/src/stalker/models/shot.py index 56aa1fea..ff16d4af 100644 --- a/src/stalker/models/shot.py +++ b/src/stalker/models/shot.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """Shot related functions and classes are situated here.""" -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Union +from typing import Any, Dict, Optional, TYPE_CHECKING, Union -from sqlalchemy import Column, Float, ForeignKey, Integer, Table +from sqlalchemy import Float, ForeignKey from sqlalchemy.exc import OperationalError, UnboundExecutionError from sqlalchemy.orm import ( Mapped, @@ -14,7 +14,6 @@ validates, ) -from stalker.db.declarative import Base from stalker.db.session import DBSession from stalker.log import get_logger from stalker.models.format import ImageFormat diff --git a/src/stalker/models/structure.py b/src/stalker/models/structure.py index 56cda5d1..90212f2d 100644 --- a/src/stalker/models/structure.py +++ b/src/stalker/models/structure.py @@ -208,9 +208,9 @@ def _validate_templates( """ if not isinstance(template, FilenameTemplate): raise TypeError( - f"All the items in the {self.__class__.__name__}.templates should " - "be a stalker.models.template.FilenameTemplate instance, not " - f"{template.__class__.__name__}: '{template}'" + f"{self.__class__.__name__}.templates should only contain " + "instances of stalker.models.template.FilenameTemplate, " + f"not {template.__class__.__name__}: '{template}'" ) return template diff --git a/src/stalker/models/studio.py b/src/stalker/models/studio.py index 04c3abd8..c17b1ff4 100644 --- a/src/stalker/models/studio.py +++ b/src/stalker/models/studio.py @@ -860,7 +860,7 @@ def _validate_working_hours_value(self, value: List) -> List: Raises: TypeError: If the given value is not a list. TypeError: If the immediate items in the list is not a list. - RuntimeError: If the length of the items in the given list is not 2. + TypeError: If the length of the items in the given list is not 2. TypeError: If the items in the lists inside the list are not integers. ValueError: If the integer values in the secondary lists are smaller than 0 or larger than 1440 (which is 24 * 60). diff --git a/src/stalker/models/task.py b/src/stalker/models/task.py index 7177ea92..0cc55ae6 100644 --- a/src/stalker/models/task.py +++ b/src/stalker/models/task.py @@ -1305,8 +1305,8 @@ def _validate_time_logs(self, key: str, time_log: TimeLog) -> TimeLog: """ if not isinstance(time_log, TimeLog): raise TypeError( - "{}.time_logs should be all stalker.models.task.TimeLog instances, " - "not {}: '{}'".format( + "{}.time_logs should only contain instances of " + "stalker.models.task.TimeLog, not {}: '{}'".format( self.__class__.__name__, time_log.__class__.__name__, time_log ) ) @@ -1336,8 +1336,8 @@ def _validate_reviews(self, key: str, review: Review) -> Review: """ if not isinstance(review, Review): raise TypeError( - "{}.reviews should be all stalker.models.review.Review " - "instances, not {}: '{}'".format( + "{}.reviews should only contain instances of " + "stalker.models.review.Review, not {}: '{}'".format( self.__class__.__name__, review.__class__.__name__, review ) ) @@ -1366,8 +1366,8 @@ def _validate_task_depends_on(self, key: str, task_depends_on: "Task") -> "Task" """ if not isinstance(task_depends_on, TaskDependency): raise TypeError( - "All the items in the {}.task_depends_on should be a " - "TaskDependency instance, not {}: '{}'".format( + "{}.task_depends_on should only contain instances of " + "TaskDependency, not {}: '{}'".format( self.__class__.__name__, task_depends_on.__class__.__name__, task_depends_on, @@ -1776,9 +1776,11 @@ def _validate_resources(self, key: str, resource: User) -> User: """ if not isinstance(resource, User): raise TypeError( - "{}.resources should be a list of stalker.models.auth.User instances, " - "not {}: '{}'".format( - self.__class__.__name__, resource.__class__.__name__, resource + "{}.resources should only contain instances of " + "stalker.models.auth.User, not {}: '{}'".format( + self.__class__.__name__, + resource.__class__.__name__, + resource, ) ) return resource @@ -1800,8 +1802,8 @@ def _validate_alternative_resources(self, key: str, resource: User) -> User: """ if not isinstance(resource, User): raise TypeError( - "{}.alternative_resources should be a list of stalker.models.auth.User " - "instances, not {}: '{}'".format( + "{}.alternative_resources should only contain instances of " + "stalker.models.auth.User, not {}: '{}'".format( self.__class__.__name__, resource.__class__.__name__, resource, @@ -1826,8 +1828,8 @@ def _validate_computed_resources(self, key: str, resource: User) -> User: """ if not isinstance(resource, User): raise TypeError( - "{}.computed_resources should be a list of " - "stalker.models.auth.User instances, not {}: '{}'".format( + "{}.computed_resources should only contain instances of " + "stalker.models.auth.User, not {}: '{}'".format( self.__class__.__name__, resource.__class__.__name__, resource ) ) @@ -1938,9 +1940,11 @@ def _validate_watchers(self, key: str, watcher: User) -> User: """ if not isinstance(watcher, User): raise TypeError( - "{}.watchers should be a list of stalker.models.auth.User " - "instances, not {}: '{}'".format( - self.__class__.__name__, watcher.__class__.__name__, watcher + "{}.watchers should only contain instances of " + "stalker.models.auth.User, not {}: '{}'".format( + self.__class__.__name__, + watcher.__class__.__name__, + watcher, ) ) return watcher @@ -1965,9 +1969,11 @@ def _validate_versions(self, key: str, version: "Version"): if not isinstance(version, Version): raise TypeError( - "{}.versions should only have stalker.models.version.Version " - "instances, and not {}: '{}'".format( - self.__class__.__name__, version.__class__.__name__, version + "{}.versions should only contain instances of " + "stalker.models.version.Version, and not {}: '{}'".format( + self.__class__.__name__, + version.__class__.__name__, + version, ) ) @@ -2482,9 +2488,11 @@ def _validate_responsible(self, key, responsible: User) -> User: """ if not isinstance(responsible, User): raise TypeError( - "{}.responsible should be a list of stalker.models.auth.User " - "instances, not {}: '{}'".format( - self.__class__.__name__, responsible.__class__.__name__, responsible + "{}.responsible should only contain instances of " + "stalker.models.auth.User, not {}: '{}'".format( + self.__class__.__name__, + responsible.__class__.__name__, + responsible, ) ) return responsible @@ -3327,7 +3335,7 @@ def _validate_depends_on(self, key: str, dependency: Task) -> Task: # trust to the session for checking the depends_on attribute if dependency is not None and not isinstance(dependency, Task): raise TypeError( - "{}.depends_on can should be and instance of stalker.models.task.Task, " + "{}.depends_on should be and instance of stalker.models.task.Task, " "not {}: '{}'".format( self.__class__.__name__, dependency.__class__.__name__, dependency ) diff --git a/src/stalker/models/ticket.py b/src/stalker/models/ticket.py index 8350dfd1..2f3a89ad 100644 --- a/src/stalker/models/ticket.py +++ b/src/stalker/models/ticket.py @@ -305,8 +305,8 @@ def _validate_related_tickets(self, key: str, related_ticket: "Ticket") -> "Tick """ if not isinstance(related_ticket, Ticket): raise TypeError( - "{}.related_ticket attribute should be a list of other " - "stalker.models.ticket.Ticket instances, not {}: '{}'".format( + "{}.related_ticket should only contain instances of " + "stalker.models.ticket.Ticket, not {}: '{}'".format( self.__class__.__name__, related_ticket.__class__.__name__, related_ticket, diff --git a/src/stalker/models/version.py b/src/stalker/models/version.py index 1a5e4b08..40db50c5 100644 --- a/src/stalker/models/version.py +++ b/src/stalker/models/version.py @@ -2,36 +2,37 @@ """Version related functions and classes are situated here.""" import os -import re -from typing import Any, Dict, Generator, List, Optional, Union +from pathlib import Path +from typing import Any, Dict, List, Optional import jinja2 -from sqlalchemy import Column, ForeignKey, Integer, String, Table +from sqlalchemy import Column, ForeignKey, Integer, Table from sqlalchemy.exc import OperationalError, UnboundExecutionError from sqlalchemy.orm import Mapped, mapped_column, relationship, synonym, validates from stalker.db.declarative import Base from stalker.db.session import DBSession from stalker.log import get_logger -from stalker.models.enum import TraversalDirection +from stalker.models.entity import Entity from stalker.models.file import File from stalker.models.mixins import DAGMixin from stalker.models.review import Review from stalker.models.task import Task -from stalker.utils import walk_hierarchy logger = get_logger(__name__) -class Version(File, DAGMixin): +class Version(Entity, DAGMixin): """Holds information about the versions created for a class:`.Task`. A :class:`.Version` instance holds information about the versions created - related to a class:`.Task`. So if one creates a new version for a file or a - sequences of files for a :class:`.Task` then the information can be hold in - the :class:`.Version` instance. + related for a class:`.Task`. This is not directly related to the stored + files, but instead holds the information about the incremental change + itself (i.e who has created it, when it is created, the revision and + version numbers etc.). All the related files are stored in the + :attr:`.Version.files` attribute. .. versionadded: 0.2.13 @@ -67,6 +68,20 @@ class Version(File, DAGMixin): organized alongside revisions or big changes, without relying on the now removed `variant_name` attribute. + .. versionadded: 1.1.0 + + Version class is not deriving from File class anymore. So they are not + directly related to any file. And the File relation is stored in the new + :attr:`.Version.files` attribute. + + .. versionadded: 1.1.0 + + Added the `files` attribute, which replaces the `outputs` attribute and + the `inputs` attribute is moved to the :class:`.File` class as the + `references` attribute, which makes much more sense as individual files + may reference different `Files` so storing the `references` in `Version` + doesn't make much sense. + Args: revision_number (Optional[int]): A positive non-zero integer number holding the major version counter. This can be set with an @@ -80,15 +95,10 @@ class Version(File, DAGMixin): :attr:`.revision_number` under the same :class:`.Task` will be considered in the same version stream and version number attribute will be set accordingly. The default is 1. - inputs (List[File]): A list o :class:`.File` instances, holding the - inputs of the current version. It could be a texture for a Maya - file or an image sequence for Nuke, or anything those you can think - as the input for the current Version. - outputs (List[File]): A list of :class:`.File` instances, holding the - outputs of the current version. It could be the rendered image - sequences out of Maya or Nuke, or it can be a Targa file which is - the output of a Photoshop file, or anything that you can think as - the output which is created out of this Version. + files (List[File]): A list of :class:`.File` instances that are created + for this :class:`.Version` instance. This can be different + representations (i.e. base, Alembic, USD, ASS, RS etc.) of the same + data. task (Task): A :class:`.Task` instance showing the owner of this Version. parent (Version): A :class:`.Version` instance which is the parent of @@ -105,7 +115,7 @@ class Version(File, DAGMixin): __dag_cascade__ = "save-update, merge" version_id: Mapped[int] = mapped_column( - "id", ForeignKey("Files.id"), primary_key=True + "id", ForeignKey("Entities.id"), primary_key=True ) __id_column__ = "version_id" @@ -132,21 +142,11 @@ class Version(File, DAGMixin): """, ) - inputs: Mapped[Optional[List[File]]] = relationship( - secondary="Version_Inputs", - primaryjoin="Versions.c.id==Version_Inputs.c.version_id", - secondaryjoin="Version_Inputs.c.file_id==Files.c.id", - doc="""The inputs of the current version. - - It is a list of :class:`.File` instances. - """, - ) - - outputs: Mapped[Optional[List[File]]] = relationship( - secondary="Version_Outputs", - primaryjoin="Versions.c.id==Version_Outputs.c.version_id", - secondaryjoin="Version_Outputs.c.file_id==Files.c.id", - doc="""The outputs of the current version. + files: Mapped[Optional[List[File]]] = relationship( + secondary="Version_Files", + primaryjoin="Versions.c.id==Version_Files.c.version_id", + secondaryjoin="Version_Files.c.file_id==Files.c.id", + doc="""The files related to the current version. It is a list of :class:`.File` instances. """, @@ -157,16 +157,13 @@ class Version(File, DAGMixin): ) is_published: Mapped[Optional[bool]] = mapped_column(default=False) - created_with: Mapped[Optional[str]] = mapped_column(String(256)) def __init__( self, task: Optional[Task] = None, - inputs: Optional[List["File"]] = None, - outputs: Optional[List["File"]] = None, + files: Optional[List[File]] = None, parent: Optional["Version"] = None, full_path: Optional[str] = None, - created_with: Optional[str] = None, revision_number: Optional[int] = None, **kwargs: Dict[str, Any], ) -> None: @@ -181,19 +178,13 @@ def __init__( revision_number = 1 self.revision_number = revision_number self.version_number = None - if inputs is None: - inputs = [] - - if outputs is None: - outputs = [] - self.inputs = inputs - self.outputs = outputs + if files is None: + files = [] + self.files = files self.is_published = False - self.created_with = created_with - def __repr__(self) -> str: """Return the str representation of the Version. @@ -245,7 +236,11 @@ def _revision_number_getter(self) -> int: return self._revision_number def _revision_number_setter(self, revision_number: int): - """Set the revision attribute value.""" + """Set the revision attribute value. + + Args: + revision_number (int): The new revision number value. + """ revision_number = self._validate_revision_number(revision_number) is_updating_revision_number = False @@ -392,61 +387,37 @@ def _validate_task(self, key, task) -> Task: if not isinstance(task, Task): raise TypeError( - "{}.task should be a stalker.models.task.Task instance, " - "not {}: '{}'".format( + "{}.task should be a Task, Asset, Shot, Scene, Sequence or " + "Variant instance, not {}: '{}'".format( self.__class__.__name__, task.__class__.__name__, task ) ) return task - @validates("inputs") - def _validate_inputs(self, key, input_): - """Validate the given input value. - - Args: - key (str): The name of the validated column. - input_ (File): The input value to be validated. - - Raises: - TypeError: If the input is not a :class:`.File` instance. - - Returns: - File: The validated input value. - """ - if not isinstance(input_, File): - raise TypeError( - "All elements in {}.inputs should be all " - "stalker.models.file.File instances, not {}: '{}'".format( - self.__class__.__name__, input_.__class__.__name__, input_ - ) - ) - - return input_ - - @validates("outputs") - def _validate_outputs(self, key, output) -> File: - """Validate the given output value. + @validates("files") + def _validate_files(self, key: str, file: File) -> File: + """Validate the given file value. Args: key (str): The name of the validated column. - output (File): The output value to be validated. + file (File): The file value to be validated. Raises: - TypeError: If the output is not a :class:`.File` instance. + TypeError: If the file is not a :class:`.File` instance. Returns: - File: The validated output value. + File: The validated file value. """ - if not isinstance(output, File): + if not isinstance(file, File): raise TypeError( - "All elements in {}.outputs should be all " - "stalker.models.file.File instances, not {}: '{}'".format( - self.__class__.__name__, output.__class__.__name__, output + "{}.files should only contain instances of " + "stalker.models.file.File, not {}: '{}'".format( + self.__class__.__name__, file.__class__.__name__, file ) ) - return output + return file def _template_variables(self) -> dict: """Return the variables used in rendering the filename template. @@ -456,18 +427,31 @@ def _template_variables(self) -> dict: """ version_template_vars = { "version": self, - "extension": self.extension, + # "extension": self.extension, } version_template_vars.update(self.task._template_variables()) return version_template_vars - def update_paths(self) -> None: - """Update the path variables. + def generate_path(self, extension: Optional[str] = None) -> Path: + """Generate a Path with the template variables from the parent project. + + Args: + extension (Optional[str]): An optional string containing the + extension for the resulting Path. Raises: + TypeError: If the extension is not None and not a str. RuntimeError: If no Version related FilenameTemplate is found in the related `Project.structure`. + + Returns: + Path: A `pathlib.Path` object. """ + if extension is not None and not isinstance(extension, str): + raise TypeError( + "extension should be a str, " + f"not {extension.__class__.__name__}: '{extension}'" + ) kwargs = self._template_variables() # get a suitable FilenameTemplate @@ -486,19 +470,24 @@ def update_paths(self) -> None: "(target_entity_type == '{entity_type}') defined in the Structure of " "the related Project instance, please create a new " "stalker.models.template.FilenameTemplate instance with its " - "'target_entity_type' attribute is set to '{entity_type}' and assign " + "'target_entity_type' attribute is set to '{entity_type}' and add " "it to the `templates` attribute of the structure of the " "project".format(entity_type=self.task.entity_type) ) - temp_filename = jinja2.Template(vers_template.filename).render( - **kwargs, trim_blocks=True, lstrip_blocks=True - ) - temp_path = jinja2.Template(vers_template.path).render( - **kwargs, trim_blocks=True, lstrip_blocks=True + path = Path( + jinja2.Template(vers_template.path).render( + **kwargs, trim_blocks=True, lstrip_blocks=True + ) + ) / Path( + jinja2.Template(vers_template.filename).render( + **kwargs, trim_blocks=True, lstrip_blocks=True + ) ) - self.filename = temp_filename - self.path = temp_path + if extension is not None: + path = path.with_suffix(extension) + + return path @property def absolute_full_path(self) -> str: @@ -509,7 +498,11 @@ def absolute_full_path(self) -> str: Returns: str: The absolute full path of this Version instance. """ - return os.path.normpath(os.path.expandvars(self.full_path)).replace("\\", "/") + return Path( + os.path.normpath(os.path.expandvars(str(self.generate_path()))).replace( + "\\", "/" + ) + ) @property def absolute_path(self) -> str: @@ -518,7 +511,41 @@ def absolute_path(self) -> str: Returns: str: The absolute path. """ - return os.path.normpath(os.path.expandvars(self.path)).replace("\\", "/") + return Path( + os.path.normpath( + os.path.expandvars(str(self.generate_path().parent)) + ).replace("\\", "/") + ) + + @property + def full_path(self) -> Path: + """Return the full path of this version. + + This full path includes the repository path of the related project as + it is. + + Returns: + Path: The full path of this Version instance. + """ + return self.generate_path() + + @property + def path(self) -> Path: + """Return the path. + + Returns: + Path: The path. + """ + return self.full_path.parent + + @property + def filename(self) -> str: + """Return the filename bit of the path. + + Returns: + str: The filename. + """ + return self.full_path.name def is_latest_published_version(self) -> bool: """Return True if this is the latest published Version False otherwise. @@ -546,33 +573,6 @@ def latest_published_version(self) -> "Version": .first() ) - @validates("created_with") - def _validate_created_with( - self, key: str, created_with: Union[None, str] - ) -> Union[None, str]: - """Validate the given created_with value. - - Args: - key (str): The name of the validated column. - created_with (str): The name of the DCC used to create this Version - instance. - - Raises: - TypeError: If the given created_with attribute is not None and not a string. - - Returns: - Union[None, str]: The validated created with value. - """ - if created_with is not None and not isinstance(created_with, str): - raise TypeError( - "{}.created_with should be an instance of str, not {}: '{}'".format( - self.__class__.__name__, - created_with.__class__.__name__, - created_with, - ) - ) - return created_with - def __eq__(self, other): """Check the equality. @@ -612,22 +612,14 @@ def naming_parents(self) -> List[Task]: all_parents = self.task.parents all_parents.append(self.task) naming_parents = [] - if all_parents: - significant_parent = all_parents[0] + if not all_parents: + return naming_parents - for parent in reversed(all_parents): - if parent.entity_type in ["Asset", "Shot", "Sequence"]: - significant_parent = parent - break + for parent in reversed(all_parents): + naming_parents.insert(0, parent) + if parent.entity_type in ["Asset", "Shot", "Sequence"]: + break - # now start from that parent towards the task - past_significant_parent = False - naming_parents = [] - for parent in all_parents: - if parent is significant_parent: - past_significant_parent = True - if past_significant_parent: - naming_parents.append(parent) return naming_parents @property @@ -641,34 +633,21 @@ def nice_name(self) -> str: "_".join(map(lambda x: x.nice_name, self.naming_parents)) ) - def walk_inputs( - self, - method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst, - ) -> Generator[None, "Version", None]: - """Walk the inputs of this version instance. - - Args: - method (Union[int, str, TraversalDirection]): The walk method defined by - the :class:`.TraversalDirection` enum. - - Yields: - Version: Yield the Version instances. - """ - for v in walk_hierarchy(self, "inputs", method=method): - yield v - - def request_review(self): - """Call the self.task.request_review(). + def request_review(self) -> List[Review]: + """Request a review. This is a shortcut to the Task.request_review() method of the related task. + + Returns: + List[Review]: The created Review instances. """ return self.task.request_review(version=self) -# VERSION INPUTS -Version_Inputs = Table( - "Version_Inputs", +# VERSION FILES +Version_Files = Table( + "Version_Files", Base.metadata, Column("version_id", Integer, ForeignKey("Versions.id"), primary_key=True), Column( @@ -678,11 +657,3 @@ def request_review(self): primary_key=True, ), ) - -# VERSION_OUTPUTS -Version_Outputs = Table( - "Version_Outputs", - Base.metadata, - Column("version_id", Integer, ForeignKey("Versions.id"), primary_key=True), - Column("file_id", Integer, ForeignKey("Files.id"), primary_key=True), -) diff --git a/tests/db/test_db.py b/tests/db/test_db.py index 4d15ca0e..7549390f 100644 --- a/tests/db/test_db.py +++ b/tests/db/test_db.py @@ -1927,8 +1927,8 @@ def test_persistence_of_daily(setup_postgresql_db): test_file3 = File(original_filename="test_render3.jpg") test_file4 = File(original_filename="test_render4.jpg") - test_version1.outputs = [test_file1, test_file2, test_file3] - test_version4.outputs = [test_file4] + test_version1.files = [test_file1, test_file2, test_file3] + test_version4.files = [test_file4] DBSession.add_all( [ @@ -1995,7 +1995,7 @@ def test_persistence_of_daily(setup_postgresql_db): assert isinstance(test_file4_db, File) assert DailyFile.query.all() == [] - assert File.query.count() == 8 # including versions + assert File.query.count() == 4 # Versions are not files anymore def test_persistence_of_department(setup_postgresql_db): @@ -2476,13 +2476,36 @@ def test_persistence_of_file(setup_postgresql_db): DBSession.commit() # create a file Type sound_file_type = Type(name="Sound", code="sound", target_entity_type="File") + image_seq_type = Type( + name="JPEG Sequence", code="JPEGSeq", target_entity_type="File" + ) + video_type = Type(name="Video", code="Video", target_entity_type="File") + + # create some reference Files + ref1 = File( + name="My Image Sequence #1", + full_path="M:/PROJECTS/my_image_sequence.#.jpg", + type=image_seq_type, + created_by=user1, + created_with="Maya", + ) + ref2 = File( + name="My Movie #1", + full_path="M:/PROJECTS/my_movie.mp4", + type=video_type, + created_by=user1, + created_with="Blender", + ) + DBSession.save([ref1, ref2]) - # create a File + # create the main File kwargs = { "name": "My Sound", "full_path": "M:/PROJECTS/my_movie_sound.wav", + "references": [ref1, ref2], "type": sound_file_type, "created_by": user1, + "created_with": "Houdini", } file1 = File(**kwargs) @@ -2503,13 +2526,17 @@ def test_persistence_of_file(setup_postgresql_db): # store attributes created_by = file1.created_by + created_with = file1.created_with date_created = file1.date_created date_updated = file1.date_updated description = file1.description + full_path = file1.full_path name = file1.name nice_name = file1.nice_name notes = file1.notes - full_path = file1.full_path + references = file1.references + assert isinstance(references, list) + assert len(references) > 0 tags = file1.tags type_ = file1.type updated_by = file1.updated_by @@ -2523,13 +2550,15 @@ def test_persistence_of_file(setup_postgresql_db): assert isinstance(file1_db, File) assert file1_db.created_by == created_by + assert file1_db.created_with == created_with assert file1_db.date_created == date_created assert file1_db.date_updated == date_updated assert file1_db.description == description + assert file1_db.full_path == full_path assert file1_db.name == name assert file1_db.nice_name == nice_name assert file1_db.notes == notes - assert file1_db.full_path == full_path + assert file1_db.references == references assert file1_db.tags == tags assert file1_db.type == type_ assert file1_db.updated_by == updated_by @@ -3489,8 +3518,9 @@ def test_persistence_of_structure(setup_postgresql_db): name="Character Asset Template", description="This is the template for character assets", path="Assets/{{asset_type.name}}/{{pipeline_step.code}}", - filename="{{asset.name}}_{{asset_type.name}}_\ - v{{version.version_number}}", + filename="{{asset.name}}_{{asset_type.name}}" + "_r{{version.revision_number}}" + "_v{{version.version_number}}", target_entity_type="Asset", type=v_type, ) @@ -3751,6 +3781,18 @@ def test_persistence_of_task(setup_postgresql_db): end=datetime.datetime.now(pytz.utc) + datetime.timedelta(3), ) # Versions + repr_type = Type(name="Representation", code="Repr", target_entity_type="File") + DBSession.save(repr_type) + + file1 = File(name="Version1 Base Repr", type=repr_type) + file2 = File(name="Version2 Base Repr", type=repr_type) + file3 = File(name="Version3 Base Repr", type=repr_type) + file4 = File(name="Version4 Base Repr", type=repr_type) + DBSession.save([file1, file2, file3, file4]) + file3.references = [file2] + file2.references = [file1, file4] + DBSession.commit() + version1 = Version(task=task1) DBSession.add(version1) DBSession.commit() @@ -3767,9 +3809,6 @@ def test_persistence_of_task(setup_postgresql_db): DBSession.add(version4) DBSession.commit() - version3.inputs = [version2] - version2.inputs = [version1] - version2.inputs = [version4] DBSession.add(version1) DBSession.commit() @@ -3872,25 +3911,6 @@ def test_persistence_of_task(setup_postgresql_db): assert isinstance(task1_db.schedule_model, ScheduleModel) assert task1_db.schedule_timing == schedule_timing assert task1_db.schedule_unit == schedule_unit - assert version2.inputs == [version4] - assert version3.inputs == [version2] - assert version4.inputs == [] - - # delete tests - - # deleting a Task should also delete: - # - # Child Tasks - # TimeLogs - # Versions - # And orphan-references - # - # task1_db.references = [] - # with DBSession.no_autoflush: - # for v in task1_db.versions: - # v.inputs = [] - # for tv in Version.query.filter(Version.inputs.contains(v)): - # tv.inputs.remove(v) DBSession.delete(task1_db) DBSession.commit() @@ -3925,9 +3945,6 @@ def test_persistence_of_task(setup_postgresql_db): assert resources == [user1] assert another_task_db.resources == resources - assert version3.inputs == [] - assert version4.inputs == [] - def test_persistence_of_review(setup_postgresql_db): """Persistence of Review.""" @@ -4413,6 +4430,27 @@ def test_persistence_of_vacation(setup_postgresql_db): def test_persistence_of_version(setup_postgresql_db): """Persistence of Version instances.""" + # create a FilenameTemplate for Tasks + test_filename_template = FilenameTemplate( + name="Task Filename Template", + target_entity_type="Task", + path="{{project.code}}/{%- for parent_task in parent_tasks -%}" + "{{parent_task.nice_name}}/{%- endfor -%}", + filename="{{version.nice_name}}" + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}', + ) + DBSession.add(test_filename_template) + DBSession.commit() + + # create a Structure + test_structure = Structure( + name="Project Structure", + templates=[test_filename_template], + ) + DBSession.add(test_structure) + DBSession.commit() + # create a project test_project = Project( name="Test Project", @@ -4424,8 +4462,10 @@ def test_persistence_of_version(setup_postgresql_db): linux_path="/mnt/M/", macos_path="/Users/Volumes/M/", ), + structure=test_structure, ) DBSession.add(test_project) + DBSession.commit() # create a task test_task = Task( @@ -4465,9 +4505,7 @@ def test_persistence_of_version(setup_postgresql_db): revision_number=12, full_path="M:/Shows/Proj1/Seq1/Shots/SH001/Lighting" "/Proj1_Seq1_Sh001_MAIN_Lighting_v002.ma", - inputs=[test_version], ) - assert test_version_2.inputs == [test_version] DBSession.add(test_version_2) DBSession.commit() assert test_version_2.revision_number == 12 @@ -4479,11 +4517,10 @@ def test_persistence_of_version(setup_postgresql_db): name = test_version.name nice_name = test_version.nice_name notes = test_version.notes - outputs = test_version.outputs + files = test_version.files is_published = test_version.is_published - full_path = test_version.full_path + full_path = test_version.generate_path() tags = test_version.tags - # tickets = test_version.tickets type_ = test_version.type updated_by = test_version.updated_by revision_number = test_version.revision_number @@ -4504,9 +4541,9 @@ def test_persistence_of_version(setup_postgresql_db): assert test_version_db.name == name assert test_version_db.nice_name == nice_name assert test_version_db.notes == notes - assert test_version_db.outputs == outputs + assert test_version_db.files == files assert test_version_db.is_published == is_published - assert test_version_db.full_path == full_path + assert test_version_db.generate_path() == full_path assert test_version_db.tags == tags assert test_version_db.type == type_ assert test_version_db.updated_by == updated_by @@ -4519,9 +4556,6 @@ def test_persistence_of_version(setup_postgresql_db): DBSession.delete(test_version_db) DBSession.commit() - assert test_version_2.inputs == [] - - # create a new version append it to version_2.inputs and then delete # version_2 test_version_3 = Version( name="version for task modeling", @@ -4529,8 +4563,6 @@ def test_persistence_of_version(setup_postgresql_db): full_path="M:/Shows/Proj1/Seq1/Shots/SH001/Lighting" "/Proj1_Seq1_Sh001_MAIN_Lighting_v003.ma", ) - test_version_2.inputs.append(test_version_3) - assert test_version_2.inputs == [test_version_3] DBSession.add(test_version_3) DBSession.commit() diff --git a/tests/mixins/test_dag_mixin.py b/tests/mixins/test_dag_mixin.py index 8f7785d0..a0a7db85 100644 --- a/tests/mixins/test_dag_mixin.py +++ b/tests/mixins/test_dag_mixin.py @@ -175,8 +175,8 @@ def test_children_attribute_accepts_correct_class_instances_only(dag_mixin_test_ d.children = ["not", 1, "", "of", "correct", "instances"] assert str(cm.value) == ( - "DAGMixinFooMixedInClass.children should be a list of " - "DAGMixinFooMixedInClass (or derivative) instances, not str: 'not'" + "DAGMixinFooMixedInClass.children should only contain instances of " + "DAGMixinFooMixedInClass (or derivative), not str: 'not'" ) diff --git a/tests/mixins/test_reference_mixin.py b/tests/mixins/test_reference_mixin.py index 4f7eeacb..47545fb1 100644 --- a/tests/mixins/test_reference_mixin.py +++ b/tests/mixins/test_reference_mixin.py @@ -113,8 +113,8 @@ def test_references_attribute_accepting_only_lists_of_file_instances( data["test_foo_obj"].references = test_value assert str(cm.value) == ( - "All the items in the RefMixFooClass.references should be " - "stalker.models.file.File instances, not int: '1'" + "RefMixFooClass.references should only contain instances of " + "stalker.models.file.File, not int: '1'" ) @@ -125,8 +125,8 @@ def test_references_attribute_elements_accepts_files_only(setup_reference_mixin_ data["test_foo_obj"].references = [data["test_entity1"], data["test_entity2"]] assert str(cm.value) == ( - "All the items in the RefMixFooClass.references should be " - "stalker.models.file.File instances, not Entity: ''" + "RefMixFooClass.references should only contain instances of " + "stalker.models.file.File, not Entity: ''" ) diff --git a/tests/models/test_budget.py b/tests/models/test_budget.py index 9638275b..d8462235 100644 --- a/tests/models/test_budget.py +++ b/tests/models/test_budget.py @@ -116,7 +116,7 @@ def test_entries_attribute_is_set_to_a_list_of_other_instances_than_a_budget_ent data["test_budget"].entries = ["some", "string", 1, 2] assert str(cm.value) == ( - "Budget.entries should be a list of BudgetEntry instances, not str: 'some'" + "Budget.entries should only contain instances of BudgetEntry, not str: 'some'" ) diff --git a/tests/models/test_client.py b/tests/models/test_client.py index a2861eef..ce273dc1 100644 --- a/tests/models/test_client.py +++ b/tests/models/test_client.py @@ -449,8 +449,8 @@ def test_goods_attribute_is_set_to_a_list_of_non_good_instances(setup_client_tes client1.goods = ["not", 1, "list", "of", "goods"] assert str(cm.value) == ( - "Client.goods attribute should be all " - "stalker.models.budget.Good instances, not str: 'not'" + "Client.goods should only contain instances of " + "stalker.models.budget.Good, not str: 'not'" ) diff --git a/tests/models/test_daily.py b/tests/models/test_daily.py index 1a4d712a..1a12a60d 100644 --- a/tests/models/test_daily.py +++ b/tests/models/test_daily.py @@ -93,12 +93,12 @@ def setup_daily_tests(): data["test_file3"] = File(original_filename="test_render3.jpg") data["test_file4"] = File(original_filename="test_render4.jpg") - data["test_version1"].outputs = [ + data["test_version1"].files = [ data["test_file1"], data["test_file2"], data["test_file3"], ] - data["test_version4"].outputs = [data["test_file4"]] + data["test_version4"].files = [data["test_file4"]] return data @@ -301,12 +301,12 @@ def setup_daily_db_tests(setup_postgresql_db): ] ) - data["test_version1"].outputs = [ + data["test_version1"].files = [ data["test_file1"], data["test_file2"], data["test_file3"], ] - data["test_version4"].outputs = [data["test_file4"]] + data["test_version4"].files = [data["test_file4"]] DBSession.commit() yield data diff --git a/tests/models/test_file.py b/tests/models/test_file.py index b99a1136..cd632f8d 100644 --- a/tests/models/test_file.py +++ b/tests/models/test_file.py @@ -5,6 +5,7 @@ import pytest from stalker import File, Type +from stalker.db.session import DBSession @pytest.fixture(scope="function") @@ -24,11 +25,34 @@ def setup_file_tests(): target_entity_type="File", ) + image_sequence_type = Type( + name="Image Sequence", + code="ImSeq", + target_entity_type="File", + ) + + # a File for the input file + data["test_input_file1"] = File( + name="Input File 1", + full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" + "Outputs/SH001_beauty_v001.###.exr", + type=image_sequence_type, + ) + + data["test_input_file2"] = File( + name="Input File 2", + full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" + "Outputs/SH001_occ_v001.###.exr", + type=image_sequence_type, + ) + data["kwargs"] = { "name": "An Image File", "full_path": "C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.exr", + "references": [data["test_input_file1"], data["test_input_file2"]], "original_filename": "this_is_an_image.jpg", "type": data["test_file_type1"], + "created_with": "Houdini", } data["test_file"] = File(**data["kwargs"]) @@ -76,9 +100,7 @@ def test_full_path_argument_is_not_a_string(setup_file_tests): data["kwargs"]["full_path"] = test_value with pytest.raises(TypeError) as cm: File(**data["kwargs"]) - assert str(cm.value) == ( - "File.full_path should be an instance of string, not int: '1'" - ) + assert str(cm.value) == ("File.full_path should be a str, not int: '1'") def test_full_path_attribute_is_not_a_string(setup_file_tests): @@ -88,9 +110,7 @@ def test_full_path_attribute_is_not_a_string(setup_file_tests): with pytest.raises(TypeError) as cm: data["test_file"].full_path = test_value - assert str(cm.value) == ( - "File.full_path should be an instance of string, not int: '1'" - ) + assert str(cm.value) == ("File.full_path should be a str, not int: '1'") def test_full_path_windows_to_other_conversion(setup_file_tests): @@ -144,9 +164,7 @@ def test_original_filename_argument_accepts_string_only(setup_file_tests): with pytest.raises(TypeError) as cm: File(**data["kwargs"]) - assert str(cm.value) == ( - "File.original_filename should be an instance of string, not int: '1'" - ) + assert str(cm.value) == ("File.original_filename should be a str, not int: '1'") def test_original_filename_attribute_accepts_string_only(setup_file_tests): @@ -156,9 +174,7 @@ def test_original_filename_attribute_accepts_string_only(setup_file_tests): with pytest.raises(TypeError) as cm: data["test_file"].original_filename = test_value - assert str(cm.value) == ( - "File.original_filename should be an instance of string, not float: '1.1'" - ) + assert str(cm.value) == ("File.original_filename should be a str, not float: '1.1'") def test_original_filename_argument_is_working_as_expected(setup_file_tests): @@ -232,7 +248,7 @@ def test_path_attribute_is_set_to_a_value_other_then_string(setup_file_tests): data = setup_file_tests with pytest.raises(TypeError) as cm: data["test_file"].path = 1 - assert str(cm.value) == "File.path should be an instance of str, not int: '1'" + assert str(cm.value) == "File.path should be a str, not int: '1'" def test_path_attribute_value_comes_from_full_path(setup_file_tests): @@ -265,7 +281,7 @@ def test_filename_attribute_is_set_to_a_value_other_then_string(setup_file_tests data = setup_file_tests with pytest.raises(TypeError) as cm: data["test_file"].filename = 3 - assert str(cm.value) == "File.filename should be an instance of str, not int: '3'" + assert str(cm.value) == "File.filename should be a str, not int: '3'" def test_filename_attribute_is_set_to_empty_string(setup_file_tests): @@ -318,9 +334,7 @@ def test_extension_attribute_is_set_to_a_value_other_then_string(setup_file_test data = setup_file_tests with pytest.raises(TypeError) as cm: data["test_file"].extension = 123 - assert str(cm.value) == ( - "File.extension should be an instance of str, not int: '123'" - ) + assert str(cm.value) == ("File.extension should be a str, not int: '123'") def test_extension_attribute_value_comes_from_full_path(setup_file_tests): @@ -383,3 +397,198 @@ def test__hash__is_working_as_expected(setup_file_tests): result = hash(data["test_file"]) assert isinstance(result, int) assert result == data["test_file"].__hash__() + + +def test_references_arg_is_skipped(setup_file_tests): + """references attr is an empty list if the references argument is skipped.""" + data = setup_file_tests + data["kwargs"].pop("references") + new_version = File(**data["kwargs"]) + assert new_version.references == [] + + +def test_references_arg_is_none(setup_file_tests): + """references attr is an empty list if the references argument is None.""" + data = setup_file_tests + data["kwargs"]["references"] = None + new_file = File(**data["kwargs"]) + assert new_file.references == [] + + +def test_references_attr_is_none(setup_file_tests): + """TypeError raised if the references attr is set to None.""" + data = setup_file_tests + with pytest.raises(TypeError) as cm: + data["test_file"].references = None + assert str(cm.value) == "Incompatible collection type: None is not list-like" + + +def test_references_arg_is_not_a_list_of_file_instances(setup_file_tests): + """TypeError raised if the references arg is not a File instance.""" + data = setup_file_tests + test_value = [132, "231123"] + data["kwargs"]["references"] = test_value + with pytest.raises(TypeError) as cm: + File(**data["kwargs"]) + + assert str(cm.value) == ( + "File.references should only contain instances of " + "stalker.models.file.File, not int: '132'" + ) + + +def test_references_attr_is_not_a_list_of_file_instances(setup_file_tests): + """TypeError raised if the references attr is set to something other than a File.""" + data = setup_file_tests + test_value = [132, "231123"] + with pytest.raises(TypeError) as cm: + data["test_file"].references = test_value + + assert str(cm.value) == ( + "File.references should only contain instances of " + "stalker.models.file.File, not int: '132'" + ) + + +def test_references_attr_is_working_as_expected(setup_file_tests): + """references attr is working as expected.""" + data = setup_file_tests + data["kwargs"].pop("references") + new_file = File(**data["kwargs"]) + assert data["test_input_file1"] not in new_file.references + assert data["test_input_file2"] not in new_file.references + + new_file.references = [data["test_input_file1"], data["test_input_file2"]] + assert data["test_input_file1"] in new_file.references + assert data["test_input_file2"] in new_file.references + + +def test_created_with_argument_can_be_skipped(setup_file_tests): + """created_with argument can be skipped.""" + data = setup_file_tests + data["kwargs"].pop("created_with") + File(**data["kwargs"]) + + +def test_created_with_argument_can_be_none(setup_file_tests): + """created_with argument can be None.""" + data = setup_file_tests + data["kwargs"]["created_with"] = None + File(**data["kwargs"]) + + +def test_created_with_attribute_can_be_set_to_none(setup_file_tests): + """created with attribute can be set to None.""" + data = setup_file_tests + data["test_file"].created_with = None + + +def test_created_with_argument_accepts_only_string_or_none(setup_file_tests): + """TypeError raised if the created_with arg is not a string or None.""" + data = setup_file_tests + data["kwargs"]["created_with"] = 234 + with pytest.raises(TypeError) as cm: + File(**data["kwargs"]) + assert str(cm.value) == ( + "File.created_with should be an instance of str, not int: '234'" + ) + + +def test_created_with_attribute_accepts_only_string_or_none(setup_file_tests): + """TypeError raised if the created_with attr is not a str or None.""" + data = setup_file_tests + with pytest.raises(TypeError) as cm: + data["test_file"].created_with = 234 + + assert str(cm.value) == ( + "File.created_with should be an instance of str, not int: '234'" + ) + + +def test_created_with_argument_is_working_as_expected(setup_file_tests): + """created_with argument value is passed to created_with attribute.""" + data = setup_file_tests + test_value = "Maya" + data["kwargs"]["created_with"] = test_value + test_file = File(**data["kwargs"]) + assert test_file.created_with == test_value + + +def test_created_with_attribute_is_working_as_expected(setup_file_tests): + """created_with attribute is working as expected.""" + data = setup_file_tests + test_value = "Maya" + assert data["test_file"].created_with != test_value + data["test_file"].created_with = test_value + assert data["test_file"].created_with == test_value + + +def test_walk_references_is_working_as_expected_in_dfs_mode(setup_file_tests): + """walk_references() method is working in DFS mode correctly.""" + # data = setup_file_tests + + repr_type = Type(name="Representation", code="Repr", target_entity_type="File") + # DBSession.add(repr_type) + # DBSession.commit() + + # File 1 + # v1 = Version(task=data["test_task1"]) + v1_base_repr = File( + name="Base Repr.", + # full_path=str(v1.generate_path().with_suffix(".ma")), + type=repr_type, + ) + # v1.files.append(v1_base_repr) + + # Version 2 + # v2 = Version(task=data["test_task1"]) + v2_base_repr = File( + name="Base Repr.", + # full_path=str(v2.generate_path().with_suffix(".ma")), + type=repr_type, + ) + # v2.files.append(v2_base_repr) + + # Version 3 + # v3 = Version(task=data["test_task1"]) + v3_base_repr = File( + name="Base Repr.", + # full_path=str(v3.generate_path().with_suffix(".ma")), + type=repr_type, + ) + # v3.files.append(v3_base_repr) + + # v4 = Version(task=data["test_task1"]) + v4_base_repr = File( + name="Base Repr.", + # full_path=str(v4.generate_path().with_suffix(".ma")), + type=repr_type, + ) + # v4.files.append(v4_base_repr) + + # v5 = Version(task=data["test_task1"]) + v5_base_repr = File( + name="Base Repr.", + # full_path=str(v5.generate_path().with_suffix(".ma")), + type=repr_type, + ) + # v5.files.append(v5_base_repr) + + v5_base_repr.references = [v4_base_repr] + v4_base_repr.references = [v3_base_repr, v2_base_repr] + v3_base_repr.references = [v1_base_repr] + v2_base_repr.references = [v1_base_repr] + + expected_result = [ + v5_base_repr, + v4_base_repr, + v3_base_repr, + v1_base_repr, + v2_base_repr, + v1_base_repr, + ] + visited_versions = [] + for v in v5_base_repr.walk_references(): + visited_versions.append(v) + + assert expected_result == visited_versions diff --git a/tests/models/test_filename_template.py b/tests/models/test_filename_template.py index 2b34eac8..6ba39265 100644 --- a/tests/models/test_filename_template.py +++ b/tests/models/test_filename_template.py @@ -352,12 +352,10 @@ def test_naming_case(setup_postgresql_db): DBSession.commit() v = Version(task=maya) - v.update_paths() - v.extension = ".ma" DBSession.add(v) DBSession.commit() - - assert v.filename == "ep101_s001c001_fxA_v01.ma" + path = v.generate_path(extension=".ma") + assert path.name == "ep101_s001c001_fxA_v01.ma" def test__hash__is_working_as_expected(setup_filename_template_tests): diff --git a/tests/models/test_group.py b/tests/models/test_group.py index 711df4b6..1230d347 100644 --- a/tests/models/test_group.py +++ b/tests/models/test_group.py @@ -63,8 +63,8 @@ def test_users_argument_is_not_a_list_of_user_instances(setup_group_tests): Group(**data["kwargs"]) assert str(cm.value) == ( - "Group.users attribute must all be stalker.models.auth.User instances, " - "not int: '12'" + "Group.users should only contain instances of " + "stalker.models.auth.User, not int: '12'" ) @@ -75,8 +75,8 @@ def test_users_attribute_is_not_a_list_of_user_instances(setup_group_tests): data["test_group"].users = [12, "not a user"] assert str(cm.value) == ( - "Group.users attribute must all be stalker.models.auth.User instances, " - "not int: '12'" + "Group.users should only contain instances of " + "stalker.models.auth.User, not int: '12'" ) diff --git a/tests/models/test_price_list.py b/tests/models/test_price_list.py index ac1c1b89..055a4d0a 100644 --- a/tests/models/test_price_list.py +++ b/tests/models/test_price_list.py @@ -77,8 +77,8 @@ def test_goods_argument_is_a_list_of_objects_which_are_not_goods( PriceList(**data["kwargs"]) assert str(cm.value) == ( - "PriceList.goods should be a list of stalker.model.budget.Good instances, " - "not str: 'not'" + "PriceList.goods should only contain instances of " + "stalker.model.budget.Good, not str: 'not'" ) @@ -92,8 +92,8 @@ def test_good_attribute_is_a_list_of_objects_which_are_not_goods( p.goods = ["not", 1, "good", "instances"] assert str(cm.value) == ( - "PriceList.goods should be a list of " - "stalker.model.budget.Good instances, not str: 'not'" + "PriceList.goods should only contain instances of " + "stalker.model.budget.Good, not str: 'not'" ) diff --git a/tests/models/test_scene.py b/tests/models/test_scene.py index 2d6ecdb0..6c0bcd7e 100644 --- a/tests/models/test_scene.py +++ b/tests/models/test_scene.py @@ -101,7 +101,8 @@ def test_shots_attribute_is_set_to_other_than_a_list(setup_scene_db_tests): with pytest.raises(TypeError) as cm: data["test_scene"].shots = test_value assert str(cm.value) == ( - "Scene.shots needs to be all stalker.models.shot.Shot instances, not int: '1'" + "Scene.shots should only contain instances of " + "stalker.models.shot.Shot, not int: '1'" ) @@ -112,7 +113,8 @@ def test_shots_attribute_is_a_list_of_other_objects(setup_scene_db_tests): with pytest.raises(TypeError) as cm: data["test_scene"].shots = test_value assert str(cm.value) == ( - "Scene.shots needs to be all stalker.models.shot.Shot instances, not int: '1'" + "Scene.shots should only contain instances of " + "stalker.models.shot.Shot, not int: '1'" ) @@ -124,8 +126,8 @@ def test_shots_attribute_elements_tried_to_be_set_to_non_shot_object( with pytest.raises(TypeError) as cm: data["test_scene"].shots.append("a string") assert str(cm.value) == ( - "Scene.shots needs to be all stalker.models.shot.Shot instances, " - "not str: 'a string'" + "Scene.shots should only contain instances of " + "stalker.models.shot.Shot, not str: 'a string'" ) diff --git a/tests/models/test_sequence.py b/tests/models/test_sequence.py index 5470a872..b1a0cdfa 100644 --- a/tests/models/test_sequence.py +++ b/tests/models/test_sequence.py @@ -123,7 +123,8 @@ def test_shots_attribute_is_a_list_of_other_objects(setup_sequence_db_tests): data["test_sequence"].shots = test_value assert str(cm.value) == ( - "Sequence.shots should be all stalker.models.shot.Shot instances, not int: '1'" + "Sequence.shots should only contain instances of " + "stalker.models.shot.Shot, not int: '1'" ) @@ -137,8 +138,8 @@ def test_shots_attribute_elements_tried_to_be_set_to_non_Shot_object( data["test_sequence"].shots.append(test_value) assert str(cm.value) == ( - "Sequence.shots should be all stalker.models.shot.Shot instances, " - "not str: 'a string'" + "Sequence.shots should only contain instances of " + "stalker.models.shot.Shot, not str: 'a string'" ) diff --git a/tests/models/test_simple_entity.py b/tests/models/test_simple_entity.py index 4d71ed9a..407ddf39 100644 --- a/tests/models/test_simple_entity.py +++ b/tests/models/test_simple_entity.py @@ -730,9 +730,7 @@ def test_html_style_arg_is_not_a_string(setup_simple_entity_tests): data["kwargs"]["html_style"] = 123 with pytest.raises(TypeError) as cm: SimpleEntity(**data["kwargs"]) - assert str(cm.value) == ( - "SimpleEntity.html_style should be a str, not int: '123'" - ) + assert str(cm.value) == ("SimpleEntity.html_style should be a str, not int: '123'") def test_html_style_attr_is_not_set_to_a_string(setup_simple_entity_tests): @@ -792,9 +790,7 @@ def test_html_class_arg_is_not_a_string(setup_simple_entity_tests): data["kwargs"]["html_class"] = 123 with pytest.raises(TypeError) as cm: SimpleEntity(**data["kwargs"]) - assert str(cm.value) == ( - "SimpleEntity.html_class should be a str, not int: '123'" - ) + assert str(cm.value) == ("SimpleEntity.html_class should be a str, not int: '123'") def test_html_class_attr_is_not_set_to_a_string(setup_simple_entity_tests): diff --git a/tests/models/test_structure.py b/tests/models/test_structure.py index 17cae7f9..d9c31a9d 100644 --- a/tests/models/test_structure.py +++ b/tests/models/test_structure.py @@ -158,8 +158,8 @@ def test_templates_argument_accepts_only_list_of_filename_template_instances( with pytest.raises(TypeError) as cm: Structure(**data["kwargs"]) assert str(cm.value) == ( - "All the items in the Structure.templates should be a " - "stalker.models.template.FilenameTemplate instance, not int: '1'" + "Structure.templates should only contain instances of " + "stalker.models.template.FilenameTemplate, not int: '1'" ) @@ -182,8 +182,8 @@ def test_templates_attribute_accpets_only_list_of_filename_template_instances( data["test_structure"].templates = test_value assert str(cm.value) == ( - "All the items in the Structure.templates should be a " - "stalker.models.template.FilenameTemplate instance, not int: '1'" + "Structure.templates should only contain instances of " + "stalker.models.template.FilenameTemplate, not int: '1'" ) diff --git a/tests/models/test_task.py b/tests/models/test_task.py index 96e54348..03bbb5f8 100644 --- a/tests/models/test_task.py +++ b/tests/models/test_task.py @@ -364,8 +364,8 @@ def test_resources_arg_is_set_to_a_list_of_other_values_then_user( Task(**kwargs) assert str(cm.value) == ( - "Task.resources should be a list of stalker.models.auth.User instances, " - "not str: 'a'" + "Task.resources should only contain instances of " + "stalker.models.auth.User, not str: 'a'" ) @@ -379,8 +379,8 @@ def test_resources_attr_is_set_to_a_list_of_other_values_then_user( new_task.resources = ["a", "list", "of", "resources", data["test_user1"]] assert str(cm.value) == ( - "Task.resources should be a list of stalker.models.auth.User instances, " - "not str: 'a'" + "Task.resources should only contain instances of " + "stalker.models.auth.User, not str: 'a'" ) @@ -553,8 +553,8 @@ def test_watchers_arg_is_set_to_a_list_of_other_values_then_user(setup_task_test Task(**kwargs) assert str(cm.value) == ( - "Task.watchers should be a list of stalker.models.auth.User instances," - " not str: 'a'" + "Task.watchers should only contain instances of " + "stalker.models.auth.User, not str: 'a'" ) @@ -731,7 +731,7 @@ def test_depends_arg_is_a_list_of_other_objects_than_a_task(setup_task_tests): Task(**kwargs) assert str(cm.value) == ( - "TaskDependency.depends_on can should be and instance of " + "TaskDependency.depends_on should be and instance of " "stalker.models.task.Task, not str: 'a'" ) @@ -746,7 +746,7 @@ def test_depends_attr_is_a_list_of_other_objects_than_a_task(setup_task_tests): new_task.depends_on = test_value assert str(cm.value) == ( - "TaskDependency.depends_on can should be and instance of " + "TaskDependency.depends_on should be and instance of " "stalker.models.task.Task, not str: 'a'" ) @@ -1203,8 +1203,8 @@ def test_time_logs_attr_is_not_a_list_of_timelog_instances(setup_task_tests): new_task.time_logs = [1, "1", 1.2, "a time_log"] assert str(cm.value) == ( - "Task.time_logs should be all stalker.models.task.TimeLog instances, " - "not int: '1'" + "Task.time_logs should only contain instances of " + "stalker.models.task.TimeLog, not int: '1'" ) @@ -1619,8 +1619,8 @@ def test_versions_attr_is_not_a_list_of_version_instances(setup_task_tests): new_task.versions = [1, 1.2, "a version"] assert str(cm.value) == ( - "Task.versions should only have stalker.models.version.Version instances, " - "and not int: '1'" + "Task.versions should only contain instances of " + "stalker.models.version.Version, and not int: '1'" ) @@ -3571,8 +3571,8 @@ def test_responsible_arg_is_not_a_list_of_user_instance(setup_task_tests): Task(**kwargs) assert str(cm.value) == ( - "Task.responsible should be a list of stalker.models.auth.User instances, " - "not str: 'not a user instance'" + "Task.responsible should only contain instances of " + "stalker.models.auth.User, not str: 'not a user instance'" ) @@ -3588,8 +3588,8 @@ def test_responsible_attr_is_set_to_something_other_than_a_list_of_user_instance new_task.responsible = ["not a user instance"] assert str(cm.value) == ( - "Task.responsible should be a list of stalker.models.auth.User instances, " - "not str: 'not a user instance'" + "Task.responsible should only contain instances of " + "stalker.models.auth.User, not str: 'not a user instance'" ) @@ -3799,8 +3799,8 @@ def test_reviews_is_not_a_list_of_review_instances(setup_task_tests): new_task.reviews = test_value assert str(cm.value) == ( - "Task.reviews should be all stalker.models.review.Review " - "instances, not int: '1234'" + "Task.reviews should only contain instances of " + "stalker.models.review.Review, not int: '1234'" ) @@ -3900,8 +3900,8 @@ def test_task_depends_on_is_not_a_task_dependency_object(setup_task_tests): with pytest.raises(TypeError) as cm: new_task.task_depends_on.append("not a TaskDependency object.") assert str(cm.value) == ( - "All the items in the Task.task_depends_on should be a TaskDependency " - "instance, not str: 'not a TaskDependency object.'" + "Task.task_depends_on should only contain instances of TaskDependency, " + "not str: 'not a TaskDependency object.'" ) @@ -3965,8 +3965,8 @@ def test_alternative_resources_arg_elements_are_not_user_instances( Task(**kwargs) assert str(cm.value) == ( - "Task.alternative_resources should be a list of stalker.models.auth.User " - "instances, not str: 'not'" + "Task.alternative_resources should only contain instances of " + "stalker.models.auth.User, not str: 'not'" ) @@ -3980,8 +3980,8 @@ def test_alternative_resources_attr_elements_are_not_all_user_instances( new_task.alternative_resources = ["not", 1, "user"] assert str(cm.value) == ( - "Task.alternative_resources should be a list of stalker.models.auth.User " - "instances, not str: 'not'" + "Task.alternative_resources should only contain instances of " + "stalker.models.auth.User, not str: 'not'" ) @@ -4201,8 +4201,8 @@ def test_computed_resources_is_not_a_user_instance(setup_task_tests): new_task.computed_resources.append("not a user") assert str(cm.value) == ( - "Task.computed_resources should be a list of stalker.models.auth.User " - "instances, not str: 'not a user'" + "Task.computed_resources should only contain instances of " + "stalker.models.auth.User, not str: 'not a user'" ) diff --git a/tests/models/test_task_dependency.py b/tests/models/test_task_dependency.py index 48b2eea3..3eea5f59 100644 --- a/tests/models/test_task_dependency.py +++ b/tests/models/test_task_dependency.py @@ -166,7 +166,7 @@ def test_depends_on_argument_is_not_a_task_instance(setup_task_dependency_db_tes TaskDependency(**data["kwargs"]) assert ( - str(cm.value) == "TaskDependency.depends_on can should be and instance of " + str(cm.value) == "TaskDependency.depends_on should be and instance of " "stalker.models.task.Task, not str: 'Not a Task instance'" ) @@ -179,7 +179,7 @@ def test_depends_on_attribute_is_not_a_task_instance(setup_task_dependency_db_te new_dep.depends_on = "not a task" assert ( - str(cm.value) == "TaskDependency.depends_on can should be and instance of " + str(cm.value) == "TaskDependency.depends_on should be and instance of " "stalker.models.task.Task, not str: 'not a task'" ) diff --git a/tests/models/test_task_juggler_scheduler.py b/tests/models/test_task_juggler_scheduler.py index 8361d825..4cc812c9 100644 --- a/tests/models/test_task_juggler_scheduler.py +++ b/tests/models/test_task_juggler_scheduler.py @@ -747,8 +747,8 @@ def test_projects_argument_is_not_a_list(setup_tsk_juggler_scheduler_db_tests): TaskJugglerScheduler(compute_resources=True, projects="not a list of projects") assert str(cm.value) == ( - "TaskJugglerScheduler.projects should be a list of " - "stalker.models.project.Project instances, not str: 'not a list of projects'" + "TaskJugglerScheduler.projects should only contain instances of " + "stalker.models.project.Project, not str: 'not a list of projects'" ) @@ -759,8 +759,8 @@ def test_projects_attribute_is_not_a_list(setup_tsk_juggler_scheduler_db_tests): tjp.projects = "not a list of projects" assert str(cm.value) == ( - "TaskJugglerScheduler.projects should be a list of " - "stalker.models.project.Project instances, not str: 'not a list of projects'" + "TaskJugglerScheduler.projects should only contain instances of " + "stalker.models.project.Project, not str: 'not a list of projects'" ) @@ -772,8 +772,8 @@ def test_projects_argument_is_not_a_list_of_all_projects(): ) assert str(cm.value) == ( - "TaskJugglerScheduler.projects should be a list of " - "stalker.models.project.Project instances, not str: 'not'" + "TaskJugglerScheduler.projects should only contain instances of " + "stalker.models.project.Project, not str: 'not'" ) @@ -783,9 +783,9 @@ def test_projects_attribute_is_not_list_of_all_projects(): with pytest.raises(TypeError) as cm: tjp.projects = ["not", 1, [], "of", "projects"] - assert ( - str(cm.value) == "TaskJugglerScheduler.projects should be a list of " - "stalker.models.project.Project instances, not str: 'not'" + assert str(cm.value) == ( + "TaskJugglerScheduler.projects should only contain instances of " + "stalker.models.project.Project, not str: 'not'" ) diff --git a/tests/models/test_ticket.py b/tests/models/test_ticket.py index 0662dd96..99f12946 100644 --- a/tests/models/test_ticket.py +++ b/tests/models/test_ticket.py @@ -289,8 +289,8 @@ def test_related_tickets_attribute_is_set_to_something_other_then_a_list_of_tick data["test_ticket"].related_tickets = ["a ticket"] assert str(cm.value) == ( - "Ticket.related_ticket attribute should be a list of other " - "stalker.models.ticket.Ticket instances, not str: 'a ticket'" + "Ticket.related_ticket should only contain instances of " + "stalker.models.ticket.Ticket, not str: 'a ticket'" ) diff --git a/tests/models/test_version.py b/tests/models/test_version.py index fb340fa8..184d69df 100644 --- a/tests/models/test_version.py +++ b/tests/models/test_version.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import copy import logging +from pathlib import Path import sys import pytest @@ -20,12 +21,14 @@ Task, Type, User, + Variant, Version, defaults, log, ) from stalker.db.session import DBSession from stalker.exceptions import CircularDependencyError +from stalker.models.entity import Entity from tests.utils import PlatformPatcher @@ -74,8 +77,23 @@ def setup_version_db_tests(setup_postgresql_db): ) DBSession.add(data["test_project_type"]) + # create a filename template for Variants + data["test_filename_template"] = FilenameTemplate( + name="Variant Filename Template", + target_entity_type="Variant", + path="{{project.code}}/{%- for parent_task in parent_tasks -%}" + "{{parent_task.nice_name}}/{%- endfor -%}", + filename="{{version.nice_name}}" + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}' + "{{extension}}", + ) + DBSession.add(data["test_filename_template"]) + DBSession.commit() # create a structure - data["test_structure"] = Structure(name="Test Project Structure") + data["test_structure"] = Structure( + name="Test Project Structure", templates=[data["test_filename_template"]] + ) DBSession.add(data["test_structure"]) # create a project @@ -118,10 +136,29 @@ def setup_version_db_tests(setup_postgresql_db): DBSession.commit() # create a group of Tasks for the shot - data["test_task1"] = Task(name="Task1", parent=data["test_shot1"]) + data["test_task1"] = Task(name="FX", parent=data["test_shot1"]) DBSession.add(data["test_task1"]) DBSession.commit() + data["test_variant1"] = Variant(name="Main", parent=data["test_task1"]) + DBSession.add(data["test_variant1"]) + DBSession.commit() + + # a File for the files attribute + data["test_file1"] = File( + name="File 1", + full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" + "SH001_FX_Main_r01_v001.ma", + ) + DBSession.add(data["test_file1"]) + + data["test_file2"] = File( + name="File 2", + full_path="/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/" + "SH001_FX_Main_r01_v002.ma", + ) + DBSession.add(data["test_file2"]) + # a File for the input file data["test_input_file1"] = File( name="Input File 1", @@ -155,10 +192,8 @@ def setup_version_db_tests(setup_postgresql_db): # now create a version for the Task data["kwargs"] = { - "inputs": [data["test_input_file1"], data["test_input_file2"]], - "outputs": [data["test_output_file1"], data["test_output_file2"]], - "task": data["test_task1"], - "created_with": "Houdini", + "files": [data["test_file1"], data["test_file2"]], + "task": data["test_variant1"], } # and the Version @@ -178,6 +213,11 @@ def test___auto_name__class_attribute_is_set_to_true(): assert Version.__auto_name__ is True +def test_version_derives_from_entity(): + """Version class derives from Entity.""" + assert Entity == Version.__mro__[1] + + def test_task_argument_is_skipped(setup_version_db_tests): """TypeError raised if the task argument is skipped.""" data = setup_version_db_tests @@ -211,7 +251,8 @@ def test_task_argument_is_not_a_task(setup_version_db_tests): with pytest.raises(TypeError) as cm: Version(**data["kwargs"]) assert str(cm.value) == ( - "Version.task should be a stalker.models.task.Task instance, not str: 'a task'" + "Version.task should be a Task, Asset, Shot, Scene, Sequence or Variant " + "instance, not str: 'a task'" ) @@ -221,15 +262,16 @@ def test_task_attribute_is_not_a_task(setup_version_db_tests): with pytest.raises(TypeError) as cm: data["test_version"].task = "a task" assert str(cm.value) == ( - "Version.task should be a stalker.models.task.Task instance, not str: 'a task'" + "Version.task should be a Task, Asset, Shot, Scene, Sequence or Variant " + "instance, not str: 'a task'" ) def test_task_attribute_is_working_as_expected(setup_version_db_tests): """task attribute is working as expected.""" data = setup_version_db_tests - new_task = Task( - name="New Test Task", + new_task = Variant( + name="New Test Variant", parent=data["test_shot1"], ) DBSession.add(new_task) @@ -558,132 +600,68 @@ def test_version_number_attribute_is_set_to_a_lower_then_it_should_be( assert new_version.version_number == 100 -def test_inputs_argument_is_skipped(setup_version_db_tests): - """inputs attribute an empty list if the inputs argument is skipped.""" +def test_files_argument_is_skipped(setup_version_db_tests): + """files attribute an empty list if the files argument is skipped.""" data = setup_version_db_tests - data["kwargs"].pop("inputs") + data["kwargs"].pop("files") new_version = Version(**data["kwargs"]) - assert new_version.inputs == [] + assert new_version.files == [] -def test_inputs_argument_is_none(setup_version_db_tests): - """inputs attribute an empty list if the inputs argument is None.""" +def test_files_argument_is_none(setup_version_db_tests): + """files attribute an empty list if the files argument is None.""" data = setup_version_db_tests - data["kwargs"]["inputs"] = None + data["kwargs"]["files"] = None new_version = Version(**data["kwargs"]) - assert new_version.inputs == [] + assert new_version.files == [] -def test_inputs_attribute_is_none(setup_version_db_tests): - """TypeError raised if the inputs argument is set to None.""" +def test_files_attribute_is_none(setup_version_db_tests): + """TypeError raised if the files argument is set to None.""" data = setup_version_db_tests with pytest.raises(TypeError) as cm: - data["test_version"].inputs = None + data["test_version"].files = None assert str(cm.value) == "Incompatible collection type: None is not list-like" -def test_inputs_argument_is_not_a_list_of_file_instances(setup_version_db_tests): - """TypeError raised if the inputs attr is not a File instance.""" +def test_files_argument_is_not_a_list_of_file_instances(setup_version_db_tests): + """TypeError raised if the files attr is not a list of File instances.""" data = setup_version_db_tests test_value = [132, "231123"] - data["kwargs"]["inputs"] = test_value + data["kwargs"]["files"] = test_value with pytest.raises(TypeError) as cm: Version(**data["kwargs"]) assert ( - str(cm.value) == "All elements in Version.inputs should be all " - "stalker.models.file.File instances, not int: '132'" + str(cm.value) == "Version.files should only contain instances of " + "stalker.models.file.File, not int: '132'" ) -def test_inputs_attribute_is_not_a_list_of_file_instances(setup_version_db_tests): - """TypeError raised if the inputs attr is set to something other than a File.""" +def test_files_attribute_is_not_a_list_of_file_instances(setup_version_db_tests): + """TypeError raised if the files attr is set to something other than a File.""" data = setup_version_db_tests test_value = [132, "231123"] with pytest.raises(TypeError) as cm: - data["test_version"].inputs = test_value + data["test_version"].files = test_value assert ( - str(cm.value) == "All elements in Version.inputs should be all " - "stalker.models.file.File instances, not int: '132'" + str(cm.value) == "Version.files should only contain instances of " + "stalker.models.file.File, not int: '132'" ) -def test_inputs_attribute_is_working_as_expected(setup_version_db_tests): - """inputs attribute is working as expected.""" +def test_files_attribute_is_working_as_expected(setup_version_db_tests): + """files attribute is working as expected.""" data = setup_version_db_tests - data["kwargs"].pop("inputs") + data["kwargs"].pop("files") new_version = Version(**data["kwargs"]) - assert data["test_input_file1"] not in new_version.inputs - assert data["test_input_file2"] not in new_version.inputs - - new_version.inputs = [data["test_input_file1"], data["test_input_file2"]] - assert data["test_input_file1"] in new_version.inputs - assert data["test_input_file2"] in new_version.inputs + assert data["test_file1"] not in new_version.files + assert data["test_file2"] not in new_version.files - -def test_outputs_argument_is_skipped(setup_version_db_tests): - """outputs attribute an empty list if the outputs argument is skipped.""" - data = setup_version_db_tests - data["kwargs"].pop("outputs") - new_version = Version(**data["kwargs"]) - assert new_version.outputs == [] - - -def test_outputs_argument_is_none(setup_version_db_tests): - """outputs attribute an empty list if the outputs argument is None.""" - data = setup_version_db_tests - data["kwargs"]["outputs"] = None - new_version = Version(**data["kwargs"]) - assert new_version.outputs == [] - - -def test_outputs_attribute_is_none(setup_version_db_tests): - """TypeError raised if the outputs argument is set to None.""" - data = setup_version_db_tests - with pytest.raises(TypeError) as cm: - data["test_version"].outputs = None - assert str(cm.value) == "Incompatible collection type: None is not list-like" - - -def test_outputs_argument_is_not_a_list_of_file_instances(setup_version_db_tests): - """TypeError raised if the outputs attr is not a File instance.""" - data = setup_version_db_tests - test_value = [132, "231123"] - data["kwargs"]["outputs"] = test_value - with pytest.raises(TypeError) as cm: - Version(**data["kwargs"]) - - assert ( - str(cm.value) == "All elements in Version.outputs should be all " - "stalker.models.file.File instances, not int: '132'" - ) - - -def test_outputs_attribute_is_not_a_list_of_file_instances(setup_version_db_tests): - """TypeError raised if the outputs attr is not a File instance.""" - data = setup_version_db_tests - test_value = [132, "231123"] - with pytest.raises(TypeError) as cm: - data["test_version"].outputs = test_value - - assert ( - str(cm.value) == "All elements in Version.outputs should be all " - "stalker.models.file.File instances, not int: '132'" - ) - - -def test_outputs_attribute_is_working_as_expected(setup_version_db_tests): - """outputs attribute is working as expected.""" - data = setup_version_db_tests - data["kwargs"].pop("outputs") - new_version = Version(**data["kwargs"]) - assert data["test_output_file1"] not in new_version.outputs - assert data["test_output_file2"] not in new_version.outputs - - new_version.outputs = [data["test_output_file1"], data["test_output_file2"]] - assert data["test_output_file1"] in new_version.outputs - assert data["test_output_file2"] in new_version.outputs + new_version.files = [data["test_file1"], data["test_file2"]] + assert data["test_file1"] in new_version.files + assert data["test_file2"] in new_version.files def test_is_published_attribute_is_false_by_default(setup_version_db_tests): @@ -800,8 +778,8 @@ def test_parent_attribute_will_not_allow_circular_dependencies(setup_version_db_ data["test_version"].parent = version1 assert ( - str(cm.value) == " (Version) and " - " (Version) are in a " + str(cm.value) == " (Version) and " + " (Version) are in a " 'circular dependency in their "children" attribute' ) @@ -824,8 +802,8 @@ def test_parent_attribute_will_not_allow_deeper_circular_dependencies( data["test_version"].parent = version2 assert ( - str(cm.value) == " (Version) and " - " (Version) are in a " + str(cm.value) == " (Version) and " + " (Version) are in a " 'circular dependency in their "children" attribute' ) @@ -857,8 +835,8 @@ def test_children_attribute_is_not_set_to_a_list_of_version_instances( data["test_version"].children = ["not a Version instance", 3] assert str(cm.value) == ( - "Version.children should be a list of Version (or derivative) " - "instances, not str: 'not a Version instance'" + "Version.children should only contain instances of " + "Version (or derivative), not str: 'not a Version instance'" ) @@ -906,8 +884,8 @@ def test_children_attribute_will_not_allow_circular_dependencies( new_version1.children.append(new_version2) assert ( - str(cm.value) == " (Version) and " - " (Version) are in a " + str(cm.value) == " (Version) and " + " (Version) are in a " 'circular dependency in their "children" attribute' ) @@ -936,132 +914,108 @@ def test_children_attribute_will_not_allow_deeper_circular_dependencies( new_version1.children.append(new_version3) assert ( - str(cm.value) == " (Version) and " - " (Version) are in a " + str(cm.value) == " (Version) and " + " (Version) are in a " 'circular dependency in their "children" attribute' ) -def test_update_paths_will_render_the_appropriate_template_from_the_related_project( - setup_version_db_tests, -): - """update_paths method updates Version.full_path by rendering the related Project - FilenameTemplate..""" - data = setup_version_db_tests - # create a FilenameTemplate for Task instances - - # A Template for Assets - # ......../Assets/{{asset.type.name}}/{{asset.nice_name}}/{{task.type.name}}/ - # - # Project1/Assets/Character/Sero/Modeling/Sero_Modeling_Main_v001.ma - # - # + Project1 - # | - # +-+ Assets (Task) - # | | - # | +-+ Characters - # | | - # | +-+ Sero (Asset) - # | | - # | +-> Version 1 - # | | - # | +-+ Modeling (Task) - # | | - # | +-+ Body Modeling (Task) - # | | - # | +-+ Coarse Modeling (Task) - # | | | - # | | +-> Version 1 (Version) - # | | - # | +-+ Fine Modeling (Task) - # | | - # | +-> Version 1 (Version): Fine_Modeling_Main_v001.ma - # | Assets/Characters/Sero/Modeling/Body_Modeling/Fine_Modeling/Fine_Modeling_Main_v001.ma - # | - # +-+ Shots (Task) - # | - # +-+ Shot 10 (Shot) - # | | - # | +-+ Layout (Task) - # | | - # | +-> Version 1 (Version): Layout_v001.ma - # | Shots/Shot_1/Layout/Layout_Main_v001.ma - # | - # +-+ Shot 2 (Shot) - # | - # +-+ FX (Task) - # | - # +-> Version 1 (Version) +def test_generate_path_extension_can_be_skipped(setup_version_db_tests): + """generate_path() extension can be skipped.""" + data = setup_version_db_tests + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() + # extension can be skipped + _ = new_version1.generate_path() - ft = FilenameTemplate( - name="Task Filename Template", - target_entity_type="Task", - path="{{project.code}}/{%- for parent_task in parent_tasks -%}" - "{{parent_task.nice_name}}/{%- endfor -%}", - filename="{{task.nice_name}}" - '_v{{"%03d"|format(version.version_number)}}{{extension}}', - ) - data["test_project"].structure.templates.append(ft) + +def test_generate_path_extension_can_be_None(setup_version_db_tests): + """generate_path() extension can be None.""" + data = setup_version_db_tests + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() + # extension can be skipped + path = new_version1.generate_path(extension=None) + assert path.suffix == "" + + +def test_generate_path_extension_is_not_a_str(setup_version_db_tests): + """generate_path() extension is not a str will raise a TypeError.""" + data = setup_version_db_tests new_version1 = Version(**data["kwargs"]) DBSession.add(new_version1) DBSession.commit() - new_version1.update_paths() - assert new_version1.path == "tp/SH001/Task1" + # extension is not str raises TypeError + with pytest.raises(TypeError) as cm: + _ = new_version1.generate_path(extension=1234) - new_version1.extension = ".ma" - assert new_version1.filename == "Task1_v002.ma" + assert str(cm.value) == "extension should be a str, not int: '1234'" -def test_update_paths_will_preserve_extension(setup_version_db_tests): - """update_paths method will preserve the extension.""" +def test_generate_path_extension_can_be_an_empty_str(setup_version_db_tests): + """generate_path() extension can be an empty str.""" + data = setup_version_db_tests + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() + # extension can be an empty string + path = new_version1.generate_path(extension="") + assert path.suffix == "" + + +def test_generate_path_will_render_the_appropriate_template_from_the_related_project( + setup_version_db_tests, +): + """generate_path() generates a Path by rendering the related Project FilenameTemplate.""" data = setup_version_db_tests - # create a FilenameTemplate for Task instances - ft = FilenameTemplate( - name="Task Filename Template", - target_entity_type="Task", - path="{{project.code}}/{%- for parent_task in parent_tasks -%}" - "{{parent_task.nice_name}}/{%- endfor -%}", - filename="{{task.nice_name}}" - '_v{{"%03d"|format(version.version_number)}}{{extension}}', - ) - data["test_project"].structure.templates.append(ft) new_version1 = Version(**data["kwargs"]) DBSession.add(new_version1) DBSession.commit() - new_version1.update_paths() - assert new_version1.path == "tp/SH001/Task1" + path = new_version1.generate_path() + assert isinstance(path, Path) + assert str(path.parent) == "tp/SH001/FX/Main" - extension = ".ma" - new_version1.extension = extension - assert new_version1.filename == "Task1_v002.ma" + path = path.with_suffix(".ma") + assert str(path.name) == "SH001_FX_Main_r01_v002.ma" - # rename the task and update the paths - data["test_task1"].name = "Task2" - # now call update_paths and expect the extension to be preserved - new_version1.update_paths() - assert new_version1.filename == "Task2_v002.ma" - assert new_version1.extension == extension +def test_generate_path_will_use_the_given_extension(setup_version_db_tests): + """generate_path method uses the given extension.""" + data = setup_version_db_tests + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() + path = new_version1.generate_path(extension=".ma") + assert isinstance(path, Path) + assert str(path.parent) == "tp/SH001/FX/Main" + assert str(path.name) == "SH001_FX_Main_r01_v002.ma" -def test_update_paths_will_raise_a_runtime_error_if_there_is_no_suitable_filename_template( +def test_generate_path_will_raise_a_runtime_error_if_there_is_no_suitable_filename_template( setup_version_db_tests, ): - """update_paths method raises a RuntimeError if there is no suitable + """generate_path method raises a RuntimeError if there is no suitable FilenameTemplate instance found.""" data = setup_version_db_tests + data["test_structure"].templates.remove(data["test_filename_template"]) + DBSession.commit() + + DBSession.delete(data["test_filename_template"]) + DBSession.commit() data["kwargs"]["parent"] = None new_version1 = Version(**data["kwargs"]) with pytest.raises(RuntimeError) as cm: - new_version1.update_paths() + new_version1.generate_path() assert ( str(cm.value) == "There are no suitable FilenameTemplate (target_entity_type == " - "'Task') defined in the Structure of the related Project " + "'Variant') defined in the Structure of the related Project " "instance, please create a new " "stalker.models.template.FilenameTemplate instance with its " - "'target_entity_type' attribute is set to 'Task' and assign it " + "'target_entity_type' attribute is set to 'Variant' and add it " "to the `templates` attribute of the structure of the project" ) @@ -1156,53 +1110,146 @@ def test_template_variables_for_a_shot_version_contains_sequence( def test_absolute_path_works_as_expected(setup_version_db_tests): """absolute_path attribute works as expected.""" data = setup_version_db_tests - data["patcher"].patch("Linux") + # data["patcher"].patch("Linux") + + data["test_structure"].templates.remove(data["test_filename_template"]) + DBSession.delete(data["test_filename_template"]) + DBSession.commit() + ft = FilenameTemplate( name="Task Filename Template", - target_entity_type="Task", - path="{{project.repositories[0].path}}/{{project.code}}/" + target_entity_type="Variant", + path="$REPO{{project.repositories[0].code}}/{{project.code}}/" "{%- for parent_task in parent_tasks -%}" "{{parent_task.nice_name}}/" "{%- endfor -%}", filename="{{task.nice_name}}" - '_v{{"%03d"|format(version.version_number)}}{{extension}}', + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}', ) data["test_project"].structure.templates.append(ft) new_version1 = Version(**data["kwargs"]) DBSession.add(new_version1) DBSession.commit() - new_version1.update_paths() - new_version1.extension = ".ma" - assert new_version1.extension == ".ma" - - assert new_version1.absolute_path == "/mnt/T/tp/SH001/Task1" + repo_path = data["test_repo"].path + assert new_version1.absolute_path == Path(f"{repo_path}/tp/SH001/FX/Main") def test_absolute_full_path_works_as_expected(setup_version_db_tests): """absolute_full_path attribute works as expected.""" data = setup_version_db_tests + # data["patcher"].patch("Linux") + + data["test_structure"].templates.remove(data["test_filename_template"]) + DBSession.delete(data["test_filename_template"]) + DBSession.commit() + + ft = FilenameTemplate( + name="Task Filename Template", + target_entity_type="Variant", + path="$REPO{{project.repositories[0].code}}/{{project.code}}/" + "{%- for parent_task in parent_tasks -%}" + "{{parent_task.nice_name}}/" + "{%- endfor -%}", + filename="{{task.nice_name}}" + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}', + ) + data["test_project"].structure.templates.append(ft) + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() + + repo_path = data["test_repo"].path + assert new_version1.absolute_full_path == Path( + f"{repo_path}/tp/SH001/FX/Main/Main_r01_v002" + ) + + +def test_path_works_as_expected(setup_version_db_tests): + """path attribute works as expected.""" + data = setup_version_db_tests + data["patcher"].patch("Linux") + + data["test_structure"].templates.remove(data["test_filename_template"]) + DBSession.delete(data["test_filename_template"]) + DBSession.commit() + + ft = FilenameTemplate( + name="Task Filename Template", + target_entity_type="Variant", + path="$REPO{{project.repositories[0].code}}/{{project.code}}/" + "{%- for parent_task in parent_tasks -%}" + "{{parent_task.nice_name}}/" + "{%- endfor -%}", + filename="{{task.nice_name}}" + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}', + ) + data["test_project"].structure.templates.append(ft) + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() + + assert new_version1.path == Path("$REPOTR/tp/SH001/FX/Main") + + +def test_full_path_works_as_expected(setup_version_db_tests): + """full_path attribute works as expected.""" + data = setup_version_db_tests data["patcher"].patch("Linux") + + data["test_structure"].templates.remove(data["test_filename_template"]) + DBSession.delete(data["test_filename_template"]) + DBSession.commit() + ft = FilenameTemplate( name="Task Filename Template", - target_entity_type="Task", - path="{{project.repositories[0].path}}/{{project.code}}/" + target_entity_type="Variant", + path="$REPO{{project.repositories[0].code}}/{{project.code}}/" "{%- for parent_task in parent_tasks -%}" "{{parent_task.nice_name}}/" "{%- endfor -%}", filename="{{task.nice_name}}" - '_v{{"%03d"|format(version.version_number)}}{{extension}}', + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}', ) data["test_project"].structure.templates.append(ft) new_version1 = Version(**data["kwargs"]) DBSession.add(new_version1) DBSession.commit() - new_version1.update_paths() - new_version1.extension = ".ma" - assert new_version1.extension == ".ma" + assert new_version1.full_path == Path("$REPOTR/tp/SH001/FX/Main/Main_r01_v002") + + +def test_filename_works_as_expected(setup_version_db_tests): + """filename attribute works as expected.""" + data = setup_version_db_tests + data["patcher"].patch("Linux") + + data["test_structure"].templates.remove(data["test_filename_template"]) + DBSession.delete(data["test_filename_template"]) + DBSession.commit() + + ft = FilenameTemplate( + name="Task Filename Template", + target_entity_type="Variant", + path="$REPO{{project.repositories[0].code}}/{{project.code}}/" + "{%- for parent_task in parent_tasks -%}" + "{{parent_task.nice_name}}/" + "{%- endfor -%}", + filename="{{task.nice_name}}" + '_r{{"%02d"|format(version.revision_number)}}' + '_v{{"%03d"|format(version.version_number)}}', + ) + data["test_project"].structure.templates.append(ft) + new_version1 = Version(**data["kwargs"]) + DBSession.add(new_version1) + DBSession.commit() - assert new_version1.absolute_full_path == "/mnt/T/tp/SH001/Task1/Task1_v002.ma" + assert new_version1.filename == "Main_r01_v002" + assert isinstance(new_version1.filename, str) def test_latest_published_version_is_read_only(setup_version_db_tests): @@ -1467,66 +1514,6 @@ def test_inequality_operator(setup_version_db_tests): assert (new_version8 != new_version8) is False -def test_created_with_argument_can_be_skipped(setup_version_db_tests): - """created_with argument can be skipped.""" - data = setup_version_db_tests - data["kwargs"].pop("created_with") - Version(**data["kwargs"]) - - -def test_created_with_argument_can_be_none(setup_version_db_tests): - """created_with argument can be None.""" - data = setup_version_db_tests - data["kwargs"]["created_with"] = None - Version(**data["kwargs"]) - - -def test_created_with_attribute_can_be_set_to_none(setup_version_db_tests): - """created with attribute can be set to None.""" - data = setup_version_db_tests - data["test_version"].created_with = None - - -def test_created_with_argument_accepts_only_string_or_none(setup_version_db_tests): - """TypeError raised if the created_with arg is not a string or None.""" - data = setup_version_db_tests - data["kwargs"]["created_with"] = 234 - with pytest.raises(TypeError) as cm: - Version(**data["kwargs"]) - assert str(cm.value) == ( - "Version.created_with should be an instance of str, not int: '234'" - ) - - -def test_created_with_attribute_accepts_only_string_or_none(setup_version_db_tests): - """TypeError raised if the created_with attr is not a str or None.""" - data = setup_version_db_tests - with pytest.raises(TypeError) as cm: - data["test_version"].created_with = 234 - - assert str(cm.value) == ( - "Version.created_with should be an instance of str, not int: '234'" - ) - - -def test_created_with_argument_is_working_as_expected(setup_version_db_tests): - """created_with argument value is passed to created_with attribute.""" - data = setup_version_db_tests - test_value = "Maya" - data["kwargs"]["created_with"] = test_value - test_version = Version(**data["kwargs"]) - assert test_version.created_with == test_value - - -def test_created_with_attribute_is_working_as_expected(setup_version_db_tests): - """created_with attribute is working as expected.""" - data = setup_version_db_tests - test_value = "Maya" - assert data["test_version"].created_with != test_value - data["test_version"].created_with = test_value - assert data["test_version"].created_with == test_value - - def test_max_version_number_attribute_is_read_only(setup_version_db_tests): """max_version_number attribute is read only.""" data = setup_version_db_tests @@ -1689,6 +1676,7 @@ def test_naming_parents_attribute_is_working_as_expected(setup_version_db_tests) assert data["test_version"].naming_parents == [ data["test_shot1"], data["test_task1"], + data["test_variant1"], ] # for a new version of a task @@ -1760,6 +1748,7 @@ def test_nice_name_attribute_is_working_as_expected(setup_version_db_tests): assert data["test_version"].naming_parents == [ data["test_shot1"], data["test_task1"], + data["test_variant1"], ] # for a new version of a task @@ -1834,7 +1823,7 @@ def test_nice_name_attribute_is_working_as_expected(setup_version_db_tests): def test_string_representation_is_a_little_bit_meaningful(setup_version_db_tests): """__str__ or __repr__ result is meaningful.""" data = setup_version_db_tests - assert "" == f'{data["test_version"]}' + assert "" == f'{data["test_version"]}' def test_walk_hierarchy_is_working_as_expected_in_dfs_mode(setup_version_db_tests): @@ -1852,28 +1841,6 @@ def test_walk_hierarchy_is_working_as_expected_in_dfs_mode(setup_version_db_test assert expected_result == visited_versions -def test_walk_inputs_is_working_as_expected_in_dfs_mode(setup_version_db_tests): - """walk_inputs() method is working in DFS mode correctly.""" - data = setup_version_db_tests - v1 = Version(task=data["test_task1"]) - v2 = Version(task=data["test_task1"]) - v3 = Version(task=data["test_task1"]) - v4 = Version(task=data["test_task1"]) - v5 = Version(task=data["test_task1"]) - - v5.inputs = [v4] - v4.inputs = [v3, v2] - v3.inputs = [v1] - v2.inputs = [v1] - - expected_result = [v5, v4, v3, v1, v2, v1] - visited_versions = [] - for v in v5.walk_inputs(): - visited_versions.append(v) - - assert expected_result == visited_versions - - # def test_path_attribute_value_is_calculated_on_init(setup_version_db_tests): # """path attribute value is automatically calculated on # Version instance initialize @@ -1899,12 +1866,12 @@ def test_walk_inputs_is_working_as_expected_in_dfs_mode(setup_version_db_tests): def test_reviews_attribute_is_a_list_of_reviews(setup_version_db_tests): """Version.reviews attribute is filled with Review instances.""" data = setup_version_db_tests - data["test_task1"].status = data["status_wip"] - data["test_task1"].responsible = [data["test_user1"], data["test_user2"]] - version = Version(task=data["test_task1"]) + data["test_variant1"].status = data["status_wip"] + data["test_variant1"].responsible = [data["test_user1"], data["test_user2"]] + version = Version(task=data["test_variant1"]) # request a review - reviews = data["test_task1"].request_review(version=version) + reviews = data["test_variant1"].request_review(version=version) assert reviews[0].version == version assert reviews[1].version == version assert isinstance(version.reviews, list) @@ -2088,10 +2055,7 @@ def setup_version_tests(): # now create a version for the Task data["kwargs"] = { - "inputs": [data["test_input_file1"], data["test_input_file2"]], - "outputs": [data["test_output_file1"], data["test_output_file2"]], "task": data["test_task1"], - "created_with": "Houdini", } # and the Version @@ -2219,7 +2183,7 @@ def patched_request_review(self, version=None): def test_request_review_method_returns_reviews(setup_version_db_tests): """request_review() returns Reviews.""" data = setup_version_db_tests - task = data["test_task1"] + task = data["test_variant1"] task.responsible = [ data["test_user1"], data["test_user2"], diff --git a/tests/test_readme_tutorial.py b/tests/test_readme_tutorial.py index 3cea0d6c..504f409a 100644 --- a/tests/test_readme_tutorial.py +++ b/tests/test_readme_tutorial.py @@ -117,18 +117,18 @@ def test_readme_tutorial_code(setup_sqlite3): DBSession.save([animation, lighting]) new_version = Version(task=animation) - new_version.update_paths() # to render the naming convention template + new_version.generate_path() # to render the naming convention template new_version.extension = ".ma" # let's say that we have created under Maya DBSession.save(new_version) - assert new_version.absolute_full_path == ( - f"{repo.path}TP/SH001/Animation/SH001_Animation_v001.ma" - ) + path = new_version.generate_path(extension=".ma") + assert str(path) == f"{repo.path}TP/SH001/Animation/SH001_Animation_v001.ma" assert new_version.version_number == 1 new_version2 = Version(task=animation) - new_version2.update_paths() # to render the naming convention template - new_version2.extension = ".ma" # let's say that we have created under Maya DBSession.save(new_version2) + # to render the naming convention template + # let's say that we have created under Maya + # path = new_version2.generate_path(extension = ".ma") assert new_version2.version_number == 2 diff --git a/whitelist.txt b/whitelist.txt index 53ca4961..2ee53720 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -10,6 +10,7 @@ DDL DEFERRABLE DFS DREV +expandvars fetchall FilenameTemplates formatter @@ -21,6 +22,7 @@ mixin Mixins myapp mymodel +normpath nullable num OH From d29f2d62c75f097b8a22f66b4e42fefa02dc7c09 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Tue, 14 Jan 2025 15:24:51 +0000 Subject: [PATCH 08/11] [#147] Update version number. --- src/stalker/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stalker/VERSION b/src/stalker/VERSION index e6d5cb83..64b95b0c 100644 --- a/src/stalker/VERSION +++ b/src/stalker/VERSION @@ -1 +1 @@ -1.0.2 \ No newline at end of file +1.1.0.dev0 \ No newline at end of file From 944b14642d502bcbee9ce36f80fc2d9ddc3d6a24 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Fri, 17 Jan 2025 19:31:32 +0000 Subject: [PATCH 09/11] [#147] Add migration for the latest changes. The downgrade action is still in development. --- .../2252e51506de_multiple_repositories.py | 15 +- .../2e4a3813ae76_created_daily_class.py | 57 +- ...added_exclude_and_check_constraints_to_.py | 148 ++-- ...33d9caaafab_task_review_status_workflow.py | 27 +- .../46775e4a3d96_create_enum_types.py | 11 +- .../5355b569237b_version_version_of_r.py | 13 +- ...45b210e6907_fix_non_existing_thumbnails.py | 19 +- .../9f9b88fef376_link_renamed_to_file.py | 805 ++++++++++++++++++ src/stalker/db/setup.py | 2 +- src/stalker/models/entity.py | 12 +- 10 files changed, 943 insertions(+), 166 deletions(-) create mode 100644 alembic/versions/9f9b88fef376_link_renamed_to_file.py diff --git a/alembic/versions/2252e51506de_multiple_repositories.py b/alembic/versions/2252e51506de_multiple_repositories.py index cfd8db81..fc174474 100644 --- a/alembic/versions/2252e51506de_multiple_repositories.py +++ b/alembic/versions/2252e51506de_multiple_repositories.py @@ -46,14 +46,13 @@ def downgrade(): # before dropping Project_Repositories, carry all the data back, # note that only the first repository found per project will be # restored to the Project.repository_id column - op.execute( - 'update "Projects" ' - " set repository_id = (" - " select " - " repo_id" - ' from "Project_Repositories" ' - ' where project_id = "Projects".id limit 1' - " )" + op.execute(""" + UPDATE "Projects" SET repository_id = ( + SELECT + repo_id + FROM "Project_Repositories" + WHERE project_id = "Projects".id LIMIT 1 + )""" ) op.drop_table("Project_Repositories") diff --git a/alembic/versions/2e4a3813ae76_created_daily_class.py b/alembic/versions/2e4a3813ae76_created_daily_class.py index 9e672326..5dcb4dd2 100644 --- a/alembic/versions/2e4a3813ae76_created_daily_class.py +++ b/alembic/versions/2e4a3813ae76_created_daily_class.py @@ -98,39 +98,39 @@ def create_status(name, code): # Insert in to SimpleEntities op.execute( f"""INSERT INTO "SimpleEntities" (entity_type, name, description, -created_by_id, updated_by_id, date_created, date_updated, type_id, -thumbnail_id, html_style, html_class, stalker_version) -VALUES ('StatusList', 'Daily Statuses', '', NULL, NULL, -(SELECT CAST(NOW() at time zone 'utc' AS timestamp)), -(SELECT CAST(NOW() at time zone 'utc' AS timestamp)), NULL, NULL, -'', '', '{stalker.__version__}')""" + created_by_id, updated_by_id, date_created, date_updated, type_id, + thumbnail_id, html_style, html_class, stalker_version) + VALUES ('StatusList', 'Daily Statuses', '', NULL, NULL, + (SELECT CAST(NOW() at time zone 'utc' AS timestamp)), + (SELECT CAST(NOW() at time zone 'utc' AS timestamp)), NULL, NULL, + '', '', '{stalker.__version__}')""" ) # insert in to Entities and StatusLists op.execute( """INSERT INTO "Entities" (id) -VALUES (( - SELECT id - FROM "SimpleEntities" - WHERE "SimpleEntities".name = 'Daily Statuses' -)); -INSERT INTO "StatusLists" (id, target_entity_type) -VALUES (( - SELECT id - FROM "SimpleEntities" - WHERE "SimpleEntities".name = 'Daily Statuses'), 'Daily');""" + VALUES (( + SELECT id + FROM "SimpleEntities" + WHERE "SimpleEntities".name = 'Daily Statuses' + )); + INSERT INTO "StatusLists" (id, target_entity_type) + VALUES (( + SELECT id + FROM "SimpleEntities" + WHERE "SimpleEntities".name = 'Daily Statuses'), 'Daily');""" ) # Add Review Statues To StatusList_Statuses # Add new Task statuses to StatusList op.execute( """INSERT INTO "StatusList_Statuses" (status_list_id, status_id) -VALUES - ((SELECT id FROM "StatusLists" WHERE target_entity_type = 'Daily'), - (SELECT id FROM "Statuses" WHERE code = 'OPEN')), - ((SELECT id FROM "StatusLists" WHERE target_entity_type = 'Daily'), - (SELECT id FROM "Statuses" WHERE code = 'CLS')) -""" + VALUES + ((SELECT id FROM "StatusLists" WHERE target_entity_type = 'Daily'), + (SELECT id FROM "Statuses" WHERE code = 'OPEN')), + ((SELECT id FROM "StatusLists" WHERE target_entity_type = 'Daily'), + (SELECT id FROM "Statuses" WHERE code = 'CLS')) + """ ) @@ -142,17 +142,12 @@ def downgrade(): # Delete Open Status op.execute( """DELETE FROM "StatusList_Statuses" WHERE - status_id IN ( - select id FROM "SimpleEntities" WHERE - name = 'Open'); + status_id IN (select id FROM "SimpleEntities" WHERE name = 'Open'); DELETE FROM "Statuses" WHERE - id IN (select id FROM "SimpleEntities" WHERE - name = 'Open'); + id IN (select id FROM "SimpleEntities" WHERE name = 'Open'); DELETE FROM "Entities" WHERE - id IN (select id FROM "SimpleEntities" WHERE - name = 'Open'); - DELETE FROM "SimpleEntities" WHERE - name = 'Open'; + id IN (select id FROM "SimpleEntities" WHERE name = 'Open'); + DELETE FROM "SimpleEntities" WHERE name = 'Open'; """ ) diff --git a/alembic/versions/31b1e22b455e_added_exclude_and_check_constraints_to_.py b/alembic/versions/31b1e22b455e_added_exclude_and_check_constraints_to_.py index 98c23fa0..4f13241e 100644 --- a/alembic/versions/31b1e22b455e_added_exclude_and_check_constraints_to_.py +++ b/alembic/versions/31b1e22b455e_added_exclude_and_check_constraints_to_.py @@ -22,97 +22,95 @@ def upgrade(): # First cleanup TimeLogs table logger.info("Removing duplicate TimeLog entries") op.execute( - """ --- first remove direct duplicates -with cte as ( - select - row_number() over (partition by resource_id, start) as rn, - id, - start, - "end", - resource_id - from "TimeLogs" - where exists ( - select - 1 - from "TimeLogs" as tlogs - where tlogs.start <= "TimeLogs".start - and "TimeLogs".start < tlogs.end - and tlogs.id != "TimeLogs".id - and tlogs.resource_id = "TimeLogs".resource_id - ) - order by start -) delete from "TimeLogs" -where "TimeLogs".id in (select id from cte where rn > 1);""" + """-- first remove direct duplicates + with cte as ( + select + row_number() over (partition by resource_id, start) as rn, + id, + start, + "end", + resource_id + from "TimeLogs" + where exists ( + select + 1 + from "TimeLogs" as tlogs + where tlogs.start <= "TimeLogs".start + and "TimeLogs".start < tlogs.end + and tlogs.id != "TimeLogs".id + and tlogs.resource_id = "TimeLogs".resource_id + ) + order by start + ) delete from "TimeLogs" + where "TimeLogs".id in (select id from cte where rn > 1);""" ) logger.info( "Removing contained TimeLog entries (TimeLogs surrounded by other " "TimeLogs" ) op.execute( - """ --- remove any contained (TimeLogs surrounded by other TimeLogs) TimeLogs -with cte as ( - select - "TimeLogs".id, - "TimeLogs".start, - "TimeLogs".end, - "TimeLogs".resource_id - from "TimeLogs" - join "TimeLogs" as tlogs on - "TimeLogs".start > tlogs.start and "TimeLogs".start < tlogs.end - and "TimeLogs".end > tlogs.start and "TimeLogs".end < tlogs.end - and "TimeLogs".resource_id = tlogs.resource_id -) delete from "TimeLogs" -where "TimeLogs".id in (select id from cte);""" + """-- remove any contained (TimeLogs surrounded by other TimeLogs) TimeLogs + with cte as ( + select + "TimeLogs".id, + "TimeLogs".start, + "TimeLogs".end, + "TimeLogs".resource_id + from "TimeLogs" + join "TimeLogs" as tlogs on + "TimeLogs".start > tlogs.start and "TimeLogs".start < tlogs.end + and "TimeLogs".end > tlogs.start and "TimeLogs".end < tlogs.end + and "TimeLogs".resource_id = tlogs.resource_id + ) delete from "TimeLogs" + where "TimeLogs".id in (select id from cte);""" ) logger.info("Trimming residual overlapping TimeLog.end values") op.execute( + """-- then trim the end dates of the TimeLogs that are still overlapping with others + update "TimeLogs" + set "end" = ( + select + tlogs.start + from "TimeLogs" as tlogs + where "TimeLogs".start < tlogs.start and "TimeLogs".end > tlogs.start + and "TimeLogs".resource_id = tlogs.resource_id + limit 1 + ) + where "TimeLogs".end - "TimeLogs".start > interval '10 min' + and exists( + select + 1 + from "TimeLogs" as tlogs + where "TimeLogs".start < tlogs.start and "TimeLogs".end > tlogs.start + and "TimeLogs".resource_id = tlogs.resource_id + ); """ --- then trim the end dates of the TimeLogs that are still overlapping with others -update "TimeLogs" -set "end" = ( - select - tlogs.start - from "TimeLogs" as tlogs - where "TimeLogs".start < tlogs.start and "TimeLogs".end > tlogs.start - and "TimeLogs".resource_id = tlogs.resource_id - limit 1 -) -where "TimeLogs".end - "TimeLogs".start > interval '10 min' - and exists( - select - 1 - from "TimeLogs" as tlogs - where "TimeLogs".start < tlogs.start and "TimeLogs".end > tlogs.start - and "TimeLogs".resource_id = tlogs.resource_id - );""" ) logger.info("Trimming residual overlapping TimeLog.start values") op.execute( + """-- then trim the start dates of the TimeLogs that are still overlapping with + -- others (there may be 10 min TimeLogs left in the previous query) + update "TimeLogs" + set start = ( + select + tlogs.end + from "TimeLogs" as tlogs + where "TimeLogs".start > tlogs.start and "TimeLogs".start < tlogs.end + and "TimeLogs".resource_id = tlogs.resource_id + limit 1 + ) + where "TimeLogs".end - "TimeLogs".start > interval '10 min' + and exists( + select + 1 + from "TimeLogs" as tlogs + where "TimeLogs".start > tlogs.start and "TimeLogs".start < tlogs.end + and "TimeLogs".resource_id = tlogs.resource_id + limit 1 + ); """ --- then trim the start dates of the TimeLogs that are still overlapping with --- others (there may be 10 min TimeLogs left in the previous query) -update "TimeLogs" -set start = ( - select - tlogs.end - from "TimeLogs" as tlogs - where "TimeLogs".start > tlogs.start and "TimeLogs".start < tlogs.end - and "TimeLogs".resource_id = tlogs.resource_id - limit 1 -) -where "TimeLogs".end - "TimeLogs".start > interval '10 min' - and exists( - select - 1 - from "TimeLogs" as tlogs - where "TimeLogs".start > tlogs.start and "TimeLogs".start < tlogs.end - and "TimeLogs".resource_id = tlogs.resource_id - limit 1 - );""" ) logger.info("Adding CheckConstraint(end > start) to TimeLogs table") diff --git a/alembic/versions/433d9caaafab_task_review_status_workflow.py b/alembic/versions/433d9caaafab_task_review_status_workflow.py index 824934b6..9767fe98 100644 --- a/alembic/versions/433d9caaafab_task_review_status_workflow.py +++ b/alembic/versions/433d9caaafab_task_review_status_workflow.py @@ -157,14 +157,7 @@ def upgrade(): sa.Column("dependency_target", task_dependency_target_enum, nullable=True), ) # fill data - op.execute( - """ - UPDATE - "Task_Dependencies" - SET - dependency_target = 'onend' - """ - ) + op.execute("""UPDATE "Task_Dependencies" SET dependency_target = 'onend'""") # alter column to be nullable false op.alter_column( @@ -182,14 +175,7 @@ def upgrade(): "Task_Dependencies", sa.Column("gap_constraint", sa.Integer(), nullable=True) ) # fill data - op.execute( - """ - UPDATE - "Task_Dependencies" - SET - gap_constraint = 0 - """ - ) + op.execute("""UPDATE "Task_Dependencies" SET gap_constraint = 0 """) # alter column to be nullable false op.alter_column( @@ -204,14 +190,7 @@ def upgrade(): sa.Column("gap_model", task_dependency_gap_model, nullable=True), ) # fill data - op.execute( - """ - UPDATE - "Task_Dependencies" - SET - gap_model = 'length' - """ - ) + op.execute("""UPDATE "Task_Dependencies" SET gap_model = 'length'""") # alter column to be nullable false op.alter_column( diff --git a/alembic/versions/46775e4a3d96_create_enum_types.py b/alembic/versions/46775e4a3d96_create_enum_types.py index 1ff0c761..3ddbba13 100644 --- a/alembic/versions/46775e4a3d96_create_enum_types.py +++ b/alembic/versions/46775e4a3d96_create_enum_types.py @@ -19,8 +19,7 @@ def upgrade(): # create new types op.execute( - """ - CREATE TYPE "ResourceAllocationStrategy" AS ENUM + """CREATE TYPE "ResourceAllocationStrategy" AS ENUM ('minallocated', 'maxloaded', 'minloaded', 'order', 'random'); CREATE TYPE "TaskDependencyGapModel" AS ENUM ('length', 'duration'); CREATE TYPE "TaskDependencyTarget" AS ENUM ('onend', 'onstart'); @@ -34,7 +33,7 @@ def upgrade(): """ ALTER TABLE "Tasks" ALTER COLUMN bid_unit TYPE "TimeUnit" USING ((bid_unit::text)::"TimeUnit"); - """ + """ ) # remove unnecessary types @@ -47,7 +46,7 @@ def downgrade(): op.execute( """CREATE TYPE "TaskBidUnit" AS ENUM ('min', 'h', 'd', 'w', 'm', 'y'); - """ + """ ) # update the Task column to use the TimeUnit type instead of TaskBidUnit @@ -55,7 +54,7 @@ def downgrade(): """ ALTER TABLE "Tasks" ALTER COLUMN bid_unit TYPE "TaskBidUnit" USING ((bid_unit::text)::"TaskBidUnit"); - """ + """ ) # rename types @@ -68,5 +67,5 @@ def downgrade(): DROP TYPE IF EXISTS "TaskDependencyGapModel" CASCADE; DROP TYPE IF EXISTS "TaskDependencyTarget" CASCADE; DROP TYPE IF EXISTS "ReviewScheduleModel" CASCADE; - """ + """ ) diff --git a/alembic/versions/5355b569237b_version_version_of_r.py b/alembic/versions/5355b569237b_version_version_of_r.py index 95ab9613..64253008 100644 --- a/alembic/versions/5355b569237b_version_version_of_r.py +++ b/alembic/versions/5355b569237b_version_version_of_r.py @@ -29,9 +29,9 @@ def upgrade(): ) # copy data from Links.path to Links_Temp.full_path op.execute( - 'INSERT INTO "Versions".task_id ' - 'SELECT "Versions".version_of_id ' - 'FROM "Versions"' + """INSERT INTO "Versions".task_id + SELECT "Versions".version_of_id FROM "Versions" + """ ) @@ -49,7 +49,8 @@ def downgrade(): sa.Column(sa.Integer, sa.ForeignKey("Tasks.id"), nullable=False), ) op.execute( - 'INSERT INTO "Versions".version_of_id ' - 'SELECT "Versions".task_id ' - 'FROM "Versions"' + """INSERT INTO "Versions".version_of_id + SELECT "Versions".task_id + FROM "Versions" + """ ) diff --git a/alembic/versions/745b210e6907_fix_non_existing_thumbnails.py b/alembic/versions/745b210e6907_fix_non_existing_thumbnails.py index 9921b9b3..c6c1bd3c 100644 --- a/alembic/versions/745b210e6907_fix_non_existing_thumbnails.py +++ b/alembic/versions/745b210e6907_fix_non_existing_thumbnails.py @@ -16,17 +16,14 @@ def upgrade(): """Fix SimpleEntities with none-existing thumbnail_id's.""" op.execute( """ - update - "SimpleEntities" - set thumbnail_id = NULL - where - "SimpleEntities".thumbnail_id is not NULL - and not exists( - select - thum.id - from "SimpleEntities" as thum - where thum.id = "SimpleEntities".thumbnail_id - ) + UPDATE "SimpleEntities" SET thumbnail_id = NULL + WHERE "SimpleEntities".thumbnail_id is not NULL + and not exists( + select + thum.id + from "SimpleEntities" as thum + where thum.id = "SimpleEntities".thumbnail_id + ) """ ) diff --git a/alembic/versions/9f9b88fef376_link_renamed_to_file.py b/alembic/versions/9f9b88fef376_link_renamed_to_file.py new file mode 100644 index 00000000..88851d20 --- /dev/null +++ b/alembic/versions/9f9b88fef376_link_renamed_to_file.py @@ -0,0 +1,805 @@ +"""Link renamed to File + +Revision ID: 9f9b88fef376 +Revises: 3be540ad3a93 +Create Date: 2025-01-14 15:37:15.746961 +""" + +from alembic import op + +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +import stalker + +# revision identifiers, used by Alembic. +revision = "9f9b88fef376" +down_revision = "3be540ad3a93" + + +def upgrade(): + """Upgrade the tables.""" + # ------------------------------------------------------------------------- + # Links -> Files + op.rename_table("Links", "Files") + + # link_id_fkey + op.drop_constraint("Links_id_fkey", table_name="Files", type_="foreignkey") + op.create_foreign_key("Files_id_fkey", "Files", "Entities", ["id"], ["id"]) + # Links_pkey -> Files_pkey + # This requires a lot of other constraints to be dropped first!!! + op.drop_constraint("Daily_Links_link_id_fkey", "Daily_Links", type_="foreignkey") + op.drop_constraint( + "Project_References_link_id_fkey", "Project_References", type_="foreignkey" + ) + op.drop_constraint( + "Task_References_link_id_fkey", "Task_References", type_="foreignkey" + ) + op.drop_constraint( + "Version_Inputs_link_id_fkey", "Version_Inputs", type_="foreignkey" + ) + op.drop_constraint( + "Version_Outputs_link_id_fkey", "Version_Outputs", type_="foreignkey" + ) + op.drop_constraint("Versions_id_fkey", "Versions", type_="foreignkey") + op.drop_constraint("z", "SimpleEntities", type_="foreignkey") + op.drop_constraint("Links_pkey", "Files", type_="primary") + + op.create_primary_key("Files_pkey", "Files", ["id"]) + + # Update "SimpleEntities".entity_type to 'File' + # and replace the 'Link_%' in the name with 'File_%' + op.execute( + """UPDATE "SimpleEntities" + SET (entity_type, name) = ('File', REPLACE("SimpleEntities".name, 'Link_', 'File_')) + WHERE "SimpleEntities".entity_type = 'Link' + """ + ) + + # Update "EntityTypes".name for 'Link' to 'File' + op.execute( + """UPDATE "EntityTypes" + SET name = 'File' + WHERE "EntityTypes".name = 'Link' + """ + ) + + # Update any Types where target_entity_type == 'Link' to 'File' + op.execute( + """UPDATE "Types" + SET target_entity_type = 'File' + WHERE target_entity_type = 'Link' + """ + ) + + # create "Files".created_with + op.add_column( + "Files", + sa.Column("created_with", sa.String(length=256), nullable=True), + ) + + # migrate the created_with data from Versions to the Files table + op.execute( + """UPDATE "Files" SET created_with = "Versions".created_with + FROM "Versions" + WHERE "Versions".id = "Files".id + """ + ) + + # ------------------------------------------------------------------------- + # remove created_with from Versions table + op.drop_column("Versions", "created_with") + + # ------------------------------------------------------------------------- + # Daily_Links -> Daily_Files + op.rename_table("Daily_Links", "Daily_Files") + + # drop constraints + # daily_id_fkey + op.drop_constraint( + "Daily_Links_daily_id_fkey", table_name="Daily_Files", type_="foreignkey" + ) + # link_id_fkey + # dropped already above! + # pkey + op.drop_constraint("Daily_Links_pkey", table_name="Daily_Files") + + # link_id -> file_id + op.alter_column( + table_name="Daily_Files", + column_name="link_id", + new_column_name="file_id", + ) + + # daily_id_fkey + op.create_foreign_key( + "Daily_Files_daily_id_fkey", + "Daily_Files", + "Dailies", + ["daily_id"], + ["id"], + ) + + # link_id_fkey -> file_id_fkey + op.create_foreign_key( + "Daily_Files_file_id_fkey", + "Daily_Files", + "Files", + ["file_id"], + ["id"], + ) + + # pkey + op.create_primary_key("Daily_Files_pkey", "Daily_Files", ["daily_id", "file_id"]) + + # ------------------------------------------------------------------------- + # Version_Outputs -> Version_Files (to preserve data) + op.rename_table("Version_Outputs", "Version_Files") + + # Constraints + # drop constraints + # link_id_fkey + # dropped already above! + # version_id_fkey + op.drop_constraint( + "Version_Outputs_version_id_fkey", "Version_Files", type_="foreignkey" + ) + # pkey + op.drop_constraint( + "Version_Outputs_pkey", table_name="Version_Files", type_="primary" + ) + + # link_id -> file_id + op.alter_column("Version_Files", "link_id", new_column_name="file_id") + + # version_id_fkey + op.create_foreign_key( + "Version_Files_version_id_fkey", + "Version_Files", + "Versions", + ["version_id"], + ["id"], + ) + + # file_id_fkey + op.create_foreign_key( + "Version_Files_file_id_fkey", + "Version_Files", + "Files", + ["file_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + + # pkey + op.create_primary_key( + "Version_Files_pkey", "Version_Files", ["version_id", "file_id"] + ) + + # ------------------------------------------------------------------------- + # Rename Version_Inputs to File_References: + # + # This table is storing which Version was referencing which other + # version. + # + # Data needs to be migrated in tandem with the data moved to the + # Version_Files table. + op.rename_table("Version_Inputs", "File_References") + op.alter_column("File_References", "link_id", new_column_name="reference_id") + op.alter_column("File_References", "version_id", new_column_name="file_id") + + # Version_Inputs_version_id_fkey -> File_References_file_id_fkey + # Version_Inputs_link_id_fkey -> File_References_reference_id_fkey + op.drop_constraint( + "Version_Inputs_version_id_fkey", + table_name="File_References", + type_="foreignkey", + ) + # "Version_Inputs_link_id_fkey" dropped already above! + op.drop_constraint( + "Version_Inputs_pkey", + table_name="File_References", + type_="primary", + ) + + # File_References_file_id_fkey + op.create_foreign_key( + "File_References_file_id_fkey", + "File_References", + "Files", + ["file_id"], + ["id"], + ) + + # File_References_reference_id_fkey + op.create_foreign_key( + "File_References_reference_id_fkey", + "File_References", + "Files", + ["reference_id"], + ["id"], + ) + + # File_References_pkey + op.create_primary_key( + "File_References_pkey", + "File_References", + ["file_id", "reference_id"], + ) + + # ------------------------------------------------------------------------- + # Project_References + # link_id -> file_id + # dropped already above! + op.alter_column("Project_References", "link_id", new_column_name="reference_id") + + # Constraints + # link_id_fkey -> reference_fkey + op.create_foreign_key( + "Project_References_reference_id_fkey", + "Project_References", + "Files", + ["reference_id"], + ["id"], + ) + + # ------------------------------------------------------------------------- + # Fix fkey names + + # SimpleEntities_thumbnail_id_fkey + # dropped already above! + op.create_foreign_key( + "SimpleEntities_thumbnail_id_fkey", + "SimpleEntities", + "Files", + ["thumbnail_id"], + ["id"], + use_alter=True, + ) + + # SimpleEntities_created_by_id_fkey + op.drop_constraint("xc", "SimpleEntities", type_="foreignkey") + op.create_foreign_key( + "SimpleEntities_created_by_id_fkey", + "SimpleEntities", + "Users", + ["created_by_id"], + ["id"], + use_alter=True, + ) + + # SimpleEntities_updated_by_id_fkey + op.drop_constraint("xu", "SimpleEntities", type_="foreignkey") + op.create_foreign_key( + "SimpleEntities_updated_by_id_fkey", + "SimpleEntities", + "Users", + ["updated_by_id"], + ["id"], + use_alter=True, + ) + + # SimpleEntities_type_id_fkey + op.drop_constraint("y", "SimpleEntities", type_="foreignkey") + op.create_foreign_key( + "SimpleEntities_type_id_fkey", + "SimpleEntities", + "Types", + ["type_id"], + ["id"], + use_alter=True, + ) + + # ------------------------------------------------------------------------- + # Task_References link_id -> reference_id + # dropped already above! + + # rename the column: link_id -> reference_id + op.alter_column("Task_References", "link_id", new_column_name="reference_id") + + # link_id_fkey -> file_id_fkey + op.create_foreign_key( + "Task_References_reference_id_fkey", + "Task_References", + "Files", + ["reference_id"], + ["id"], + ) + + # ------------------------------------------------------------------------- + # Versions is now deriving from Entities + # dropped already above! + op.create_foreign_key("Versions_id_fkey", "Versions", "Entities", ["id"], ["id"]) + + # ------------------------------------------------------------------------- + # Migrate Files that were Versions before, to new entries... + # - Go back to Files tables + # - Search for Files that have the same ids with Versions + # - Create a new entry with the same data to Files, Entities and SimpleEntities + # tables. + # - Add them to the Version_Files tables, as they were previous versions + # - Delete the old files + op.execute( + f""" + -- reshuffle names for entities that have autoname and that are clashing + UPDATE "SimpleEntities" + SET name = ("SimpleEntities".entity_type || '_' || gen_random_uuid()) + WHERE name in ( + SELECT + name + FROM "SimpleEntities" + WHERE length(name) > 37 -- entity_type_uuid4 + GROUP BY name + HAVING COUNT(*) > 1 + ORDER BY name + ); + + -- create temp storage for data coming from "Files" table + ALTER TABLE "SimpleEntities" + ADD original_filename character varying(256) COLLATE pg_catalog."default", + ADD full_path text COLLATE pg_catalog."default", + ADD created_from_version_id integer, + ADD created_with character varying(256); + + -- create new entry for all Files that were originally Versions + WITH sel1 as ( + SELECT + "File_SimpleEntities".id, + 'File' as entity_type, + REPLACE("File_SimpleEntities".name, 'Version_', 'File_') as entity_name, + "File_SimpleEntities".description, + "File_SimpleEntities".created_by_id, + "File_SimpleEntities".updated_by_id, + "File_SimpleEntities".date_created, + "File_SimpleEntities".date_updated, + "File_SimpleEntities".type_id, + "File_SimpleEntities".generic_text, + "File_SimpleEntities".thumbnail_id, + "File_SimpleEntities".html_style, + "File_SimpleEntities".html_class, + "File_SimpleEntities".stalker_version, + "Files".original_filename, + "Files".full_path, + "Files".created_with + FROM "Files" + JOIN "SimpleEntities" AS "File_SimpleEntities" ON "Files".id = "File_SimpleEntities".id + WHERE "File_SimpleEntities".entity_type = 'Version' + ORDER BY "File_SimpleEntities".id + ), ins1 as ( + INSERT INTO "SimpleEntities" ( + entity_type, + name, + description, + created_by_id, + updated_by_id, + date_created, + date_updated, + type_id, + html_style, + html_class, + stalker_version, + original_filename, + full_path, + created_with, + created_from_version_id + ) ( + SELECT + sel1.entity_type, + sel1.entity_name, + sel1.description, + sel1.created_by_id, + sel1.updated_by_id, + sel1.date_created, + sel1.date_updated, + sel1.type_id, + sel1.html_style, + sel1.html_class, + sel1.stalker_version, + sel1.original_filename, + sel1.full_path, + sel1.created_with, + sel1.id as created_from_version_id + FROM sel1 + ) + RETURNING id as file_id, name as entity_name + ) + INSERT INTO "Entities" (id) (SELECT ins1.file_id FROM ins1); + + -- Insert into Files + INSERT INTO "Files" (id, original_filename, full_path, created_with) + ( + SELECT + "SimpleEntities".id, + "SimpleEntities".original_filename, + "SimpleEntities".full_path, + "SimpleEntities".created_with + FROM "SimpleEntities" + WHERE "SimpleEntities".created_from_version_id IS NOT NULL + ); + + -- Insert into Version_Files + INSERT INTO "Version_Files" (version_id, file_id) + ( + SELECT + "SimpleEntities".created_from_version_id as version_id, + "SimpleEntities".id as file_id + FROM "SimpleEntities" + WHERE "SimpleEntities".created_from_version_id IS NOT NULL + ); + + -- Update File_References + -- so that the newly created Files + -- are referencing the newly create other Files and not the old versions + + -- Update the file_id column first + UPDATE "File_References" SET file_id = sel1.file_id + FROM ( + SELECT + id as file_id, + created_from_version_id + FROM "SimpleEntities" + WHERE created_from_version_id IS NOT NULL + ) as sel1 + WHERE "File_References".file_id = sel1.created_from_version_id; + + -- then the reference_id column + UPDATE "File_References" SET reference_id = sel1.file_id + FROM ( + SELECT + id as file_id, + created_from_version_id + FROM "SimpleEntities" + WHERE created_from_version_id IS NOT NULL + ) as sel1 + WHERE "File_References".reference_id = sel1.created_from_version_id; + + -- Drop all the Files that previously was a Version + + -- Remove constraints first (Otherwise it will be incredibly slow!) + ALTER TABLE "SimpleEntities" DROP CONSTRAINT "SimpleEntities_thumbnail_id_fkey"; + ALTER TABLE "Daily_Files" DROP CONSTRAINT "Daily_Files_file_id_fkey"; + ALTER TABLE "Project_References" DROP CONSTRAINT "Project_References_reference_id_fkey"; + ALTER TABLE "Task_References" DROP CONSTRAINT "Task_References_reference_id_fkey"; + ALTER TABLE "File_References" DROP CONSTRAINT "File_References_file_id_fkey"; + ALTER TABLE "File_References" DROP CONSTRAINT "File_References_reference_id_fkey"; + ALTER TABLE "Version_Files" DROP CONSTRAINT "Version_Files_file_id_fkey"; + ALTER TABLE "Files" DROP CONSTRAINT "Files_id_fkey"; + + -- Really delete data now + DELETE FROM "Files" + WHERE id in ( + SELECT + id + FROM "SimpleEntities" + WHERE entity_type = 'Version' + ); + + -- Recreate constraints + ALTER TABLE "SimpleEntities" + ADD CONSTRAINT "SimpleEntities_thumbnail_id_fkey" FOREIGN KEY (thumbnail_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + ALTER TABLE "Daily_Files" + ADD CONSTRAINT "Daily_Files_file_id_fkey" FOREIGN KEY (file_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + ALTER TABLE "Project_References" + ADD CONSTRAINT "Project_References_reference_id_fkey" FOREIGN KEY (reference_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + ALTER TABLE "Task_References" + ADD CONSTRAINT "Task_References_reference_id_fkey" FOREIGN KEY (reference_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + ALTER TABLE "File_References" + ADD CONSTRAINT "File_References_file_id_fkey" FOREIGN KEY (file_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION, + ADD CONSTRAINT "File_References_reference_id_fkey" FOREIGN KEY (reference_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + ALTER TABLE "Version_Files" + ADD CONSTRAINT "Version_Files_file_id_fkey" FOREIGN KEY (file_id) + REFERENCES public."Files" (id) MATCH SIMPLE + ON UPDATE CASCADE + ON DELETE CASCADE; + ALTER TABLE "Files" + ADD CONSTRAINT "Files_id_fkey" FOREIGN KEY (id) + REFERENCES public."Entities" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION; + + -- delete temp data in "SimpleEntities" + ALTER TABLE "SimpleEntities" + DROP COLUMN original_filename, + DROP COLUMN full_path, + DROP COLUMN created_from_version_id, + DROP COLUMN created_with; + """ + ) + + +def downgrade(): + """Downgrade the tables.""" + # drop constraints first + op.drop_constraint("Daily_Files_pkey", "Daily_Files", type_="primary") + op.drop_constraint("Daily_Files_file_id_fkey", "Daily_Files", type_="foreignkey") + op.drop_constraint("Daily_Files_daily_id_fkey", "Daily_Files", type_="foreignkey") + op.drop_constraint("File_References_pkey", "File_References", type_="primary") + op.drop_constraint("File_References_file_id_fkey", "File_References", type_="foreignkey") + op.drop_constraint("File_References_reference_id_fkey", "File_References", type_="foreignkey") + op.drop_constraint("Files_pkey", "Files", type_="primary") + op.drop_constraint("Files_id_fkey", "Files", type_="foreignkey") + op.drop_constraint("Project_References_reference_id_fkey", "Project_References", type_="foreignkey") + op.drop_constraint("SimpleEntities_created_by_id_fkey", "SimpleEntities", type_="foreignkey") + op.drop_constraint("SimpleEntities_thumbnail_id_fkey", "SimpleEntities", type_="foreignkey") + op.drop_constraint("SimpleEntities_updated_by_id_fkey", "SimpleEntities", type_="foreignkey") + op.drop_constraint("SimpleEntities_type_id_fkey", "SimpleEntities", type_="foreignkey") + op.drop_constraint("Task_References_reference_id_fkey", "Task_References", type_="foreignkey") + op.drop_constraint("Version_Files_pkey", "Version_Files", type_="primary") + op.drop_constraint("Version_Files_file_id_fkey", "Version_Files", type_="foreignkey") + op.drop_constraint("Versions_id_fkey", "Versions", type_="foreignkey") + + # rename tables + op.rename_table("Daily_Files", "Daily_Links") + op.rename_table("Files", "Links") + op.rename_table("File_References", "Version_Inputs") + op.rename_table("Version_Files", "Version_Outputs") + + # rename columns + op.alter_column("Daily_Links", "file_id", new_column_name="link_id") + op.alter_column("Version_Outputs", "file_id", new_column_name="link_id") + op.alter_column("Version_Inputs", "file_id", new_column_name="version_id") + op.alter_column("Version_Inputs", "reference_id", new_column_name="link_id") + op.alter_column("Project_References", "reference_id", new_column_name="link_id") + op.alter_column("Task_References", "reference_id", new_column_name="link_id") + + # migrate the data as much as you can + op.execute( + """ + -- There are Versions where there are no corresponding input in the + -- Links table anymore + -- Update all the ids of the Links that are in the Version_Inputs with the + -- id of the version, so that we have a corresponding links for all versions + -- and then delete all the entries from the Entities and SimpleEntities tables + -- for those links. + + UPDATE "Links" SET id = sel1.id FROM ( + SELECT + link_id + FROM "Version_Links" + ) + + """ + ) + + # recreate constraints + op.create_foreign_key("Daily_Links_daily_id_fkey", "Daily_Links", "Dailies", ["daily_id"], ["id"]) + op.create_foreign_key("Daily_Links_link_id_fkey", "Daily_Links", "Links", ["link_id"], ["id"]) + op.create_primary_key("Daily_Links_pkey", "Daily_Links", ["daily_id", "link_id"]) + op.create_primary_key("Links_pkey", "Links", ["id"]) + op.create_foreign_key("Link_id_fkey", "Links", "Entities", ["id"], ["id"]) + op.create_foreign_key("Project_References_link_id_fkey", "Project_References", ["link_id"], ["id"]) + op.create_foreign_key("Task_References_link_id_fkey", "Task_References", "Links", ["link_id"], ["id"]) + op.create_foreign_key("Version_Inputs_version_id_fkey", "Version_Inputs", "Versions", ["version_id"], ["id"]) + op.create_foreign_key("Version_Inputs_link_id_fkey", "Version_Inputs", "Links", ["link_id"], ["id"]) + op.create_primary_key("Version_Inputs_pkey", "Version_Inputs", ["version_id", "link_id"]) + op.create_primary_key("Version_Outputs_pkey", "Version_Outputs", ["version_id", "link_id"]) + op.create_foreign_key("Version_Outputs_link_id_fkey", "Versions_Outputs", "Links", ["link_id"], ["id"]) + op.create_foreign_key("Version_Outputs_version_id_fkey", "Version_Outputs", "Links", ["version_id"], ["id"]) + op.create_foreign_key("Versions_id_fkey", "Versions", "Links", ["id"], ["id"]) + op.create_foreign_key("xc", "SimpleEntities", "Users", ["created_by_id"], ["id"], use_alter=True) + op.create_foreign_key("xu", "SimpleEntities", "Users", ["updated_by_id"], ["id"], use_alter=True) + op.create_foreign_key("y", "SimpleEntities", "Types", ["type_id"], ["id"], use_alter=True) + op.create_foreign_key("z", "SimpleEntities", "Links", ["thumbnail_id"], ["id"], use_alter=True) + + + + + + + + + + + + + + # pkey + op.create_primary_key( + "Daily_Files_pkey", + "Daily_Files", + ["daily_id", "file_id"], + ) + + + + + # Rename tables + op.rename_table("Files", "Links") + op.rename_table("Daily_Files", "Daily_Links") + + + + # ------------------------------------------------------------------------- + # Versions should derive from Links again + + op.create_foreign_key("Versions_id_fkey", "Versions", "Entities", ["id"], ["id"]) + + + # Versions.created_with + op.add_column( + "Versions", + sa.Column( + "created_with", sa.VARCHAR(length=256), autoincrement=False, nullable=True + ), + ) + + # Versions derive from Links + op.drop_constraint(None, "Versions", type_="foreignkey") + + + # ------------------------------------------------------------------------- + # Task_References reference_id -> link_id + # dropped already above! + + # rename the column: link_id -> reference_id + op.alter_column("Task_References", "link_id", new_column_name="reference_id") + + # link_id_fkey -> file_id_fkey + op.create_foreign_key( + "Task_References_reference_id_fkey", + "Task_References", + "Files", + ["reference_id"], + ["id"], + ) + + + + + + + + + + + + + + # + op.create_foreign_key("Versions_id_fkey", "Versions", "Links", ["id"], ["id"]) + op.alter_column( + "Tasks", + "schedule_model", + existing_type=stalker.models.enum.ScheduleModelDecorator( + "effort", "duration", "length" + ), + type_=postgresql.ENUM("effort", "length", "duration", name="TaskScheduleModel"), + existing_nullable=False, + ) + op.add_column( + "Task_References", + sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), + ) + op.drop_constraint(None, "Task_References", type_="foreignkey") + op.create_foreign_key( + "Task_References_link_id_fkey", "Task_References", "Links", ["link_id"], ["id"] + ) + op.drop_column("Task_References", "reference_id") + op.alter_column( + "Task_Dependencies", + "gap_model", + existing_type=stalker.models.enum.ScheduleModelDecorator( + "effort", "duration", "length" + ), + type_=postgresql.ENUM("length", "duration", name="TaskDependencyGapModel"), + existing_nullable=False, + ) + op.drop_constraint("z", "SimpleEntities", type_="foreignkey") + op.create_foreign_key("z", "SimpleEntities", "Links", ["thumbnail_id"], ["id"]) + op.alter_column( + "Shots", + "fps", + existing_type=sa.Float(precision=3), + type_=sa.REAL(), + existing_nullable=True, + ) + op.alter_column( + "Reviews", + "schedule_model", + existing_type=stalker.models.enum.ScheduleModelDecorator( + "effort", "duration", "length" + ), + type_=postgresql.ENUM( + "effort", "length", "duration", name="ReviewScheduleModel" + ), + existing_nullable=False, + ) + op.alter_column( + "Projects", + "fps", + existing_type=sa.Float(precision=3), + type_=sa.REAL(), + existing_nullable=True, + ) + op.add_column( + "Project_References", + sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), + ) + op.drop_constraint(None, "Project_References", type_="foreignkey") + op.create_foreign_key( + "Project_References_link_id_fkey", + "Project_References", + "Links", + ["link_id"], + ["id"], + ) + op.drop_column("Project_References", "reference_id") + op.create_table( + "Links", + sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column( + "original_filename", + sa.VARCHAR(length=256), + autoincrement=False, + nullable=True, + ), + sa.Column("full_path", sa.TEXT(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(["id"], ["Entities.id"], name="Links_id_fkey"), + sa.PrimaryKeyConstraint("id", name="Links_pkey"), + postgresql_ignore_search_path=False, + ) + op.create_table( + "Version_Inputs", + sa.Column("version_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint( + ["link_id"], + ["Links.id"], + name="Version_Inputs_link_id_fkey", + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["version_id"], ["Versions.id"], name="Version_Inputs_version_id_fkey" + ), + sa.PrimaryKeyConstraint("version_id", "link_id", name="Version_Inputs_pkey"), + ) + op.create_table( + "Version_Outputs", + sa.Column("version_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint( + ["link_id"], ["Links.id"], name="Version_Outputs_link_id_fkey" + ), + sa.ForeignKeyConstraint( + ["version_id"], ["Versions.id"], name="Version_Outputs_version_id_fkey" + ), + sa.PrimaryKeyConstraint("version_id", "link_id", name="Version_Outputs_pkey"), + ) + op.create_table( + "Daily_Links", + sa.Column("daily_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column("rank", sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint( + ["daily_id"], ["Dailies.id"], name="Daily_Links_daily_id_fkey" + ), + sa.ForeignKeyConstraint( + ["link_id"], ["Links.id"], name="Daily_Links_link_id_fkey" + ), + sa.PrimaryKeyConstraint("daily_id", "link_id", name="Daily_Links_pkey"), + ) + op.drop_table("Version_Files") + op.drop_table("Daily_Files") + op.drop_table("File_References") + op.drop_table("Files") + # ### end Alembic commands ### diff --git a/src/stalker/db/setup.py b/src/stalker/db/setup.py index 52665324..e90f2eb6 100644 --- a/src/stalker/db/setup.py +++ b/src/stalker/db/setup.py @@ -36,7 +36,7 @@ logger: logging.Logger = log.get_logger(__name__) # TODO: Try to get it from the API (it was not working inside a package before) -alembic_version: str = "3be540ad3a93" +alembic_version: str = "9f9b88fef376" def setup(settings: Optional[Dict[str, Any]] = None) -> None: diff --git a/src/stalker/models/entity.py b/src/stalker/models/entity.py index ab9207b6..b77bd0ae 100644 --- a/src/stalker/models/entity.py +++ b/src/stalker/models/entity.py @@ -127,7 +127,7 @@ class attribute to control auto naming behavior. ) created_by_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("Users.id", use_alter=True, name="xc"), + ForeignKey("Users.id", use_alter=True, name="SimpleEntities_created_by_id_fkey"), doc="The id of the :class:`.User` who has created this entity.", ) @@ -139,7 +139,7 @@ class attribute to control auto naming behavior. updated_by_id: Mapped[Optional[int]] = mapped_column( "updated_by_id", - ForeignKey("Users.id", use_alter=True, name="xu"), + ForeignKey("Users.id", use_alter=True, name="SimpleEntities_updated_by_id_fkey"), nullable=True, doc="The id of the :class:`.User` who has updated this entity.", ) @@ -166,7 +166,7 @@ class attribute to control auto naming behavior. ) type_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("Types.id", use_alter=True, name="y"), + ForeignKey("Types.id", use_alter=True, name="SimpleEntities_type_id_fkey"), doc="""The id of the :class:`.Type` of this entity. Mainly used by SQLAlchemy to create a Many-to-One relates between SimpleEntities and Types. @@ -198,7 +198,11 @@ class attribute to control auto naming behavior. ) thumbnail_id: Mapped[Optional[int]] = mapped_column( - ForeignKey("Files.id", use_alter=True, name="z") + ForeignKey( + "Files.id", + use_alter=True, + name="SimpleEntities_thumbnail_id_fkey", + ) ) thumbnail: Mapped[Optional["File"]] = relationship( From 4584a2ccb8fd3316c385e44742a3287797808ede Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 20 Jan 2025 14:49:46 +0000 Subject: [PATCH 10/11] [#147] Optimized the `upgrade()` function in `9f9b88fef376` alembic upgrade script. --- .../9f9b88fef376_link_renamed_to_file.py | 750 ++++++------------ 1 file changed, 223 insertions(+), 527 deletions(-) diff --git a/alembic/versions/9f9b88fef376_link_renamed_to_file.py b/alembic/versions/9f9b88fef376_link_renamed_to_file.py index 88851d20..fe9ff6b5 100644 --- a/alembic/versions/9f9b88fef376_link_renamed_to_file.py +++ b/alembic/versions/9f9b88fef376_link_renamed_to_file.py @@ -8,9 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -import stalker # revision identifiers, used by Alembic. revision = "9f9b88fef376" @@ -20,33 +17,62 @@ def upgrade(): """Upgrade the tables.""" # ------------------------------------------------------------------------- - # Links -> Files - op.rename_table("Links", "Files") - + # drop constraints first + op.drop_constraint("Links_id_fkey", table_name="Links", type_="foreignkey") + op.drop_constraint("Daily_Links_link_id_fkey", "Daily_Links", type_="foreignkey") + op.drop_constraint("Project_References_link_id_fkey", "Project_References", type_="foreignkey") + op.drop_constraint("Task_References_link_id_fkey", "Task_References", type_="foreignkey") + op.drop_constraint("Version_Inputs_link_id_fkey", "Version_Inputs", type_="foreignkey") + op.drop_constraint("Version_Outputs_link_id_fkey", "Version_Outputs", type_="foreignkey") + op.drop_constraint("Versions_id_fkey", "Versions", type_="foreignkey") + op.drop_constraint("Daily_Links_daily_id_fkey", table_name="Daily_Links", type_="foreignkey") + op.drop_constraint("Daily_Links_pkey", table_name="Daily_Links") + op.drop_constraint("Version_Outputs_version_id_fkey", "Version_Outputs", type_="foreignkey") + op.drop_constraint("Version_Outputs_pkey", table_name="Version_Outputs", type_="primary") + op.drop_constraint("Version_Inputs_version_id_fkey", table_name="Version_Inputs", type_="foreignkey") + op.drop_constraint("Version_Inputs_pkey", table_name="Version_Inputs", type_="primary") + op.drop_constraint("xc", "SimpleEntities", type_="foreignkey") + op.drop_constraint("xu", "SimpleEntities", type_="foreignkey") + op.drop_constraint("y", "SimpleEntities", type_="foreignkey") + op.drop_constraint("z", "SimpleEntities", type_="foreignkey") # link_id_fkey - op.drop_constraint("Links_id_fkey", table_name="Files", type_="foreignkey") - op.create_foreign_key("Files_id_fkey", "Files", "Entities", ["id"], ["id"]) # Links_pkey -> Files_pkey # This requires a lot of other constraints to be dropped first!!! - op.drop_constraint("Daily_Links_link_id_fkey", "Daily_Links", type_="foreignkey") - op.drop_constraint( - "Project_References_link_id_fkey", "Project_References", type_="foreignkey" - ) - op.drop_constraint( - "Task_References_link_id_fkey", "Task_References", type_="foreignkey" - ) - op.drop_constraint( - "Version_Inputs_link_id_fkey", "Version_Inputs", type_="foreignkey" - ) - op.drop_constraint( - "Version_Outputs_link_id_fkey", "Version_Outputs", type_="foreignkey" + op.drop_constraint("Links_pkey", "Links", type_="primary") + + # ------------------------------------------------------------------------- + # rename tables + op.rename_table("Links", "Files") + op.rename_table("Daily_Links", "Daily_Files") + op.rename_table("Version_Outputs", "Version_Files") + # ------------------------------------------------------------------------- + # Rename Version_Inputs to File_References: + # + # This table is storing which Version was referencing which other + # version. + # + # Data needs to be migrated in tandem with the data moved to the + # Version_Files table. + op.rename_table("Version_Inputs", "File_References") + + # ------------------------------------------------------------------------- + # create columns + # create "Files".created_with + op.add_column( + "Files", + sa.Column("created_with", sa.String(length=256), nullable=True), ) - op.drop_constraint("Versions_id_fkey", "Versions", type_="foreignkey") - op.drop_constraint("z", "SimpleEntities", type_="foreignkey") - op.drop_constraint("Links_pkey", "Files", type_="primary") - op.create_primary_key("Files_pkey", "Files", ["id"]) + # ------------------------------------------------------------------------- + # rename columns + op.alter_column("Daily_Files", "link_id", new_column_name="file_id") + op.alter_column("File_References", "link_id", new_column_name="reference_id") + op.alter_column("File_References", "version_id", new_column_name="file_id") + op.alter_column("Project_References", "link_id", new_column_name="reference_id") + op.alter_column("Task_References", "link_id", new_column_name="reference_id") + op.alter_column("Version_Files", "link_id", new_column_name="file_id") + # migrate data # Update "SimpleEntities".entity_type to 'File' # and replace the 'Link_%' in the name with 'File_%' op.execute( @@ -55,7 +81,6 @@ def upgrade(): WHERE "SimpleEntities".entity_type = 'Link' """ ) - # Update "EntityTypes".name for 'Link' to 'File' op.execute( """UPDATE "EntityTypes" @@ -63,7 +88,6 @@ def upgrade(): WHERE "EntityTypes".name = 'Link' """ ) - # Update any Types where target_entity_type == 'Link' to 'File' op.execute( """UPDATE "Types" @@ -71,13 +95,6 @@ def upgrade(): WHERE target_entity_type = 'Link' """ ) - - # create "Files".created_with - op.add_column( - "Files", - sa.Column("created_with", sa.String(length=256), nullable=True), - ) - # migrate the created_with data from Versions to the Files table op.execute( """UPDATE "Files" SET created_with = "Versions".created_with @@ -85,233 +102,6 @@ def upgrade(): WHERE "Versions".id = "Files".id """ ) - - # ------------------------------------------------------------------------- - # remove created_with from Versions table - op.drop_column("Versions", "created_with") - - # ------------------------------------------------------------------------- - # Daily_Links -> Daily_Files - op.rename_table("Daily_Links", "Daily_Files") - - # drop constraints - # daily_id_fkey - op.drop_constraint( - "Daily_Links_daily_id_fkey", table_name="Daily_Files", type_="foreignkey" - ) - # link_id_fkey - # dropped already above! - # pkey - op.drop_constraint("Daily_Links_pkey", table_name="Daily_Files") - - # link_id -> file_id - op.alter_column( - table_name="Daily_Files", - column_name="link_id", - new_column_name="file_id", - ) - - # daily_id_fkey - op.create_foreign_key( - "Daily_Files_daily_id_fkey", - "Daily_Files", - "Dailies", - ["daily_id"], - ["id"], - ) - - # link_id_fkey -> file_id_fkey - op.create_foreign_key( - "Daily_Files_file_id_fkey", - "Daily_Files", - "Files", - ["file_id"], - ["id"], - ) - - # pkey - op.create_primary_key("Daily_Files_pkey", "Daily_Files", ["daily_id", "file_id"]) - - # ------------------------------------------------------------------------- - # Version_Outputs -> Version_Files (to preserve data) - op.rename_table("Version_Outputs", "Version_Files") - - # Constraints - # drop constraints - # link_id_fkey - # dropped already above! - # version_id_fkey - op.drop_constraint( - "Version_Outputs_version_id_fkey", "Version_Files", type_="foreignkey" - ) - # pkey - op.drop_constraint( - "Version_Outputs_pkey", table_name="Version_Files", type_="primary" - ) - - # link_id -> file_id - op.alter_column("Version_Files", "link_id", new_column_name="file_id") - - # version_id_fkey - op.create_foreign_key( - "Version_Files_version_id_fkey", - "Version_Files", - "Versions", - ["version_id"], - ["id"], - ) - - # file_id_fkey - op.create_foreign_key( - "Version_Files_file_id_fkey", - "Version_Files", - "Files", - ["file_id"], - ["id"], - onupdate="CASCADE", - ondelete="CASCADE", - ) - - # pkey - op.create_primary_key( - "Version_Files_pkey", "Version_Files", ["version_id", "file_id"] - ) - - # ------------------------------------------------------------------------- - # Rename Version_Inputs to File_References: - # - # This table is storing which Version was referencing which other - # version. - # - # Data needs to be migrated in tandem with the data moved to the - # Version_Files table. - op.rename_table("Version_Inputs", "File_References") - op.alter_column("File_References", "link_id", new_column_name="reference_id") - op.alter_column("File_References", "version_id", new_column_name="file_id") - - # Version_Inputs_version_id_fkey -> File_References_file_id_fkey - # Version_Inputs_link_id_fkey -> File_References_reference_id_fkey - op.drop_constraint( - "Version_Inputs_version_id_fkey", - table_name="File_References", - type_="foreignkey", - ) - # "Version_Inputs_link_id_fkey" dropped already above! - op.drop_constraint( - "Version_Inputs_pkey", - table_name="File_References", - type_="primary", - ) - - # File_References_file_id_fkey - op.create_foreign_key( - "File_References_file_id_fkey", - "File_References", - "Files", - ["file_id"], - ["id"], - ) - - # File_References_reference_id_fkey - op.create_foreign_key( - "File_References_reference_id_fkey", - "File_References", - "Files", - ["reference_id"], - ["id"], - ) - - # File_References_pkey - op.create_primary_key( - "File_References_pkey", - "File_References", - ["file_id", "reference_id"], - ) - - # ------------------------------------------------------------------------- - # Project_References - # link_id -> file_id - # dropped already above! - op.alter_column("Project_References", "link_id", new_column_name="reference_id") - - # Constraints - # link_id_fkey -> reference_fkey - op.create_foreign_key( - "Project_References_reference_id_fkey", - "Project_References", - "Files", - ["reference_id"], - ["id"], - ) - - # ------------------------------------------------------------------------- - # Fix fkey names - - # SimpleEntities_thumbnail_id_fkey - # dropped already above! - op.create_foreign_key( - "SimpleEntities_thumbnail_id_fkey", - "SimpleEntities", - "Files", - ["thumbnail_id"], - ["id"], - use_alter=True, - ) - - # SimpleEntities_created_by_id_fkey - op.drop_constraint("xc", "SimpleEntities", type_="foreignkey") - op.create_foreign_key( - "SimpleEntities_created_by_id_fkey", - "SimpleEntities", - "Users", - ["created_by_id"], - ["id"], - use_alter=True, - ) - - # SimpleEntities_updated_by_id_fkey - op.drop_constraint("xu", "SimpleEntities", type_="foreignkey") - op.create_foreign_key( - "SimpleEntities_updated_by_id_fkey", - "SimpleEntities", - "Users", - ["updated_by_id"], - ["id"], - use_alter=True, - ) - - # SimpleEntities_type_id_fkey - op.drop_constraint("y", "SimpleEntities", type_="foreignkey") - op.create_foreign_key( - "SimpleEntities_type_id_fkey", - "SimpleEntities", - "Types", - ["type_id"], - ["id"], - use_alter=True, - ) - - # ------------------------------------------------------------------------- - # Task_References link_id -> reference_id - # dropped already above! - - # rename the column: link_id -> reference_id - op.alter_column("Task_References", "link_id", new_column_name="reference_id") - - # link_id_fkey -> file_id_fkey - op.create_foreign_key( - "Task_References_reference_id_fkey", - "Task_References", - "Files", - ["reference_id"], - ["id"], - ) - - # ------------------------------------------------------------------------- - # Versions is now deriving from Entities - # dropped already above! - op.create_foreign_key("Versions_id_fkey", "Versions", "Entities", ["id"], ["id"]) - # ------------------------------------------------------------------------- # Migrate Files that were Versions before, to new entries... # - Go back to Files tables @@ -457,14 +247,14 @@ def upgrade(): -- Drop all the Files that previously was a Version -- Remove constraints first (Otherwise it will be incredibly slow!) - ALTER TABLE "SimpleEntities" DROP CONSTRAINT "SimpleEntities_thumbnail_id_fkey"; - ALTER TABLE "Daily_Files" DROP CONSTRAINT "Daily_Files_file_id_fkey"; - ALTER TABLE "Project_References" DROP CONSTRAINT "Project_References_reference_id_fkey"; - ALTER TABLE "Task_References" DROP CONSTRAINT "Task_References_reference_id_fkey"; - ALTER TABLE "File_References" DROP CONSTRAINT "File_References_file_id_fkey"; - ALTER TABLE "File_References" DROP CONSTRAINT "File_References_reference_id_fkey"; - ALTER TABLE "Version_Files" DROP CONSTRAINT "Version_Files_file_id_fkey"; - ALTER TABLE "Files" DROP CONSTRAINT "Files_id_fkey"; + -- ALTER TABLE "SimpleEntities" DROP CONSTRAINT "SimpleEntities_thumbnail_id_fkey"; + -- ALTER TABLE "Daily_Files" DROP CONSTRAINT "Daily_Files_file_id_fkey"; + -- ALTER TABLE "Project_References" DROP CONSTRAINT "Project_References_reference_id_fkey"; + -- ALTER TABLE "Task_References" DROP CONSTRAINT "Task_References_reference_id_fkey"; + -- ALTER TABLE "File_References" DROP CONSTRAINT "File_References_file_id_fkey"; + -- ALTER TABLE "File_References" DROP CONSTRAINT "File_References_reference_id_fkey"; + -- ALTER TABLE "Version_Files" DROP CONSTRAINT "Version_Files_file_id_fkey"; + -- ALTER TABLE "Files" DROP CONSTRAINT "Files_id_fkey"; -- Really delete data now DELETE FROM "Files" @@ -476,45 +266,45 @@ def upgrade(): ); -- Recreate constraints - ALTER TABLE "SimpleEntities" - ADD CONSTRAINT "SimpleEntities_thumbnail_id_fkey" FOREIGN KEY (thumbnail_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - ALTER TABLE "Daily_Files" - ADD CONSTRAINT "Daily_Files_file_id_fkey" FOREIGN KEY (file_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - ALTER TABLE "Project_References" - ADD CONSTRAINT "Project_References_reference_id_fkey" FOREIGN KEY (reference_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - ALTER TABLE "Task_References" - ADD CONSTRAINT "Task_References_reference_id_fkey" FOREIGN KEY (reference_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - ALTER TABLE "File_References" - ADD CONSTRAINT "File_References_file_id_fkey" FOREIGN KEY (file_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION, - ADD CONSTRAINT "File_References_reference_id_fkey" FOREIGN KEY (reference_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; - ALTER TABLE "Version_Files" - ADD CONSTRAINT "Version_Files_file_id_fkey" FOREIGN KEY (file_id) - REFERENCES public."Files" (id) MATCH SIMPLE - ON UPDATE CASCADE - ON DELETE CASCADE; - ALTER TABLE "Files" - ADD CONSTRAINT "Files_id_fkey" FOREIGN KEY (id) - REFERENCES public."Entities" (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION; +-- ALTER TABLE "SimpleEntities" +-- ADD CONSTRAINT "SimpleEntities_thumbnail_id_fkey" FOREIGN KEY (thumbnail_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION; +-- ALTER TABLE "Daily_Files" +-- ADD CONSTRAINT "Daily_Files_file_id_fkey" FOREIGN KEY (file_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION; +-- ALTER TABLE "Project_References" +-- ADD CONSTRAINT "Project_References_reference_id_fkey" FOREIGN KEY (reference_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION; +-- ALTER TABLE "Task_References" +-- ADD CONSTRAINT "Task_References_reference_id_fkey" FOREIGN KEY (reference_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION; +-- ALTER TABLE "File_References" +-- ADD CONSTRAINT "File_References_file_id_fkey" FOREIGN KEY (file_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION, +-- ADD CONSTRAINT "File_References_reference_id_fkey" FOREIGN KEY (reference_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION; +-- ALTER TABLE "Version_Files" +-- ADD CONSTRAINT "Version_Files_file_id_fkey" FOREIGN KEY (file_id) +-- REFERENCES public."Files" (id) MATCH SIMPLE +-- ON UPDATE CASCADE +-- ON DELETE CASCADE; +-- ALTER TABLE "Files" +-- ADD CONSTRAINT "Files_id_fkey" FOREIGN KEY (id) +-- REFERENCES public."Entities" (id) MATCH SIMPLE +-- ON UPDATE NO ACTION +-- ON DELETE NO ACTION; -- delete temp data in "SimpleEntities" ALTER TABLE "SimpleEntities" @@ -525,6 +315,111 @@ def upgrade(): """ ) + # recreate constraints + op.create_foreign_key("Files_id_fkey", "Files", "Entities", ["id"], ["id"]) + op.create_primary_key("Files_pkey", "Files", ["id"]) + op.create_foreign_key( + "Daily_Files_daily_id_fkey", + "Daily_Files", + "Dailies", + ["daily_id"], + ["id"], + ) + op.create_foreign_key( + "Daily_Files_file_id_fkey", + "Daily_Files", + "Files", + ["file_id"], + ["id"], + ) + op.create_primary_key("Daily_Files_pkey", "Daily_Files", ["daily_id", "file_id"]) + op.create_foreign_key( + "Version_Files_version_id_fkey", + "Version_Files", + "Versions", + ["version_id"], + ["id"], + ) + op.create_foreign_key( + "Version_Files_file_id_fkey", + "Version_Files", + "Files", + ["file_id"], + ["id"], + onupdate="CASCADE", + ondelete="CASCADE", + ) + op.create_primary_key( + "Version_Files_pkey", "Version_Files", ["version_id", "file_id"] + ) + op.create_foreign_key( + "File_References_file_id_fkey", + "File_References", + "Files", + ["file_id"], + ["id"], + ) + op.create_foreign_key( + "File_References_reference_id_fkey", + "File_References", + "Files", + ["reference_id"], + ["id"], + ) + op.create_primary_key( + "File_References_pkey", + "File_References", + ["file_id", "reference_id"], + ) + op.create_foreign_key( + "Project_References_reference_id_fkey", + "Project_References", + "Files", + ["reference_id"], + ["id"], + ) + op.create_foreign_key( + "SimpleEntities_thumbnail_id_fkey", + "SimpleEntities", + "Files", + ["thumbnail_id"], + ["id"], + use_alter=True, + ) + op.create_foreign_key( + "SimpleEntities_created_by_id_fkey", + "SimpleEntities", + "Users", + ["created_by_id"], + ["id"], + use_alter=True, + ) + op.create_foreign_key( + "SimpleEntities_updated_by_id_fkey", + "SimpleEntities", + "Users", + ["updated_by_id"], + ["id"], + use_alter=True, + ) + op.create_foreign_key( + "SimpleEntities_type_id_fkey", + "SimpleEntities", + "Types", + ["type_id"], + ["id"], + use_alter=True, + ) + op.create_foreign_key( + "Task_References_reference_id_fkey", + "Task_References", + "Files", + ["reference_id"], + ["id"], + ) + # Versions is now deriving from Entities + op.create_foreign_key("Versions_id_fkey", "Versions", "Entities", ["id"], ["id"]) + def downgrade(): """Downgrade the tables.""" @@ -562,23 +457,28 @@ def downgrade(): op.alter_column("Task_References", "reference_id", new_column_name="link_id") # migrate the data as much as you can - op.execute( - """ - -- There are Versions where there are no corresponding input in the - -- Links table anymore - -- Update all the ids of the Links that are in the Version_Inputs with the - -- id of the version, so that we have a corresponding links for all versions - -- and then delete all the entries from the Entities and SimpleEntities tables - -- for those links. - - UPDATE "Links" SET id = sel1.id FROM ( - SELECT - link_id - FROM "Version_Links" - ) + # op.execute( + # """ + # -- There are Versions where there are no corresponding input in the + # -- Links table anymore + # -- Update all the ids of the Links that are in the Version_Inputs with the + # -- id of the version, so that we have a corresponding links for all versions + # -- and then delete all the entries from the Entities and SimpleEntities tables + # -- for those links. + # + # UPDATE "Links" SET id = sel1.id FROM ( + # SELECT + # link_id + # FROM "Version_Links" + # ) + # + # """ + # ) - """ - ) + # ------------------------------------------------------------------------- + # drop columns + # remove created_with from Versions table + op.drop_column("Links", "created_with") # recreate constraints op.create_foreign_key("Daily_Links_daily_id_fkey", "Daily_Links", "Dailies", ["daily_id"], ["id"]) @@ -599,207 +499,3 @@ def downgrade(): op.create_foreign_key("xu", "SimpleEntities", "Users", ["updated_by_id"], ["id"], use_alter=True) op.create_foreign_key("y", "SimpleEntities", "Types", ["type_id"], ["id"], use_alter=True) op.create_foreign_key("z", "SimpleEntities", "Links", ["thumbnail_id"], ["id"], use_alter=True) - - - - - - - - - - - - - - # pkey - op.create_primary_key( - "Daily_Files_pkey", - "Daily_Files", - ["daily_id", "file_id"], - ) - - - - - # Rename tables - op.rename_table("Files", "Links") - op.rename_table("Daily_Files", "Daily_Links") - - - - # ------------------------------------------------------------------------- - # Versions should derive from Links again - - op.create_foreign_key("Versions_id_fkey", "Versions", "Entities", ["id"], ["id"]) - - - # Versions.created_with - op.add_column( - "Versions", - sa.Column( - "created_with", sa.VARCHAR(length=256), autoincrement=False, nullable=True - ), - ) - - # Versions derive from Links - op.drop_constraint(None, "Versions", type_="foreignkey") - - - # ------------------------------------------------------------------------- - # Task_References reference_id -> link_id - # dropped already above! - - # rename the column: link_id -> reference_id - op.alter_column("Task_References", "link_id", new_column_name="reference_id") - - # link_id_fkey -> file_id_fkey - op.create_foreign_key( - "Task_References_reference_id_fkey", - "Task_References", - "Files", - ["reference_id"], - ["id"], - ) - - - - - - - - - - - - - - # - op.create_foreign_key("Versions_id_fkey", "Versions", "Links", ["id"], ["id"]) - op.alter_column( - "Tasks", - "schedule_model", - existing_type=stalker.models.enum.ScheduleModelDecorator( - "effort", "duration", "length" - ), - type_=postgresql.ENUM("effort", "length", "duration", name="TaskScheduleModel"), - existing_nullable=False, - ) - op.add_column( - "Task_References", - sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), - ) - op.drop_constraint(None, "Task_References", type_="foreignkey") - op.create_foreign_key( - "Task_References_link_id_fkey", "Task_References", "Links", ["link_id"], ["id"] - ) - op.drop_column("Task_References", "reference_id") - op.alter_column( - "Task_Dependencies", - "gap_model", - existing_type=stalker.models.enum.ScheduleModelDecorator( - "effort", "duration", "length" - ), - type_=postgresql.ENUM("length", "duration", name="TaskDependencyGapModel"), - existing_nullable=False, - ) - op.drop_constraint("z", "SimpleEntities", type_="foreignkey") - op.create_foreign_key("z", "SimpleEntities", "Links", ["thumbnail_id"], ["id"]) - op.alter_column( - "Shots", - "fps", - existing_type=sa.Float(precision=3), - type_=sa.REAL(), - existing_nullable=True, - ) - op.alter_column( - "Reviews", - "schedule_model", - existing_type=stalker.models.enum.ScheduleModelDecorator( - "effort", "duration", "length" - ), - type_=postgresql.ENUM( - "effort", "length", "duration", name="ReviewScheduleModel" - ), - existing_nullable=False, - ) - op.alter_column( - "Projects", - "fps", - existing_type=sa.Float(precision=3), - type_=sa.REAL(), - existing_nullable=True, - ) - op.add_column( - "Project_References", - sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), - ) - op.drop_constraint(None, "Project_References", type_="foreignkey") - op.create_foreign_key( - "Project_References_link_id_fkey", - "Project_References", - "Links", - ["link_id"], - ["id"], - ) - op.drop_column("Project_References", "reference_id") - op.create_table( - "Links", - sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column( - "original_filename", - sa.VARCHAR(length=256), - autoincrement=False, - nullable=True, - ), - sa.Column("full_path", sa.TEXT(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint(["id"], ["Entities.id"], name="Links_id_fkey"), - sa.PrimaryKeyConstraint("id", name="Links_pkey"), - postgresql_ignore_search_path=False, - ) - op.create_table( - "Version_Inputs", - sa.Column("version_id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint( - ["link_id"], - ["Links.id"], - name="Version_Inputs_link_id_fkey", - onupdate="CASCADE", - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["version_id"], ["Versions.id"], name="Version_Inputs_version_id_fkey" - ), - sa.PrimaryKeyConstraint("version_id", "link_id", name="Version_Inputs_pkey"), - ) - op.create_table( - "Version_Outputs", - sa.Column("version_id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint( - ["link_id"], ["Links.id"], name="Version_Outputs_link_id_fkey" - ), - sa.ForeignKeyConstraint( - ["version_id"], ["Versions.id"], name="Version_Outputs_version_id_fkey" - ), - sa.PrimaryKeyConstraint("version_id", "link_id", name="Version_Outputs_pkey"), - ) - op.create_table( - "Daily_Links", - sa.Column("daily_id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column("rank", sa.INTEGER(), autoincrement=False, nullable=True), - sa.ForeignKeyConstraint( - ["daily_id"], ["Dailies.id"], name="Daily_Links_daily_id_fkey" - ), - sa.ForeignKeyConstraint( - ["link_id"], ["Links.id"], name="Daily_Links_link_id_fkey" - ), - sa.PrimaryKeyConstraint("daily_id", "link_id", name="Daily_Links_pkey"), - ) - op.drop_table("Version_Files") - op.drop_table("Daily_Files") - op.drop_table("File_References") - op.drop_table("Files") - # ### end Alembic commands ### From e35c560dd2fb6a38a4f45805613a76b33022252a Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Tue, 21 Jan 2025 09:47:22 +0000 Subject: [PATCH 11/11] [#147] Fixed a warning in `tests.models.test_project`. --- tests/models/test_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/models/test_project.py b/tests/models/test_project.py index c001e716..71bbf6c3 100644 --- a/tests/models/test_project.py +++ b/tests/models/test_project.py @@ -47,7 +47,7 @@ def condition_tjp_output(data: str) -> str: str: The formatted data. """ assert isinstance(data, str) - data_out = re.subn("[\s]+", " ", data)[0] + data_out = re.subn(r"[\s]+", " ", data)[0] return data_out