pyseq 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyseq/__init__.py +53 -0
- pyseq/config.py +85 -0
- pyseq/lss.py +206 -0
- pyseq/scopy.py +190 -0
- pyseq/sdiff.py +164 -0
- pyseq/seq.py +1302 -0
- pyseq/sfind.py +99 -0
- pyseq/smove.py +190 -0
- pyseq/sstat.py +167 -0
- pyseq/stree.py +134 -0
- pyseq/util.py +174 -0
- pyseq-0.9.1.dist-info/AUTHORS +15 -0
- pyseq-0.9.1.dist-info/LICENSE +12 -0
- pyseq-0.9.1.dist-info/METADATA +297 -0
- pyseq-0.9.1.dist-info/RECORD +18 -0
- pyseq-0.9.1.dist-info/WHEEL +5 -0
- pyseq-0.9.1.dist-info/entry_points.txt +8 -0
- pyseq-0.9.1.dist-info/top_level.txt +1 -0
pyseq/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# - Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# - Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# - Neither the name of the software nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software
|
|
17
|
+
# without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
__doc__ = """PySeq is a python module that finds groups of items that follow a naming
|
|
33
|
+
convention containing a numerical sequence index, e.g. ::
|
|
34
|
+
|
|
35
|
+
fileA.001.png, fileA.002.png, fileA.003.png...
|
|
36
|
+
|
|
37
|
+
and serializes them into a compressed sequence string representing the entire
|
|
38
|
+
sequence, e.g. ::
|
|
39
|
+
|
|
40
|
+
fileA.1-3.png
|
|
41
|
+
|
|
42
|
+
It should work regardless of where the numerical sequence index is embedded
|
|
43
|
+
in the name.
|
|
44
|
+
|
|
45
|
+
Docs and latest version available for download at
|
|
46
|
+
|
|
47
|
+
http://github.com/rsgalloway/pyseq
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__author__ = "Ryan Galloway"
|
|
51
|
+
__version__ = "0.9.1"
|
|
52
|
+
|
|
53
|
+
from .seq import *
|
pyseq/config.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# - Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# - Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# - Neither the name of the software nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software
|
|
17
|
+
# without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
__doc__ = """
|
|
33
|
+
Contains pyseq configs and default settings.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
import re
|
|
38
|
+
|
|
39
|
+
# default serialization format string
|
|
40
|
+
DEFAULT_FORMAT = "%h%r%t"
|
|
41
|
+
default_format = os.getenv("PYSEQ_DEFAULT_FORMAT", DEFAULT_FORMAT)
|
|
42
|
+
|
|
43
|
+
# default serialization format string for global sequences
|
|
44
|
+
DEFAULT_GLOBAL_FORMAT = "%4l %h%p%t %R"
|
|
45
|
+
global_format = os.getenv("PYSEQ_GLOBAL_FORMAT", DEFAULT_GLOBAL_FORMAT)
|
|
46
|
+
|
|
47
|
+
# use strict padding on sequences (pad length must match)
|
|
48
|
+
PYSEQ_STRICT_PAD = os.getenv("PYSEQ_STRICT_PAD", 0)
|
|
49
|
+
PYSEQ_NOT_STRICT = os.getenv("PYSEQ_NOT_STRICT", 1)
|
|
50
|
+
strict_pad = int(PYSEQ_STRICT_PAD) == 1 or int(PYSEQ_NOT_STRICT) == 0
|
|
51
|
+
|
|
52
|
+
# regex pattern for matching all numbers in a filename
|
|
53
|
+
digits_re = re.compile(r"\d+")
|
|
54
|
+
|
|
55
|
+
# regex pattern for matching frame numbers only
|
|
56
|
+
# the default is \d+ for maximum compatibility
|
|
57
|
+
DEFAULT_FRAME_PATTERN = r"\d+"
|
|
58
|
+
PYSEQ_FRAME_PATTERN = os.getenv("PYSEQ_FRAME_PATTERN", DEFAULT_FRAME_PATTERN)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def set_frame_pattern(pattern: str = DEFAULT_FRAME_PATTERN):
|
|
62
|
+
"""
|
|
63
|
+
Set the regex pattern for matching frame numbers.
|
|
64
|
+
|
|
65
|
+
:param pattern: The regex pattern to use for matching frame numbers.
|
|
66
|
+
"""
|
|
67
|
+
global frames_re
|
|
68
|
+
global PYSEQ_FRAME_PATTERN
|
|
69
|
+
PYSEQ_FRAME_PATTERN = pattern
|
|
70
|
+
try:
|
|
71
|
+
frames_re = re.compile(pattern)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print("Error: Invalid regex pattern: %s" % e)
|
|
74
|
+
frames_re = re.compile(DEFAULT_FRAME_PATTERN)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# set the default frame pattern
|
|
78
|
+
set_frame_pattern(PYSEQ_FRAME_PATTERN)
|
|
79
|
+
|
|
80
|
+
# regex for matching format directives
|
|
81
|
+
format_re = re.compile(r"%(?P<pad>\d+)?(?P<var>\w+)")
|
|
82
|
+
|
|
83
|
+
# character to join explicit frame ranges on
|
|
84
|
+
DEFAULT_RANGE_SEP = ", "
|
|
85
|
+
range_join = os.getenv("PYSEQ_RANGE_SEP", DEFAULT_RANGE_SEP)
|
pyseq/lss.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# - Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# - Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# - Neither the name of the software nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software
|
|
17
|
+
# without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
__doc__ = """
|
|
33
|
+
Contains the main lss functions for the pyseq module.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import glob
|
|
37
|
+
import optparse
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
from typing import Any, Optional
|
|
41
|
+
|
|
42
|
+
from pyseq import __version__, get_sequences
|
|
43
|
+
from pyseq import seq as pyseq
|
|
44
|
+
from pyseq.util import cli_catch_keyboard_interrupt
|
|
45
|
+
from pyseq import walk
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tree(source: str, level: Optional[int], seq_format: str):
|
|
49
|
+
"""Recursively walk from the source and display all the folders and sequences."""
|
|
50
|
+
if sys.stdout.isatty():
|
|
51
|
+
blue = "\033[94m"
|
|
52
|
+
endc = "\033[0m"
|
|
53
|
+
else:
|
|
54
|
+
blue = ""
|
|
55
|
+
endc = ""
|
|
56
|
+
ends = {}
|
|
57
|
+
done = []
|
|
58
|
+
|
|
59
|
+
print("{0}{1}".format(blue, os.path.relpath(source)))
|
|
60
|
+
|
|
61
|
+
for root, dirs, seqs in walk(source, level):
|
|
62
|
+
if len(dirs) > 0:
|
|
63
|
+
ends[root] = dirs[-1]
|
|
64
|
+
else:
|
|
65
|
+
ends[root] = None
|
|
66
|
+
|
|
67
|
+
sp = ""
|
|
68
|
+
if root != sorted(source):
|
|
69
|
+
p = root
|
|
70
|
+
while p != source:
|
|
71
|
+
dir_name, base = os.path.split(p)
|
|
72
|
+
if dir_name == source:
|
|
73
|
+
break
|
|
74
|
+
elif dir_name in done:
|
|
75
|
+
sp = " " + sp
|
|
76
|
+
elif ends[dir_name] != base:
|
|
77
|
+
sp = "│ " + sp
|
|
78
|
+
elif ends[dir_name] == base:
|
|
79
|
+
sp = "│ " + sp
|
|
80
|
+
else:
|
|
81
|
+
sp = " " + sp
|
|
82
|
+
p = dir_name
|
|
83
|
+
|
|
84
|
+
base = os.path.basename(root)
|
|
85
|
+
if root == source:
|
|
86
|
+
pass
|
|
87
|
+
elif ends[os.path.dirname(root)] == base:
|
|
88
|
+
print("".join([sp, "└── ", "%s%s%s" % (blue, base, endc)]))
|
|
89
|
+
done.append(root)
|
|
90
|
+
ends[os.path.dirname(root)] = None
|
|
91
|
+
sp += " "
|
|
92
|
+
else:
|
|
93
|
+
print("".join([sp, "├── ", "%s%s%s" % (blue, base, endc)]))
|
|
94
|
+
sp += "│ "
|
|
95
|
+
|
|
96
|
+
sequence_length = len(seqs)
|
|
97
|
+
for i, seq in enumerate(seqs):
|
|
98
|
+
if i == (sequence_length - 1) and len(dirs) == 0:
|
|
99
|
+
print("".join([sp, "└── ", seq.format(seq_format)]))
|
|
100
|
+
else:
|
|
101
|
+
print("".join([sp, "├── ", seq.format(seq_format)]))
|
|
102
|
+
print(endc)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _recur_cb(option: Any, opt_str: str, value: Optional[str], parser: Any):
|
|
106
|
+
"""Callback for the `recursive` argument."""
|
|
107
|
+
if value is None:
|
|
108
|
+
value = -1
|
|
109
|
+
else:
|
|
110
|
+
value = int(value)
|
|
111
|
+
setattr(parser.values, option.dest, value)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@cli_catch_keyboard_interrupt
|
|
115
|
+
def main():
|
|
116
|
+
"""Command-line interface."""
|
|
117
|
+
|
|
118
|
+
usage = (
|
|
119
|
+
"""
|
|
120
|
+
lss [path] [-f format] [-d] [-r]
|
|
121
|
+
|
|
122
|
+
Formatting options:
|
|
123
|
+
|
|
124
|
+
You can format the output of lss using the --format option and passing
|
|
125
|
+
in a format string.
|
|
126
|
+
|
|
127
|
+
Default format string is "%s"
|
|
128
|
+
|
|
129
|
+
Supported directives:
|
|
130
|
+
%%s sequence start
|
|
131
|
+
%%e sequence end
|
|
132
|
+
%%l sequence length
|
|
133
|
+
%%f list of found files
|
|
134
|
+
%%m list of missing files
|
|
135
|
+
%%p padding, e.g. %%06d
|
|
136
|
+
%%r absolute range, start-end
|
|
137
|
+
%%R expanded range, start-end [missing]
|
|
138
|
+
%%d disk usage
|
|
139
|
+
%%h string preceding sequence number
|
|
140
|
+
%%t string after the sequence number
|
|
141
|
+
|
|
142
|
+
Format directives support padding, for example: "%%04l".
|
|
143
|
+
"""
|
|
144
|
+
% pyseq.global_format
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
parser = optparse.OptionParser(usage=usage, version="%prog " + __version__)
|
|
148
|
+
parser.add_option(
|
|
149
|
+
"-f",
|
|
150
|
+
"--format",
|
|
151
|
+
dest="format",
|
|
152
|
+
default=None,
|
|
153
|
+
help="Format the sequence string.",
|
|
154
|
+
)
|
|
155
|
+
parser.add_option(
|
|
156
|
+
"-r",
|
|
157
|
+
"--recursive",
|
|
158
|
+
dest="recursive",
|
|
159
|
+
action="callback",
|
|
160
|
+
callback=_recur_cb,
|
|
161
|
+
help="Walks the entire directory structure.",
|
|
162
|
+
)
|
|
163
|
+
parser.add_option(
|
|
164
|
+
"-s",
|
|
165
|
+
"--strict",
|
|
166
|
+
dest="strict",
|
|
167
|
+
action="store_true",
|
|
168
|
+
default=pyseq.strict_pad,
|
|
169
|
+
help="Strict padding (default false).",
|
|
170
|
+
)
|
|
171
|
+
(options, args) = parser.parse_args()
|
|
172
|
+
|
|
173
|
+
pyseq.strict_pad = options.strict
|
|
174
|
+
|
|
175
|
+
# stdin is piped, read from stdin if no cli args provided
|
|
176
|
+
if not args and not sys.stdin.isatty():
|
|
177
|
+
args = [line.strip() for line in sys.stdin if line.strip()]
|
|
178
|
+
|
|
179
|
+
# if no args are given, use cwd
|
|
180
|
+
elif len(args) == 0:
|
|
181
|
+
args = [os.getcwd()]
|
|
182
|
+
|
|
183
|
+
items = []
|
|
184
|
+
for path in args:
|
|
185
|
+
if os.path.isdir(path):
|
|
186
|
+
join = os.path.join
|
|
187
|
+
items = [join(path, x) for x in os.listdir(path)]
|
|
188
|
+
else:
|
|
189
|
+
items.extend(glob.glob(path))
|
|
190
|
+
|
|
191
|
+
if options.recursive is None:
|
|
192
|
+
for seq in get_sequences(items):
|
|
193
|
+
print(seq.format(options.format or pyseq.global_format))
|
|
194
|
+
else:
|
|
195
|
+
level = options.recursive
|
|
196
|
+
for path in args:
|
|
197
|
+
path = os.path.abspath(path.rstrip(os.sep))
|
|
198
|
+
if not os.path.isdir(path):
|
|
199
|
+
continue
|
|
200
|
+
tree(path, level, options.format or "%h%r%t")
|
|
201
|
+
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
sys.exit(main())
|
pyseq/scopy.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# - Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# - Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# - Neither the name of the software nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software
|
|
17
|
+
# without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
__doc__ = """
|
|
33
|
+
Contains the main scopy functions for the pyseq module.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import sys
|
|
37
|
+
import os
|
|
38
|
+
import argparse
|
|
39
|
+
import shutil
|
|
40
|
+
import fnmatch
|
|
41
|
+
from typing import Optional
|
|
42
|
+
|
|
43
|
+
import pyseq
|
|
44
|
+
from pyseq.util import (
|
|
45
|
+
cli_catch_keyboard_interrupt,
|
|
46
|
+
is_compressed_format_string,
|
|
47
|
+
resolve_sequence,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def copy_sequence(
|
|
52
|
+
seq: pyseq.Sequence,
|
|
53
|
+
src_dir: str,
|
|
54
|
+
dest_dir: str,
|
|
55
|
+
rename: Optional[str] = None,
|
|
56
|
+
renumber: Optional[int] = None,
|
|
57
|
+
pad: Optional[int] = None,
|
|
58
|
+
force: bool = False,
|
|
59
|
+
dryrun: bool = False,
|
|
60
|
+
verbose: bool = False,
|
|
61
|
+
):
|
|
62
|
+
"""Copy a sequence of files from src_dir to dest_dir.
|
|
63
|
+
|
|
64
|
+
:param seq: The sequence object to copy.
|
|
65
|
+
:param src_dir: The source directory containing the files.
|
|
66
|
+
:param dest_dir: The destination directory to copy files to.
|
|
67
|
+
:param rename: Optional new basename for the copied files.
|
|
68
|
+
:param renumber: Optional new starting frame number.
|
|
69
|
+
:param pad: Optional number of digits for padding the frame numbers.
|
|
70
|
+
:param force: If True, overwrite existing files.
|
|
71
|
+
:param dryrun: If True, print the operations without executing them.
|
|
72
|
+
:param verbose: If True, print detailed information about the operations.
|
|
73
|
+
"""
|
|
74
|
+
dest_basename = rename or seq.head()
|
|
75
|
+
dest_pad = pad or seq.pad
|
|
76
|
+
start_frame = renumber or seq.start()
|
|
77
|
+
|
|
78
|
+
for i, frame in enumerate(seq):
|
|
79
|
+
src_path = os.path.join(src_dir, frame.name)
|
|
80
|
+
frame_num = start_frame + i
|
|
81
|
+
dest_frame_name = f"{dest_basename}{frame_num:0{dest_pad}d}{seq.tail()}"
|
|
82
|
+
dest_path = os.path.join(dest_dir, dest_frame_name)
|
|
83
|
+
|
|
84
|
+
if verbose or dryrun:
|
|
85
|
+
print(f"{src_path} -> {dest_path}")
|
|
86
|
+
|
|
87
|
+
if not dryrun:
|
|
88
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
89
|
+
if os.path.exists(dest_path) and not force:
|
|
90
|
+
print(
|
|
91
|
+
f"File exists: {dest_path} (use --force to overwrite)",
|
|
92
|
+
file=sys.stderr,
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
shutil.copy2(src_path, dest_path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli_catch_keyboard_interrupt
|
|
99
|
+
def main():
|
|
100
|
+
"""Main function to parse cli args and copy sequences."""
|
|
101
|
+
|
|
102
|
+
parser = argparse.ArgumentParser(
|
|
103
|
+
description="Copy image sequences with renaming/renumbering support",
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"sources",
|
|
107
|
+
nargs="+",
|
|
108
|
+
help="Source sequences (wildcards or compressed format strings)",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"dest",
|
|
112
|
+
help="Destination directory",
|
|
113
|
+
)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--rename",
|
|
116
|
+
help="Rename sequence basename",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--renumber",
|
|
120
|
+
type=int,
|
|
121
|
+
help="New starting frame",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--pad",
|
|
125
|
+
type=int,
|
|
126
|
+
help="Padding digits",
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"-f",
|
|
130
|
+
"--force",
|
|
131
|
+
action="store_true",
|
|
132
|
+
help="Overwrite existing files",
|
|
133
|
+
)
|
|
134
|
+
parser.add_argument(
|
|
135
|
+
"-d",
|
|
136
|
+
"--dryrun",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help="Preview copy without performing it",
|
|
139
|
+
)
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
"-v",
|
|
142
|
+
"--verbose",
|
|
143
|
+
action="store_true",
|
|
144
|
+
help="Verbose output",
|
|
145
|
+
)
|
|
146
|
+
args = parser.parse_args()
|
|
147
|
+
|
|
148
|
+
if not os.path.isdir(args.dest):
|
|
149
|
+
print(f"Error: destination {args.dest} is not a directory", file=sys.stderr)
|
|
150
|
+
return 1
|
|
151
|
+
|
|
152
|
+
for source in args.sources:
|
|
153
|
+
try:
|
|
154
|
+
if is_compressed_format_string(source):
|
|
155
|
+
seq = resolve_sequence(source)
|
|
156
|
+
dirname = os.path.dirname(source) or "."
|
|
157
|
+
else:
|
|
158
|
+
# treat as glob
|
|
159
|
+
dirname = os.path.dirname(source) or "."
|
|
160
|
+
basename = os.path.basename(source)
|
|
161
|
+
matches = [
|
|
162
|
+
f for f in os.listdir(dirname) if fnmatch.fnmatchcase(f, basename)
|
|
163
|
+
]
|
|
164
|
+
sequences = pyseq.get_sequences(matches)
|
|
165
|
+
if not sequences:
|
|
166
|
+
print(f"No sequence found matching {source}", file=sys.stderr)
|
|
167
|
+
continue
|
|
168
|
+
seq = sequences[0]
|
|
169
|
+
|
|
170
|
+
copy_sequence(
|
|
171
|
+
seq,
|
|
172
|
+
dirname,
|
|
173
|
+
args.dest,
|
|
174
|
+
rename=args.rename,
|
|
175
|
+
renumber=args.renumber,
|
|
176
|
+
pad=args.pad,
|
|
177
|
+
force=args.force,
|
|
178
|
+
dryrun=args.dryrun,
|
|
179
|
+
verbose=args.verbose,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Error processing {source}: {e}", file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
return 0
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
sys.exit(main())
|
pyseq/sdiff.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2011-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# - Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# - Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# - Neither the name of the software nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software
|
|
17
|
+
# without specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
# -----------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
__doc__ = """
|
|
33
|
+
Contains the main sdiff functions for the pyseq module.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
import pyseq
|
|
41
|
+
from pyseq.util import cli_catch_keyboard_interrupt
|
|
42
|
+
from pyseq.util import resolve_sequence
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def diff_sequences(
|
|
46
|
+
seq1: pyseq.Sequence,
|
|
47
|
+
seq2: pyseq.Sequence,
|
|
48
|
+
compare_size: bool = False,
|
|
49
|
+
):
|
|
50
|
+
"""Compares two sequences and returns a dictionary of differences.
|
|
51
|
+
|
|
52
|
+
:param seq1: The first sequence to compare.
|
|
53
|
+
:param seq2: The second sequence to compare.
|
|
54
|
+
:param compare_size: Boolean indicating whether to compare disk usage.
|
|
55
|
+
:return: A dictionary containing differences between the two sequences.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def intval(val):
|
|
59
|
+
try:
|
|
60
|
+
return int(val)
|
|
61
|
+
except:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
diff = {
|
|
65
|
+
"head": (seq1.head(), seq2.head()),
|
|
66
|
+
"tail": (seq1.tail(), seq2.tail()),
|
|
67
|
+
"pad": (seq1.pad, seq2.pad),
|
|
68
|
+
"start": (seq1.start(), seq2.start()),
|
|
69
|
+
"end": (seq1.end(), seq2.end()),
|
|
70
|
+
"length": (seq1.length(), seq2.length()),
|
|
71
|
+
"missing": {
|
|
72
|
+
"a_only": sorted(set(seq1.missing()) - set(seq2.missing())),
|
|
73
|
+
"b_only": sorted(set(seq2.missing()) - set(seq1.missing())),
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if compare_size:
|
|
78
|
+
disk_a = intval(seq1.format("%d"))
|
|
79
|
+
disk_b = intval(seq2.format("%d"))
|
|
80
|
+
diff["disk_bytes"] = [disk_a, disk_b]
|
|
81
|
+
diff["disk_human"] = [seq1.format("%H"), seq2.format("%H")]
|
|
82
|
+
|
|
83
|
+
return diff
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def print_diff(diff: dict, compare_size: bool = False):
|
|
87
|
+
"""Prints the differences between two sequences.
|
|
88
|
+
|
|
89
|
+
:param diff: The dictionary containing differences between sequences.
|
|
90
|
+
:param compare_size: Boolean indicating whether to compare disk usage.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def show(label: str, a: str, b: str):
|
|
94
|
+
if a != b:
|
|
95
|
+
print(f"{label} mismatch:\n A: {a}\n B: {b}")
|
|
96
|
+
|
|
97
|
+
show("Head", *diff["head"])
|
|
98
|
+
show("Tail", *diff["tail"])
|
|
99
|
+
show("Padding", *diff["pad"])
|
|
100
|
+
show("Start", *diff["start"])
|
|
101
|
+
show("End", *diff["end"])
|
|
102
|
+
show("Length", *diff["length"])
|
|
103
|
+
|
|
104
|
+
a_only = diff["missing"]["a_only"]
|
|
105
|
+
b_only = diff["missing"]["b_only"]
|
|
106
|
+
if a_only:
|
|
107
|
+
print(f"Missing in A: {a_only}")
|
|
108
|
+
if b_only:
|
|
109
|
+
print(f"Missing in B: {b_only}")
|
|
110
|
+
|
|
111
|
+
if compare_size and "disk_bytes" in diff:
|
|
112
|
+
a, b = diff["disk_human"]
|
|
113
|
+
if a != b:
|
|
114
|
+
print(f"Disk usage mismatch:\n A: {a}\n B: {b}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@cli_catch_keyboard_interrupt
|
|
118
|
+
def main():
|
|
119
|
+
"""Main function to parse arguments and display sequence diffs."""
|
|
120
|
+
|
|
121
|
+
parser = argparse.ArgumentParser(
|
|
122
|
+
description="Compare two file sequences and report differences.",
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"seq1",
|
|
126
|
+
help="First sequence (glob or %%d format)",
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"seq2",
|
|
130
|
+
help="Second sequence",
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--size",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help="Compare disk usage",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--json",
|
|
139
|
+
action="store_true",
|
|
140
|
+
help="Output result as JSON",
|
|
141
|
+
)
|
|
142
|
+
args = parser.parse_args()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
s1 = resolve_sequence(args.seq1)
|
|
146
|
+
s2 = resolve_sequence(args.seq2)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"sdiff: error resolving sequence: {e}", file=sys.stderr)
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
diff = diff_sequences(s1, s2, compare_size=args.size)
|
|
152
|
+
|
|
153
|
+
if args.json:
|
|
154
|
+
print(json.dumps(diff, indent=4))
|
|
155
|
+
else:
|
|
156
|
+
print(f"Sequence A: {str(s1)}")
|
|
157
|
+
print(f"Sequence B: {str(s2)}\n")
|
|
158
|
+
print_diff(diff, compare_size=args.size)
|
|
159
|
+
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
sys.exit(main())
|