23 Commits

Author SHA1 Message Date
688402d195 some notes to myslf 2025-12-20 13:44:29 -08:00
4e81810584 ruff configuration 2025-12-18 22:07:08 -08:00
faf2596a57 ruff for linting 2025-12-18 21:45:24 -08:00
1e9fbe339c ruff for linting 2025-12-18 21:44:28 -08:00
a8d00470d4 ruff for linting 2025-12-18 21:43:12 -08:00
fe1e59e731 fix __all__ for module 2025-12-18 21:36:11 -08:00
ec8a08074d doc twiddles 2025-12-18 20:50:06 -08:00
Jamie Hardt
ef683a7683 Update pip install command for documentation 2025-12-18 20:46:33 -08:00
Jamie Hardt
d778f64230 Merge pull request #21 from iluvcapra/uv-build
PEP 621 compliance
2025-12-18 20:43:40 -08:00
1b0ccd4ef7 doc twiddles 2025-12-18 20:41:44 -08:00
498d5c8fea doc tiwddles 2025-12-18 17:24:26 -08:00
af5d937aeb doc tiwddles 2025-12-18 17:19:20 -08:00
637f4ab9a4 tweaking rtd config 2025-12-18 16:00:25 -08:00
4cd635ff17 nudging docs python version 2025-12-18 15:54:48 -08:00
26e1e38320 nudging docs python version 2025-12-18 15:46:01 -08:00
0e97742336 added 3.14 to matrix, some dependency work 2025-12-18 13:30:52 -08:00
67ea12042f updated .flake8 path 2025-12-18 12:35:53 -08:00
6b910a0920 pyproject.toml now PEP 621 compliant
Update build system to uv
2025-12-18 12:27:21 -08:00
a8f35a9ffc twiddle, nudge version number 2025-12-18 10:18:04 -08:00
Jamie Hardt
242f2e08d5 Merge pull request #20 from iluvcapra/19-unusual-edl
Tolerant Parsing Mode
2025-12-18 10:06:07 -08:00
610d406e97 flake8 2025-12-17 15:13:49 -08:00
28307608fc Added raw ASC_SOP line method 2025-12-17 15:12:34 -08:00
9ab3804c89 Removed explicit imports of pycmx 2025-12-17 15:09:05 -08:00
18 changed files with 129 additions and 81 deletions

View File

@@ -1,5 +0,0 @@
[flake8]
per-file-ignores =
pycmx/__init__.py: F401
tests/__init__.py: F401

View File

@@ -16,7 +16,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@@ -26,13 +26,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install flake8 pytest python -m pip install -e .[dev]
python -m pip install -e . - name: Lint with ruff
- name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names ruff check src/
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --max-line-length=79 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
pytest pytest

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

View File

@@ -9,11 +9,11 @@ version: 2
build: build:
os: ubuntu-20.04 os: ubuntu-20.04
tools: tools:
python: "3.10" python: "3.13"
# You can also specify other tool versions: jobs:
# nodejs: "16" install:
# rust: "1.55" - pip install --upgrade pip
# golang: "1.17" - pip install --group 'doc' -e .
# Build documentation in the docs/ directory with Sphinx # Build documentation in the docs/ directory with Sphinx
sphinx: sphinx:
@@ -28,5 +28,3 @@ python:
install: install:
- method: pip - method: pip
path: . path: .
extra_requirements:
- doc

View File

@@ -14,7 +14,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
read. Event number field and source name field sizes are determined read. Event number field and source name field sizes are determined
dynamically for each statement for a high level of compliance at the expense dynamically for each statement for a high level of compliance at the expense
of strictness. of strictness.
* An more relaxed "tolerant" mode allows parsing of an EDL file where columns * A more relaxed "tolerant" mode allows parsing of an EDL file where columns
use non-standard widths. use non-standard widths.
* Preserves relationship between events and individual edits/clips. * Preserves relationship between events and individual edits/clips.
* Remark or comment fields with common recognized forms are read and * Remark or comment fields with common recognized forms are read and
@@ -37,7 +37,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
### Opening and Parsing EDL Files ### Opening and Parsing EDL Files
``` ```
>>> import pycmx >>> import pycmx
>>> with open("tests/edls/TEST.edl") as f >>> with open("tests/edls/TEST.edl") as f:
... edl = pycmx.parse_cmx3600(f) ... edl = pycmx.parse_cmx3600(f)
... ...
>>> edl.title >>> edl.title

View File

