Source code for grill.names

from __future__ import annotations

import uuid
import typing
import textwrap
import itertools
import collections
from datetime import datetime

import naming
try:
    from pxr import Sdf
    _USD_SUFFIXES = tuple(ext for ext in Sdf.FileFormat.FindAllFileFormatExtensions() if ext.startswith('usd'))
except ImportError:  # Don't fail if Sdf is not importable to facilitate portability
    _USD_SUFFIXES = ("usd", "usda", "usdc", "usdz", "usdt")

from grill.tokens import ids


def _table_from_id(token_ids):
    headers = [
        'Token',
        'Pattern',
        'Default',
        'Description',
    ]
    table_sep = tuple([''] * len(headers))
    sorter = lambda value: (
        # cleanup backslashes formatting
        value.pattern.replace('\\', '\\\\'),
        value.default,
        # replace new lines with empty strings to avoid malformed tables.
        value.description.replace('\n', ' '),
    )
    rows = [table_sep, headers, table_sep]
    rows.extend([token.name, *sorter(token.value)] for token in token_ids)
    rows.append(table_sep)
    max_sizes = [(max(len(i) for i in r)) for r in zip(*rows)]

    format_rows = []
    for r in rows:
        filler = '=<' if r == table_sep else ''
        format_rows.append(' '.join(
            f"{{:{f'{filler}'}{f'{size}'}}}".format(i)
            for size, i in zip(max_sizes, r))
        )
    return textwrap.indent('\n'.join(format_rows), prefix="    ")


