pycmx 1.4.0__tar.gz → 1.5.0__tar.gz

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.
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycmx
3
- Version: 1.4.0
3
+ Version: 1.5.0
4
4
  Summary: Python CMX 3600 Edit Decision List Parser
5
- License: MIT
6
- License-File: LICENSE
7
5
  Keywords: parser,film,broadcast
8
6
  Author: Jamie Hardt
9
- Author-email: jamiehardt@me.com
10
- Requires-Python: >=3.8,<4.0
7
+ Author-email: Jamie Hardt <jamiehardt@me.com>
8
+ License-File: LICENSE
11
9
  Classifier: Development Status :: 5 - Production/Stable
12
10
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Multimedia
12
+ Classifier: Topic :: Multimedia :: Video
13
+ Classifier: Topic :: Text Processing
14
14
  Classifier: Programming Language :: Python :: 3.8
15
15
  Classifier: Programming Language :: Python :: 3.9
16
16
  Classifier: Programming Language :: Python :: 3.10
@@ -18,16 +18,17 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: Programming Language :: Python :: 3.14
21
- Classifier: Topic :: Multimedia
22
- Classifier: Topic :: Multimedia :: Video
23
- Classifier: Topic :: Text Processing
24
- Provides-Extra: doc
25
- Requires-Dist: sphinx (>=5.3.0) ; extra == "doc"
26
- Requires-Dist: sphinx_rtd_theme (>=1.1.1) ; extra == "doc"
27
- Project-URL: Documentation, https://pycmx.readthedocs.io/
21
+ Requires-Dist: pytest ; extra == 'dev'
22
+ Requires-Dist: ruff>=0.14.10 ; extra == 'dev'
23
+ Requires-Dist: sphinx>=5.3.0 ; extra == 'doc'
24
+ Requires-Dist: sphinx-rtd-theme>=1.1.1 ; extra == 'doc'
25
+ Requires-Python: >3.8
28
26
  Project-URL: Homepage, https://github.com/iluvcapra/pycmx
27
+ Project-URL: Documentation, https://pycmx.readthedocs.io/
29
28
  Project-URL: Repository, https://github.com/iluvcapra/pycmx.git
30
29
  Project-URL: Tracker, https://github.com/iluvcapra/pycmx/issues
30
+ Provides-Extra: dev
31
+ Provides-Extra: doc
31
32
  Description-Content-Type: text/markdown
32
33
 
