Skip to content

Commit

Permalink
* [#147] File class is now also deriving from ReferenceMixin addi…
Browse files Browse the repository at this point in the history
…ng 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.
  • Loading branch information
eoyilmaz committed Jan 14, 2025
1 parent 3a49f49 commit 7200c58
Show file tree
Hide file tree
Showing 39 changed files with 990 additions and 713 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/stalker/db/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions src/stalker/models/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/stalker/models/budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/stalker/models/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"
)

Expand Down
17 changes: 14 additions & 3 deletions src/stalker/models/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
77 changes: 68 additions & 9 deletions src/stalker/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand All @@ -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}'"
)

Expand Down Expand Up @@ -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}'"
)

Expand Down Expand Up @@ -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}'"
)

Expand Down Expand Up @@ -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}'"
)

Expand All @@ -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.
Expand Down
Loading

0 comments on commit 7200c58

Please sign in to comment.