[docs] class DefaultName(naming.Name): """ Inherited by: :class:`grill.names.CGAsset` Base class for any Name object that wishes to provide `default` functionality via the `get_default` method. Subclass implementations can override the `_defaults` member to return a mapping appropriate to that class. """ _defaults = {} @classmethod def get_default(cls, **kwargs) -> DefaultName: """Get a new Name object with default values and overrides from **kwargs.""" name = cls() defaults = dict(name._defaults, **kwargs) name.name = name.get(**defaults) return name
[docs] class DefaultFile(DefaultName, naming.File): """ Inherited by: :class:`grill.names.DateTimeFile` Similar to :class:`grill.names.DefaultName`, provides File Name objects default creation via the `get_default` method. Adds an extra ``DEFAULT_SUFFIX='ext'`` member that will be used when creating objects. """ DEFAULT_SUFFIX = 'ext' @property def _defaults(self): result = super()._defaults result['suffix'] = type(self).DEFAULT_SUFFIX return result
[docs] class DateTimeFile(DefaultFile): """Time based file names respecting iso standard. ============= ================ **Config:** ============================== *year* Between :py:data:`datetime.MINYEAR` and :py:data:`datetime.MAXYEAR` inclusive. *month* Between 1 and 12 inclusive. *day* Between 1 and the number of days in the given month of the given year. *hour* In ``range(24)``. *minute* In ``range(60)``. *second* In ``range(60)``. *microsecond* In ``range(1000000)``. ============= ================ ====== ============ **Composed Fields:** ==================== *date* `year` `month` `day` *time* `hour` `minute` `second` `microsecond` ====== ============ .. note:: When getting a new default name, current ISO time at the moment of execution is used. Example: >>> tf = DateTimeFile.get_default(suffix='txt') >>> tf.day '28' >>> tf.date '2019-10-28' >>> tf.year = 1999 >>> tf DateTimeFile("1999-10-28 22-29-31-926548.txt") >>> tf.month = 14 # ISO format validation Traceback (most recent call last): ... ValueError: month must be in 1..12 >>> tf.datetime datetime.datetime(1999, 10, 28, 22, 29, 31, 926548) """ config = dict.fromkeys( ('month', 'day', 'hour', 'minute', 'second'), r'\d{1,2}' ) config.update(year=r'\d{1,4}', microsecond=r'\d{1,6}') join = dict( date=('year', 'month', 'day'), time=('hour', 'minute', 'second', 'microsecond'), ) join_sep = '-' @property def _defaults(self): result = super()._defaults time_field = {'year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'} now = datetime.now() result.update({f: getattr(now, f) for f in time_field}) return result
[docs] def get_pattern_list(self) -> typing.List[str]: """Fields / properties names (sorted) to be used when building names. Defaults to [`date`, `time`] + keys of this name's config """ return ["date", "time"] + super().get_pattern_list()
@property def name(self) -> str: return super().name @name.setter def name(self, name: str): prev_name = self._name super(DateTimeFile, self.__class__).name.fset(self, name) if name: try: # validate via datetime conversion self.datetime except ValueError: if prev_name: # if we had a previous valid name, revert to it self.name = prev_name raise @property def datetime(self) -> datetime: """ Return a :py:class:`datetime.datetime` object using this name values. >>> tf = DateTimeFile("1999-10-28 22-29-31-926548.txt") >>> tf.datetime datetime.datetime(1999, 10, 28, 22, 29, 31, 926548) """ if not self.name: raise AttributeError("Can not retrieve datetime from an empty name") date = f"{int(self.year):04d}-{int(self.month):02d}-{int(self.day):02d}" time = (f"{int(self.hour):02d}:{int(self.minute):02d}:{int(self.second):02d}." f"{int(self.microsecond):06d}") return datetime.fromisoformat(f'{date}T{time}')
[docs] class CGAsset(DefaultName): """Inherited by: :class:`grill.names.CGAssetFile` Elemental resources that, when composed, generate the entities that bring an idea to a tangible product through their life cycles (e.g. a character, a film, a videogame). """ config = {token.name: token.value.pattern for token in ids.CGAsset} __doc__ += '\n' + _table_from_id(ids.CGAsset) + '\n' def __init__(self, *args, sep='-', **kwargs): super().__init__(*args, sep=sep, **kwargs) @property def _defaults(self): result = super()._defaults result.update({token.name: token.value.default for token in ids.CGAsset}) return result
[docs] class CGAssetFile(CGAsset, DefaultFile, naming.PipeFile): """Inherited by: :class:`grill.names.UsdAsset` Versioned files in the pipeline for a CGAsset. Example: >>> name = CGAssetFile.get_default(version=7) >>> name.suffix 'ext' >>> name.suffix = 'abc' >>> name.path WindowsPath('demo/3d/abc/entity/rnd/lead/atom/main/all/whole/7/demo-3d-abc-entity-rnd-lead-atom-main-all-whole.7.abc') """ @property def _defaults(self): result = super()._defaults result.update(version=1) return result def get_path_pattern_list(self) -> typing.List[str]: pattern = super().get_pattern_list() pattern.append('version') return pattern
[docs] class UsdAsset(CGAssetFile): """Specialized :class:`grill.names.CGAssetFile` name object for USD asset resources. .. admonition:: Inheritance Diagram for UsdAsset :class: dropdown, note .. inheritance-diagram:: grill.names.UsdAsset This is the currency for USD asset identifiers in the pipeline. Examples: >>> asset_id = UsdAsset.get_default() >>> asset_id UsdAsset("demo-3d-abc-entity-rnd-main-atom-lead-base-whole.1.usda") >>> asset_id.suffix = 'usdc' >>> asset_id.version = 42 >>> asset_id UsdAsset("demo-3d-abc-entity-rnd-main-atom-lead-base-whole.42.usdc") >>> asset_id.suffix = 'abc' Traceback (most recent call last): ... ValueError: Can't set invalid name 'demo-3d-abc-entity-rnd-main-atom-lead-base-whole.42.abc' on UsdAsset("demo-3d-abc-entity-rnd-main-atom-lead-base-whole.42.usdc"). Valid convention is: '{code}-{media}-{kingdom}-{cluster}-{area}-{stream}-{item}-{step}-{variant}-{part}.{pipe}.{suffix}' with pattern: '^(?P<code>...(?P<suffix>sdf|usd|usda|usdc|usdz))$' .. seealso:: :class:`grill.names.CGAsset` for a description of available fields, :class:`naming.Name` for an overview of the core API. """ DEFAULT_SUFFIX = 'usd' file_config = naming.NameConfig( # NOTE: limit to only extensions starting with USD (some environments register other extensions untested by the grill) {'suffix': "|".join(_USD_SUFFIXES)} )
[docs] @classmethod def get_anonymous(cls, **values) -> UsdAsset: """Get an anonymous :class:`UsdAsset` name with optional field overrides. Useful for situations where a temporary but valid identifier is needed. :param values: Variable keyword arguments with the keys referring to the name's fields which will use the given values. Example: >>> UsdAsset.get_anonymous(stream='test') UsdAsset("4209091047-34604-19646-169-123-test-4209091047-34604-19646-169.1.usda") """ keys = cls.get_default().get_pattern_list() anon = itertools.cycle(uuid.uuid4().fields) return cls.get_default(**collections.ChainMap(values, dict(zip(keys, anon))))
[docs] class LifeTR(naming.Name): """Taxonomic Rank used for biological classification. """ config = {token.name: token.value.pattern for token in ids.LifeTR} __doc__ += '\n' + _table_from_id(ids.LifeTR) + '\n' def __init__(self, *args, sep=':', **kwargs): super().__init__(*args, sep=sep, **kwargs)