mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 21:32:15 +01:00
168 lines
4.9 KiB
Python
168 lines
4.9 KiB
Python
|
|
__all__ = ['parse', 'ls', 'ls_iter', 'format']
|
|
|
|
import os
|
|
import re
|
|
import string
|
|
import glob
|
|
|
|
from .error import ParseError
|
|
|
|
# Regex pattern that matches valid file
|
|
# A filename may not contain \/:*?"<>|
|
|
RE_FILENAME = r"[^\\/:\"*?<>|]"
|
|
|
|
|
|
def format(pattern, data, allow_partial=True):
|
|
"""Format a pattern with a set of data
|
|
|
|
Examples:
|
|
|
|
Full formatting
|
|
>>> format("{a}/{b}/{c}", {"a": "foo", "b": "bar", "c": "nugget"})
|
|
'foo/bar/nugget'
|
|
|
|
Partial formatting
|
|
>>> format("{asset}/{character}", {"asset": "hero"})
|
|
'hero/{character}'
|
|
|
|
Disallow partial formatting
|
|
>>> format("{asset}/{character}", {"asset": "hero"},
|
|
... allow_partial=False)
|
|
Traceback (most recent call last):
|
|
...
|
|
KeyError: 'character'
|
|
|
|
Args:
|
|
pattern (str): The pattern to format.
|
|
data (dict): The key, value pairs used for formatting.
|
|
allow_partial (bool): Whether to raise error on partial format.
|
|
|
|
Returns:
|
|
str: The formatted result
|
|
"""
|
|
|
|
assert isinstance(data, dict)
|
|
|
|
if not all(isinstance(value, basestring) for value in data.values()):
|
|
raise TypeError("The values in the data "
|
|
"dictionary must be strings")
|
|
|
|
if allow_partial:
|
|
return _partial_format(pattern, data)
|
|
else:
|
|
return pattern.format(**data)
|
|
|
|
|
|
def parse(pattern, path):
|
|
"""Parse data from a path based on a pattern
|
|
|
|
Example:
|
|
>>> pattern = "root/{task}/{version}/data/"
|
|
>>> path = "root/modeling/v001/data/"
|
|
>>> parse(pattern, path)
|
|
{'task': 'modeling', 'version': 'v001'}
|
|
|
|
Returns:
|
|
dict: The data retrieved from path using pattern.
|
|
"""
|
|
|
|
pattern = os.path.normpath(pattern)
|
|
path = os.path.normpath(path)
|
|
|
|
# Force forward slashes
|
|
path = path.replace('\\', '/')
|
|
pattern = pattern.replace('\\', '/')
|
|
|
|
# Escape characters in path that are regex patterns so they are
|
|
# excluded by the regex searches. Exclude '{' and '}' in escaping.
|
|
pattern = re.escape(pattern)
|
|
pattern = pattern.replace('\{', '{').replace('\}', '}')
|
|
|
|
keys = re.findall(r'{(%s+)}' % RE_FILENAME,
|
|
pattern)
|
|
if not keys:
|
|
return []
|
|
|
|
# Find the corresponding values
|
|
value_pattern = re.sub(r'{(%s+)}' % RE_FILENAME,
|
|
r'(%s+)' % RE_FILENAME,
|
|
pattern)
|
|
match_values = re.match(value_pattern, path)
|
|
|
|
if not match_values:
|
|
raise ParseError("Path doesn't match with pattern. No values parsed")
|
|
|
|
values = match_values.groups()
|
|
|
|
return dict(zip(keys, values))
|
|
|
|
|
|
def ls_iter(pattern, include=None, with_matches=False):
|
|
"""Yield all matches for the given pattern.
|
|
|
|
If the pattern starts with a relative path (or a dynamic key) the search
|
|
will start from the current working directory, defined by os.path.realpath.
|
|
|
|
Arguments:
|
|
pattern (str): The pattern to match and search against.
|
|
include (dict): A dictionary used to target the search with the pattern
|
|
to include only those key-value pairs within the pattern. With this
|
|
you can reduce the filesystem query to a specified subset.
|
|
|
|
Example:
|
|
>>> data = {"root": "foobar", "content": "nugget"}
|
|
>>> for path in ls_iter("{root}/{project}/data/{content}/",
|
|
... include=data):
|
|
... print path
|
|
|
|
Returns:
|
|
(str, tuple): The matched paths (and data if `with_matches` is True)
|
|
|
|
The returned value changes whether `with_matches` parameter is True or
|
|
False. If True a 2-tuple is yielded for each match as (path, data) else
|
|
only the path is returned
|
|
"""
|
|
|
|
# format rule by data already provided to reduce query
|
|
if include is not None:
|
|
pattern = format(pattern, include, allow_partial=True)
|
|
|
|
pattern = os.path.expandvars(pattern)
|
|
pattern = os.path.realpath(pattern)
|
|
|
|
glob_pattern = re.sub(r'([/\\]{\w+}[/\\])', '/*/', pattern) # folder
|
|
glob_pattern = re.sub(r'({\w+})', '*', glob_pattern) # filename
|
|
|
|
for path in glob.iglob(glob_pattern):
|
|
path = os.path.realpath(path)
|
|
if with_matches:
|
|
data = parse(pattern, path)
|
|
yield path, data
|
|
else:
|
|
yield path
|
|
|
|
|
|
def ls(pattern, include=None, with_matches=False):
|
|
return list(ls_iter(pattern, include, with_matches=with_matches))
|
|
|
|
|
|
def _partial_format(s, data):
|
|
"""Return string `s` formatted by `data` allowing a partial format
|
|
|
|
Arguments:
|
|
s (str): The string that will be formatted
|
|
data (dict): The dictionary used to format with.
|
|
|
|
Example:
|
|
>>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"})
|
|
'left {a} and {c} left'
|
|
"""
|
|
|
|
class FormatDict(dict):
|
|
def __missing__(self, key):
|
|
return "{" + key + "}"
|
|
|
|
formatter = string.Formatter()
|
|
mapping = FormatDict(**data)
|
|
return formatter.vformat(s, (), mapping)
|