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 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())