@@ -16,7 +16,7 @@ The `pycmx` package parses a CMX 3600 EDL and its most most common variations.
read. Event number field and source name field sizes are determined read. Event number field and source name field sizes are determined
dynamically for each statement for a high level of compliance at the expense dynamically for each statement for a high level of compliance at the expense
of strictness. of strictness.
* An more relaxed "tolerant" mode allows parsing of an EDL file where columns * A more relaxed "tolerant" mode allows parsing of an EDL file where columns
use non-standard widths. use non-standard widths.
* Preserves relationship between events and individual edits/clips. * Preserves relationship between events and individual edits/clips.
* Remark or comment fields with common recognized forms are read and * Remark or comment fields with common recognized forms are read and

View File

@@ -1,15 +1,16 @@
[tool.poetry] [project]
name = "pycmx" name = "pycmx"
version = "1.4.0" version = "1.5.0"
description = "Python CMX 3600 Edit Decision List Parser" description = "Python CMX 3600 Edit Decision List Parser"
authors = ["Jamie Hardt <jamiehardt@me.com>"] authors = [{name = "Jamie Hardt", email= "jamiehardt@me.com"}]
license = "MIT" license-files = ["LICENSE"]
readme = "README.md" readme = "README.md"
keywords = [ keywords = [
'parser', 'parser',
'film', 'film',
'broadcast' 'broadcast'
] ]
requires-python = '>3.8'
classifiers = [ classifiers = [
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
@@ -21,36 +22,59 @@ classifiers = [
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13' 'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14'
] ]
homepage = "https://github.com/iluvcapra/pycmx"
documentation = "https://pycmx.readthedocs.io/"
repository = "https://github.com/iluvcapra/pycmx.git"
urls.Tracker = "https://github.com/iluvcapra/pycmx/issues"
[tool.poetry.extras] [project.optional-dependencies]
doc = [
'sphinx >= 5.3.0',
'sphinx_rtd_theme >= 1.1.1',
]
dev = [
'pytest',
'ruff>=0.14.10'
]
[project.urls]
Homepage = "https://github.com/iluvcapra/pycmx"
Documentation = "https://pycmx.readthedocs.io/"
Repository = "https://github.com/iluvcapra/pycmx.git"
Tracker = "https://github.com/iluvcapra/pycmx/issues"
[dependency-groups]
dev = ['ruff', 'pytest']
doc = ['sphinx', 'sphinx_rtd_theme'] doc = ['sphinx', 'sphinx_rtd_theme']
[tool.poetry.dependencies]
python = "^3.8"
sphinx = { version='>= 5.3.0', optional=true}
sphinx_rtd_theme = {version ='>= 1.1.1', optional=true}
[tool.pyright] [tool.pyright]
typeCheckingMode = "basic" typeCheckingMode = "basic"
[tool.pylint] [tool.ruff]
max-line-length = 88 line-length = 88
disable = [ indent-width = 4
"C0103", # (invalid-name)
"C0114", # (missing-module-docstring) [tool.ruff.lint]
"C0115", # (missing-class-docstring) select = ["E", "F", "W"]
"C0116", # (missing-function-docstring)
"R0903", # (too-few-public-methods) [tool.ruff.format]
"R0913", # (too-many-arguments) docstring-code-line-length = 88
"W0105", # (pointless-string-statement)
] # [tool.pylint]
# max-line-length = 88
# disable = [
# "C0103", # (invalid-name)
# "C0114", # (missing-module-docstring)
# "C0115", # (missing-class-docstring)
# "C0116", # (missing-function-docstring)
# "R0903", # (too-few-public-methods)
# "R0913", # (too-many-arguments)
# "W0105", # (pointless-string-statement)
# ]
#
[build-system] [build-system]
requires = ["poetry-core"] requires = ["uv_build>=0.9.18,<0.10.0"]
build-backend = "poetry.core.masonry.api" build-backend = "uv_build"

View File

@@ -11,3 +11,5 @@ from .parse_cmx_events import parse_cmx3600
from .transition import Transition from .transition import Transition
from .event import Event from .event import Event
from .edit import Edit from .edit import Edit
__all__ = ("parse_cmx3600", "Transition", "Event", "Edit")

View File

@@ -1,8 +1,8 @@
# pycmx # pycmx
# (c) 2018-2025 Jamie Hardt # (c) 2018-2025 Jamie Hardt
from pycmx.cdl import AscSopComponents, FramecountTriple from .cdl import AscSopComponents, FramecountTriple
from pycmx.statements import ( from .statements import (
StmtCdlSat, StmtCdlSat,
StmtCdlSop, StmtCdlSop,
StmtFrmc, StmtFrmc,
@@ -35,7 +35,6 @@ class Edit:
asc_sat_statement: Optional[StmtCdlSat] = None, asc_sat_statement: Optional[StmtCdlSat] = None,
frmc_statement: Optional[StmtFrmc] = None, frmc_statement: Optional[StmtFrmc] = None,
) -> None: ) -> None:
# Assigning types for the attributes explicitly
self._edit_statement: StmtEvent = edit_statement self._edit_statement: StmtEvent = edit_statement
self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
self._clip_name_statement: Optional[StmtClipName] = clip_name_statement self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
@@ -51,8 +50,8 @@ class Edit:
def line_number(self) -> int: def line_number(self) -> int:
""" """
Get the line number for the "standard form" statement associated with Get the line number for the "standard form" statement associated with
this edit. Line numbers a zero-indexed, such that the this edit. Line numbers a zero-indexed, such that the "TITLE:" record
"TITLE:" record is line zero. is line zero.
""" """
return self._edit_statement.line_number return self._edit_statement.line_number
@@ -177,10 +176,20 @@ class Edit:
return self._asc_sop_statement.cdl_sop return self._asc_sop_statement.cdl_sop
@property
def asc_sop_raw(self) -> Optional[str]:
"""
ASC CDL Slope-Offset-Power statement raw line.
"""
if self._asc_sop_statement is None:
return None
return self._asc_sop_statement.line
@property @property
def asc_sat(self) -> Optional[float]: def asc_sat(self) -> Optional[float]:
""" """
Get ASC CDL saturation value for clip, if present Get ASC CDL saturation value for clip, if present.
""" """
if self._asc_sat_statement is None: if self._asc_sat_statement is None:
return None return None

View File

@@ -1,7 +1,7 @@
# pycmx # pycmx
# (c) 2018-2025 Jamie Hardt # (c) 2018-2025 Jamie Hardt
from pycmx.statements import (StmtCorruptRemark, StmtTitle, StmtEvent, from .statements import (StmtCorruptRemark, StmtTitle, StmtEvent,
StmtUnrecognized, StmtSourceUMID) StmtUnrecognized, StmtSourceUMID)
from .event import Event from .event import Event
from .channel_map import ChannelMap from .channel_map import ChannelMap
@@ -12,7 +12,7 @@ from typing import Any, Generator
class EditList: class EditList:
""" """
Represents an entire edit decision list as returned by Represents an entire edit decision list as returned by
:func:`~pycmx.parse_cmx3600()`. :func:`~pycmx.parse_cmx_events.parse_cmx3600()`.
""" """
def __init__(self, statements: list): def __init__(self, statements: list):

View File

@@ -1,10 +1,9 @@
# pycmx # pycmx
# (c) 2023-2025 Jamie Hardt # (c) 2023-2025 Jamie Hardt
from pycmx.statements import StmtFrmc from .statements import (StmtFrmc, StmtEvent, StmtClipName, StmtSourceFile,
from .parse_cmx_statements import ( StmtAudioExt, StmtUnrecognized, StmtEffectsName,
StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtCdlSop, StmtCdlSat)
StmtEffectsName, StmtCdlSop, StmtCdlSat)
from .edit import Edit from .edit import Edit
from typing import List, Generator, Optional, Tuple, Any from typing import List, Generator, Optional, Tuple, Any
@@ -32,12 +31,27 @@ class Event:
will have multiple edits when a dissolve, wipe or key transition needs will have multiple edits when a dissolve, wipe or key transition needs
to be performed. to be performed.
""" """
# FTR this is a totall bonkers way of doing this, I wrote this when
# I was still learning Python and I'm sure there's easier ways to do
# it. The job is complicated because multiple edits can occur in one
# event and then other statements can modify the event in different
# ways.
edits_audio = list(self._statements_with_audio_ext()) edits_audio = list(self._statements_with_audio_ext())
clip_names = self._clip_name_statements() clip_names = self._clip_name_statements()
source_files = self._source_file_statements() source_files = self._source_file_statements()
# We first get the edit events combined with their extra audio
# channel statements, if any.
# The list the_zip contains one element for each initialization
# parameter in Edit()
the_zip: List[List[Any]] = [edits_audio] the_zip: List[List[Any]] = [edits_audio]
# If there are two Clip Name statements and two edits, we look for
# "FROM" and "TO" clip name lines. Otherwise we just look for on
# each per edit.
if len(edits_audio) == 2: if len(edits_audio) == 2:
start_name: Optional[StmtClipName] = None start_name: Optional[StmtClipName] = None
end_name: Optional[StmtClipName] = None end_name: Optional[StmtClipName] = None
@@ -55,6 +69,10 @@ class Event:
else: else:
the_zip.append([None] * len(edits_audio)) the_zip.append([None] * len(edits_audio))
# if there's one source file statemnent per clip, we allocate them to
# each edit in order. Otherwise if there's only one, we assign the one
# to all the edits. If there's no source_file statements, we provide
# None.
if len(edits_audio) == len(source_files): if len(edits_audio) == len(source_files):
the_zip.append(source_files) the_zip.append(source_files)
elif len(source_files) == 1: elif len(source_files) == 1:
@@ -62,7 +80,7 @@ class Event:
else: else:
the_zip.append([None] * len(edits_audio)) the_zip.append([None] * len(edits_audio))
# attach trans name to last event # attach effects name to last event
try: try:
trans_statement = self._trans_name_statements()[0] trans_statement = self._trans_name_statements()[0]
trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1) trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1)
@@ -70,6 +88,7 @@ class Event:
the_zip.append(trans_names) the_zip.append(trans_names)
except IndexError: except IndexError:
the_zip.append([None] * len(edits_audio)) the_zip.append([None] * len(edits_audio))
return [Edit(edit_statement=e1[0], return [Edit(edit_statement=e1[0],
audio_ext_statement=e1[1], audio_ext_statement=e1[1],
clip_name_statement=n1, clip_name_statement=n1,

View File

@@ -4,7 +4,7 @@
import re import re
from typing import TextIO, List from typing import TextIO, List
from pycmx.cdl import AscSopComponents, Rgb from .cdl import AscSopComponents, Rgb
from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc, from .statements import (StmtCdlSat, StmtCdlSop, StmtCorruptRemark, StmtFrmc,
StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM, StmtRemark, StmtTitle, StmtUnrecognized, StmtFCM,
@@ -60,9 +60,8 @@ def _parse_cmx3600_line(line: str, line_number: int,
source_field_len = len(line) - (event_field_len + 65) source_field_len = len(line) - (event_field_len + 65)
try: try:
return _parse_columns_for_standard_form(line, event_field_len, return _parse_columns_for_standard_form(
source_field_len, line, event_field_len, source_field_len, line_number)
line_number)
except EventFormError: except EventFormError:
if tolerant: if tolerant:
@@ -134,12 +133,16 @@ def _parse_remark(line, line_number) -> object:
else: else:
try: try:
return StmtCdlSop(cdl_sop=AscSopComponents( return StmtCdlSop(line=line,
slope=Rgb(red=float(v[0][0]), green=float(v[0][1]), cdl_sop=AscSopComponents(
slope=Rgb(red=float(v[0][0]),
green=float(v[0][1]),
blue=float(v[0][2])), blue=float(v[0][2])),
offset=Rgb(red=float(v[1][0]), green=float(v[1][1]), offset=Rgb(red=float(v[1][0]),
green=float(v[1][1]),
blue=float(v[1][2])), blue=float(v[1][2])),
power=Rgb(red=float(v[2][0]), green=float(v[2][1]), power=Rgb(red=float(v[2][0]),
green=float(v[2][1]),
blue=float(v[2][2])) blue=float(v[2][2]))
), ),
line_number=line_number) line_number=line_number)

View File

@@ -3,9 +3,7 @@
from typing import Any, NamedTuple from typing import Any, NamedTuple
from pycmx.cdl import AscSopComponents from .cdl import AscSopComponents
# type str = str
class StmtTitle(NamedTuple): class StmtTitle(NamedTuple):
@@ -50,6 +48,7 @@ class StmtSourceFile(NamedTuple):
class StmtCdlSop(NamedTuple): class StmtCdlSop(NamedTuple):
line: str
cdl_sop: AscSopComponents[float] cdl_sop: AscSopComponents[float]
line_number: int line_number: int

View File

@@ -24,7 +24,7 @@ class Transition:
@property @property
def kind(self) -> Optional[str]: def kind(self) -> Optional[str]:
""" """
Return the kind of transition: Cut, Wipe, etc Return the kind of transition: Cut, Wipe, etc.
""" """
if self.cut: if self.cut:
return Transition.Cut return Transition.Cut
@@ -56,7 +56,8 @@ class Transition:
@property @property
def effect_duration(self) -> int: def effect_duration(self) -> int:
"""The duration of this transition, in frames of the record target. """
The duration of this transition, in frames of the record target.
In the event of a key event, this is the duration of the fade in. In the event of a key event, this is the duration of the fade in.
""" """