diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 945f35b..678b180 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -5,10 +5,15 @@ on: types: [published] workflow_dispatch: - +permissions: + contents: read + id-token: write + jobs: deploy: runs-on: ubuntu-latest + environment: + name: release steps: - uses: actions/checkout@v3.5.2 - name: Set up Python @@ -18,14 +23,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools build wheel twine - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }} - run: | - python -m build . - twine upload dist/* + pip install build + - name: Build package + run: python -m build + - name: pypi-publish + uses: pypa/gh-action-pypi-publish@v1.8.6 - name: Report to Mastodon uses: cbrgm/mastodon-github-action@v1.0.1 with: diff --git a/docs/source/conf.py b/docs/source/conf.py index 708e879..01ca47d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,18 +15,19 @@ import os import sys sys.path.insert(0, os.path.abspath('../..')) -print(sys.path) + +import pycmx # -- Project information ----------------------------------------------------- project = u'pycmx' -copyright = u'2022, Jamie Hardt' +copyright = u'(c) 2023, Jamie Hardt' author = u'Jamie Hardt' # The short X.Y version -version = u'' +version = pycmx.__version__ # The full version, including alpha/beta/rc tags -release = u'' +release = pycmx.__version__ # -- General configuration --------------------------------------------------- diff --git a/pycmx/__init__.py b/pycmx/__init__.py index 4a20f1c..80e4951 100644 --- a/pycmx/__init__.py +++ b/pycmx/__init__.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- """ -pycmx is a module for parsing CMX 3600-style EDLs. For more information and -examples see README.md +pycmx is a parser for CMX 3600-style EDLs. -This module (c) 2022 Jamie Hardt. For more information on your rights to +This module (c) 2023 Jamie Hardt. For more information on your rights to copy and reuse this software, refer to the LICENSE file included with the distribution. """ -__version__ = '1.2.0' +__version__ = '1.2.1' from .parse_cmx_events import parse_cmx3600 from .transition import Transition diff --git a/pycmx/channel_map.py b/pycmx/channel_map.py index a7071ef..f52cc4f 100644 --- a/pycmx/channel_map.py +++ b/pycmx/channel_map.py @@ -2,7 +2,7 @@ # (c) 2018 Jamie Hardt from re import (compile, match) -from typing import Dict, Tuple +from typing import Dict, Tuple, Generator class ChannelMap: """ @@ -24,62 +24,62 @@ class ChannelMap: self.v = v @property - def video(self): + def video(self) -> bool: 'True if video is included' return self.v @property - def audio(self): + def audio(self) -> bool: 'True if an audio channel is included' return len(self._audio_channel_set) > 0 @property - def channels(self): + def channels(self) -> Generator[int, None, None]: 'A generator for each audio channel' for c in self._audio_channel_set: yield c @property - def a1(self): + def a1(self) -> bool: """True if A1 is included""" return self.get_audio_channel(1) @a1.setter - def a1(self,val): + def a1(self, val: bool): self.set_audio_channel(1,val) @property - def a2(self): + def a2(self) -> bool: """True if A2 is included""" return self.get_audio_channel(2) @a2.setter - def a2(self,val): + def a2(self, val: bool): self.set_audio_channel(2,val) @property - def a3(self): + def a3(self) -> bool: """True if A3 is included""" return self.get_audio_channel(3) @a3.setter - def a3(self,val): + def a3(self, val: bool): self.set_audio_channel(3,val) @property - def a4(self): + def a4(self) -> bool: """True if A4 is included""" return self.get_audio_channel(4) @a4.setter - def a4(self,val): + def a4(self,val: bool): self.set_audio_channel(4,val) - def get_audio_channel(self,chan_num): + def get_audio_channel(self, chan_num) -> bool: """True if chan_num is included""" return (chan_num in self._audio_channel_set) - def set_audio_channel(self,chan_num,enabled): + def set_audio_channel(self,chan_num, enabled: bool): """If enabled is true, chan_num will be included""" if enabled: self._audio_channel_set.add(chan_num) diff --git a/pycmx/edit.py b/pycmx/edit.py index bbbbab3..3f60b3c 100644 --- a/pycmx/edit.py +++ b/pycmx/edit.py @@ -3,7 +3,9 @@ from .transition import Transition from .channel_map import ChannelMap -from .parse_cmx_statements import StmtEffectsName +# from .parse_cmx_statements import StmtEffectsName + +from typing import Optional class Edit: """ @@ -18,7 +20,7 @@ class Edit: self.trans_name_statement = trans_name_statement @property - def line_number(self): + def line_number(self) -> int: """ Get the line number for the "standard form" statement associated with this edit. Line numbers a zero-indexed, such that the @@ -27,7 +29,7 @@ class Edit: return self.edit_statement.line_number @property - def channels(self): + def channels(self) -> ChannelMap: """ Get the :obj:`ChannelMap` object associated with this Edit. """ @@ -38,7 +40,7 @@ class Edit: return cm @property - def transition(self): + def transition(self) -> Transition: """ Get the :obj:`Transition` object associated with this edit. """ @@ -48,14 +50,14 @@ class Edit: return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None) @property - def source_in(self): + def source_in(self) -> str: """ Get the source in timecode. """ return self.edit_statement.source_in @property - def source_out(self): + def source_out(self) -> str: """ Get the source out timecode. """ @@ -63,7 +65,7 @@ class Edit: return self.edit_statement.source_out @property - def record_in(self): + def record_in(self) -> str: """ Get the record in timecode. """ @@ -71,7 +73,7 @@ class Edit: return self.edit_statement.record_in @property - def record_out(self): + def record_out(self) -> str: """ Get the record out timecode. """ @@ -79,7 +81,7 @@ class Edit: return self.edit_statement.record_out @property - def source(self): + def source(self) -> str: """ Get the source column. This is the 8, 32 or 128-character string on the event record line, this usually references the tape name of the source. @@ -87,21 +89,21 @@ class Edit: return self.edit_statement.source @property - def black(self): + def black(self) -> bool: """ Black video or silence should be used as the source for this event. """ return self.source == "BL" @property - def aux_source(self): + def aux_source(self) -> bool: """ An auxiliary source is the source of this event. """ return self.source == "AX" @property - def source_file(self): + def source_file(self) -> Optional[str]: """ Get the source file, as attested by a "* SOURCE FILE" remark on the EDL. This will return None if the information is not present. @@ -112,7 +114,7 @@ class Edit: return self.source_file_statement.filename @property - def clip_name(self): + def clip_name(self) -> Optional[str]: """ Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP NAME" remark on the EDL. This will return None if the information is diff --git a/pycmx/edit_list.py b/pycmx/edit_list.py index a8c27f8..b126b69 100644 --- a/pycmx/edit_list.py +++ b/pycmx/edit_list.py @@ -5,21 +5,21 @@ from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSou from .event import Event from .channel_map import ChannelMap +from typing import Generator + class EditList: """ - Represents an entire edit decision list as returned by `parse_cmx3600()`. - + Represents an entire edit decision list as returned by :func:`~pycmx.parse_cmx3600()`. """ def __init__(self, statements): self.title_statement = statements[0] self.event_statements = statements[1:] - @property - def format(self): + def format(self) -> str: """ - The detected format of the EDL. Possible values are: `3600`,`File32`, - `File128`, and `unknown` + The detected format of the EDL. Possible values are: "3600", "File32", + "File128", and "unknown". """ first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None) @@ -37,7 +37,7 @@ class EditList: @property - def channels(self): + def channels(self) -> ChannelMap: """ Return the union of every channel channel. """ @@ -51,7 +51,7 @@ class EditList: @property - def title(self): + def title(self) -> str: """ The title of this edit list. """ @@ -59,7 +59,7 @@ class EditList: @property - def unrecognized_statements(self): + def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]: """ A generator for all the unrecognized statements in the list. """ @@ -69,7 +69,7 @@ class EditList: @property - def events(self): + def events(self) -> Generator[Event, None, None]: 'A generator for all the events in the edit list' is_drop = None current_event_num = None @@ -97,7 +97,7 @@ class EditList: yield Event(statements=event_statements) @property - def sources(self): + def sources(self) -> Generator[StmtSourceUMID, None, None]: """ A generator for all of the sources in the list """ diff --git a/pycmx/event.py b/pycmx/event.py index f43e8f8..3ff0df7 100644 --- a/pycmx/event.py +++ b/pycmx/event.py @@ -1,26 +1,28 @@ # pycmx -# (c) 2018 Jamie Hardt +# (c) 2023 Jamie Hardt from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName) -from .edit import Edit +from .edit import Edit + +from typing import List, Generator, Optional, Tuple, Any class Event: """ - Represents a collection of :class:`Edit`s, all with the same event number. + Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same event number. """ def __init__(self, statements): self.statements = statements @property - def number(self): + def number(self) -> int: """ Return the event number. """ return int(self._edit_statements()[0].event) @property - def edits(self): + def edits(self) -> List[Edit]: """ Returns the edits. Most events will have a single edit, a single event will have multiple edits when a dissolve, wipe or key transition needs @@ -30,17 +32,19 @@ class Event: clip_names = self._clip_name_statements() source_files= self._source_file_statements() - the_zip = [edits_audio] + the_zip: List[List[Any]] = [edits_audio] if len(edits_audio) == 2: - cn = [None, None] + start_name: Optional[StmtClipName] = None + end_name: Optional[StmtClipName] = None + for clip_name in clip_names: if clip_name.affect == 'from': - cn[0] = clip_name + start_name = clip_name elif clip_name.affect == 'to': - cn[1] = clip_name + end_name = clip_name - the_zip.append(cn) + the_zip.append([start_name, end_name]) else: if len(edits_audio) == len(clip_names): the_zip.append(clip_names) @@ -57,17 +61,20 @@ class Event: # attach trans name to last event try: trans_statement = self._trans_name_statements()[0] - trans_names = [None] * (len(edits_audio) - 1) + trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1) trans_names.append(trans_statement) the_zip.append(trans_names) except IndexError: the_zip.append([None] * len(edits_audio) ) - - return [ Edit(e1[0],e1[1],n1,s1,u1) for (e1,n1,s1,u1) in zip(*the_zip) ] + return [ Edit(edit_statement=e1[0], + audio_ext_statement=e1[1], + clip_name_statement=n1, + source_file_statement=s1, + trans_name_statement=u1) for (e1,n1,s1,u1) in zip(*the_zip) ] @property - def unrecognized_statements(self): + def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]: """ A generator for all the unrecognized statements in the event. """ @@ -75,19 +82,19 @@ class Event: if type(s) is StmtUnrecognized: yield s - def _trans_name_statements(self): + def _trans_name_statements(self) -> List[StmtEffectsName]: return [s for s in self.statements if type(s) is StmtEffectsName] - def _edit_statements(self): + def _edit_statements(self) -> List[StmtEvent]: return [s for s in self.statements if type(s) is StmtEvent] - def _clip_name_statements(self): + def _clip_name_statements(self) -> List[StmtClipName]: return [s for s in self.statements if type(s) is StmtClipName] - def _source_file_statements(self): + def _source_file_statements(self) -> List[StmtSourceFile]: return [s for s in self.statements if type(s) is StmtSourceFile] - def _statements_with_audio_ext(self): + def _statements_with_audio_ext(self) -> Generator[Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]: for (s1, s2) in zip(self.statements, self.statements[1:]): if type(s1) is StmtEvent and type(s2) is StmtAudioExt: yield (s1,s2) diff --git a/pycmx/parse_cmx_events.py b/pycmx/parse_cmx_events.py index 211e2ad..176c0cc 100644 --- a/pycmx/parse_cmx_events.py +++ b/pycmx/parse_cmx_events.py @@ -1,20 +1,19 @@ # pycmx # (c) 2018 Jamie Hardt -from collections import namedtuple +# from collections import namedtuple -from .parse_cmx_statements import (parse_cmx3600_statements, StmtEvent,StmtFCM ) +from .parse_cmx_statements import (parse_cmx3600_statements) from .edit_list import EditList -def parse_cmx3600(f): +from typing import TextIO + +def parse_cmx3600(f: TextIO): """ Parse a CMX 3600 EDL. - Args: - f : a file-like object, anything that's readlines-able. - - Returns: - An :class:`pycmx.edit_list.EditList`. + :param TextIO f: a file-like object, anything that's readlines-able. + :returns: An :class:`pycmx.edit_list.EditList`. """ statements = parse_cmx3600_statements(f) return EditList(statements) diff --git a/pycmx/parse_cmx_statements.py b/pycmx/parse_cmx_statements.py index d391698..957c238 100644 --- a/pycmx/parse_cmx_statements.py +++ b/pycmx/parse_cmx_statements.py @@ -5,6 +5,8 @@ import re import sys from collections import namedtuple from itertools import count +from typing import TextIO, List + from .util import collimate @@ -18,12 +20,12 @@ StmtSourceFile = namedtuple("SourceFile",["filename","line_number"]) StmtRemark = namedtuple("Remark",["text","line_number"]) StmtEffectsName = namedtuple("EffectsName",["name","line_number"]) StmtSourceUMID = namedtuple("Source",["name","umid","line_number"]) -StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"]) +StmtSplitEdit = namedtuple("SplitEdit",["video","magnitude", "line_number"]) StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"]) -def parse_cmx3600_statements(file): +def parse_cmx3600_statements(file: TextIO) -> List[object]: """ Return a list of every statement in the file argument. """ @@ -109,7 +111,7 @@ def _parse_extended_audio_channels(line, line_number): else: return StmtUnrecognized(content=line, line_number=line_number) -def _parse_remark(line, line_number): +def _parse_remark(line, line_number) -> object: if line.startswith("FROM CLIP NAME:"): return StmtClipName(name=line[15:].strip() , affect="from", line_number=line_number) elif line.startswith("TO CLIP NAME:"): @@ -119,7 +121,7 @@ def _parse_remark(line, line_number): else: return StmtRemark(text=line, line_number=line_number) -def _parse_effects_name(line, line_number): +def _parse_effects_name(line, line_number) -> StmtEffectsName: name = line[16:].strip() return StmtEffectsName(name=name, line_number=line_number) diff --git a/pycmx/transition.py b/pycmx/transition.py index 988b411..5d2ebe3 100644 --- a/pycmx/transition.py +++ b/pycmx/transition.py @@ -1,5 +1,7 @@ # pycmx -# (c) 2018 Jamie Hardt +# (c) 2023 Jamie Hardt + +from typing import Optional class Transition: """ @@ -19,7 +21,7 @@ class Transition: self.name = name @property - def kind(self): + def kind(self) -> Optional[str]: """ Return the kind of transition: Cut, Wipe, etc """ @@ -37,22 +39,22 @@ class Transition: return Transition.KeyOut @property - def cut(self): + def cut(self) -> bool: "`True` if this transition is a cut." return self.transition == 'C' @property - def dissolve(self): + def dissolve(self) -> bool: "`True` if this traansition is a dissolve." return self.transition == 'D' @property - def wipe(self): + def wipe(self) -> bool: "`True` if this transition is a wipe." return self.transition.startswith('W') @property - def effect_duration(self): + def effect_duration(self) -> int: """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. @@ -60,7 +62,7 @@ class Transition: return int(self.operand) @property - def wipe_number(self): + def wipe_number(self) -> Optional[int]: "Wipes are identified by a particular number." if self.wipe: return int(self.transition[1:]) @@ -68,19 +70,21 @@ class Transition: return None @property - def key_background(self): + def key_background(self) -> bool: "`True` if this edit is a key background." return self.transition == Transition.KeyBackground @property - def key_foreground(self): + def key_foreground(self) -> bool: "`True` if this edit is a key foreground." return self.transition == Transition.Key @property - def key_out(self): + def key_out(self) -> bool: """ `True` if this edit is a key out. This material will removed from the key foreground and replaced with the key background. """ return self.transition == Transition.KeyOut + +