33
34
  [![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/pycmx.svg) ![](https://img.shields.io/pypi/pyversions/pycmx.svg) [![](https://img.shields.io/pypi/v/pycmx.svg)](https://pypi.org/project/pycmx/) ![](https://img.shields.io/pypi/wheel/pycmx.svg)
@@ -46,6 +47,8 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
46
47
  read. Event number field and source name field sizes are determined
47
48
  dynamically for each statement for a high level of compliance at the expense
48
49
  of strictness.
50
+ * A more relaxed "tolerant" mode allows parsing of an EDL file where columns
51
+ use non-standard widths.
49
52
  * Preserves relationship between events and individual edits/clips.
50
53
  * Remark or comment fields with common recognized forms are read and
51
54
  available to the client, including clip name and source file data.
@@ -67,7 +70,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
67
70
  ### Opening and Parsing EDL Files
68
71
  ```
69
72
  >>> import pycmx
70
- >>> with open("tests/edls/TEST.edl") as f
73
+ >>> with open("tests/edls/TEST.edl") as f:
71
74
  ... edl = pycmx.parse_cmx3600(f)
72
75
  ...
73
76
  >>> edl.title
@@ -122,4 +125,3 @@ Audio channel 7 is present
122
125
  >>> events[2].edits[0].channels.video
123
126
  False
124
127
  ```
125
-
@@ -14,6 +14,8 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
14
14
  read. Event number field and source name field sizes are determined
15
15
  dynamically for each statement for a high level of compliance at the expense
16
16
  of strictness.
17
+ * A more relaxed "tolerant" mode allows parsing of an EDL file where columns
18
+ use non-standard widths.
17
19
  * Preserves relationship between events and individual edits/clips.
18
20
  * Remark or comment fields with common recognized forms are read and
19
21
  available to the client, including clip name and source file data.
@@ -35,7 +37,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
35
37
  ### Opening and Parsing EDL Files
36
38
  ```
37
39
  >>> import pycmx
38
- >>> with open("tests/edls/TEST.edl") as f
40
+ >>> with open("tests/edls/TEST.edl") as f:
39
41
  ... edl = pycmx.parse_cmx3600(f)
40
42
  ...
41
43
  >>> edl.title
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "pycmx"
3
+ version = "1.5.0"
4
+ description = "Python CMX 3600 Edit Decision List Parser"
5
+ authors = [{name = "Jamie Hardt", email= "jamiehardt@me.com"}]
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ keywords = [
9
+ 'parser',
10
+ 'film',
11
+ 'broadcast'
12
+ ]
13
+ requires-python = '>3.8'
14
+ classifiers = [
15
+ 'Development Status :: 5 - Production/Stable',
16
+ 'License :: OSI Approved :: MIT License',
17
+ 'Topic :: Multimedia',
18
+ 'Topic :: Multimedia :: Video',
19
+ 'Topic :: Text Processing',
20
+ 'Programming Language :: Python :: 3.8',
21
+ 'Programming Language :: Python :: 3.9',
22
+ 'Programming Language :: Python :: 3.10',
23
+ 'Programming Language :: Python :: 3.11',
24
+ 'Programming Language :: Python :: 3.12',
25
+ 'Programming Language :: Python :: 3.13',
26
+ 'Programming Language :: Python :: 3.14'
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ doc = [
31
+ 'sphinx >= 5.3.0',
32
+ 'sphinx_rtd_theme >= 1.1.1',
33
+ ]
34
+ dev = [
35
+ 'pytest',
36
+ 'ruff>=0.14.10'
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/iluvcapra/pycmx"
41
+ Documentation = "https://pycmx.readthedocs.io/"
42
+ Repository = "https://github.com/iluvcapra/pycmx.git"
43
+ Tracker = "https://github.com/iluvcapra/pycmx/issues"
44
+
45
+ [dependency-groups]
46
+ dev = ['ruff', 'pytest']
47
+ doc = ['sphinx', 'sphinx_rtd_theme']
48
+
49
+
50
+ [tool.pyright]
51
+ typeCheckingMode = "basic"
52
+
53
+ [tool.ruff]
54
+ line-length = 88
55
+ indent-width = 4
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "W"]
59
+
60
+ [tool.ruff.format]
61
+ docstring-code-line-length = 88
62
+
63
+ # [tool.pylint]
64
+ # max-line-length = 88
65
+ # disable = [
66
+ # "C0103", # (invalid-name)
67
+ # "C0114", # (missing-module-docstring)
68
+ # "C0115", # (missing-class-docstring)
69
+ # "C0116", # (missing-function-docstring)
70
+ # "R0903", # (too-few-public-methods)
71
+ # "R0913", # (too-many-arguments)
72
+ # "W0105", # (pointless-string-statement)
73
+ # ]
74
+ #
75
+
76
+
77
+
78
+ [build-system]
79
+ requires = ["uv_build>=0.9.18,<0.10.0"]
80
+ build-backend = "uv_build"
@@ -11,3 +11,5 @@ from .parse_cmx_events import parse_cmx3600
11
11
  from .transition import Transition
12
12
  from .event import Event
13
13
  from .edit import Edit
14
+
15
+ __all__ = ("parse_cmx3600", "Transition", "Event", "Edit")
@@ -35,7 +35,6 @@ class Edit:
35
35
  asc_sat_statement: Optional[StmtCdlSat] = None,
36
36
  frmc_statement: Optional[StmtFrmc] = None,
37
37
  ) -> None:
38
- # Assigning types for the attributes explicitly
39
38
  self._edit_statement: StmtEvent = edit_statement
40
39
  self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
41
40
  self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
@@ -51,8 +50,8 @@ class Edit:
51
50
  def line_number(self) -> int:
52
51
  """
53
52
  Get the line number for the "standard form" statement associated with
54
- this edit. Line numbers a zero-indexed, such that the
55
- "TITLE:" record is line zero.
53
+ this edit. Line numbers a zero-indexed, such that the "TITLE:" record
54
+ is line zero.
56
55
  """
57
56
  return self._edit_statement.line_number
58
57
 
@@ -180,7 +179,7 @@ class Edit:
180
179
  @property
181
180
  def asc_sop_raw(self) -> Optional[str]:
182
181
  """
183
- ASC CDL Slope-Offset-Power statement raw line
182
+ ASC CDL Slope-Offset-Power statement raw line.
184
183
  """
185
184
  if self._asc_sop_statement is None:
186
185
  return None
@@ -190,7 +189,7 @@ class Edit:
190
189
  @property
191
190
  def asc_sat(self) -> Optional[float]:
192
191
  """
193
- Get ASC CDL saturation value for clip, if present
192
+ Get ASC CDL saturation value for clip, if present.
194
193
  """
195
194
  if self._asc_sat_statement is None:
196
195
  return None
@@ -12,10 +12,10 @@ from typing import Any, Generator
12
12
  class EditList:
13
13
  """
14
14
  Represents an entire edit decision list as returned by
15
- :func:`~pycmx.parse_cmx3600()`.
15
+ :func:`~pycmx.parse_cmx_events.parse_cmx3600()`.
16
16
  """
17
17
 
18
- def __init__(self, statements):
18
+ def __init__(self, statements: list):
19
19
  self.title_statement: StmtTitle = statements[0]
20
20
  self.event_statements = statements[1:]
21
21
 
@@ -31,12 +31,27 @@ class Event:
31
31
  will have multiple edits when a dissolve, wipe or key transition needs
32
32
  to be performed.
33
33
  """
34
+
35
+ # FTR this is a totall bonkers way of doing this, I wrote this when
36
+ # I was still learning Python and I'm sure there's easier ways to do
37
+ # it. The job is complicated because multiple edits can occur in one
38
+ # event and then other statements can modify the event in different
39
+ # ways.
40
+
34
41
  edits_audio = list(self._statements_with_audio_ext())
35
42
  clip_names = self._clip_name_statements()
36
43
  source_files = self._source_file_statements()
37
44
 
45
+ # We first get the edit events combined with their extra audio
46
+ # channel statements, if any.
47
+
48
+ # The list the_zip contains one element for each initialization
49
+ # parameter in Edit()
38
50
  the_zip: List[List[Any]] = [edits_audio]
39
51
 
52
+ # If there are two Clip Name statements and two edits, we look for
53
+ # "FROM" and "TO" clip name lines. Otherwise we just look for on
54
+ # each per edit.
40
55
  if len(edits_audio) == 2:
41
56
  start_name: Optional[StmtClipName] = None
42
57
  end_name: Optional[StmtClipName] = None
@@ -54,6 +69,10 @@ class Event:
54
69
  else:
55
70
  the_zip.append([None] * len(edits_audio))
56
71
 
72
+ # if there's one source file statemnent per clip, we allocate them to
73
+ # each edit in order. Otherwise if there's only one, we assign the one
74
+ # to all the edits. If there's no source_file statements, we provide
75
+ # None.
57
76
  if len(edits_audio) == len(source_files):
58
77
  the_zip.append(source_files)
59
78
  elif len(source_files) == 1:
@@ -61,7 +80,7 @@ class Event:
61
80
  else:
62
81
  the_zip.append([None] * len(edits_audio))
63
82
 
64
- # attach trans name to last event
83
+ # attach effects name to last event
65
84
  try:
66
85
  trans_statement = self._trans_name_statements()[0]
67
86
  trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
@@ -104,11 +123,16 @@ class Event:
104
123
 
105
124
  def _statements_with_audio_ext(self) -> Generator[
106
125
  Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
107
- for (s1, s2) in zip(self.statements, self.statements[1:]):
108
- if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
109
- yield (s1, s2)
110
- elif type(s1) is StmtEvent:
111
- yield (s1, None)
126
+
127
+ if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
128
+ yield (self.statements[0], None)
129
+
130
+ else:
131
+ for (s1, s2) in zip(self.statements, self.statements[1:]):
132
+ if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
133
+ yield (s1, s2)
134
+ elif type(s1) is StmtEvent:
135
+ yield (s1, None)
112
136
 
113
137
  def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
114
138
  return next((s for s in self.statements if type(s) is StmtCdlSop),
@@ -7,12 +7,14 @@ from .parse_cmx_statements import (parse_cmx3600_statements)
7
7
  from .edit_list import EditList
8
8
 
9
9
 
10
- def parse_cmx3600(f: TextIO) -> EditList:
10
+ def parse_cmx3600(f: TextIO, tolerant: bool = False) -> EditList:
11
11
  """
12
12
  Parse a CMX 3600 EDL.
13
13
 
14
14
  :param TextIO f: a file-like object, an opened CMX 3600 .EDL file.
15
+ :param bool tolerant: If `True`, a relaxed event line parsing method will
16
+ be used, in the case the default method fails.
15
17
  :returns: An :class:`pycmx.edit_list.EditList`.
16
18
  """
17
- statements = parse_cmx3600_statements(f)
19
+ statements = parse_cmx3600_statements(f, tolerant)
18
20
  return EditList(statements)
@@ -13,12 +13,13 @@ from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
13
13
  from .util import collimate
14
14
 
15
15
 
16
- def parse_cmx3600_statements(file: TextIO) -> List[object]:
16
+ def parse_cmx3600_statements(file: TextIO,
17
+ tolerant: bool = False) -> List[object]:
17
18
  """
18
19
  Return a list of every statement in the file argument.
19
20
  """
20
21
  lines = file.readlines()
21
- return [_parse_cmx3600_line(line.strip(), line_number)
22
+ return [_parse_cmx3600_line(line.strip(), line_number, tolerant)
22
23
  for (line_number, line) in enumerate(lines)]
23
24
 
24
25
 
@@ -38,7 +39,8 @@ def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
38
39
  # 8,8,1,4,2,1,4,13,3,1,1]
39
40
 
40
41
 
41
- def _parse_cmx3600_line(line: str, line_number: int) -> object:
42
+ def _parse_cmx3600_line(line: str, line_number: int,
43
+ tolerant: bool = False) -> object:
42
44
  """
43
45
  Parses a single CMX EDL line.
44
46
 
@@ -54,9 +56,19 @@ def _parse_cmx3600_line(line: str, line_number: int) -> object:
54
56
  return _parse_fcm(line, line_number)
55
57
  if line_matcher is not None:
56
58
  event_field_len = len(line_matcher.group(1))
59
+
57
60
  source_field_len = len(line) - (event_field_len + 65)
58
- return _parse_columns_for_standard_form(line, event_field_len,
59
- source_field_len, line_number)
61
+
62
+ try:
63
+ return _parse_columns_for_standard_form(
64
+ line, event_field_len, source_field_len, line_number)
65
+
66
+ except EventFormError:
67
+ if tolerant:
68
+ return _parse_columns_tolerant(line, line_number)
69
+ else:
70
+ return StmtUnrecognized(line, line_number)
71
+
60
72
  if line.startswith("AUD"):
61
73
  return _parse_extended_audio_channels(line, line_number)
62
74
  if line.startswith("*"):
@@ -190,6 +202,10 @@ def _parse_split(line: str, line_number):
190
202
  # return StmtMotionMemory(source="", fps="")
191
203
  #
192
204
 
205
+ class EventFormError(RuntimeError):
206
+ pass
207
+
208
+
193
209
  def _parse_unrecognized(line, line_number):
194
210
  return StmtUnrecognized(content=line, line_number=line_number)
195
211
 
@@ -197,17 +213,24 @@ def _parse_unrecognized(line, line_number):
197
213
  def _parse_columns_for_standard_form(line: str, event_field_length: int,
198
214
  source_field_length: int,
199
215
  line_number: int):
216
+ # breakpoint()
200
217
  col_widths = _edl_column_widths(event_field_length, source_field_length)
201
218
 
202
219
  if sum(col_widths) > len(line):
203
- return StmtUnrecognized(content=line, line_number=line_number)
220
+ raise EventFormError()
204
221
 
205
222
  column_strings = collimate(line, col_widths)
206
223
 
224
+ channels = column_strings[4].strip()
225
+ trans = column_strings[6].strip()
226
+
227
+ if len(channels) == 0 or len(trans) == 0:
228
+ raise EventFormError()
229
+
207
230
  return StmtEvent(event=column_strings[0],
208
231
  source=column_strings[2].strip(),
209
- channels=column_strings[4].strip(),
210
- trans=column_strings[6].strip(),
232
+ channels=channels,
233
+ trans=trans,
211
234
  trans_op=column_strings[8].strip(),
212
235
  source_in=column_strings[10].strip(),
213
236
  source_out=column_strings[12].strip(),
@@ -217,6 +240,26 @@ def _parse_columns_for_standard_form(line: str, event_field_length: int,
217
240
  source_field_size=source_field_length)
218
241
 
219
242
 
243
+ def _parse_columns_tolerant(line: str, line_number: int):
244
+ pattern = re.compile(r'^\s*(\d+)\s+(.{8,128}?)\s+'
245
+ r'(V|A|A2|AA|NONE|AA/V|A2/V|B)\s+'
246
+ r'(C|D|W|KB|K|KO)\s+(\d*)\s+(\d\d.\d\d.\d\d.\d\d)\s'
247
+ r'(\d\d.\d\d.\d\d.\d\d)\s(\d\d.\d\d.\d\d.\d\d)\s'
248
+ r'(\d\d.\d\d.\d\d.\d\d)'
249
+ )
250
+
251
+ match = pattern.match(line)
252
+ if match:
253
+ return StmtEvent(event=int(match.group(1)), source=match.group(2),
254
+ channels=match.group(3), trans=match.group(4),
255
+ trans_op=match.group(5), source_in=match.group(6),
256
+ source_out=match.group(7), record_in=match.group(8),
257
+ record_out=match.group(9), line_number=line_number,
258
+ source_field_size=len(match.group(2)))
259
+ else:
260
+ return StmtUnrecognized(line, line_number)
261
+
262
+
220
263
  def _parse_source_umid_statement(line, line_number):
221
264
  # trimmed = line[3:].strip()
222
265
  # return StmtSourceUMID(name=None, umid=None, line_number=line_number)
@@ -5,8 +5,6 @@ from typing import Any, NamedTuple
5
5
 
6
6
  from .cdl import AscSopComponents
7
7
 
8
- # type str = str
9
-
10
8
 
11
9
  class StmtTitle(NamedTuple):
12
10
  title: str
@@ -24,7 +24,7 @@ class Transition:
24
24
  @property
25
25
  def kind(self) -> Optional[str]:
26
26
  """
27
- Return the kind of transition: Cut, Wipe, etc
27
+ Return the kind of transition: Cut, Wipe, etc.
28
28
  """
29
29
  if self.cut:
30
30
  return Transition.Cut
@@ -56,7 +56,8 @@ class Transition:
56
56
 
57
57
  @property
58
58
  def effect_duration(self) -> int:
59
- """The duration of this transition, in frames of the record target.
59
+ """
60
+ The duration of this transition, in frames of the record target.
60
61
 
61
62
  In the event of a key event, this is the duration of the fade in.
62
63
  """
@@ -1,56 +0,0 @@
1
- [tool.poetry]
2
- name = "pycmx"
3
- version = "1.4.0"
4
- description = "Python CMX 3600 Edit Decision List Parser"
5
- authors = ["Jamie Hardt <jamiehardt@me.com>"]
6
- license = "MIT"
7
- readme = "README.md"
8
- keywords = [
9
- 'parser',
10
- 'film',
11
- 'broadcast'
12
- ]
13
- classifiers = [
14
- 'Development Status :: 5 - Production/Stable',
15
- 'License :: OSI Approved :: MIT License',
16
- 'Topic :: Multimedia',
17
- 'Topic :: Multimedia :: Video',
18
- 'Topic :: Text Processing',
19
- 'Programming Language :: Python :: 3.8',
20
- 'Programming Language :: Python :: 3.9',
21
- 'Programming Language :: Python :: 3.10',
22
- 'Programming Language :: Python :: 3.11',
23
- 'Programming Language :: Python :: 3.12',
24
- 'Programming Language :: Python :: 3.13'
25
- ]
26
- homepage = "https://github.com/iluvcapra/pycmx"
27
- documentation = "https://pycmx.readthedocs.io/"
28
- repository = "https://github.com/iluvcapra/pycmx.git"
29
- urls.Tracker = "https://github.com/iluvcapra/pycmx/issues"
30
-
31
- [tool.poetry.extras]
32
- doc = ['sphinx', 'sphinx_rtd_theme']
33
-
34
- [tool.poetry.dependencies]
35
- python = "^3.8"
36
- sphinx = { version='>= 5.3.0', optional=true}
37
- sphinx_rtd_theme = {version ='>= 1.1.1', optional=true}
38
-
39
- [tool.pyright]
40
- typeCheckingMode = "basic"
41
-
42
- [tool.pylint]
43
- max-line-length = 88
44
- disable = [
45
- "C0103", # (invalid-name)
46
- "C0114", # (missing-module-docstring)
47
- "C0115", # (missing-class-docstring)
48
- "C0116", # (missing-function-docstring)
49
- "R0903", # (too-few-public-methods)
50
- "R0913", # (too-many-arguments)
51
- "W0105", # (pointless-string-statement)
52
- ]
53
-
54
- [build-system]
55
- requires = ["poetry-core"]
56
- build-backend = "poetry.core.masonry.api"
File without changes
File without changes
File without changes
File without changes